LangGraph's production differentiator over prompt-chained agents is durable execution: graph state is checkpointed at every step, so a run can pause for hours waiting on a human decision and resume exactly where it stopped.
Tier: Advanced · Workflow area: Agentic systems
This playbook builds a RAG-agent that routes each query to retrieve / answer / escalate, and interrupts for human approval when its confidence is low — the pattern behind support bots, compliance assistants, and any agent whose wrong answer costs more than a pause.
For a linear retrieve-then-answer pipeline with no routing and no pauses, plain RAG (your pb from the retrieval module) is simpler — a graph earns its complexity only when control flow branches or pauses.
pip install langgraph langgraph-checkpoint-postgres psycopg[binary] and a Postgres instance (Docker is fine); langgraph.checkpoint.memory.InMemorySaver works for development.Define state and nodes. State is a typed dict: question, docs, draft, confidence, final. Nodes: router (classify: needs retrieval / answerable directly / out-of-scope), retrieve, generate (produce draft + a 0–1 confidence from a rubric or logprob-free self-grade), human_review, respond, escalate.
Wire the graph with conditional edges.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command
from langgraph.checkpoint.postgres import PostgresSaver
class State(TypedDict, total=False):
question: str; docs: list; draft: str; confidence: float; final: str
def human_review(state: State):
decision = interrupt({ # pauses run, persists state
"draft": state["draft"],
"confidence": state["confidence"],
"ask": "approve / edit / escalate",
})
if decision["action"] == "edit":
return {"final": decision["text"]}
if decision["action"] == "escalate":
return {"final": "ESCALATED"}
return {"final": state["draft"]}
builder = StateGraph(State)
# ... add_node for router/retrieve/generate/human_review/respond/escalate
builder.add_conditional_edges("router", route_fn,
{"retrieve": "retrieve", "answer": "generate", "escalate": "escalate"})
builder.add_conditional_edges("generate",
lambda s: "human_review" if s["confidence"] < 0.6 else "respond",
{"human_review": "human_review", "respond": "respond"})
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
checkpointer.setup() # first run only
graph = builder.compile(checkpointer=checkpointer)
Run with a thread id; detect the pause. config = {"configurable": {"thread_id": "case-123"}}; result = graph.invoke({"question": q}, config). If the result contains an __interrupt__ entry, the run is paused — surface the payload to your reviewer UI (even a CLI prompt for the lab).
Resume with the human decision. graph.invoke(Command(resume={"action": "approve"}), config) — the value passed to Command(resume=...) becomes the return value of the interrupt() call inside the node, and execution continues from the checkpoint. Kill and restart the process between pause and resume to prove durability.
Tune the confidence threshold empirically. Run 30 labeled questions; plot review-rate vs error-rate at thresholds 0.4–0.8 and pick the knee. The threshold is a product decision — document it.
interrupt() + a checkpointer turns "human in the loop" from a blocking input() call into a durable, resumable workflow — the property that separates production HITL from notebook HITL.
InMemorySaver for dev; PostgresSaver for anything shared; SQLite saver for single-machine tools.thread_ids do not leak state into each other.interrupt() requires one — without it the graph cannot pause and resume. This is the most common setup error.interrupt() call.thread_id for all users. Thread id is the conversation key; collisions merge strangers' histories.Everything self-hosts: LangGraph and the Postgres checkpointer are OSS, the retriever is your local vector store, and the LLM can be a vLLM-served open-weight model behind an OpenAI-compatible endpoint. For client engagements, keep checkpoint tables in the client's own Postgres — checkpoints contain full state including retrieved document text.
Checkpoint rows are conversation transcripts in disguise — apply retention and access policies to the checkpoint schema, not just the app database.