mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-21 00:26:23 +03:00
Comprehensive LangGraph skill covering: - Core architecture: Graph API, Functional API, state management, agent loops - Three multi-agent patterns: supervisor (~94% accuracy), swarm (~40% fewer LLM calls), hierarchical teams (subgraphs with nested state) - Persistence: checkpointers vs stores, per-invocation/per-thread/stateless modes - Production: Agent Server deployment, LangSmith observability, 8 failure modes - Evals: routing accuracy, resolution coverage, LLM-as-judge methodology - Troubleshooting: symptom→cause→fix tables per pattern - 3 Python scripts: supervisor scaffold, swarm scaffold, eval generator - 3 runnable templates: supervisor, swarm, subgraph composition Ships 8 reference files, 3 scripts, and 3 templates.
268 lines
9.5 KiB
Markdown
268 lines
9.5 KiB
Markdown
# Swarm Pattern — Direct Agent-to-Agent Handoffs
|
||
|
||
The swarm pattern eliminates the central supervisor. Agents hand off directly to each other using `Command` objects returned from handoff tools. When an agent calls a handoff tool, `Command(goto=target, graph=Command.PARENT)` tells LangGraph to navigate to a different node in the parent graph.
|
||
|
||
This pattern is the right choice when **latency is the primary constraint**, domain boundaries are clear, and agents rarely misroute on their own.
|
||
|
||
## Architecture
|
||
|
||
```
|
||
[User] → [Triage Agent] → [Billing Agent] → [Response]
|
||
│ │
|
||
├──→ [Tech Support] ─┘
|
||
└──→ [Account Mgmt]
|
||
```
|
||
|
||
No central orchestrator. Each agent decides whether to handle the request itself or hand off to another specialist using a `Command`.
|
||
|
||
## Comparative Metrics
|
||
|
||
| Metric | Swarm | Supervisor |
|
||
|--------|-------|------------|
|
||
| Avg latency (single-domain) | ~2.8s | ~4.2s |
|
||
| Avg latency (handoff required) | ~5.4s | ~9.1s |
|
||
| LLM calls (single-domain) | 1 (specialist only) | 2 (route + specialist) |
|
||
| LLM calls (handoff required) | 2 (specialist×2) | 4 (route×2 + specialist×2) |
|
||
| Avg tokens per request | ~1,900 | ~2,800 |
|
||
| Routing accuracy | ~91% | ~94% |
|
||
|
||
## Implementation
|
||
|
||
### 1. Define State
|
||
|
||
```python
|
||
from typing import Annotated, TypedDict
|
||
from langgraph.graph import MessagesState
|
||
import operator
|
||
|
||
class SwarmState(MessagesState):
|
||
current_agent: str
|
||
resolution_notes: Annotated[list[str], operator.add]
|
||
handoff_count: int # recursion guard — increment on each handoff
|
||
```
|
||
|
||
### 2. Create Handoff Tool Factory
|
||
|
||
The handoff tool returns a `Command` that tells LangGraph to navigate to a different node:
|
||
|
||
```python
|
||
from langgraph.types import Command
|
||
from langchain_core.tools import tool
|
||
|
||
def make_handoff_tool(target_agent: str, description: str):
|
||
"""Factory that creates a handoff tool for transferring to another agent."""
|
||
|
||
@tool(f"transfer_to_{target_agent}")
|
||
def handoff(reason: str) -> Command:
|
||
"""Transfer the conversation to another specialist agent."""
|
||
return Command(
|
||
goto=target_agent,
|
||
update={"current_agent": target_agent},
|
||
graph=Command.PARENT,
|
||
)
|
||
|
||
handoff.__doc__ = description
|
||
return handoff
|
||
```
|
||
|
||
### 3. Create Handoff Tools
|
||
|
||
```python
|
||
transfer_to_billing = make_handoff_tool(
|
||
"billing",
|
||
"Transfer to the billing specialist for invoices, payments, or discounts.",
|
||
)
|
||
transfer_to_tech = make_handoff_tool(
|
||
"tech_support",
|
||
"Transfer to technical support for SSO, integrations, or system issues.",
|
||
)
|
||
transfer_to_account = make_handoff_tool(
|
||
"account",
|
||
"Transfer to account management for plan changes or upgrades.",
|
||
)
|
||
```
|
||
|
||
### 4. Create Agents with Handoff Tools
|
||
|
||
The **triage agent** has no domain tools — it only routes:
|
||
|
||
```python
|
||
from langchain.agents import create_agent
|
||
|
||
triage_agent = create_agent(
|
||
llm,
|
||
tools=[transfer_to_billing, transfer_to_tech, transfer_to_account],
|
||
system_prompt="You are a triage agent. Analyze the customer's request and "
|
||
"transfer to the appropriate specialist. Do not answer questions "
|
||
"yourself — always transfer. If multiple issues exist, transfer "
|
||
"to the most urgent one first.",
|
||
)
|
||
```
|
||
|
||
**Specialist agents** have domain tools PLUS handoff tools for other domains:
|
||
|
||
```python
|
||
billing_swarm_agent = create_agent(
|
||
llm,
|
||
tools=[lookup_billing_info, apply_discount,
|
||
transfer_to_tech, transfer_to_account],
|
||
system_prompt="You are a billing specialist. Help with invoices, payments, "
|
||
"and discounts. If the customer has unresolved issues outside your "
|
||
"domain, transfer to the appropriate specialist.",
|
||
)
|
||
|
||
tech_swarm_agent = create_agent(
|
||
llm,
|
||
tools=[diagnose_sso, check_system_status,
|
||
transfer_to_billing, transfer_to_account],
|
||
system_prompt="You are a technical support specialist. Help with technical "
|
||
"issues, SSO, and integrations. If the customer has unresolved "
|
||
"issues outside your domain, transfer to the appropriate specialist.",
|
||
)
|
||
|
||
account_swarm_agent = create_agent(
|
||
llm,
|
||
tools=[lookup_account_details, update_plan,
|
||
transfer_to_billing, transfer_to_tech],
|
||
system_prompt="You are an account management specialist. Help with plan "
|
||
"changes and upgrades. If the customer has unresolved issues "
|
||
"outside your domain, transfer to the appropriate specialist.",
|
||
)
|
||
```
|
||
|
||
### 5. Create Node Wrappers
|
||
|
||
```python
|
||
def triage_node(state: SwarmState) -> Command:
|
||
result = triage_agent.invoke({"messages": state["messages"]})
|
||
return result # Command from the handoff tool
|
||
|
||
def billing_swarm_node(state: SwarmState) -> dict:
|
||
result = billing_swarm_agent.invoke({"messages": state["messages"]})
|
||
return {
|
||
"messages": result["messages"][-1:],
|
||
"resolution_notes": [
|
||
f"Billing: {result['messages'][-1].content[:200]}"
|
||
],
|
||
}
|
||
|
||
def tech_swarm_node(state: SwarmState) -> dict:
|
||
result = tech_swarm_agent.invoke({"messages": state["messages"]})
|
||
return {
|
||
"messages": result["messages"][-1:],
|
||
"resolution_notes": [
|
||
f"Tech Support: {result['messages'][-1].content[:200]}"
|
||
],
|
||
}
|
||
|
||
def account_swarm_node(state: SwarmState) -> dict:
|
||
result = account_swarm_agent.invoke({"messages": state["messages"]})
|
||
return {
|
||
"messages": result["messages"][-1:],
|
||
"resolution_notes": [
|
||
f"Account: {result['messages'][-1].content[:200]}"
|
||
],
|
||
}
|
||
```
|
||
|
||
### 6. Wire the Graph
|
||
|
||
```python
|
||
from langgraph.graph import StateGraph, START, END
|
||
from typing import Literal
|
||
|
||
def route_after_agent(
|
||
state: SwarmState,
|
||
) -> Literal["billing", "tech_support", "account", "__end__"]:
|
||
# If the last message is an AIMessage without tool_calls, we're done
|
||
messages = state.get("messages", [])
|
||
if messages:
|
||
last_msg = messages[-1]
|
||
if isinstance(last_msg, AIMessage) and not last_msg.tool_calls:
|
||
return "__end__"
|
||
# Otherwise, continue with the current agent (handoff via Command)
|
||
current = state.get("current_agent", "")
|
||
if current in ("billing", "tech_support", "account"):
|
||
return current
|
||
return "__end__"
|
||
|
||
swarm_builder = StateGraph(SwarmState)
|
||
|
||
swarm_builder.add_node("triage", triage_node)
|
||
swarm_builder.add_node("billing", billing_swarm_node)
|
||
swarm_builder.add_node("tech_support", tech_swarm_node)
|
||
swarm_builder.add_node("account", account_swarm_node)
|
||
|
||
swarm_builder.add_edge(START, "triage")
|
||
|
||
# Each specialist can route to any other specialist or end
|
||
for node in ["billing", "tech_support", "account"]:
|
||
swarm_builder.add_conditional_edges(
|
||
node, route_after_agent,
|
||
["billing", "tech_support", "account", END],
|
||
)
|
||
|
||
swarm_graph = swarm_builder.compile()
|
||
```
|
||
|
||
## Key Details
|
||
|
||
### Command-based Routing
|
||
|
||
The `Command` object is the key mechanism. When an agent calls a handoff tool, the tool returns `Command(goto=target_agent, graph=Command.PARENT)` which:
|
||
1. Updates `current_agent` in state
|
||
2. Tells LangGraph to navigate to the target node in the parent graph
|
||
3. Carries any additional state updates via `update=`
|
||
|
||
### Recursion Guard
|
||
|
||
The swarm pattern has no natural recursion limit. Two agents can ping-pong indefinitely. Add a handoff counter:
|
||
|
||
```python
|
||
from langgraph.types import Command
|
||
|
||
def make_handoff_tool(target_agent, description, max_handoffs=3):
|
||
@tool(f"transfer_to_{target_agent}")
|
||
def handoff(reason: str, _state: SwarmState = None) -> Command:
|
||
"""Transfer the conversation to another specialist agent."""
|
||
# You'd need to access state — use ToolRuntime or inject via closure
|
||
return Command(
|
||
goto=target_agent,
|
||
update={"current_agent": target_agent},
|
||
graph=Command.PARENT,
|
||
)
|
||
handoff.__doc__ = description
|
||
return handoff
|
||
```
|
||
|
||
Track `handoff_count` in state and check it in `route_after_agent` — after 3, force END.
|
||
|
||
### Context Propagation
|
||
|
||
When Agent A hands off to Agent B, the response from Agent A must be in the message history. The `Command.update` should carry the relevant messages. Without this, Agent B sees the original user message but has no context about what Agent A already did.
|
||
|
||
## Failure Modes
|
||
|
||
### Context Loss on Handoff
|
||
Agent A resolves part of the issue and hands off to Agent B. Agent B sees the original message but doesn't know what Agent A already did.
|
||
|
||
**Fix:** Ensure `Command.update` includes the resolution context. The `resolution_notes` accumulator is one approach; forwarding the last AIMessage is another.
|
||
|
||
### Swarm Ping-Pong
|
||
Agent A doesn't know the answer → hands off to Agent B. Agent B also doesn't know → hands off back to Agent A. Repeats until recursion limit.
|
||
|
||
**Fix:** Track `handoff_count` in state. Hard limit at 3, then escalate to human or fallback agent.
|
||
|
||
### Lost Messages During Handoff
|
||
The handoff tool returns a `Command(graph=Command.PARENT)`, but the specialist agent's internal tool-calling loop messages don't propagate to the parent graph.
|
||
|
||
**Fix:** Ensure your `Command.update` includes the relevant messages. LLMs expect tool calls to be paired with `ToolMessage` responses. If you break that pairing, the next agent sees malformed history.
|
||
|
||
## When to Use
|
||
|
||
- Latency is your primary constraint
|
||
- Domain boundaries are clear and agents rarely misroute
|
||
- Requests often span multiple domains (latency savings compound)
|
||
- You want agents to maintain conversational context through handoffs
|
||
- You have good per-agent evals to catch misroutes
|