20 LangGraph Interview Questions (With Answers)
Crack your AI engineering interview with these 20 LangGraph interview questions covering state machines, nodes, edges, checkpointing, human-in-the-loop, and production deployment patterns.
LangGraph is the go-to framework for building stateful, multi-step AI agents. These questions are asked in senior AI engineering and ML engineering roles. Each answer is production-focused.
Core Concepts
1. What is LangGraph and how does it differ from LangChain?
LangGraph is a graph-based orchestration framework built on top of LangChain. While LangChain provides linear chains of prompts and tools, LangGraph allows you to model agents as cyclic directed graphs — where nodes can loop back, branch conditionally, and maintain explicit state across steps.
Key difference: LangChain chains are DAGs (no cycles). LangGraph supports cycles, which is essential for agentic loops where the model iterates until a stopping condition is met.
2. What is a StateGraph in LangGraph?
A StateGraph is the main class used to define agent workflows. You pass it a state schema (usually a TypedDict) that defines the shared data structure flowing through all nodes.
from langgraph.graph import StateGraph
from typing import TypedDict, Listclass AgentState(TypedDict): messages: List[str] tool_calls: List[dict] final_answer: str
graph = StateGraph(AgentState) ```
Every node reads from and writes to this shared state object.
3. What is a node in LangGraph?
A node is a Python function that receives the current state and returns a dictionary of state updates. Nodes are the units of work — each one may call an LLM, run a tool, validate output, or make a routing decision.
def call_llm(state: AgentState) -> dict:
response = llm.invoke(state["messages"])
return {"messages": [response]}graph.add_node("llm", call_llm) ```
4. What is an edge and what types exist?
Edges define control flow between nodes:
- Normal edge: Always moves from node A to node B.
- Conditional edge: Routes to different nodes based on state values.
- Entry point: Sets the first node to execute.
- END: Special terminal node that stops execution.
graph.add_conditional_edges(
"llm",
route_decision, # function returning next node name
{"continue": "tools", "done": END}
)5. What is the difference between `add_edge` and `add_conditional_edges`?
add_edge(a, b) is deterministic — always goes from a to b. add_conditional_edges(a, fn, mapping) calls fn(state) to decide the next node. Use conditional edges whenever you need branching logic based on model output or state values.
State Management
6. How does state persistence work in LangGraph?
LangGraph uses a checkpointer to persist state between steps. This allows: - Resuming interrupted runs - Human-in-the-loop pauses - Debugging past executions
from langgraph.checkpoint.sqlite import SqliteSavermemory = SqliteSaver.from_conn_string(":memory:") app = graph.compile(checkpointer=memory) ```
Each run gets a thread_id. You pass it in the config to resume.
7. What is a thread in LangGraph?
A thread is an isolated execution context identified by a thread_id. Think of it like a conversation session. Multiple threads can run independently with separate states.
config = {"configurable": {"thread_id": "user-123"}}
result = app.invoke({"messages": [user_input]}, config)8. How do you handle state accumulation vs. state replacement?
By default, returning a key from a node replaces that key in state. To accumulate (append), use Annotated with a reducer function:
from typing import Annotated
import operatorclass AgentState(TypedDict): messages: Annotated[list, operator.add] # appends instead of replacing ```
This is critical for message history — you want to append, not overwrite.
Human-in-the-Loop
9. How do you implement human-in-the-loop in LangGraph?
Use interrupt_before or interrupt_after when compiling the graph:
app = graph.compile(
checkpointer=memory,
interrupt_before=["execute_action"] # pause before this node
)The graph pauses, saves state, and waits. You resume by calling invoke again with the same thread_id. This is essential for high-stakes actions like sending emails or running database mutations.
10. What is the difference between `interrupt_before` and `interrupt_after`?
interrupt_before: Pauses before the specified node runs. Use this when you want a human to approve the planned action.interrupt_after: Pauses after the node runs. Use this when you want a human to review the output before continuing.
Multi-Agent Patterns
11. How do you build a supervisor agent in LangGraph?
A supervisor is a routing node that decides which sub-agent to call next. It uses conditional edges to direct flow:
def supervisor(state):
decision = llm.invoke(supervisor_prompt + str(state["messages"]))
return decision.next_agent # "researcher" | "coder" | "FINISH"graph.add_conditional_edges("supervisor", supervisor, { "researcher": "researcher_agent", "coder": "coder_agent", "FINISH": END }) ```
12. What is a subgraph in LangGraph and when do you use it?
A subgraph is a compiled LangGraph graph used as a node inside a parent graph. Use subgraphs to encapsulate complex agent logic and reuse it across multiple parent graphs without duplicating code.
sub_app = sub_graph.compile()
parent_graph.add_node("sub_agent", sub_app)13. How do you share state between a parent graph and a subgraph?
The subgraph must declare the same top-level state keys it needs to read from or write to. Keys not declared in the subgraph's state are invisible to it. Design the state schema carefully — subgraphs can only access what they declare.
Production Patterns
14. How do you add retry logic to a LangGraph node?
Wrap the node function with a try/except and return a state update with an error counter. Use a conditional edge to route to a retry node or the error handler based on the retry count:
def call_tool(state):
try:
result = tool.run(state["tool_input"])
return {"tool_result": result, "retries": 0}
except Exception as e:
return {"error": str(e), "retries": state.get("retries", 0) + 1}15. How do you add streaming output to a LangGraph application?
Use .stream() instead of .invoke(). LangGraph streams node outputs as they complete:
for chunk in app.stream({"messages": [input]}, config):
for node_name, node_output in chunk.items():
print(f"{node_name}: {node_output}")For LLM token-level streaming, pass stream_mode="messages".
16. What is `RunnableConfig` and why does it matter in production?
RunnableConfig carries metadata through the graph: thread_id, recursion_limit, tags, callbacks, and metadata. The recursion limit (default 25) prevents infinite loops — set it based on your maximum expected steps.
config = {
"configurable": {"thread_id": "abc"},
"recursion_limit": 50,
"tags": ["production", "customer-123"]
}Debugging & Observability
17. How do you trace a LangGraph run for debugging?
Use LangSmith for full trace capture. Set environment variables and every run is automatically traced:
import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "your-key"
os.environ["LANGCHAIN_PROJECT"] = "blueprint-of-ai"You can replay, compare, and annotate traces in the LangSmith UI.
18. How do you inspect the current state of a paused graph?
Use the checkpointer's get_state method with the thread config:
snapshot = app.get_state(config)
print(snapshot.values) # current state dict
print(snapshot.next) # which nodes would run nextThis is essential for debugging interrupted or paused runs.
Advanced
19. What is the `MessagesState` convenience class?
MessagesState is a pre-built state schema with a messages key that uses add_messages as its reducer (handles appending and deduplication). Use it to avoid boilerplate for simple chat-based agents:
from langgraph.graph import MessagesStategraph = StateGraph(MessagesState) ```
20. How do you test a LangGraph agent deterministically?
Mock the LLM with a deterministic response function, then assert on the final state:
def mock_llm(messages):
return AIMessage(content='{"action": "search", "query": "test"}')# Patch llm.invoke with mock_llm, then: result = app.invoke({"messages": [HumanMessage(content="search test")]}) assert result["final_answer"] == "expected" assert result["tool_calls"][0]["action"] == "search" ```
Use pytest fixtures to inject different mock responses and test edge cases systematically.