Autonomous Multi-Agent Architecture: LangGraph, CrewAI & Tool Loops
What You Will Master in This Tutorial
- Understand StateGraph architecture: Nodes, Edges, and Conditional Routing.
- Implement self-correcting code generation with automated test feedback.
- Configure Human-In-The-Loop approval gates for critical actions.
1. Building Stateful Agent Graphs
Single prompt calls struggle with complex software workflows. Multi-agent graphs divide tasks into specialized roles (Planner, Coder, Reviewer, Tester) that communicate over a shared state.
from typing import TypedDict, List
class AgentState(TypedDict):
task: str
code: str
review_notes: List[str]
approved: bool
def coder_node(state: AgentState):
# Generates or modifies implementation code
return {"code": "def solve(): pass", "review_notes": []}
def reviewer_node(state: AgentState):
# Inspects code against quality checklist
is_valid = "def solve" in state["code"]
return {"approved": is_valid, "review_notes": ["Looks good"] if is_valid else ["Missing function"]}
Knowledge Check: Test Your Understanding
Frequently Asked Questions
2. Step by step: wire the StateGraph with conditional routing
Section 1 defined the state and the two nodes. A graph becomes an agent when you wire them with edges — including the edge that loops back, which is the entire reason to choose LangGraph over a chain. Here is the complete assembly, reusing the exact coder_node and reviewer_node from section 1:
from typing import TypedDict, List
from langgraph.graph import StateGraph, START, END
class AgentState(TypedDict):
task: str
code: str
review_notes: List[str]
approved: bool
def coder_node(state: AgentState):
# Generates or modifies implementation code
return {"code": "def solve(): pass", "review_notes": []}
def reviewer_node(state: AgentState):
# Inspects code against the quality checklist
is_valid = "def solve" in state["code"]
return {
"approved": is_valid,
"review_notes": ["Looks good"] if is_valid else ["Missing function"],
}
def route_after_review(state: AgentState) -> str:
# The conditional edge: approve -> finish, reject -> loop back to coder
return END if state["approved"] else "coder"
graph = StateGraph(AgentState)
graph.add_node("coder", coder_node)
graph.add_node("reviewer", reviewer_node)
graph.add_edge(START, "coder")
graph.add_edge("coder", "reviewer")
graph.add_conditional_edges(
"reviewer",
route_after_review,
{"coder": "coder", END: END},
)
app = graph.compile()
result = app.invoke({
"task": "Write solve()",
"code": "",
"review_notes": [],
"approved": False,
})
print(result["approved"], result["review_notes"])
Read the wiring, not the nodes: nodes return partial state updates, edges define the possible paths, and the router function is the only place control flow lives. Swap the deterministic reviewer for an LLM call and the topology does not change — which is why the human-in-the-loop gate from this tutorial’s objectives drops in cleanly as one more node between reviewer and END, pausing the graph before anything irreversible runs.
Add the human gate the objectives promised
A reviewer node that is itself an LLM still lets the graph run unattended end to end. For anything irreversible, insert a human node that pauses the graph with LangGraph’s interrupt: execution stops, the state is checkpointed, and a person resumes it with a decision. It needs a checkpointer so the paused state survives:
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import Command, interrupt
def human_gate(state: AgentState):
decision = interrupt({
"question": "Approve this code for the next step?",
"code": state["code"],
})
return {"approved": decision == "approve"}
# graph.add_node("human", human_gate) and route reviewer -> human -> END/coder
app = graph.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "review-1"}}
app.invoke(
{"task": "Write solve()", "code": "", "review_notes": [],
"approved": False},
config=config,
) # runs until the gate, then pauses with state saved
# A human reviews the code and resumes the same thread:
app.invoke(Command(resume="approve"), config=config)
The thread_id is the resumption key: the same graph, the same state, continued hours later by a different process if necessary. That pause-and-resume property is the concrete difference between a demo agent and one you may leave running near production systems.
3. A complete worked example
The same pipeline in CrewAI trades the graph for roles. Where LangGraph made you declare state and edges, CrewAI asks for agents with goals and tasks in sequence — the coder/reviewer pair from section 1, expressed as a crew:
from crewai import Agent, Crew, Process, Task
coder = Agent(
role="Python Developer",
goal="Write correct, minimal Python for the assigned task",
backstory="Senior Python engineer who favors small, testable functions.",
verbose=True,
)
reviewer = Agent(
role="Code Reviewer",
goal="Reject any code that fails the quality checklist",
backstory="Staff engineer known for strict, specific review notes.",
verbose=True,
)
write_task = Task(
description="Implement the function described here: {task}",
expected_output="A single Python code block with the implementation.",
agent=coder,
)
review_task = Task(
description=(
"Review the implementation produced by the previous task. "
"Check: correct signature, edge cases handled, no placeholder code."
),
expected_output="Either APPROVED, or a numbered list of required fixes.",
agent=reviewer,
)
crew = Crew(
agents=[coder, reviewer],
tasks=[write_task, review_task],
process=Process.sequential,
verbose=True,
)
result = crew.kickoff(
inputs={"task": "Write solve() returning the sum of even numbers in a list"}
)
print(result.raw)
Run both versions on the same task and the trade-off from the FAQ becomes tangible. The LangGraph version gives you the loop for free — a rejection routes back to the coder until approval or the recursion limit — plus a state object you can inspect, persist, and resume. The CrewAI version is a third of the code and reads like an org chart, but the retry discipline lives in the reviewer’s instructions rather than in enforceable control flow. Prototype in CrewAI; harden in LangGraph.
4. Troubleshooting
- The graph never terminates. The reviewer never approves, so the conditional edge loops forever. Two guards, both cheap: a max-attempts counter carried in state that the router checks, and a hard ceiling on the invoke call — app.invoke(state, config={"recursion_limit": 10}). A loop without a counter is a bug waiting for a token bill.
- State fields come back empty or reset between nodes. Nodes must return partial updates — only the keys they changed — and list fields that should accumulate need a reducer, declared as Annotated[List[str], operator.add] in the TypedDict. Returning a whole fresh state dict from a node silently discards every other node’s work.
- The CrewAI reviewer approves everything. Role-playing agents are agreeable by default. Sharpen the goal ("reject any code that fails the checklist"), spell the checklist out in the task description, and set expected_output to force a verdict format — APPROVED or a numbered fix list — so approval is a decision, not a compliment.
- ImportError or version chaos after installing both frameworks. LangGraph and CrewAI pull overlapping LangChain dependencies. Install them into one fresh virtual environment together, let the resolver pick compatible versions in a single pass, and pin the result (pip freeze) before the project grows around a working set.
Going deeper: multi-agent systems in practice
The StateGraph in this tutorial — Planner, Coder, Reviewer, and Tester nodes communicating over a shared TypedDict state — generalizes into a design method. Define the state schema first: the task, the generated code, the review notes, and the approval flag are the whole contract between agents. Keep each node single-purpose, route conditionally on state fields such as approved, and let the graph cycle — generate, test, inspect the errors, retry — because that loop is precisely what a linear chain cannot do. Then put the human-in-the-loop approval gate in front of anything irreversible, and the same architecture that feels like a demo becomes safe to leave running.
Common mistakes
- Building one mega-agent instead of roles. A single prompt that plans, codes, and reviews its own work has no independent check on itself. The role split is what makes the feedback loop self-correcting rather than self-confirming.
- Letting retry loops run unbounded. A cyclical graph with no exit condition will happily regenerate failing code forever, burning tokens on every pass. Cap iterations, and route to a human or a failure state when the cap is hit.
- Skipping approval gates on critical actions. This tutorial configures human-in-the-loop approval for a reason: deploys, deletions, and anything that spends money or touches production should pause for a person, exactly like the approved flag in the state schema.
- Stuffing the entire conversation into shared state. State should carry the fields nodes actually need — task, code, review notes, approval. Dragging full transcripts through every node inflates every call and buries the signal the routing logic depends on.
Frequently asked questions
Should my first project use LangGraph or CrewAI?
As the tutorial's FAQ puts it, LangGraph is low-level and gives precise control over state machines and conditional branches, while CrewAI offers a high-level role-playing abstraction — agents with roles, goals, and backstories — for quick orchestration. Start with CrewAI for speed; move to LangGraph when the control flow becomes the interesting part.
Why is a graph better than a simple linear chain for coding agents?
Because of the loops. As the knowledge check emphasizes, graph frameworks support cyclical feedback: an agent can test its output, fail, inspect the errors, and retry. A chain moves in one direction and has no way back to the step that produced the error.
How do the agents in the example actually share information?
Through the shared typed state. Each node receives the AgentState — task, code, review_notes, approved — returns updates to it, and conditional edges read those fields to decide which node runs next.