A Graph-Shaped Pipeline Agent
Lesson 06 · Building Your Own Agents · ~12 min read
What makes an agent “graph-shaped”
Working definition: a graph-shaped agent’s control flow can be drawn as a directed graph with named nodes and labelled edges. The path through the graph depends on intermediate state — branches fire, loops repeat, steps get skipped — but the topology is fixed at compile time. You can hand someone a picture of the agent and they’ll know what it does. Contrast that with three other shapes:
- Tool-heavy agents (Lesson 04) — control flow is implicit in the LLM’s tool-call sequence. The same agent on two different questions takes two different paths through “the system,” but there’s no system diagram to show. The LLM is the diagram, regenerated each turn.
- Single-tool chains — A → B → C, no branches and no loops. Three lines of Python; a graph framework would be overkill.
- Multi-agent crews (Module 5) — many agents coordinating, not one agent with a shape. Different abstraction, different framework choice.
LangGraph is at its sweet spot when (a) the stages have names you’d write on a whiteboard, (b) the transitions between them depend on intermediate state, and (c) the LLM lives inside the nodes rather than orchestrating across them. This lesson’s planner → fetcher → critique → writer pipeline ticks all three boxes by construction.
Why this is LangGraph’s sweet spot
Lesson 05’s closing section listed four criteria for picking LangGraph over the Claude Agent SDK, OpenAI Agents SDK, or CrewAI. Let’s replay that list against today’s pipeline:
-
“Shape worth declaring” — yes:
plan,fetch,write,critiqueare four named stages, plus an explicit revise-loop. The whiteboard sketch is the program. -
“Durable state needed” — yes: re-running the LLM
write_draftstep after a crash is expensive. Resume from the last checkpoint and you skip the work you already paid for. -
“Human-in-the-loop possible” — yes: the critique
step is a natural pause point. With
interrupt_before=["critique_draft"]you can review the draft, edit the state, and resume. - “LLM-provider flexibility” — not exercised in this lesson (we use Claude), but the graph would happily run with GPT-4 or Gemini inside the nodes without touching the topology.
What would push you off LangGraph? If the LLM should decide which sources to consult and in what order — that’s tool-heavy and the Claude Agent SDK models it more cleanly. If separate sub-agents made sense — planner + writer + critic as three distinct roles with their own personalities — that’s a multi-agent shape and CrewAI is the better fit. Neither applies here. This lesson is the canonical LangGraph demo.
The state schema
Every LangGraph graph has a state schema: the typed object every node reads from and
writes to. The pipeline agent uses a TypedDict with six fields:
class PipelineState(TypedDict):
question: str # immutable; set once at the start
plan: list[str] # search queries the planner chose
notes: Annotated[list[str], operator.add] # snippets accumulated by fetch
draft: dict # {intro, bullets, sources}
revise: bool # critique sets this; route reads it
revisions: int # bailout counter, capped at 2
Three design choices are worth flagging:
-
notesusesoperator.add. This is a LangGraph reducer: when a node returns{"notes": [snippet]}, the reducer concatenates that onto the accumulating list rather than replacing it. If two sources are fetched across two different invocations (e.g., after a crash-resume), both are preserved. Without the reducer, the second run would clobber the first. -
draftis a plain dict, replace-on-write. Eachwrite_draftcall produces a fresh draft; the default last-write-wins reducer does exactly what we want. The previous draft is gone — on purpose — so the critique always sees the latest revision and nothing else. -
revisionsis a counter, not a list. The bailout decision only needs the count, not the history. Keep state minimal — every field is part of every checkpoint payload, so wide state means fat checkpoints and slow resumes.
Pydantic models would work too, with runtime validation. TypedDict is the
lighter choice and what the lab uses; switch to Pydantic when you need stricter
guarantees (e.g., “plan must have between 2 and 4 entries”).
The four stages — what each LLM call looks like
The pipeline has four nodes. Three of them call Claude; one is pure Python. Here’s each one in detail, with rough token and cost numbers for Sonnet 4.7:
-
plan_sources. Input:question. Output: aplanof 2–4 search queries the planner thinks will surface relevant material. One LLM call, prompted to return a JSON list. Cost: ~200 input tokens, ~50 output, roughly $0.001. -
fetch_sources. Input:plan. Output: appendednotes. No LLM call — just iterates the plan, queries the mocked PKB/web index, dedupes, and appends one snippet per hit. Cost: $0. -
write_draft. Input:question,notes. Output: adraftdict withintro,bullets,sources. One LLM call. Incrementsrevisions. Cost: ~600 input tokens, ~200 output, roughly $0.005. -
critique_draft. Input:question,draft,notes. Output:revise: bool. One LLM call, prompted to answer “yes/no — does the draft need revision?” Cost: ~400 input, ~10 output, roughly $0.002.
Total expected cost per question: ~$0.008–0.015 if the critic says “no revise,” ~$0.013–0.020 if it triggers one revise pass.
Not every node has to call an LLM
The fetch_sources stage is intentionally LLM-free. That’s one of
the wins of an explicit graph over an LLM-driven loop: you can intersperse cheap
deterministic steps freely. The planner picks queries (smart, expensive); the fetcher
runs them (cheap, free); the writer composes a draft (smart, expensive); the critic
judges it (smart, cheap). Mixing LLM and non-LLM nodes inside one graph is normal
LangGraph practice, not an escape hatch.
The critique-and-revise loop
This is the most novel structural idea in the lesson. After write_draft
produces a draft, critique_draft reads it and decides whether revision is
needed. If yes, the graph routes back to write_draft (which sees
the new revisions count and the same notes) — the LLM
is told “you wrote this; the critique says X; try again.” If no,
the graph routes to END.
Two safeguards keep this from going off the rails:
-
Bailout counter.
revisionsincrements on everywrite_draftcall. The route function checksrevisions < 2before allowing another revise — so the loop is bounded to 2 revisions max (3 writes total). After the cap, the graph proceeds toENDwith whatever draft is current, even if the critic still wants more. - Critique sees the draft. It’s an inspection step, not blind guidance. The critique prompt includes both the draft and the notes, so it can fairly judge “did this bullet actually appear in the sources?” rather than guessing whether the topic looks plausible.
Always cap revision loops
Without a bailout, an over-strict critic loops forever — each new draft is
different but never “good enough,” and the token bill grows without bound.
Same lesson as L02’s MAX_TURNS, applied at a higher level of
granularity. If you find your real bailout is firing every run, the critic prompt is
too strict; tune the prompt before raising the cap.
Checkpoints in practice
Lesson 05 introduced SqliteSaver as the default checkpointer. Here are real
numbers for this lesson’s pipeline:
-
Write frequency.
SqliteSaverwrites topipeline.dbafter every node transition. With 4 nodes (or 5–6 with a revise pass), that’s 4–6 sqlite writes per run. Each write is a few KB. -
Resume cost. When you call
.invoke(None, config={...})after a crash, LangGraph reads the most recent checkpoint and skips ahead to the next-not-yet-run node. No LLM calls are repeated. For a pipeline where each LLM call is ~$0.005, that’s a real savings on a real failure — and it’s the whole point of using a checkpointer. -
Storage. One checkpoint row per node transition. For 100 runs of this
pipeline, you’re at single-digit MB of sqlite — sqlite is fine. At
10,000+ runs/day with concurrent writers, switch to
langgraph-checkpoint-postgres(covered briefly in L05) and don’t look back. -
thread_iddiscipline. Onethread_idper logical task. Using a hash of the question text as thethread_id(with a length cap) means “ask the same question, get the cached run” comes for free — the next invocation reads the existing checkpoint and returns the finished draft without re-running any node.
What the lab gives you
By the end of the lab the reader will have run
python agent.py "What changed in Linux kernel 6.14’s networking stack?"
against the mocked tools and seen the four-stage trace including a critique-and-revise
pass. They’ll have killed the process mid-graph and resumed cleanly from the last
checkpoint. They’ll have used interrupt_before to pause at the critique
step and inspected the draft state before allowing the run to continue.
Most importantly: they’ll have one self-contained agent.py (~120
lines) committed at agents/lesson06-graph-shaped-agent/agent.py in the
course repo. This is the file Lesson 11’s cross-framework scorecard
imports. When you later see the same shape of agent rebuilt with the Claude
Agent SDK (L04, already done), OpenAI Agents SDK (L08), and CrewAI (L10), you’ll
know exactly what each framework is rewriting and what it gives up versus an explicit
graph. The shape of the agent stays constant; what changes is the framework around it.
References
- LangGraph docs —
GraphandSqliteSaverreferences atlangchain-ai.github.io/langgraph/reference/. - LangGraph Time travel guide —
langchain-ai.github.io/langgraph/how-tos/time-travel/. - LangGraph Human in the loop guide —
langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/. - L04 —
agents/lesson04-tool-heavy-agent/theory.html, the Claude Agent SDK contrast (same shape of task, very different control flow). - L05 —
agents/lesson05-langgraph-intro/theory.html, the LangGraph primer this lesson builds on.