Last month I watched an agent spend 47 API calls trying to answer a question it could have resolved in two. The setup looked reasonable—tool calling enabled, memory buffer in place, error handling on paper. The agent just had no idea which tool to use first, kept calling the same thing, and had no way to know it was looping.
This is what happens when you build an agent without understanding its actual failure modes. Architecture matters. Here’s what I’ve learned from shipping agents into production.
The Three Core Patterns
An AI agent needs three things to work: a way to decide what to do, access to tools, and memory of what happened. The way you connect these three things determines whether your agent runs clean or burns through tokens chasing its tail.
Pattern 1: Simple Routing (Decision → Tool → Response)
This is the foundation. The agent sees a user request, picks a tool, executes it, and returns a result. No loops. No recursion. Single decision point.
from anthropic import Anthropic
client = Anthropic()
def route_and_execute(user_message: str) -> str:
tools = [
{
"name": "lookup_price",
"description": "Get current price for a product",
"input_schema": {
"type": "object",
"properties": {
"product_id": {"type": "string"}
},
"required": ["product_id"]
}
},
{
"name": "check_stock",
"description": "Check inventory for a product",
"input_schema": {
"type": "object",
"properties": {
"product_id": {"type": "string"}
},
"required": ["product_id"]
}
}
]
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=[{
"role": "user",
"content": user_message
}]
)
# Check response type
for block in response.content:
if block.type == "tool_use":
tool_name = block.name
tool_input = block.input
# Execute tool (mock implementation)
if tool_name == "lookup_price":
result = f"Price for {tool_input['product_id']}: $29.99"
elif tool_name == "check_stock":
result = f"Stock for {tool_input['product_id']}: 15 units"
else:
result = "Tool not found"
return result
# If no tool was called, return text response
return response.content[0].text if response.content else ""
# Usage
response = route_and_execute("What's the price of product ABC-123?")
print(response)
This works well for straightforward tasks—customer service queries, data lookups, single decisions. It fails when the answer requires multiple steps.
Pattern 2: Agentic Loop (Decide → Tool → Evaluate → Decide Again)
When a task needs multiple steps—research something, then calculate, then format—you need a loop. The agent makes a decision, executes it, gets feedback, and decides what to do next. The trap: without exit conditions, this becomes infinite.
def agentic_loop(user_message: str, max_iterations: int = 5) -> str:
tools = [
{
"name": "search_knowledge_base",
"description": "Search internal docs for information",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string"}
},
"required": ["query"]
}
},
{
"name": "calculate_metric",
"description": "Perform calculation on data",
"input_schema": {
"type": "object",
"properties": {
"values": {"type": "array", "items": {"type": "number"}},
"operation": {"type": "string", "enum": ["sum", "average", "max"]}
},
"required": ["values", "operation"]
}
}
]
messages = [{
"role": "user",
"content": user_message
}]
for iteration in range(max_iterations):
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=messages
)
# Check if we have a final answer
if response.stop_reason == "end_turn":
return response.content[0].text if response.content else ""
# Process tool calls
if response.stop_reason == "tool_use":
# Add assistant response to message history
messages.append({
"role": "assistant",
"content": response.content
})
# Execute tools and collect results
tool_results = []
for block in response.content:
if block.type == "tool_use":
# Mock execution
if block.name == "search_knowledge_base":
result = f"Found docs on: {block.input['query']}"
elif block.name == "calculate_metric":
values = block.input['values']
op = block.input['operation']
if op == "sum":
result = f"Result: {sum(values)}"
elif op == "average":
result = f"Result: {sum(values)/len(values)}"
else:
result = f"Result: {max(values)}"
else:
result = "Unknown tool"
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
# Add tool results back to messages
messages.append({
"role": "user",
"content": tool_results
})
return "Max iterations reached"
# Usage
result = agentic_loop("Find our Q3 revenue data and calculate the average monthly value")
print(result)
Key point: The max_iterations limit is not optional. I’ve seen agents hit 100+ calls because nobody set a ceiling. Set it to 5 by default and increase only when testing proves you need more.
Pattern 3: Hierarchical Planning (Plan → Execute Subtasks → Synthesize)
For complex workflows—analyzing a report, planning a campaign, debugging a system—break the problem into subtasks first. Have the agent plan, then execute the plan, then synthesize. This gives you visibility into what the agent thinks it needs to do before it wastes tokens doing it.
Tool Calling: Setup and Failure Modes
Tool calling in Claude (since March 2025) is reliable, but the setup matters. You define tools as JSON schemas. The model decides which tool to call and with what parameters. You execute the tool and feed results back.
Most failures come from two places:
1. Vague Tool Descriptions
Bad: "search" - search for information
Good: "search_customer_database" - search by email, name, or ID. Returns customer record with purchase history. Use this when you need to verify customer details before processing orders.
The model picks tools based on descriptions. Vague descriptions lead to wrong tool selection.
2. Missing Error Feedback
When a tool fails, tell the agent specifically why. Don’t just return a generic error. In the agentic loop code above, each tool result goes back as a message. If a search fails, say "No results found for query 'xyz'. Try a different search term." instead of "Error."
Memory: Stateless vs Persistent
Agents need to remember context. You have two options:
Conversation history (stateless): Pass the entire message thread to the model each time. Works for short interactions. Gets expensive fast. Token cost = O(conversation_length) per call.
Persistent memory: Store conversation summary, key facts, and tool execution logs in a database. Pass only relevant context. Token cost stays flat.
For production agents, use persistent memory. Store the latest 10–15 messages in thread history, plus a rolling summary. If the conversation is longer than ~20 messages, ask the model to summarize and store separately.
One Pattern to Test Today
Pick Pattern 1 (Simple Routing) and implement it for one real task in your system—customer lookup, data retrieval, anything single-step. Define 2–3 tools with specific descriptions. Run 10 test queries and check:
- Did it pick the right tool? (If not, rewrite the description.)
- Did it use the right parameters? (If not, your input schema is unclear.)
- Does the output actually answer the user’s question?
Once that works, move to Pattern 2. Multi-step agents are not harder—they’re just Pattern 1 in a loop. But only add the loop when single-step isn’t enough.