Introduction
The shift from single LLM prompts to multi-step agentic workflows is the defining architectural pattern in AI systems today. Instead of sending one prompt and hoping for the best, production systems decompose complex tasks into sequences and graphs of AI interactions — with feedback loops, conditional branching, error recovery, and human-in-the-loop checkpoints.
This guide provides runnable Python implementations of the five core patterns using LangGraph v1.10+ and CrewAI v1.10+, the two most widely adopted agent frameworks in 2026. Each pattern includes a complete code example, a Mermaid diagram of the flow, and guidance on when to use it.
Pattern 1: Prompt Chaining
Decompose a task into sequential steps where each step’s output feeds into the next.
flowchart LR
A[Input] --> B[Step 1: Extract]
B --> C[Step 2: Analyze]
C --> D[Step 3: Format]
D --> E[Output]
LangGraph Implementation
from langgraph.graph import StateGraph, END
from typing import TypedDict
class ChainState(TypedDict):
query: str
facts: str
analysis: str
summary: str
def extract_facts(state: ChainState) -> ChainState:
"""Step 1: Extract key facts from the query."""
prompt = f"Extract 3-5 key facts from: {state['query']}"
state["facts"] = llm.invoke(prompt) # llm is your configured model
return state
def analyze(state: ChainState) -> ChainState:
"""Step 2: Analyze the extracted facts."""
prompt = f"Analyze these facts and identify implications:\n{state['facts']}"
state["analysis"] = llm.invoke(prompt)
return state
def format_summary(state: ChainState) -> ChainState:
"""Step 3: Format the final summary."""
prompt = f"Write a concise summary based on this analysis:\n{state['analysis']}"
state["summary"] = llm.invoke(prompt)
return state
# Build the chain graph
graph = StateGraph(ChainState)
graph.add_node("extract", extract_facts)
graph.add_node("analyze", analyze)
graph.add_node("format", format_summary)
graph.set_entry_point("extract")
graph.add_edge("extract", "analyze")
graph.add_edge("analyze", "format")
graph.add_edge("format", END)
chain = graph.compile()
result = chain.invoke({"query": "What are the implications of NPU performance on mobile AI?"})
print(result["summary"])
Pattern 2: Routing
Classify an input and direct it to a specialized handler.
flowchart LR
A[Input] --> B{Classifier}
B -->|Technical| C[Tech Handler]
B -->|Billing| D[Billing Handler]
B -->|General| E[General Handler]
C --> F[Output]
D --> F
E --> F
LangGraph Implementation with Conditional Edges
from langgraph.graph import StateGraph, END
from typing import TypedDict, Literal
class RouterState(TypedDict):
query: str
category: str
response: str
def classify(state: RouterState) -> RouterState:
"""Route the query to the appropriate category."""
prompt = f"Classify this query as 'technical', 'billing', or 'general':\n{state['query']}"
state["category"] = llm.invoke(prompt).strip().lower()
return state
def handle_technical(state: RouterState) -> RouterState:
state["response"] = llm.invoke(f"Provide technical support for: {state['query']}")
return state
def handle_billing(state: RouterState) -> RouterState:
state["response"] = llm.invoke(f"Handle billing inquiry: {state['query']}")
return state
def handle_general(state: RouterState) -> RouterState:
state["response"] = llm.invoke(f"Answer general question: {state['query']}")
return state
def route_query(state: RouterState) -> Literal["tech", "billing", "general"]:
"""Conditional routing function."""
mapping = {"technical": "tech", "billing": "billing", "general": "general"}
return mapping.get(state["category"], "general")
graph = StateGraph(RouterState)
graph.add_node("classify", classify)
graph.add_node("tech", handle_technical)
graph.add_node("billing", handle_billing)
graph.add_node("general", handle_general)
graph.set_entry_point("classify")
graph.add_conditional_edges("classify", route_query, {
"tech": "tech", "billing": "billing", "general": "general"
})
graph.add_edge("tech", END)
graph.add_edge("billing", END)
graph.add_edge("general", END)
router = graph.compile()
Pattern 3: Parallelization
Execute independent subtasks concurrently and aggregate results.
flowchart LR
A[Input] --> B[Task A]
A --> C[Task B]
A --> D[Task C]
B --> E[Aggregate]
C --> E
D --> E
E --> F[Output]
LangGraph with Fan-Out / Fan-In
from langgraph.graph import StateGraph, END
from typing import TypedDict, List
import asyncio
class ParallelState(TypedDict):
query: str
results: List[str]
final: str
async def search_web(state: ParallelState) -> ParallelState:
result = llm.invoke(f"Search the web for: {state['query']}")
state["results"].append(f"Web: {result}")
return state
async def search_docs(state: ParallelState) -> ParallelState:
result = llm.invoke(f"Search documentation for: {state['query']}")
state["results"].append(f"Docs: {result}")
return state
async def search_code(state: ParallelState) -> ParallelState:
result = llm.invoke(f"Find code examples for: {state['query']}")
state["results"].append(f"Code: {result}")
return state
def synthesize(state: ParallelState) -> ParallelState:
state["final"] = llm.invoke(
f"Synthesize these sources into a coherent answer:\n" +
"\n".join(state["results"])
)
return state
# LangGraph supports async nodes for fan-out parallelism
graph = StateGraph(ParallelState)
graph.add_node("web", search_web)
graph.add_node("docs", search_docs)
graph.add_node("code", search_code)
graph.add_node("synthesize", synthesize)
graph.set_entry_point("web")
# All three search nodes run in parallel
graph.add_edge("web", "synthesize")
graph.add_edge("docs", "synthesize")
graph.add_edge("code", "synthesize")
graph.add_edge("synthesize", END)
Pattern 4: Evaluator-Optimizer
Iteratively generate, evaluate, and refine until quality criteria are met.
flowchart LR
A[Generate] --> B{Evaluate}
B -->|Pass| C[Output]
B -->|Fail| D[Optimize]
D --> A
LangGraph with Human-in-the-Loop
from langgraph.graph import StateGraph, END
from typing import TypedDict
class EvalState(TypedDict):
prompt: str
draft: str
score: int
iterations: int
MAX_ITERATIONS = 3
QUALITY_THRESHOLD = 8
def generate(state: EvalState) -> EvalState:
state["draft"] = llm.invoke(f"Write content based on: {state['prompt']}")
state["iterations"] += 1
return state
def evaluate(state: EvalState) -> EvalState:
prompt = f"Score this output 1-10 on quality, clarity, and accuracy:\n{state['draft']}"
result = llm.invoke(prompt)
# Extract numerical score from LLM response
state["score"] = extract_score(result)
return state
def optimize(state: EvalState) -> EvalState:
state["draft"] = llm.invoke(
f"Improve this draft based on quality score {state['score']}/10:\n{state['draft']}"
)
return state
def should_continue(state: EvalState) -> str:
if state["score"] >= QUALITY_THRESHOLD:
return "accept"
if state["iterations"] >= MAX_ITERATIONS:
return "accept" # Max iterations reached
return "optimize"
graph = StateGraph(EvalState)
graph.add_node("generate", generate)
graph.add_node("evaluate", evaluate)
graph.add_node("optimize", optimize)
graph.set_entry_point("generate")
graph.add_edge("generate", "evaluate")
graph.add_conditional_edges("evaluate", should_continue, {
"accept": END,
"optimize": "optimize"
})
graph.add_edge("optimize", "generate")
evaluator = graph.compile()
Pattern 5: Orchestrator-Workers
A central coordinator dynamically plans subtasks, delegates to specialized workers, and integrates results.
CrewAI Implementation
CrewAI’s role-based model maps naturally to orchestrator-workers. Define agents with roles and delegate tasks:
from crewai import Agent, Task, Crew, Process
from crewai_tools import SerperDevTool, ScrapeWebsiteTool
# Define worker agents with specialized roles
researcher = Agent(
role="Senior Research Analyst",
goal="Find comprehensive information on the assigned topic",
backstory="Expert at synthesizing information from multiple sources",
tools=[SerperDevTool(), ScrapeWebsiteTool()],
verbose=True
)
writer = Agent(
role="Technical Writer",
goal="Create clear, well-structured content from research",
backstory="Experienced at explaining complex topics clearly",
verbose=True
)
reviewer = Agent(
role="Quality Reviewer",
goal="Ensure accuracy, clarity, and completeness of output",
backstory="Detail-oriented editor with deep domain knowledge",
verbose=True
)
# Define tasks for each agent
research_task = Task(
description="Research the topic: {topic}. Gather latest information, statistics, and expert opinions.",
expected_output="A comprehensive research brief with key findings",
agent=researcher
)
writing_task = Task(
description="Write a well-structured article based on the research brief. Use clear headings and examples.",
expected_output="A complete article draft in markdown format",
agent=writer
)
review_task = Task(
description="Review the article for accuracy, clarity, and completeness. Suggest specific improvements.",
expected_output="A review with actionable feedback and the final approved article",
agent=reviewer
)
# Assemble the crew with sequential orchestration
crew = Crew(
agents=[researcher, writer, reviewer],
tasks=[research_task, writing_task, review_task],
process=Process.sequential,
verbose=True
)
result = crew.kickoff(inputs={"topic": "Edge AI developments in 2026"})
print(result)
Framework Comparison (May 2026)
| Feature | LangGraph v1.10+ | CrewAI v1.10+ | AutoGen/AG2 |
|---|---|---|---|
| Model | Directed graph with typed state | Role-based agent teams | Multi-turn conversation |
| Stars | 26K+ | 45K+ | 30K+ |
| Best for | Complex conditional workflows | Quick role-based prototypes | Emergent multi-agent behavior |
| Human-in-loop | Built-in checkpointing | Task callbacks | Native support |
| Streaming | Yes | Yes | Yes |
| Learning curve | Steep (graph API) | Gentle (role metaphor) | Moderate |
Resources
- LangGraph Documentation — StateGraph API, checkpointing, streaming
- CrewAI Documentation — Agents, tasks, crews, Flows
- AutoGen / AG2 Documentation — Event-driven multi-agent framework
- Anthropic Agent Design Patterns — Official pattern guide
- OpenAI Agents SDK — Vendor-native agent framework
Comments