Lesson 06 A Graph-Shaped Pipeline Agent
Theme:

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:

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:

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:

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:

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:

  1. Bailout counter. revisions increments on every write_draft call. The route function checks revisions < 2 before allowing another revise — so the loop is bounded to 2 revisions max (3 writes total). After the cap, the graph proceeds to END with whatever draft is current, even if the critic still wants more.
  2. 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:

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