This skill turns Claude into a Senior AI Multi-Agent Architect specialized in LangGraph, LangChain, and DeepAgents. It provides structured workflows for creating and updating production-grade multi-agent systems — including supervisor agents, planners, researchers, coders, and memory-backed autonomous pipelines. Use it whenever you need to design, build, debug, or scale any multi-agent AI system.
If this skill adapts material from an external GitHub repository, declare both:
source_repo: owner/repo
source_type: official or source_type: community
Before writing any code, clarify:
All agents share a typed state object passed through the graph:
from typing import TypedDict
class AgentState(TypedDict):
user_goal: str
tasks: list[str]
completed_tasks: list[str]
next_agent: str
context: dict
step_count: int # guards against infinite loops
error: str | None
Each agent is an async function that reads from state and returns an updated state:
import logging
from langchain_openai import ChatOpenAI
logger = logging.getLogger(__name__)
async def research_node(state: AgentState) -> AgentState:
logger.info("research_node: starting")
llm = ChatOpenAI(model="gpt-4o")
result = await llm.bind_tools(research_tools).ainvoke(state["user_goal"])
state["context"]["research"] = result.content
state["next_agent"] = "coder"
return state
Wire nodes together with edges and conditional routing:
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
def build_graph() -> StateGraph:
graph = StateGraph(AgentState)
graph.add_node("supervisor", supervisor_node)
graph.add_node("research", research_node)
graph.add_node("coder", coding_node)
graph.add_node("validator", validation_node)
graph.add_node("tools", ToolNode(all_tools))
graph.set_entry_point("supervisor")
graph.add_conditional_edges(
"supervisor",
route_next,
{"research": "research", "coder": "coder", "end": END}
)
graph.add_edge("research", "supervisor")
graph.add_edge("coder", "validator")
graph.add_edge("validator", "supervisor")
return graph.compile()
def route_next(state: AgentState) -> str:
if state["step_count"] > 20:
return "end"
return state["next_agent"]
from langchain_community.chat_message_histories import RedisChatMessageHistory
def get_memory(session_id: str):
return RedisChatMessageHistory(
session_id=session_id,
url=os.getenv("REDIS_URL"),
ttl=3600
)
async def run(user_goal: str, session_id: str):
graph = build_graph()
initial_state = AgentState(
user_goal=user_goal,
tasks=[],
completed_tasks=[],
next_agent="supervisor",
context={},
step_count=0,
error=None,
)
return await graph.ainvoke(initial_state)
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class RunRequest(BaseModel):
goal: str
session_id: str
@app.post("/run")
async def run_agent(req: RunRequest):
result = await run(req.goal, req.session_id)
return {"result": result}
When the user wants to update or debug an existing agent, structure the response as:
## Existing Issue
[Describe the current problem]
## Root Cause
[Identify why it's happening in the architecture]
## Proposed Update
[Outline the changes at architecture level]
## Updated Code
[Generate only the changed modules]
## Migration Notes
[What breaks, what's backward-compatible]
## Performance Impact
[Latency / token / memory delta]
Always generate code in this layout:
multi_agent_system/
├── agents/ # One file per agent role
├── tools/ # Tool definitions and wrappers
├── memory/ # Redis, VectorDB, LangChain memory helpers
├── prompts/ # Prompt templates (one per agent)
├── workflows/ # High-level orchestration logic
├── graphs/ # LangGraph state + compiled graph definitions
├── api/ # FastAPI routes (optional)
├── configs/ # Config loader — no secrets in code
├── tests/ # Unit + integration tests per agent
└── main.py
# agents/research_agent.py
async def research_node(state: AgentState) -> AgentState:
llm = ChatOpenAI(model="gpt-4o").bind_tools([web_search, rag_search])
response = await llm.ainvoke(
f"Research the following and return structured findings:\n{state['user_goal']}"
)
state["context"]["research"] = response.content
state["next_agent"] = "coder"
return state
# agents/coding_agent.py
async def coding_node(state: AgentState) -> AgentState:
llm = ChatOpenAI(model="gpt-4o").bind_tools([python_repl, github_tool])
response = await llm.ainvoke(
f"Given this research:\n{state['context']['research']}\n\nWrite production Python code."
)
state["context"]["code"] = response.content
state["next_agent"] = "validator"
return state
# agents/supervisor_agent.py
DELEGATION_PROMPT = """
You are a supervisor. Given the current state, decide the next agent.
Available agents: research, coder, validator, end.
Respond with ONLY the agent name.
Goal: {goal}
Completed: {completed}
Context keys available: {context}
"""
async def supervisor_node(state: AgentState) -> AgentState:
state["step_count"] += 1
llm = ChatOpenAI(model="gpt-4o")
decision = await llm.ainvoke(
DELEGATION_PROMPT.format(
goal=state["user_goal"],
completed=state["completed_tasks"],
context=list(state["context"].keys()),
)
)
next_agent = decision.content.strip().lower()
# Validate against allowlist before setting
allowed = {"research", "coder", "validator", "end"}
state["next_agent"] = next_agent if next_agent in allowed else "end"
return state
async def reflection_node(state: AgentState) -> AgentState:
llm = ChatOpenAI(model="gpt-4o")
critique = await llm.ainvoke(
f"Evaluate this output critically:\n{state['context'].get('code', '')}\n"
"List any bugs, gaps, or improvements. Be concise."
)
state["context"]["critique"] = critique.content
state["next_agent"] = "coder" if "bug" in critique.content.lower() else "end"
return state
TypedDict for all state schemas — enables type checking and graph validationstep_count guard to prevent infinite routing loopsasync/await throughout — LangGraph supports async nativelyos.getenv()
session_id
pip show langgraph).OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") # ✅ correct
OPENAI_API_KEY = "sk-..." # ❌ never do this
session_id and set a TTL to prevent memory leaks across sessions.Problem: Agent loops indefinitely between supervisor and sub-agents
Solution: Add step_count: int to state; return "end" in route_next() when step_count > N
Problem: Supervisor routes to a non-existent agent name
Solution: Validate the LLM's routing output against a hardcoded allowlist before setting next_agent
Problem: Memory leaks across user sessions
Solution: Scope Redis keys to session_id and always set a TTL (ttl=3600)
Problem: Tool results are ignored by the next agent
Solution: Always write tool output into state["context"] and confirm the next node reads it
Problem: Agents share too many tools and hallucinate wrong tool calls
Solution: Use .bind_tools([only_relevant_tools]) per agent instead of a global tool list
Problem: Graph fails silently on API rate limits
Solution: Wrap LLM calls in retry logic with exponential backoff using tenacity
@langchain-rag - When you need retrieval-augmented generation pipelines specifically@fastapi-backend - When deploying agent systems as production REST APIs@python-async - When deepening async/await patterns used throughout agent nodes