What Frameworks Actually Buy You
You built the loop, memory, retries, and tracing by hand in Modules 1–4. That was the point: now you can evaluate a framework's version of each instead of trusting it blindly. LangGraph's pitch in one sentence: your agent loop, reified as a graph with persistent state.
Every framework is selling you the same list of things — and you have personally implemented all of them from raw SDK calls. That changes how you read the marketing. The question is never "can LangGraph do X?" but "is LangGraph's X better than the 40 lines I'd write myself, and what do I give up in debuggability to get it?" Frameworks are a trade: less plumbing code for more abstraction between you and the API calls. Sometimes that trade is excellent. Sometimes you spend a day discovering that a retry you didn't know existed was silently re-running a non-idempotent tool.
| Capability | Your hand-rolled version (Modules 1–4) | What LangGraph gives you |
|---|---|---|
| State management | A messages list plus ad-hoc dicts | A typed, shared state schema every node reads and writes |
| Checkpointing / resume | Probably nothing — crash = start over | A checkpointer persists state after every step; resume by thread ID |
| Retries | Backoff-with-jitter wrapper | Configurable retry policies per node |
| Streaming | SSE deltas from one call | Streamed events across the whole graph: node starts, state updates, tokens |
| Human-in-the-loop | input() hacks that block the process | Durable interrupts: graph pauses, process can exit, resume days later |
| Tracing | Your JSONL logger | Hooks/integrations that record every node execution and state transition |
The LangGraph mental model
A LangGraph program is a directed graph. Nodes are plain Python functions that receive the current state and return a partial update. Edges say which node runs next — fixed edges always go the same way; conditional edges call a routing function that inspects state and picks a destination. A state schema (a TypedDict or Pydantic model) is the contract every node shares. You build the graph, compile() it, then invoke() or stream() it like any callable. Under the hood it's still the loop you wrote in Module 1 — call model, act, update state, decide what's next — but the control flow is now declared as data instead of buried in if-statements.
# Colab cell 1 — run once. No API key needed: the nodes are stubbed, so
# the whole graph runs on LangGraph's machinery alone.
!pip install -q langgraph
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
question: str
draft: str
approved: bool
def write_draft(state: State) -> dict:
# call your model here (Module 1 skills); stubbed for clarity.
# in real code: model = init_chat_model("anthropic:claude-sonnet-5")
# or: model = init_chat_model("openai:gpt-5.5") # one-string swap
return {"draft": f"Draft answer to: {state['question']}"}
def review(state: State) -> dict:
return {"approved": len(state["draft"]) > 10}
builder = StateGraph(State)
builder.add_node("write_draft", write_draft)
builder.add_node("review", review)
builder.add_edge(START, "write_draft")
builder.add_edge("write_draft", "review")
builder.add_edge("review", END)
graph = builder.compile()
result = graph.invoke({"question": "What is a checkpointer?"})
print(result["draft"], result["approved"]){"draft": ...}), not the whole state — LangGraph merges them in; and START/END are sentinel nodes marking entry and exit. invoke() runs the graph to completion and returns the final state.# Colab cell 2 — run cell 1 first (it defines graph).
# stream_mode="updates" yields each node's state delta as it executes —
# this is your first inter-agent trace, for free.
for step in graph.stream({"question": "What is a checkpointer?"},
stream_mode="updates"):
print(step)
# {'write_draft': {'draft': 'Draft answer to: What is a checkpointer?'}}
# {'review': {'approved': True}}
# stream_mode="values" yields the FULL state after each step instead,
# which is handier when you want to inspect accumulated fields.stream() is how you debug graphs and how you build UIs that show progress node-by-node. Log these updates to JSONL and you have an execution trace of the whole system — the habit from Module 3 carries straight over, one level up.The abstraction tax, itemized
"Framework tax" isn't one cost — it's four, and a senior engineer names them individually instead of waving at "complexity." Debugging through layers: a bug can live in your node, in LangGraph's execution engine, or in the interaction between them, and the stack trace rarely tells you which. Prompt opacity: helper methods like structured-output wrappers or message-formatting utilities can append instructions, retries, or reformatting you never wrote — and you can't fix what you can't see the model receiving. If you can't produce the exact string of text and images sent to the API for a given call, you are debugging blind. Lock-in: your state schema, node signatures, and checkpoint format are now shaped by the framework's conventions; migrating off later means rewriting the graph, not just swapping an import. Version churn: the fan-out dispatch API and the interrupt API you'll use in this module have both changed shape across LangGraph versions — code from a six-month-old tutorial may not compile against your installed version.
| Tax | What it costs you | Mitigation |
|---|---|---|
| Debugging through layers | A bug can be yours, the framework's, or the interaction — the trace doesn't say which | Keep nodes as plain, unit-testable functions; reproduce suspicious behavior with a raw SDK call before blaming your prompt |
| Prompt opacity | Can't fix what you can't see the model receiving | Enable the framework's debug/verbose tracing or callback hooks; if that's not enough, wrap the model client yourself and pass the wrapped client in, so every call funnels through code you control |
| Lock-in | State schema, node signatures, and checkpoint format are framework-shaped; switching later means rewriting the graph | Keep model-calling and business logic inside plain functions the graph merely calls — the framework should wrap your code, not the other way around |
| Version churn | APIs you depend on (fan-out dispatch, interrupts) change shape across releases | Pin an exact version in production, read the changelog before upgrading, and keep an integration test that actually compiles and runs the graph — not just unit tests of node functions in isolation |
pip install -U langgraph, the graph raises TypeError: interrupt_before is not a valid argument to compile() — and it's caught in production, not CI. What's actually wrong, and what should have caught it earlier?Whiteboard drills
- ▸Frameworks package what you already built: state, checkpointing, retries, streaming, HITL, tracing. Evaluate, don't worship.
- ▸LangGraph = nodes (functions) + edges (control flow) + a shared typed state schema, compiled into a runnable.
- ▸Nodes return partial state updates; the framework merges them.
- ▸The checkpointer is the killer feature — durable state after every step enables resume, time-travel, and HITL.
- ▸
stream(stream_mode="updates")gives per-node traces for free — log them from day one.