LangGraph and LangChain patterns
Graphs, state, checkpoints, interrupts, and when frameworks help — after you can hand-roll a ReAct loop. Architecture, patterns, ops, evals, and production failure modes end-to-end.
What these libraries are (plain English)
ShipAI’s rule: hand-roll first. Then learn LangChain / LangGraph so you can read industry code and ship faster without treating the framework as magic.
| Piece | What it is | What it is not |
|---|---|---|
| LangChain | Toolkit: prompts, retrievers, tools, LCEL runnables, integrations | Your product architecture |
| LangGraph | Durable state machine / graph runtime for agents and workflows | “Just another chain wrapper” |
LangChain helps you glue LLM I/O. LangGraph is the piece that maps cleanly to production orchestration: explicit state, conditional edges, checkpoints, interrupts.
Analogy: LangChain is a parts bin (prompts, retrievers, tool wrappers). LangGraph is the control plane — a programmable state machine that decides which parts run, in what order, and how to resume after a crash or a human “approve/deny.”
One-sentence definition you can defend in an interview
LangGraph is a durable graph runtime over typed state, with conditional edges, checkpoints, and interrupts, used to run LLM agents and workflows you can resume, audit, and evaluate as trajectories.
If you cannot say state, checkpointer, stop condition, and idempotent tools, you are describing a demo notebook — not a product agent runtime.
Interview cue: Frameworks encode patterns you should already understand (ReAct, routers, map-reduce). If you cannot draw the graph on a whiteboard without LangGraph APIs, you are not ready to debug production trajectories.
The problem they solve for LLM apps
A single chat completion is one RPC. Real agents need:
- Multi-step control flow — model → tools → model → stop
- Durable state — resume after crash or human approval
- Branching — route by intent, fan out workers, merge
- Observability hooks — where to hang traces and budgets
- Human gates — pause before irreversible side effects
Without a graph runtime you reinvent queues, retries, and “where is the thread state?” in every service. With a graph runtime and no mental model, you get unbounded loops and opaque “Agent stopped” failures.
stateDiagram-v2
[*] --> Understand
Understand --> Act: tool_call
Act --> Understand: tool_result
Understand --> Respond: final
Understand --> Stop: max_steps / deny
Respond --> [*]
Stop --> [*]
LangChain vs LangGraph vs hand-rolled (do not confuse them)
| Pattern | Control flow | When it fits |
|---|---|---|
| Hand-rolled ReAct | Your while loop |
Learning; max clarity; tiny tool sets |
| LangChain LCEL / chains | Mostly linear / DAG | Fixed pipelines: retrieve → prompt → parse |
| LangGraph | Cycles + persistence + interrupts | Agents, HITL, multi-step workflows you must resume |
| Temporal / Inngest | Durable workflows outside the LLM loop | Human tasks at org scale; long-running jobs |
Ship rule: if the path is known and stable, prefer a workflow. Reach for LangGraph when you need cycles, checkpoints, or branching you will debug for months.
What “Agent stopped” usually means
When Studio or logs say the agent stopped without a useful answer, rank these causes in order:
max_steps/ budget hit with no final message- Tool exception swallowed into an empty observation
- Conditional edge returned an unexpected label
- Interrupt waiting for human input nobody resumed
- Model emitted neither
tool_callsnor a clear final
Frameworks do not invent a brain. They make these failure modes addressable if your state and routes are explicit.
Architecture mental model
flowchart TB
subgraph graph [StateGraph]
N1[node: call_model]
N2[node: call_tools]
N3[node: human_gate]
N1 -->|conditional| N2
N1 -->|final| Out[END]
N2 --> N1
N1 -->|needs_approval| N3
N3 --> N2
end
CP[(Checkpointer)] <--> graph
App[Your API] --> graph
| Concept | Meaning |
|---|---|
| State | Typed dict / dataclass passed between nodes (often message list + counters) |
| Nodes | Functions that read state and return partial updates |
| Edges | Fixed next-node or conditional router |
| Checkpointer | Persist state per thread_id for resume / time-travel |
| Interrupt | Pause before irreversible tools (HITL) |
| Subgraph | Nested graph for a specialist skill |
| Reducer | How concurrent updates merge into a field (e.g. operator.add on messages) |
| Super-step | One coordinated wave of node execution before the next checkpoint |
What the checkpointer actually buys you
Without persistence, a process kill mid-tool means “start over” and often double side effects. With a checkpointer:
- Each super-step writes state keyed by
thread_id - You can resume after deploy, crash, or human sleep
- You can time-travel to an earlier checkpoint for debug (carefully — secrets may live there)
sequenceDiagram
participant API as Your API
participant G as StateGraph
participant CP as Checkpointer
participant T as Tools
API->>G: invoke(thread_id)
G->>CP: load / save state
G->>T: tool call
Note over G: crash here without CP = lost + maybe double-charge
G->>CP: checkpoint after node
API->>G: resume(thread_id)
G->>CP: load
Reducers: the silent correctness bug
When two parallel nodes update the same field, LangGraph needs a reducer. Classic pattern:
| Field | Reducer | Why |
|---|---|---|
messages |
operator.add / append |
Parallel workers each append notes |
steps |
replace / max | Counter must stay coherent |
citations |
union / append-unique | Merge evidence without dupes |
error |
last-write or list | Decide if one failure kills the run |
Anti-pattern: mutable in-place edits of a shared list without a reducer. Replays and fan-out then disagree about history.
How it fits an LLM product
Typical placement:
| Layer | Owns |
|---|---|
| Gateway | Auth, quotas, streaming to client |
| Agent service | LangGraph (or hand-rolled loop) |
| Tools | MCP / HTTP / DB — side-effect policy here |
| Stores | Checkpoints, trajectories, vector memory |
| Evals | Golden tasks + trajectory scorers offline |
LangGraph does not replace your gateway, auth, or evals. It replaces ad-hoc while True orchestration once the loop is proven.
flowchart TB
FE[Chat / ticket UI] --> GW[Gateway]
GW --> Auth[Auth + tenant]
Auth --> AG[Agent service / LangGraph]
AG --> LLM[Model provider]
AG --> Pol[Tool policy]
Pol --> MCP[MCP / HTTP tools]
Pol --> RAG[Retriever]
AG --> CP[(Checkpoints)]
AG --> Tr[Trajectory + OTel]
Streaming to the user vs streaming inside the graph
Two different streams:
- Token stream from the model node to the UI (UX)
- Event stream of node starts/ends, tool calls, interrupts (ops)
Ship both. Users care about tokens; on-call cares about “stuck in call_tools for 40s.” Pair token streaming with Serving and streaming and span events with OpenTelemetry for LLMs.
Minimal graph shape (code)
# Shape only — APIs evolve; pin versions in your repo
from typing import TypedDict, Annotated, Literal
import operator
class AgentState(TypedDict):
messages: Annotated[list, operator.add]
steps: int
tenant_id: str
pending_approval: dict | None
def call_model(state: AgentState) -> dict:
# LLM with tools bound → append AIMessage (maybe with tool_calls)
...
def call_tools(state: AgentState) -> dict:
# Execute tool_calls; append ToolMessages
...
def human_gate(state: AgentState) -> dict:
# interrupt() before mutating tools; resume with approval payload
...
def route(state: AgentState) -> Literal["tools", "end", "stop", "approve"]:
if state["steps"] >= 8:
return "stop"
last = state["messages"][-1]
if getattr(last, "tool_calls", None):
if needs_hitl(last):
return "approve"
return "tools"
return "end"
# builder.add_node("model", call_model)
# builder.add_node("tools", call_tools)
# builder.add_node("approve", human_gate)
# builder.add_conditional_edges("model", route, {...})
# graph = builder.compile(checkpointer=MemorySaver())
# graph.invoke(
# {"messages": [...], "steps": 0, "tenant_id": "...", "pending_approval": None},
# config={"configurable": {"thread_id": "tenant:user:session"}},
# )Ship rule: always put steps (or token budget) in state and enforce it in route — frameworks will not save you from infinite tool loops.
LCEL for linear pipelines (when LangGraph is overkill)
# Shape — retrieve → prompt → LLM → parse
# chain = retriever | prompt | llm | parser
# Prefer this when there is no cycle and no HITL.Use LCEL / simple chains for fixed RAG Q&A. Graduate to LangGraph when you add tool loops, routers, or interrupts.
Thread IDs that survive multi-tenant products
Bad thread_id |
Why it hurts |
|---|---|
| Random UUID only | Hard to audit; easy to leak across tenants if mis-keyed |
| Raw user email | PII in ops tools; collisions across envs |
| Model-invented id | Spoofable; never trust the LLM for tenancy |
Prefer tenant_id:user_id:conversation_id (or opaque server-issued ids mapped in your DB). AuthZ happens in the gateway and tool layer — state should carry tenant_id as a claim you already verified, not as something the model “remembers.”
Patterns worth stealing
- Router — classify intent → specialist subgraph (billing vs search vs code)
- Orchestrator–worker — plan, fan out parallel workers, reduce
- HITL —
interruptbefore refunds, sends, deletes - Map-reduce over docs — retrieve many, summarize per shard, merge
- Supervisor — one LLM assigns work to tool-specialist agents
- Retry with backoff node — isolated flaky-tool handling without polluting the main loop
- Critic / verifier pass — second model checks citations or policy before final
flowchart LR
U[User] --> R[Router]
R --> A[Subgraph A]
R --> B[Subgraph B]
A --> M[Merge / respond]
B --> M
Walkthrough: support refund with HITL
- User: “Refund order A-1042.”
call_model→ toolget_order→ eligible.- Graph edges to
human_gate(interrupt) beforeissue_refund. - Operator approves in your UI; API resumes same
thread_id. - Tool runs once; checkpoint records success; trajectory is eval-ready.
Without interrupt + idempotency keys, retries after timeout double-refund.
Walkthrough: research map-reduce
- Router picks “research” subgraph.
- Fan-out: N workers summarize N sources in parallel.
- Reduce node merges notes under a token budget.
- Final model writes cited brief.
Same pattern as classic MapReduce — the LLM is just the mapper/reducer function.
Walkthrough: agentic RAG inside the graph
- Model decides whether to retrieve.
- Retriever tool returns top-k with
doc_id+ spans. - Model answers with citations; if weak evidence, retrieve again (bounded by
steps). - Stop when answer + citations meet schema, or budget expires.
Do not bake “always retrieve once” into a chain if the product needs multi-hop. Do not leave retrieve unbounded either — that is how context and cost explode. Pair with RAG building blocks and LlamaIndex when the retrieval stack is the hard part.
State design that survives production
| Field | Why |
|---|---|
messages |
Working memory for the model |
steps / token_budget |
Hard stops |
tool_errors |
Consecutive failure breaker |
pending_approval |
HITL payload |
citations / node_ids |
Grounding for RAG agents |
tenant_id |
Never trust the model to carry auth |
graph_version |
Resume safety across deploys |
idempotency_keys |
Dedup mutating tools |
Anti-patterns: stuffing full PDF bytes into state; storing raw API keys in checkpoints; mutable globals outside state (unreplayable); letting the model invent tenant_id.
Context hygiene inside long runs
| Technique | When |
|---|---|
| Truncate tool dumps | Always for large JSON / HTML |
| Summarize older turns | Runs > N steps |
| Keep citation ids, drop raw text | After pack once |
| Separate “scratch” vs “user-visible” | Multi-agent / critic flows |
Context engineering is not optional once graphs loop — see Context engineering.
How it works end-to-end (request lifecycle)
sequenceDiagram
participant U as User / UI
participant GW as Gateway
participant AG as Agent service
participant G as StateGraph
participant CP as Checkpointer
participant LLM as Model API
participant T as Tools / MCP
U->>GW: POST /agent (auth, tenant)
GW->>AG: start or resume(thread_id)
AG->>CP: load checkpoint
AG->>G: invoke / stream
loop until stop / interrupt
G->>LLM: messages + tools
LLM-->>G: tool_calls or final
G->>T: execute (policy)
T-->>G: observation
G->>CP: save
end
alt interrupt
AG-->>U: needs_approval payload
else done
AG-->>U: answer + citations
end
AG->>AG: write trajectory JSONL + OTel spans
Idempotency contract for mutating tools
Every write tool should accept a client-generated idempotency key stored on success:
- Before side effect: check key → if done, return prior result
- Execute once
- Persist key + result in the same transaction as the business write when possible
Checkpoint restore + HTTP retry without this contract is how agents double-email and double-charge.
Alternatives (when not to use LangGraph)
| Need | Prefer |
|---|---|
| Learn agents | Hand-rolled ReAct — Agents and ReAct + Build real AI agents |
| Simple RAG Q&A | Direct retrieve → LLM; or LlamaIndex query engine |
| Durable workflows with human tasks at scale | Temporal / Inngest / your job queue + thin agent |
| IDE / multi-host tools | MCP as the tool bus; graph optional |
| Team already on another graph lib | Stay consistent — patterns transfer |
LangChain LCEL alone is fine for linear pipelines. Reach for LangGraph when you need cycles, persistence, or branching.
Framework vs workflow engine
| Concern | LangGraph | Temporal / Inngest |
|---|---|---|
| LLM tool loops | First-class | Possible but heavier |
| Org-scale human tasks | Interrupts help | Designed for this |
| Months-long jobs | Awkward | Native |
| Agent trajectory evals | Natural fit | You still build logging |
Many products use both: LangGraph for the agent brain, Temporal for “wait three days for a human form.”
Production gotchas
- Unbounded loops — missing
max_steps/ budget - Context blow-ups — appending full tool dumps forever; summarize or drop
- Swallowed tool errors — surface errors into state; don’t collapse to “Agent stopped”
- Checkpoints with secrets — redact API keys, tokens, PII before persist
- Non-idempotent tools — retries after checkpoint restore double-charge
- Version skew — graph code changed but old threads resume into new nodes
- Studio-only debugging — always keep a trajectory log you can replay without UI
- Over-graphing — ten nodes for a three-step chain; prefer LCEL until cycles appear
- Parallel fan-out without reducers — lost or duplicated state updates
- Trusting model-supplied tenant fields — authZ bypass waiting to happen
Always log a trajectory (messages + tool I/O + latencies) you could replay without Studio UI. Pair with OpenTelemetry for LLMs.
flowchart LR
Fail[Failure] --> Cause{Root cause?}
Cause -->|No max_steps| Fix1[Add steps + budget in route]
Cause -->|Huge tool dump| Fix2[Truncate + summarize]
Cause -->|Double side effect| Fix3[Idempotency + interrupt]
Cause -->|Resume into new graph| Fix4[Version graph + migrate threads]
How to evaluate graph agents
| Signal | What it tells you |
|---|---|
| Task success rate | Did the goal complete? |
| Steps / tokens per task | Efficiency and loop thrash |
| Tool error rate | Schema / auth / flaky deps |
| HITL approve latency | Human bottleneck |
| Resume success | Checkpoint correctness |
| Duplicate side-effect rate | Idempotency health |
| Citation precision (RAG agents) | Grounding quality |
Freeze a golden task set (goal → expected tools → final assertion). Score trajectories, not just final text — same lesson as evals fundamentals.
Offline vs online evals
| Layer | Examples |
|---|---|
| Offline golden set | 50–200 tasks; CI gate on regressions |
| Shadow traffic | New graph version on sampled prod prompts |
| Online monitors | Step explosions, tool error spikes, HITL backlog |
Never promote a graph because Studio demos looked smooth.
Debugging trajectories like production engineers
| Symptom | First place to look |
|---|---|
| “Agent stopped” with no answer | Last route decision; max_steps; swallowed tool exception |
| Duplicate emails / charges | Resume after timeout without idempotency key |
| Context length errors | Tool dumps accumulating in messages |
| Resume crashes after deploy | Graph node renames; missing migration for old threads |
| Good Studio replay, bad prod | Different checkpointer / env secrets / tool allowlists |
| Infinite polite loops | Missing stop; model never emits final; tool always “try again” |
Ship rule: store a framework-agnostic JSONL trajectory (step, node, latency_ms, tool, ok, tokens). Studio is optional; replay is not.
Debugging playbook (first hour)
- Pull trajectory for
thread_id; find last successful node. - Diff route decision vs expected.
- Re-run tools with the same args outside the graph (isolate flaky deps).
- Check checkpoint size / secrets /
graph_version. - Reproduce with pinned model + temperature 0 if nondeterminism confuses you.
- Only then change prompts — most “model bugs” are state/route bugs.
Checkpointer backends (practical)
| Backend | Fit |
|---|---|
| In-memory | Unit tests only |
| SQLite / Postgres | Single-region product threads |
| Redis | Fast ephemeral sessions (know TTL risks) |
| Custom | Encrypt + redact before write |
Whatever you pick: encryption at rest for PII, retention policy, and a plan for “delete user → delete threads.”
Retention and compliance
| Policy question | Product answer you need |
|---|---|
| How long do threads live? | TTL by plan / legal hold |
| Can support read checkpoints? | Role-based access + audit log |
| Right to deletion | Cascade threads + trajectory blobs |
| Cross-region | Pin checkpointer region with the app |
Treat checkpoints like a database of conversations, not temporary cache — because that is what they become.
Subgraphs and skills packaging
Large orgs split graphs the way they split services:
- Billing subgraph with refund interrupt
- Search subgraph with retriever tool only
- Code subgraph with sandboxed shell
The parent graph routes; child graphs own specialized state slices. Version subgraphs independently so a search change does not invalidate billing threads mid-flight.
flowchart TB
Parent[Parent router graph] --> Bill[Billing subgraph]
Parent --> Search[Search subgraph]
Parent --> Code[Code subgraph]
Bill --> HITL[Refund interrupt]
Search --> Ret[Retriever tools]
Code --> Sand[Sandbox tools]
Multi-agent preview (honest scope)
Supervisor patterns in LangGraph are still one product concern: who owns stop conditions, shared memory, and tool allowlists. See Multi-agent orchestration before spinning five agents that share a credit card tool.
Observability: what to log every run
| Field | Why |
|---|---|
thread_id, tenant_id, graph_version |
Join keys |
| Node name + latency | Find hot spots |
| Tool name, ok/fail, bytes | Thrash and bloat |
| Tokens in/out + cost estimate | Budgets |
| Interrupt reason / approver | HITL SLOs |
| Final status enum | Success / stop / deny / error |
Emit OpenTelemetry spans per node; attach prompt_version and retriever_version when RAG is involved.
Security and blast radius (non-optional)
| Risk | Mitigation |
|---|---|
| Prompt injection → tool fire | Allowlists; HITL on mutate; never discover=allow |
| Checkpoint exfil | Encrypt; redact; lock down Studio/admin |
| Cross-tenant resume | Server-issued thread ids; authZ on every resume |
| Tool over-privilege | Per-tenant scopes; least privilege servers (MCP) |
| Secret leakage into messages | Strip headers/keys before append |
Companion reads: Guardrails and safety.
Memory, cost, and sizing intuition
| Knob | Effect |
|---|---|
| Average steps × tokens/step | Dominates LLM spend |
| Checkpoint size × QPS × retention | Storage bill |
| HITL wait time | Human SLO, not GPU SLO |
| Fan-out width | Parallel LLM cost spikes |
Ballpark: if golden tasks average 6 steps at 3k tokens/step, 100k tasks/month ≈ 1.8B tokens before caching — budget before you celebrate “agents everywhere.”
How to practice
- Hand-roll a 3-tool ReAct loop (Agents and ReAct).
- Port the same state machine to a LangGraph
StateGraph. - Add a checkpointer +
thread_id; kill the process mid-run and resume. - Add an interrupt before one irreversible tool.
- Log trajectories; score against a 10-task golden set.
- Break idempotency on purpose; confirm double side effects; then fix.
Micro-project
Port a hand-rolled agent with tools search_docs, get_order, create_ticket into a LangGraph graph that:
- Enforces
max_steps=8in the router - Checkpoints after every node
- Interrupts before
create_ticket - Emits a trajectory JSONL line per step
- Records
graph_versionon the thread
Acceptance: resume after kill still issues at most one ticket for the same idempotency key.
Interview whiteboard: draw this first
When someone says “we use LangGraph,” interviewers want this sketch in under two minutes:
- State fields (messages, steps, tenant, graph_version)
- Nodes (model, tools, HITL)
- Conditional edges with an explicit stop
- Checkpointer keyed by
thread_id - Trajectory log outside the framework UI
If you skip stop conditions or idempotency, you have a demo graph — not a product runtime.
Interview prompts you should be able to answer
- When is LCEL enough vs LangGraph?
- What does a checkpointer store, and what must you redact?
- How do you prevent double refunds on resume?
- How do you version a graph so old threads do not crash?
- What metrics prove an agent got better, not just wordier?
Common interview traps
| Trap | Better answer |
|---|---|
| “LangGraph = agents” | LangGraph runs graphs; agents need loops + tools + stop + policy |
| “Checkpoints replace databases” | Checkpoints are runtime state; business data still needs a DB |
| “We’ll debug in Studio” | Production needs exportable trajectories |
| “More nodes = more enterprise” | Over-graphing hides a linear pipeline |
Anti-patterns (LangGraph edition)
- Wrapping a single LLM call in a five-node graph
- Putting PDF bytes and API keys in state
- Fan-out without reducers
- HITL that emails a human with no resume API
- One mega-graph for billing + code + search with shared write tools
- Evaluating only final prose, never tool choice
Tradeoffs summary
| Choose LangGraph when… | Avoid / defer when… |
|---|---|
| You need cycles + resume + HITL | You are still learning ReAct by hand |
| Multiple engineers share agent code | One linear RAG chain would do |
| You already measured loop needs | You are cosplaying microservices with 12 nodes |
| Trajectory evals matter | You refuse to log tool I/O |
Checklist before you ship a graph
-
max_steps/ token budget enforced in router - Mutating tools behind interrupt + idempotency
- Checkpoints redact secrets; encryption + retention set
- Trajectory export works without Studio
- Golden task suite scores tool choice + success
- Graph version recorded on every thread
-
tenant_idfrom auth, never from the model - OTel spans on nodes + tools
- Resume tested after process kill and after deploy
End-to-end lab checklist (do this once)
- Hand-roll ReAct with three tools.
- Port to StateGraph with identical tool schemas.
- Add MemorySaver; kill mid-tool; resume.
- Swap to Postgres checkpointer; repeat.
- Add interrupt + UI approve path.
- Export JSONL; score ten golden tasks.
- Rename a node; document migration strategy for open threads.
What “good” looks like in a design doc
A short design should name: state schema, stop policy, tool allowlist, checkpointer backend, HITL surfaces, eval suite, and failure modes. If it only says “we’ll use LangGraph,” it is not a design.
Glossary
| Term | Meaning |
|---|---|
| StateGraph | Typed graph of nodes + edges over shared state |
| Checkpointer | Persistence backend for thread state |
| Interrupt | Pause execution for human or external signal |
| Reducer | Merge function for concurrent state updates |
| Trajectory | Ordered record of model/tool steps for a run |
| LCEL | LangChain Expression Language — composable runnables |
| Super-step | One coordinated execution wave before checkpoint |
| HITL | Human-in-the-loop approval before side effects |
Related
Core Concept: Agents and ReAct. Guided Build real AI agents, Skills, MCP, context engineering, and Agentic workflows & multi-agent. Key tech: Model Context Protocol when tools leave process boundaries. Advanced: Multi-agent orchestration, Guardrails and safety, Context engineering. Observability: OpenTelemetry for LLMs.