LangGraph Agents · Lesson 9 of 17
Interview: Design an Agent Flow for a Use Case
Q1: What is LangGraph and how does it differ from a simple LangChain chain?
A: LangGraph is a framework for building stateful, cyclic agent workflows as graphs. A LangChain chain is a linear pipeline: A → B → C → output. LangGraph allows cycles (A → B → A → B → ...), branching (A → B or C based on state), and persistent state across multiple steps.
Key concepts:
- StateGraph: The core class. You define a TypedDict as state and nodes as functions that update state
- Nodes: Python functions that receive state and return updated state
- Edges: Connections between nodes — fixed or conditional (routing based on state)
- State: Persists across nodes and is the primary communication mechanism
LangChain chains are appropriate for simple, predictable pipelines. LangGraph is designed for agents that need to reason, iterate, and make decisions based on intermediate results.
Q2: What is the difference between an edge and a conditional edge?
A:
Edge: Always routes to the same next node.
graph.add_edge("research", "write") # Always goes to write after researchConditional edge: Routes to different nodes based on the current state, via a routing function.
def route(state) -> str:
if state["confidence"] > 0.8:
return "publish"
return "review" # Route to human review if unsure
graph.add_conditional_edges(
"evaluate",
route,
{"publish": "publish_node", "review": "review_node"},
)The routing function receives the state and returns a string key. The map argument ({"publish": "publish_node", ...}) translates that key to an actual node name. This indirection allows the routing function to return logical names decoupled from implementation node names.
Q3: How does state accumulation work in LangGraph?
A: By default, each node's return value replaces the corresponding state keys. To accumulate values (e.g., build a list), use Annotated with operator.add:
from typing import TypedDict, Annotated
import operator
class State(TypedDict):
messages: Annotated[list[str], operator.add] # Appends on each update
summary: str # Replaced on each update
def node_a(state: State) -> State:
return {"messages": ["message from A"]} # Appended to existing list
def node_b(state: State) -> State:
return {"messages": ["message from B"]} # Also appendedAfter node_a and node_b both run, state["messages"] is ["message from A", "message from B"].
Without Annotated[list, operator.add], node_b's return would replace node_a's output.
Q4: What is checkpointing in LangGraph and why does it matter?
A: Checkpointing saves the full graph state after every node execution. This enables:
- Resume after crash: If your agent process dies mid-run, you can resume from the last checkpoint
- Human-in-the-loop: Pause execution, show state to a human for review, then resume
- Time travel: Step backward through execution history to debug
- Branching: Explore different continuations from the same checkpoint
from langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver() # In-memory (dev/testing)
# or: SqliteSaver (persistent local storage)
# or: PostgresSaver (production)
app = graph.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "session_abc"}}
result = app.invoke(state, config=config)
# Resume with same thread_id continues from where it left off
result2 = app.invoke(new_state, config=config)The thread_id is the identifier for a conversation/session. Each thread has its own state history.
Q5: How do you implement human-in-the-loop approval in LangGraph?
A: Use interrupt_before or interrupt_after at compile time to pause execution at specific nodes:
app = graph.compile(
checkpointer=checkpointer,
interrupt_before=["approve_action"], # Pause BEFORE this node
)
config = {"configurable": {"thread_id": "session_1"}}
# Run until interrupt
app.invoke(initial_state, config=config)
# Execution pauses before "approve_action"
# Human reviews the state
current_state = app.get_state(config)
print(current_state.values) # Show what's pending approval
# Human approves — resume
app.invoke(None, config=config) # None continues from checkpointFor programmatic resume with modified state:
# Update state before resuming
app.update_state(config, {"approved": True}, as_node="approve_action")
app.invoke(None, config=config)Q6: When would you use LangGraph instead of a simpler agent framework?
A: Use LangGraph when:
- Cycles required: Your agent needs to retry, refine answers iteratively, or loop over a list of items
- Complex branching: Multiple different paths based on intermediate results (not just one if/else)
- Human-in-the-loop: Production workflows where humans must approve actions before execution
- Long-running workflows: Multi-step processes that might crash and need to be resumed
- Multi-agent coordination: Supervisor agents that route work between specialist subagents
- Auditability: You need to inspect, replay, or branch from any point in execution history
Simpler alternatives (OpenAI Assistants API, direct LLM calls with tool loops) are sufficient for: single-turn tool use, simple chat without state management, or quick prototypes where robustness isn't critical.
Q7: How does LangGraph handle parallel node execution?
A: LangGraph supports parallel (fan-out/fan-in) execution using Send:
from langgraph.types import Send
def fan_out(state) -> list[Send]:
"""Dispatch to multiple parallel workers."""
return [
Send("worker", {"task": drug, "results": []})
for drug in state["drug_list"]
]
graph.add_conditional_edges("router", fan_out)Each Send creates an independent branch with its own state. After all branches complete, they fan back in to a collector node. This is how you process multiple items in parallel within a single graph execution.
Q8: System design — design a LangGraph workflow for a drug safety review system.
A:
Requirement: An agent that reviews a proposed drug regimen (list of drugs), checks for interactions, and requires human pharmacist approval before issuing a safety report.
State:
class SafetyReviewState(TypedDict):
drug_list: list[str]
interactions: Annotated[list[str], operator.add]
risk_level: str # "low", "moderate", "high"
pharmacist_approved: bool
final_report: strGraph:
[start] → [screen_for_interactions] → [assess_risk]
→ if risk == "high": [flag_for_pharmacist_review] → [INTERRUPT]
→ human approves → [generate_report] → [END]
→ if risk in ("low", "moderate"): [generate_report] → [END]Key decisions:
interrupt_before=["generate_report"]if risk is high — pharmacist must review before any report is generated- Thread ID = patient encounter ID — state persists for the session
- PostgresSaver checkpointer for production durability
- Recursion limit: 10 (simple linear flow, shouldn't recurse)
pharmacist_approvedin state ensures the interrupt actually adds human judgment, not just a button click
This pattern is the standard LangGraph human-in-the-loop design for regulated workflows.