A single coherent app — a research/report agent — grown in 8 stages, where each
stage adds exactly one major LangGraph capability. Built with Claude via
langchain-anthropic. The goal is to feel what each feature does and when to reach
for it, with the most-used parts first.
Cost-conscious by design: defaults to the cheapest model (Haiku 4.5), caps tokens and recursion, and falls back to an offline mock search when no Tavily key is set. A full pass over all 8 stages costs pennies.
Contents
- Setup · Run a stage · Studio · Test · Layout
- Learning guide — the conceptual walkthrough:
# deps (already vendored in uv.lock)
uv sync
# keys
cp .env.example .env # then add ANTHROPIC_API_KEY (TAVILY_API_KEY optional)Requires Python 3.12 and uv. An ANTHROPIC_API_KEY is
required to actually call Claude; TAVILY_API_KEY is optional (omit it and search uses a
deterministic offline mock).
uv run python run.py <stage> "your question" [--thread T] [--model M]| Stage | Command | What it demonstrates |
|---|---|---|
| 1 | run.py stage1 "What is LangGraph?" |
StateGraph, the add_messages reducer, nodes, START/END |
| 2 | run.py stage2 "What is 25*8, then look up LangGraph?" |
tool-calling ReAct loop · ToolNode · tools_condition · cycles |
| 3 | run.py stage3 "My name is Allan." --thread t1 |
persistence · thread_id memory · time travel (SqliteSaver) |
| 4 | run.py stage4 "Explain agents." |
streaming: values / updates / messages token stream |
| 5 | run.py stage5 "Write a report on otters." |
human-in-the-loop interrupt() approval gate + resume |
| 6 | run.py stage6 "Compare solar, wind, and hydro power." |
parallel Send map-reduce over sub-questions |
| 7 | run.py stage7 "Research and write up black holes." |
multi-agent supervisor + subgraphs (researcher/writer/critic) |
| 8 | run.py stage8 "I prefer concise bullet points." --thread t1 |
long-term Store memory across threads |
For Stage 3, run it twice on the same --thread to see it recall the earlier turn; a fresh
thread forgets.
uv run langgraph devThis prints three URLs:
🚀 API: http://127.0.0.1:2024 ← your local server (your graphs run here)
🎨 Studio UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
📚 API Docs: http://127.0.0.1:2024/docs ← fully local, no login
Why does it redirect to a LangSmith site? That's expected, not a bug. LangGraph Studio is a
web app hosted on smith.langchain.com that connects back to your local server (note
baseUrl=http://127.0.0.1:2024). Your graphs still execute locally, and with no
LANGSMITH_API_KEY set, nothing is traced/uploaded — the hosted page is just the UI shell. It
will ask you to sign in with a free LangSmith account.
Three ways to use the project, easiest first:
- CLI (no server, no login) — the main interface:
uv run python run.py stageN "...". - Local API docs (no login, no redirect) — with
langgraph devrunning, open http://127.0.0.1:2024/docs for a local Swagger UI to invoke any graph. - Studio (the visual graph UI) — sign in with a free LangSmith account; execution stays local.
If your browser blocks the localhost connection (Safari/Firefox often do), use Chrome or run
uv run langgraph dev --tunnel. Keep thelanggraph devterminal running while you use it.
uv run pytestCompiles every graph and executes each end-to-end with a thread-safe fake model — 16 tests, zero API calls / network.
run.py CLI dispatcher (maps "stage2" -> the right module, passes your question)
langgraph.json registers the 8 graphs for `langgraph dev` / Studio
src/research_agent/
config.py Claude model factory + cost guardrails (one place builds the model)
tools.py web search (Tavily/mock), calculator, save_note/read_notes
state.py shared state schemas + reducers (ChatState = list of messages)
stage1_basic.py … stage8_memory.py
tests/test_graphs.py
Every stageN_*.py has the same three-part shape: build_graph() wires + compiles the graph,
graph = build_graph() is what Studio loads, and run(question) is what the CLI calls (it invokes
the graph and prints the result, after printing the graph's Mermaid diagram).
This section captures a Q&A walkthrough from a build session. It's the "why/when," not just the "how" — read it alongside the code.
This is one small app — a research assistant — built up in 8 steps ("stages"). Each stage is a separate file that adds one new LangGraph feature on top of the idea before it. They don't depend on each other at runtime; they're 8 self-contained lessons sharing the same theme and helpers. You drive everything from the terminal:
uv run python run.py stage2 "What is 25*8?"
▲ ▲
│ └─ the question you ask
└─ which lesson to run (stage1 … stage8)
LangGraph's whole idea: instead of one big function that calls Claude, you describe your app as a graph — boxes (nodes) connected by arrows (edges). A blob of data (the state) flows from box to box, and each box can change it. Three concepts and that's the core of everything:
| Term | What it is | In our code |
|---|---|---|
| State | The data passed between steps. Almost always a list of chat messages. |
ChatState in state.py |
| Node | A step. Just a Python function: takes the state, returns an update. | def chat(state): ... |
| Edge | An arrow: "after node A, go to node B". | add_edge("chat", END) |
You wire up nodes + edges, call .compile(), get a runnable graph, and run it with
graph.invoke({"messages": [...]}). Stage 1 is the smallest possible version — one node that
calls Claude: START ──▶ chat ──▶ END. Every other stage is a more interesting arrangement of the
same boxes-and-arrows.
| Stage | Lesson |
|---|---|
1 stage1_basic |
the minimal graph (1 node) |
2 stage2_react |
let Claude use the tools — the agent loop |
3 stage3_persistence |
remember past turns (saved to a SQLite file) |
4 stage4_streaming |
get output token-by-token as it's generated |
5 stage5_hitl |
pause and ask a human to approve before acting |
6 stage6_parallel |
split a question into pieces, research them in parallel |
7 stage7_multiagent |
a "manager" routing work to specialist sub-agents |
8 stage8_memory |
remember facts about the user across separate chats |
- 1–2 are the fundamentals (most LangGraph use). 3 = conversation memory. 4 = the "typing" effect. 5 = a human approval gate. 6 = parallelism. 7 = teams of agents. 8 = long-term memory of you.
Running stage2 with "What is 1234 × 5678? Then what is LangGraph?" physically does this:
START
▼
agent ← Claude reads the question, decides "I need calculator AND web_search"
▼ (it returns "tool calls" instead of a final answer)
tools ← LangGraph runs those tools, gets back 7,006,652 and the search results
▼
agent ← Claude sees the tool results, now writes the final English answer
▼
END
The arrow from tools back to agent is a loop — the entire trick behind "AI agents." The
model keeps calling tools until it can answer, then stops. tools_condition is the little function
that decides each pass: "more tools, or are we done?"
You don't trigger nodes individually. You invoke the graph, and the edges drive the traversal
automatically. LangGraph starts at START, follows the arrows node→node until END. That
traversal is the workflow. So "triggering a multi-node workflow" just means running a stage whose
graph has multiple nodes:
uv run python run.py stage5 "Write a report on sea otters." # 3 nodes: draft → approve(PAUSES) → save
uv run python run.py stage6 "Compare solar vs wind power." # 5 nodes: plan → research×N → synthesize
uv run python run.py stage7 "Explain transformer models." # 4 nodes: supervisor → researcher → writer → critic(Stages 1, 3, 4 are single-node; stage 2 is two nodes looping. The clearly multi-node ones are 5, 6, 7.) Stage 6's shape, for example:
START ─▶ plan ─┬─▶ research ─┐
├─▶ research ├─ all run IN PARALLEL (one per sub-question)
└─▶ research ─┘
▼
synthesize ─▶ END
To watch each node fire in any graph, stream with stream_mode="updates" — it emits a chunk
each time a node finishes (this is exactly what stage4 demonstrates):
for chunk in graph.stream(inputs, stream_mode="updates"):
print(chunk) # one print per node executionTip: if a planning/parsing node produces messy output (e.g. ragged sub-questions), that's usually the model being sloppy, not the graph. Fixes: use a sharper model for just that node (
--model claude-sonnet-4-6), or have the node use structured output instead of string-splitting.
Everything starts with: who decides the control flow — you, or the model?
YOU decide the path THE MODEL decides the path
◀───────────────────────────────────────────────────────────────────▶
"Workflow" hybrid "Agent"
fixed edges, predictable model routes between tool loop, open-ended
(stages 1, 6) fixed nodes (stages 5, 7) (stage 2)
- Push left (workflow) whenever you can. Predictable, cheap, testable, debuggable. If you can draw the steps on a whiteboard up front, hard-code them as edges.
- Push right (agent) only when you must — when the sequence of steps genuinely isn't known until runtime (the model must decide which tools, how many times, in what order).
- Most good production systems are hybrid: a deterministic workflow skeleton with one or two agentic nodes inside it. Pure open-ended agents are powerful but the hardest to keep reliable and on-budget.
| Capability (stage) | Reach for it when… | Don't bother when… | Cost / risk |
|---|---|---|---|
| Single node / linear graph (1) | One LLM call, or a fixed A→B→C pipeline you fully control. | — (this is your default; start here) | Lowest |
| Tool-calling agent loop (2) | The model needs to act (search, compute, call APIs) and the steps aren't known up front. | The flow is fixed — wire the steps as nodes instead. | Loops = unbounded tokens; needs a recursion cap |
| Persistence / checkpointer (3) | Multi-turn memory, resumability after a crash, or any HITL. | Stateless one-shot calls. | Low; pick backend (memory→sqlite→postgres) by durability need |
| Streaming (4) | A human is waiting on output, or you want progress/intermediate visibility. | Batch/offline jobs nobody watches. | Almost free; pure UX |
| Human-in-the-loop (5) | An action is irreversible/risky/regulated (spend money, send email, write to prod), or needs human judgment mid-flow. | Fully autonomous, low-stakes tasks. | Adds latency + a UI; requires persistence |
| Parallel fan-out (Send) (6) | N independent subtasks → run concurrently to cut latency. | Subtasks depend on each other (must be sequential). | Saves time, multiplies token cost (N calls at once) |
| Multi-agent + subgraphs (7) | One agent's prompt/toolset is overloaded; you want separation of concerns, independent testing, reuse. | A single well-prompted agent already does it (don't add agents for vanity). | Highest complexity; routing overhead; easy to over-engineer |
| Long-term Store memory (8) | Knowledge must survive across sessions/threads/users (preferences, learned facts). | Memory only needs to last one conversation (that's the checkpointer). | Needs a store backend + retrieval/relevance thinking |
Two of these aren't "pick one" choices — they're layers you add to any graph:
- Persistence (3) and Streaming (4) are cross-cutting concerns. You don't choose them instead of an agent or workflow — you bolt them onto whatever you built. (Stages 5 and 8 can't work without 3.)
- The rest (1, 2, 6, 7) are structural choices about the shape of your graph.
So the real decision is two questions:
- Shape: linear? agent loop? parallel fan-out? multi-agent? (1/2/6/7)
- Layers: does it need memory? durability? a human gate? streaming? (3/4/5/8)
- Start as a workflow. Earn your way to agents. Add model-driven routing only where determinism genuinely fails.
- Latency problem → parallelize (6). Reliability/prompt-bloat problem → split into agents (7). People reach for multi-agent when the real issue is one overstuffed prompt — fan-out or better tools often solve it cheaper.
- "Could this action embarrass us or cost money if wrong?" → HITL gate (5).
- "Does it need to remember after this chat ends?" No → checkpointer (3). Across chats → Store (8).
- Cost scales with loops (2) and fan-out width (6). Cap both. Use a cheap model for fan-out
workers and a strong one only for the final synthesis — exactly what stage 6 does
(
get_worker_modelvsget_model).
A production "deep research assistant" is all of them at once — which is why this project is one evolving app rather than 8 toys:
[Store: recall user prefs (8)]
▼
[supervisor agent (7)] ──▶ decides scope
▼
[plan ▶ parallel research workers (6)] ──▶ each is a tool-using agent (2)
▼
[draft report] ──▶ [HUMAN approval gate (5)] ──▶ [publish]
▼
(all of it persisted (3), streamed to the UI (4))
The 8 stages are the 8 Lego bricks; architecture is choosing which bricks and in what arrangement for the problem in front of you.
Production deployment (LangGraph Platform), a Postgres checkpointer/store, and LangSmith tracing all build directly on the local patterns shown here.