When I first started building with Large Language Models, I remember feeling a strange mix of awe and frustration.
You could ask an LLM to write a Shakespearean sonnet about Kubernetes, and it would do it in four seconds. But the moment you asked it to check the current weather, read a local SQLite database, or summarize a newly uploaded PDF, it hit an invisible wall. It was like chatting with a genius trapped inside a soundproof glass cube: brilliant, articulate, but completely disconnected from the outside world.
That wall disappears the moment you introduce agent skills.
Giving an agent "skills" (often called tool use or function calling) is what turns a passive text generator into an active problem solver. But moving from a toy demo to something reliable requires more than just passing a list of functions to an API endpoint.
In this guide, we'll walk through what agent skills actually are under the hood, explore battle-tested orchestration patterns, and cover the practical guardrails you need to keep your system safe and stable.
1. Traditional LLMs vs. Agentic Systems: The Mental Model
In a traditional setup, interaction is purely linear:
User Input -> [ LLM ] -> Output Text
You supply a prompt, the model calculates probability distributions over tokens, and it returns a stream of text. If the answer requires real-time data or an external calculation, the model has to guess-which usually leads to confident hallucinations.
An agentic architecture transforms this linear request into a continuous reasoning loop.
Instead of rushing to produce a final answer, the model follows four distinct steps:
Observe: Assess the user's request and examine the current state or environment.
Think: Reason about the goal. Does it know the answer, or does it need external data?
Act: If it needs data or needs to make a change, it triggers a skill (an API call, a database query, or a script).
Reflect: It evaluates the output returned by that skill and decides whether the task is complete or if another step is required.
2. What Is an "Agent Skill" Under the Hood?
There is no mystical AI magic happening inside a tool call. An agent skill consists of two simple pieces:
A plain code function: A Python or JavaScript function you wrote that does something concrete (like fetching an API or parsing a file).
A structured schema: A JSON specification telling the LLM what the tool is named, what it does, and what arguments it expects.
Here is what that looks like in Python:
import json
# 1. The actual executable function
def get_user_subscription(user_id: str) -> dict:
"""Fetches subscription status for a given user ID from our system."""
database = {
"usr_101": {"tier": "Pro", "status": "active", "renewal_days": 12},
"usr_102": {"tier": "Free", "status": "active", "renewal_days": 0},
}
return database.get(user_id, {"error": "User not found"})
# 2. The schema the LLM reads to know this skill exists
subscription_tool_schema = {
"type": "function",
"function": {
"name": "get_user_subscription",
"description": "Look up account subscription tier and status using a user ID.",
"parameters": {
"type": "object",
"properties": {
"user_id": {
"type": "string",
"description": "The unique user ID, formatted like 'usr_123'",
}
},
"required": ["user_id"],
},
},
}
When you send this schema alongside your user prompt, the LLM doesn't execute the function itself. Instead, it outputs a structured JSON object saying:
"Hey, I don't know the answer directly, but please run get_user_subscription(user_id='usr_101') and show me what it returns."
Your application executes the function locally, feeds the result back into the LLM as an observation, and the agent delivers the final answer.
3. Orchestration Patterns: When One Agent Isn't Enough
When you are starting out, it is tempting to give a single agent twenty different skills and hope for the best.
In practice, this causes "context pollution." The model gets overwhelmed by too many tool definitions, forgets instructions, and hallucinates parameters. To build reliable workflows, we split responsibilities across multiple specialized agents.
Pattern A: Orchestrator-Worker
Think of this like a project manager working with senior engineers:
The Orchestrator receives the high-level prompt, devises a multi-step plan, and delegates subtasks.
Each Worker has access only to 2–3 skills relevant to its specific domain.
Once the workers finish, the orchestrator aggregates the results into a cohesive final output.
Pattern B: Generator-Critic
Ever write an email when you were tired, only to cringe when reading it the next morning? That is why you need a critic.
The Generator creates an initial draft, writes a SQL query, or proposes a code patch using its skills.
The Critic inspects that output against a rubric (e.g., checking for SQL injection vulnerabilities, syntax errors, or missed requirements).
If the critic spots an issue, it provides structured feedback back to the generator to retry. This self-correction loop catches bugs before your code touches production.
4. State Management: The "Zero-Database" Philosophy
When building agent workflows, a common beginner trap is immediately reaching for complex infrastructure: vector databases, dedicated caching layers, and external message queues.
Before adding heavy databases, consider the zero-database approach: keep your state in-memory using clean state graphs.
A state graph is a mental and architectural model where every step in your agent's process is a node, and the transitions between them are edges based on conditional logic.
# A simple in-memory state dictionary
class AgentState:
def __init__(self, task: str):
self.task = task
self.history = []
self.current_worker = "planner"
self.intermediate_results = {}
self.iterations = 0
By passing a single structured state dictionary between functions:
You avoid race conditions and synchronization headaches.
Your workflow remains fully reproducible and testable in local development.
You can persist the entire state to a plain JSON file if you need persistence across app reboots.
Only reach for an external database when you have long-running tasks lasting days or when your history exceeds LLM context windows.
5. The "Infinite Loop" Problem & Circuit Breakers
Here is a rite of passage for every AI developer: you give an agent a code execution tool, run it against a tricky task, walk away to grab coffee, and return to find it has executed 78 consecutive tool calls trying to fix the same syntax error-burning through your monthly API budget in six minutes.
Agents are relentless optimizers. If a tool fails, they will often retry with minor variations indefinitely unless you put hard boundaries in place.
Enter the Circuit Breaker:
class CircuitBreaker:
def __init__(self, max_steps: int = 8, max_consecutive_errors: int = 3):
self.max_steps = max_steps
self.max_consecutive_errors = max_consecutive_errors
self.step_count = 0
self.error_count = 0
def record_step(self, is_error: bool = False):
self.step_count += 1
if is_error:
self.error_count += 1
else:
self.error_count = 0 # Reset consecutive error counter
if self.step_count >= self.max_steps:
raise RuntimeError("Circuit breaker tripped: Maximum step limit reached.")
if self.error_count >= self.max_consecutive_errors:
raise RuntimeError("Circuit breaker tripped: Too many consecutive tool failures.")
Before invoking any tool or calling the LLM, tick your circuit breaker. If the agent gets stuck in a recursive failure spiral, your circuit breaker halts execution gracefully, logs the trace, and notifies you.
6. Sandboxing: Never Give an Agent Bare Metal
If one of your agent's skills is run_python_code or run_shell_command, never execute those commands directly on your primary workstation or host server.
Even a well-intentioned model might run os.remove() on an unintended folder, clone huge repos that fill up disk space, or spawn zombie processes.
Subprocess Isolation: At a bare minimum, run code through a restricted Python subprocess with strict timeouts and memory limits:
import subprocess
def run_isolated_code(script: str, timeout_seconds: int = 5) -> str:
try:
result = subprocess.run(
["python3", "-c", script],
capture_output=True,
text=True,
timeout=timeout_seconds,
check=True
)
return result.stdout
except subprocess.TimeoutExpired:
return "Execution timed out."
except subprocess.CalledProcessError as e:
return f"Error: {e.stderr}"
Container Sandboxes: For production environments, spin up lightweight, ephemeral Docker containers or micro-VMs that are destroyed the moment the agent finishes its run.
7. Headless Mock Testing: Don't Go Broke Testing
Testing an agent by running live queries through an LLM API endpoint is slow, non-deterministic, and expensive.
Instead, practice headless mock testing:
Separate your tool logic from your LLM calling logic.
Test your tools with standard unit tests just like any normal software module.
Mock the LLM's tool-call response using static JSON fixtures to verify that your orchestration loop parses arguments, updates state, and handles exceptions correctly without spending a penny.
def test_user_subscription_tool():
# Unit test the skill directly without invoking any LLM
result = get_user_subscription("usr_101")
assert result["tier"] == "Pro"
assert result["status"] == "active"
If your tool functions aren't reliable in isolation, wrapping them in an AI prompt will only amplify the failures.
8. Human-in-the-Loop (HITL): The Final Checkpoint
A simple rule of thumb for designing agent skills:
Automate reads. Guard writes.
Read actions (searching docs, analyzing data, running calculations) are safe to run autonomously.
Write actions (sending an email, modifying a production database, executing a bank transfer) should always trigger a human confirmation prompt.
def execute_database_update(query: str, state: AgentState):
if not state.is_human_approved:
return "Action paused. Awaiting explicit human approval to run update query."
# Run the database query...
By designing your state machine to pause execution when encountering high-risk actions, you get all the speed advantages of automation without the risk of accidental chaos.
Quick Reference Summary
| Component | Responsibility | Failure Mode to Watch For |
|---|---|---|
| Tool Schema | Declares name, types, and utility to the model. | Ambiguous descriptions causing inappropriate tool selection. |
| Orchestrator | Deconstructs goals and delegates to workers. | Attempting to execute tasks directly rather than delegating. |
| Circuit Breaker | Tracks iterations and limits consecutive errors. | Silent infinite retry loops that drain API balances. |
| Execution Sandbox | Isolates runtime commands from host machine. | Accidental filesystem corruption or unbound memory leaks. |
| HITL Interceptors | Enforces human approval for destructive mutations. | Autonomous writes leading to silent data corruption. |

Top comments (0)