Skip to content

Repository files navigation

LangGraph Research & Report Agent — a hands-on tour

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

# 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).

Run a stage

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.

Visualize in LangGraph Studio (and the LangSmith redirect)

uv run langgraph dev

This 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:

  1. CLI (no server, no login) — the main interface: uv run python run.py stageN "...".
  2. Local API docs (no login, no redirect) — with langgraph dev running, open http://127.0.0.1:2024/docs for a local Swagger UI to invoke any graph.
  3. 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 the langgraph dev terminal running while you use it.

Test (no API key or network needed)

uv run pytest

Compiles every graph and executes each end-to-end with a thread-safe fake model — 16 tests, zero API calls / network.

Project layout

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).


Learning guide

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.

1. What exists and how it works

The 30-second picture

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)

The one mental model: a graph

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.

What each stage teaches

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.

How an actual run flows (the tool loop)

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?"

2. How to trigger (and watch) a multi-node workflow

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 execution

Tip: 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.

3. Architect's guide: when to choose each capability

The master decision (make this one first)

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.

When to reach for each

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

Core vs. orthogonal

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:

  1. Shape: linear? agent loop? parallel fan-out? multi-agent? (1/2/6/7)
  2. Layers: does it need memory? durability? a human gate? streaming? (3/4/5/8)

Heuristics

  • 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_model vs get_model).

How they compose (the real-world shape)

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.


Next steps (deliberately out of scope here)

Production deployment (LangGraph Platform), a Postgres checkpointer/store, and LangSmith tracing all build directly on the local patterns shown here.

About

Walk through the capabilities of LangGraph; Experience the when/where/why/how; Educate and drive your architectural decisions for agent orchestration

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages