LangGraph at its sweet spot: planner → fetcher → critique branch → writer, with checkpoint resume.
Agents Module 3 LangGraph Pipeline
write_draft only.Hits every condition on L05 Slide 11's “Pick it when” list.
Given a research question, produce a structured short report — but unlike L04, do it as an explicit pipeline: plan the sources, fetch them, critique the draft, revise if needed, then write the final.
Real example question: “What changed in Linux kernel 6.14's networking stack?”
Same question as L04 — so the reader can compare the two agents' shapes, not their outputs.
| Stage | What it does | Node function |
|---|---|---|
| plan | LLM picks 2–4 search queries given the question. | plan_sources |
| fetch | Run each search query against the mocked PKB / web index, collect snippets. | fetch_sources |
| critique | LLM looks at the draft + sources, decides if revision is needed. | critique_draft |
| write | LLM produces a 3-bullet structured draft from current notes. | write_draft |
Four nodes, each a plain Python function. No subclassing, no decorators — just functions of state.
START
-> plan_sources
-> fetch_sources
-> write_draft
-> critique_draft
|-- revise == True -> write_draft (loop, max 2 revisions)
|-- revise == False -> END
Note the critique-then-loop-back-to-write edge. This is the shape you cannot draw in a chain.
from typing import TypedDict, Annotated
import operator
class PipelineState(TypedDict):
question: str
plan: list[str] # search queries
notes: Annotated[list[str], operator.add] # accumulating snippets
draft: dict # {intro, bullets, sources}
revise: bool # critique sets this
revisions: int # bailout counter
notes uses the operator.add reducer so each fetch appends; revisions is the loop bailout.
def route_after_critique(s: PipelineState) -> str:
if s["revise"] and s["revisions"] < 2:
return "revise"
return "finish"
g.add_conditional_edges(
"critique_draft",
route_after_critique,
{"revise": "write_draft", "finish": END},
)
The revisions < 2 bailout matters: even if the critique keeps demanding revisions, the loop stops after two passes.
def write_draft(s: PipelineState) -> dict:
notes_blob = "\n".join(s["notes"])
prompt = (
f"Question: {s['question']}\n\nNotes:\n{notes_blob}\n\n"
"Write a JSON object with keys 'intro' (string), "
"'bullets' (list of 3 short strings), 'sources' (list of strings)."
)
msg = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=600,
messages=[{"role": "user", "content": prompt}],
)
draft = json.loads(msg.content[0].text)
return {"draft": draft, "revisions": s["revisions"] + 1}
Node calls anthropic directly, parses JSON, returns a partial state update. No SDK abstraction. Increments the revision counter on every write.
memory = SqliteSaver.from_conn_string("pipeline.db")
app = g.compile(checkpointer=memory)
config = {"configurable": {"thread_id": f"pipeline-{question[:30]}"}}
result = app.invoke(
{"question": question, "plan": [], "notes": [], "draft": {},
"revise": False, "revisions": 0},
config=config,
)
thread_id is per-question: reruns of the same question resume; reruns of new questions start fresh.
[node plan_sources] plan=['kernel 6.14 networking', 'BIG TCP IPv4']
[node fetch_sources] notes_count=4
[node write_draft] draft.bullets=3 revisions=1
[node critique_draft] revise=True
[node write_draft] draft.bullets=3 revisions=2
[node critique_draft] revise=False
[END] final report ready
Two write passes; the second clears critique. The bailout counter would have stopped a third even if critique stayed unhappy.
fetch_sources succeeds but before write_draft finishes.app.invoke(None, config=config) with the same thread_id.write_draft with notes already populated — no re-fetch, no re-plan. Just the work you hadn't done yet.interrupt_beforeapp = g.compile(
checkpointer=memory,
interrupt_before=["critique_draft"],
)
state.values["draft"] and decides what to do next.app.invoke(None, config=config).app.update_state(config, {"revise": False}).Plan F wires this exact agent.py into agents/_eval/run_all.py as the LangGraph row of the cross-framework scorecard. The L11 lab compares this agent's cost / latency / lines-of-code against the L04 Claude SDK agent and the L08 / L10 agents from Modules 4–5.
Today's agent.py is the canonical LangGraph entry. Don't rename run_pipeline_agent — L11 imports it by name.
critique → revise loop needs a bailout counter — otherwise “always revise” is a free pass to infinite spend.thread_id, app.invoke(None, config), done.