Create a stateful tool-using agent with nodes, edges, and checkpoints.
A LangGraph build begins with typed state, not a prompt loop.
Learners anchor the graph in explicit state instead of hidden conversation variables.
Why this matters: This prevents graph state from becoming an untestable junk drawer.
Problem anchor: you need a refund-policy agent that retrieves policy, checks account status, drafts an answer, and pauses for review before offering an exception. A plain loop hides all of that in messages. LangGraph asks you to name the state: goal, messages, retrieved_sources, account_status, draft, needs_review, approved, and status.
This is the first design decision. If source IDs are not in state, citation validation cannot run. If approval is not in state, resume logic cannot know whether the human accepted the risky action.
State fields: goal: string; messages: list; retrieved_sources: list of source IDs; account_status: enum; draft_answer: string; citation_errors: list; approval_request: object or null; approved: boolean or null; status: drafting, review, done, cancelled.
LangGraph nodes are inspectable transformations of shared state.
Learners design graph nodes that can be tested in isolation.
Why this matters: This reduces opaque agent behavior and makes failures local.
retrieve_policy reads goal and messages, then returns retrieved_sources and context_summary. draft_answer reads retrieved_sources and account_status, then returns draft_answer. validate_citations reads draft_answer and retrieved_sources, then returns citation_errors. request_review reads risk and errors, then returns an approval_request or lets the graph continue.
This shape gives you two framings: product teams can review the workflow as a process map, while engineers can unit-test each transition.
If the agent gives a refund with no citation, a single-loop design says the agent failed. The graph design asks which node failed. retrieve_policy may have returned no source; draft_answer may have ignored source IDs; validate_citations may have allowed an empty citation; request_review may have skipped risky cases. Different node, different fix.
LangGraph edges make route decisions explicit and testable.
Learners make the agent topology visible.
Why this matters: This catches wrong routes that silently bypass validation or approval.
After draft_answer, the graph should not always end. A conditional edge can route to validate_citations. If errors exist, route back to draft_answer with repair instructions. If the answer proposes an exception or external action, route to human_review. If approved, route to final_response. If rejected, route to cancelled.
Prompt text can ask the model to be careful, but the edge function enforces the topology. Test edge functions like ordinary code.
| Option | Condition | Next node | Failure prevented | When to choose | Cost | Complexity |
|---|---|---|---|---|---|---|
| No retrieved source | retrieve_policy retry or safe refusal | unsupported answer | No retrieved source | Use when the no retrieved source row is the relevant design concern. | Medium | Medium |
| Citation errors | draft_answer with repair note | uncited policy claim | Citation errors | Use when the citation errors row is the relevant design concern. | Medium | Medium |
| Exception requested | human_review | unauthorized commitment | Exception requested | Use when the exception requested row is the relevant design concern. | Medium | Medium |
| Approved and validated | final_response | endless loop or duplicate tool call | Approved and validated | Use when the approved and validated row is the relevant design concern. | Medium | Medium |
LangGraph checkpointers and interrupts let agents pause and continue with state.
Learners see the concrete LangGraph mechanics for resumable agent work.
Why this matters: This avoids agents that lose context after approval or repeat side effects on resume.
from typing import TypedDict, Literal from langgraph.graph import StateGraph, START, END from langgraph.types import interrupt class RefundState(TypedDict, total=False): goal: str retrieved_sources: list[str] draft_answer: str citation_errors: list[str] approved: bool status: str def retrieve_policy(state: RefundState): return {"retrieved_sources": ["refund-policy#45"]} def draft_answer(state: RefundState): return {"draft_answer": "Policy says no automatic refund after 30 days. [refund-policy#45]"} def validate_citations(state: RefundState): has_source = bool(state.get("retrieved_sources")) and "[refund-policy#45]" in state.get("draft_answer", "") return {"citation_errors": [] if has_source else ["missing refund-policy citation"]} def human_review(state: RefundState): approved = interrupt({"reason": "exception or sensitive refund answer", "draft": state["draft_answer"]}) return {"approved": bool(approved)} def route_after_validation(state: RefundState) -> Literal["draft_answer", "human_review"]: return "draft_answer" if state.get("citation_errors") else "human_review" def route_after_review(state: RefundState) -> Literal["final", "cancelled"]: return "final" if state.get("approved") else "cancelled" def final(state: RefundState): return {"status": "done"} def cancelled(state: RefundState): return {"status": "cancelled"} graph = StateGraph(RefundState) graph.add_node("retrieve_policy", retrieve_policy) graph.add_node("draft_answer", draft_answer) graph.add_node("validate_citations", validate_citations) graph.add_node("human_review", human_review) graph.add_node("final", final) graph.add_node("cancelled", cancelled) graph.add_edge(START, "retrieve_policy") graph.add_edge("retrieve_policy", "draft_answer") graph.add_edge("draft_answer", "validate_citations") graph.add_conditional_edges("validate_citations", route_after_validation) graph.add_conditional_edges("human_review", route_after_review) graph.add_edge("final", END) graph.add_edge("cancelled", END) app = graph.compile() # add a checkpointer for resumable production runs
The route_after_validation function returns draft_answer. The graph repairs the draft before reaching human_review, so review is not asked to approve an uncited answer.
Checklist: state type contains every field used by nodes; each node returns partial state, not a mutated hidden global; conditional edge names match real node names; END is reachable; interrupt happens before side effects that cannot be repeated; production compile uses a checkpointer; resume uses a stable thread_id.
The build finishes by testing nodes, route functions, resume behavior, and observability.
Learners turn a working graph into a reviewable product component.
Why this matters: This prevents graph edits from silently skipping validation or review.
Test nodes with fixed state fixtures. Test route functions with edge cases: no sources, citation errors, sensitive exception, approved review, rejected review. Test resume behavior by pausing at human_review and continuing with approval=false. Add trace metadata for graph_name, node_name, thread_id, route_decision, checkpoint_id, and release SHA.
The most important failure modes are graph-state bloat, conditional edges that skip safeguards, non-idempotent side effects before interrupts, and checkpoints that store private data without retention rules.
Retrieval prompt: rebuild the LangGraph agent by naming the state schema, node responsibilities, conditional edges, checkpoint thread ID, interrupt point, trace fields, and tests that prove the graph cannot skip safeguards.
Build a LangGraph refund assistant with state fields for goal, messages, retrieved sources, draft answer, approval, and status. Add nodes for retrieval, drafting, citation validation, and human review; use a checkpointer; write tests for route decisions and resume behavior. Next rung: add long-term memory or multi-agent handoff.
Your support agent's state schema includes a messages list (the full conversation) and a draft_response string. A teammate suggests also storing the LLM's chain-of-thought scratchpad in state so it's always available. What's the strongest reason to keep the scratchpad OUT of durable state?
Durable state should hold only what other nodes or a resumed run genuinely need; LLM scratchpad is ephemeral chatter that inflates checkpoint size and pollutes the contract between nodes.
A node in your graph is responsible for retrieving relevant knowledge-base articles AND deciding whether the draft needs a human review. Why is combining those two responsibilities in one node a problem, and how would you fix it?
Nodes that both decide and perform too much violate the single-responsibility principle, making the graph harder to test and debug — retrieval and routing logic should live in separate nodes or edges.
In your support graph, citation validation fails for a drafted response. Which edge pattern correctly routes the agent back for a redraft instead of terminating?
A conditional edge reads the validation result from state and routes back to the drafting node on failure or forward to END on success — this is exactly how retry loops are encoded in LangGraph without skipping safeguards.
You add a interrupt_before=["human_review"] checkpoint to your graph so a human can approve responses before they're sent. A run is paused at that interrupt. What happens when the run is resumed?
Checkpoints persist the full state at the interrupt point; resuming reloads that state and picks up execution at the interrupted node, making runs resumable without replaying completed work.
Which combination of practices best hardens a LangGraph agent against silent failures in production?
Hardening requires both targeted tests (route functions and safeguard nodes in isolation) and trace fields in state that make the execution path visible — neither alone is sufficient.