Claude subagents can independently execute tasks, but coordinating them for a complex, multi-step workflow requires a deliberate orchestration layer.
Let’s see this in action. Imagine a workflow where we need to:
- Analyze a user’s request: Determine the intent and extract key entities.
- Retrieve relevant information: Query a knowledge base or API based on extracted entities.
- Synthesize a response: Combine retrieved information into a coherent answer.
Here’s a simplified Python example using anthropic-sdk to represent this:
import anthropic
client = anthropic.Anthropic(api_key="YOUR_ANTHROPIC_API_KEY")
def analyze_request(user_query: str) -> dict:
message = client.messages.create(
model="claude-3-opus-20240229",
max_tokens=500,
messages=[
{"role": "user", "content": f"Analyze the following user query. Extract the intent and any relevant entities. Respond in JSON format. User query: '{user_query}'"}
]
)
return eval(message.content[0].text) # WARNING: eval is unsafe in production. Use JSON parsing.
def retrieve_information(query_params: dict) -> str:
# In a real scenario, this would call an API or database
# For demonstration, we'll simulate a lookup
knowledge_base = {
"product_info": {
"product_name": "Quantum Leap Coffee Maker",
"features": ["brews in 30 seconds", "self-cleaning", "smart connectivity"],
"price": "$199.99"
},
"support_faq": {
"troubleshooting_steps": "Ensure power is connected, water reservoir is full, and cleaning cycle is complete."
}
}
intent = query_params.get("intent")
entity = query_params.get("entity")
if intent == "product_inquiry" and entity == "Quantum Leap Coffee Maker":
return f"Product: {knowledge_base['product_info']['product_name']}. Features: {', '.join(knowledge_base['product_info']['features'])}. Price: {knowledge_base['product_info']['price']}."
elif intent == "support_request" and entity == "Quantum Leap Coffee Maker":
return f"Troubleshooting: {knowledge_base['support_faq']['troubleshooting_steps']}."
else:
return "I couldn't find specific information for your request."
def synthesize_response(analysis_result: dict, retrieved_data: str) -> str:
message = client.messages.create(
model="claude-3-opus-20240229",
max_tokens=1000,
messages=[
{"role": "user", "content": f"Based on the following analysis and retrieved data, craft a helpful and polite response to the user. Analysis: {analysis_result}. Retrieved Data: {retrieved_data}"}
]
)
return message.content[0].text
# --- Orchestration ---
user_input = "Tell me about the Quantum Leap Coffee Maker and its features."
# Step 1: Analyze Request
analysis = analyze_request(user_input)
print(f"Analysis: {analysis}")
# Step 2: Retrieve Information
retrieved = retrieve_information(analysis)
print(f"Retrieved Data: {retrieved}")
# Step 3: Synthesize Response
final_response = synthesize_response(analysis, retrieved)
print(f"Final Response: {final_response}")
This example shows how distinct Claude calls can be chained. The output of one call (the analysis) becomes the input for the next (retrieval), and so on. This forms a basic "agentic workflow."
The core problem this solves is breaking down a complex, multi-faceted task into smaller, manageable sub-tasks that individual Claude agents (or even different configurations of Claude) can handle effectively. Each agent acts as a specialized tool. The orchestrator is the conductor, deciding which tool to use, when, and with what information.
Internally, the process is a directed acyclic graph (DAG) of operations. Each node in the graph is a call to a Claude agent with a specific prompt and context. The edges represent the flow of data from the output of one agent to the input of another. You control the workflow by defining this graph: which agents are available, the prompts they use, and the dependencies between them.
The model parameter in the client.messages.create call isn’t just about choosing Claude-3-Opus; it’s a lever. You could use a cheaper, faster model like claude-3-haiku-20240307 for the initial analysis if it’s sufficient, and reserve Opus for the final synthesis step where nuance is critical. This "model routing" based on task complexity is a powerful optimization.
A common pattern is to use a "router" agent that examines the user’s query and decides which other specialized agent (or tool, or function) is best suited to handle it. This router agent doesn’t perform the task itself but directs the request. For instance, if the user asks about pricing, the router might trigger a pricing_lookup_agent which then calls an external API. The output of that API call is then passed back to a synthesis agent.
When orchestrating, the prompt for each sub-agent needs to be highly specific about its role, its expected input format, and its desired output format. If an agent is supposed to return JSON, and it returns a string that looks like JSON but isn’t valid, the entire chain can break. Robust error handling and strict output validation (beyond eval in the example) are crucial.
The next step in complexity involves agents that can choose their own tools or sub-agents dynamically based on the input, rather than following a predefined, static sequence.