核心概念
LangGraph 把 agent 流程建模成状态机图:节点是处理步骤,边是流转,状态是节点间传递的数据。面试主线:图怎么建、状态怎么定义、分支怎么走。
图的三个要素
节点 + 边 + 条件边构成图
| 要素 | API | 作用 |
|---|---|---|
| 节点 | add_node("name", fn) |
一个处理步骤,函数接收 state 返回 state 更新 |
| 边 | add_edge(a, b) |
固定流转 |
| 条件边 | add_conditional_edges(a, router) |
按状态动态路由 |
| 入口/出口 | START / END |
图边界 |
from langgraph.graph import StateGraph, START, END
builder = StateGraph(AgentState)
builder.add_node("model", call_model)
builder.add_node("tools", call_tools)
builder.add_edge(START, "model")
builder.add_conditional_edges("model", should_continue, # 路由函数
{"continue": "tools", "end": END})
builder.add_edge("tools", "model")
graph = builder.compile()状态与 reducer
状态是 TypedDict(或 Pydantic 模型),reducer 决定字段如何合并更新:
from typing import Annotated, TypedDict, operator
class AgentState(TypedDict):
messages: Annotated[list, operator.add] # 追加合并
user_id: str # 默认覆盖
steps: int # 计数器| 字段定义 | 合并行为 |
|---|---|
| 无 reducer | 后写的覆盖先写的 |
Annotated[list, operator.add] |
列表追加(消息历史的标准写法) |
| 自定义 reducer | 任意合并逻辑(去重、取最大) |
reducer 是考点核心:节点返回的只是“增量”,reducer 决定怎么合并进状态。messages 用 operator.add 才能跨节点累积对话历史,覆盖语义会丢掉历史。
条件边与路由
def should_continue(state: AgentState):
if state["messages"][-1].tool_calls:
return "continue" # 还有工具要调
return "end"
# 模型输出 → 有 tool_calls 去工具节点, 否则结束条件边让图有了循环能力:模型节点 → 工具节点 → 模型节点,直到模型不再请求工具。这就是 create_agent 循环的 LangGraph 底层形态(标准 harness 只是这套图的封装)。
与普通代码的区别
| 维度 | 普通函数调用 | LangGraph |
|---|---|---|
| 状态 | 隐式(局部变量) | 显式(State 类型) |
| 流转 | 代码顺序 | 图结构声明 |
| 持久化 | 无 | 可挂 checkpoint(见持久化篇) |
| 可视化/调试 | 无 | 图即文档(LangGraph Studio) |
面试话术:LangGraph 的价值不是“图”本身,而是状态显式化 + 可持久化 + 可中断,这是复杂 agent 可靠性的前提。
面试追问
- LangGraph 的三个要素? 节点(处理)、边(流转)、状态(数据)。条件边实现分支和循环
- reducer 是什么? 字段的合并规则:默认覆盖,operator.add 追加。messages 必须追加才能累积历史
- 节点函数返回什么? 状态的增量(部分字段),reducer 合并进完整状态。不是返回整个状态
- 循环怎么实现? 条件边:模型节点根据是否还有 tool_calls 路由到工具节点或 END
- 为什么用图而不是代码? 状态显式、可持久化、可中断、可可视化。复杂 agent 的可靠性靠这些