LangGraph 核心架构入门:State、Node、Edge 与 Reducer 的工程理解
导读:LangGraph 把 Agent 工作流抽象成一张有状态图。三个核心概念 State(共享数据快照)、Node(执行逻辑)和 Edge(流程路由)贯穿始终。本文从构建器到编译执行,从普通边到条件边,再到 Reducer 合并规则,配合一个完整的「智能客服意图分类」案例,一次性讲透 LangGraph 核心架构。
适合读者
- 刚接触 LangGraph,需要建立整体认知的新手
- 已经用过 LangChain 但不知道 LangGraph 解决什么问题的开发者
- 准备面试中回答「LangGraph 的核心组件是什么」的求职者
阅读收益
- 理解 StateGraph 构建 -> compile -> invoke 的完整生命周期
- 掌握 Node 函数签名、异步写法、显式命名规范
- 学会普通 Edge 和 Conditional Edge 的用法
- 理解 Reducer 的本质:不是语法糖,而是并行节点的合并规则
- 获得一份可直接运行的「智能客服意图分类」完整代码
目录
- [LangGraph 为什么用图来建模 Agent](#LangGraph 为什么用图来建模 Agent)
- [核心三要素:State + Node + Edge](#核心三要素:State + Node + Edge)
- [StateGraph 构建与编译](#StateGraph 构建与编译)
- State:共享快照
- Node:执行单元
- Edge:流程控制
- Reducer:合并规则
- 完整案例:智能客服意图分类
- 踩坑清单
- 面试速答版
- 总结与延伸
- 思考题
1. LangGraph 为什么用图来建模 Agent
传统 LangChain 的 LCEL(LangChain Expression Language)是管道式链式调用:
prompt -> model -> output_parser
适合简单的问答,但 Agent 需要:
- 循环(模型生成 tool_call -> 执行工具 -> 回到模型)
- 条件分支(根据意图走不同路径)
- 状态持久化(中断后恢复、多轮对话)
- 并行执行(多个工具同时调用)
LangGraph 把 Agent 建模成有向图:
START -> classify_intent -> [query_order | query_logistics | human_handoff] -> final_answer -> END
这不是架构上的炫技,而是让 Agent 的行为从「黑箱链式调用」变成「可观测、可调试、可中断的状态图」。
2. 核心三要素:State + Node + Edge
| 概念 | 角色 | 类比 |
|---|---|---|
| State | 共享数据快照 | 图运行时的「全局黑板」 |
| Node | 执行逻辑 | 每个节点是具体干活的函数 |
| Edge | 流程控制 | 决定下一步走哪个节点 |
一句话:Node 负责做事,Edge 负责决定下一步,State 负责保存过程中的数据。
再加上两个基础设施:
| 概念 | 角色 |
|---|---|
| Reducer | 多个节点写同一个字段时的合并规则 |
| Checkpointer | 保存图状态快照,支持中断与恢复 |
3. StateGraph 构建与编译
python
from langgraph.graph import StateGraph
from typing_extensions import TypedDict
class State(TypedDict):
question: str
answer: str
builder = StateGraph(State) # ① 构建器:声明 State 结构
builder.add_node("analyze", analyze_fn) # ② 添加节点
builder.add_edge(START, "analyze") # ③ 添加边
builder.add_edge("analyze", END)
graph = builder.compile() # ④ 编译:生成可执行图
result = graph.invoke({"question": "你好"}) # ⑤ 执行
编译的意义:StateGraph 只是构建器,不能直接执行。compile() 后会验证节点和边的完整性,生成 CompiledGraph。如果边连接了不存在的节点,编译时报错。
4. State:共享快照
4.1 TypedDict 定义
python
from typing_extensions import TypedDict
class State(TypedDict):
question: str
answer: str
intent: str
State 是 TypeDict,不是普通 dict。编译时 LangGraph 会根据字段名检查节点返回的更新是否合法。
4.2 节点如何读写 State
节点函数签名:
python
def analyze(state: State):
"""读取 question,写入 intent"""
intent = classify(state["question"])
return {"intent": intent} # 只返回更新,不是完整 State
关键:节点返回的是 State 的局部更新,LangGraph 自动合并。不要返回完整 State 字典,也不要直接原地修改 state。
4.3 State 不等于 messages
初学者常把 State 理解为聊天消息列表。messages 只是 State 里的一个字段:
python
class AgentState(TypedDict):
messages: list # 聊天历史只是 State 的一部分
order_id: str # 业务数据也要存
intent: str # 意图分类结果
risk_level: str # 风险评估
真实项目中,State 应该保存结构化业务数据,而不只是聊天历史。
4.4 State 设计原则
- 字段语义清晰:不要所有东西都塞到 messages 或 metadata
- 只放流程需要的数据:不要把无关数据放进全局 state
- 中间结果结构化 :
order_status: "shipped"比自然语言描述更容易判断 - 注意并行更新:多个节点会写同一个字段时,要定义 reducer
- 区分 runtime context:user_id、trace_id 等不一定要持久化的上下文,用 config
思考题 1
假设你在设计一个「电商退货 Agent」,State 里应该包含哪些字段?哪些字段需要 Reducer?哪些字段是运行时 context 不需要进 State?
5. Node:执行单元
Node 可以是普通 Python 函数、异步函数、LLM 调用、Tool 调用、子图或 Runnable。
5.1 最简单的 Node
python
def greet(state: State):
return {"answer": f"你好,{state['question']}"}
5.2 显式命名 vs 省略命名
python
# 显式命名:生产项目推荐
builder.add_node("intent_classifier", classify_intent)
# 省略命名:默认用函数名
builder.add_node(classify_intent) # 节点名 = classify_intent
显式命名让图结构更清晰,日志和 tracing 更容易看。
5.3 异步 Node
python
async def async_fetch(state: State):
result = await aiohttp.get(state["url"])
return {"data": result}
调用异步图:
python
result = await graph.ainvoke({"url": "https://api.example.com"})
5.4 Node 设计原则
- 职责单一:一个节点只做一件事
- 输入输出清晰:函数签名明确 state 类型
- 不偷偷修改外部全局状态:副作用显式通过 state 传递
- 返回结构化更新 :不要写
return state - 出错时容易定位:节点名就是定位线索
不要写成这样:
python
def bad_node(state):
state["x"] = 1 # 原地修改!LangGraph 无法追踪
call_external_api() # 副作用隐藏在函数里
return state # 返回完整 state,冗余
更好的拆法:
python
def prepare(state):
return {"x": 1}
def call_api(state):
result = call_external_api(state["params"])
return {"api_result": result}
6. Edge:流程控制
6.1 普通 Edge:固定流转
python
from langgraph.constants import START, END
builder.add_edge(START, "classify") # 入口
builder.add_edge("classify", "answer") # 顺序执行
builder.add_edge("answer", END) # 结束
START 和 END 是特殊节点,必须指定入口,否则编译报错。
6.2 Conditional Edge:条件分支
根据 State 的值动态选择下一步:
python
def route_by_intent(state: State) -> str:
intent = state["intent"]
if intent == "order":
return "query_order"
elif intent == "logistics":
return "query_logistics"
else:
return "human_handoff"
# conditional edge:classify 节点之后,根据 state 决定走向
builder.add_conditional_edges("classify", route_by_intent)
返回的字符串必须是已添加的节点名,或者 END。
6.3 条件边的可视化理解
START -> classify_intent
|
|-- intent="order" -> query_order -> final_answer -> END
|-- intent="logistics" -> query_logistics -> final_answer -> END
|-- intent="other" -> human_handoff -> END
LangGraph 的条件边本质是一个路由函数,在节点执行完后调用,根据当前 State 返回下一个节点名。
7. Reducer:合并规则
7.1 为什么需要 Reducer
假设有两个节点并行执行,都往 audit_log 里写数据:
python
node_a returns {"audit_log": ["A 完成"]}
node_b returns {"audit_log": ["B 完成"]}
如果没有 reducer,LangGraph 不知道 audit_log 最终应该是 ["A 完成", "B 完成"] 还是只保留最后一个 ["B 完成"]。
7.2 使用 Annotated 定义 Reducer
python
from typing import Annotated
import operator
class State(TypedDict):
audit_log: Annotated[list[str], operator.add]
这表示:当多个节点都返回 audit_log 时,用 operator.add 把列表拼起来。
7.3 Reducer 函数签名
Reducer 本质上是一个函数。接收旧值和新值,返回合并后的值:
python
def merge_unique(old: list, new: list) -> list:
return old + [x for x in new if x not in old]
class State(TypedDict):
tags: Annotated[list[str], merge_unique]
7.4 常见 Reducer 写法
| 字段类型 | 推荐 reducer | 说明 |
|---|---|---|
| liststr | operator.add | 列表拼接 |
| messages | add_messages | 聊天消息合并 |
| set | 自定义 union | 集合合并 |
| dict | 自定义 merge | 字典合并 |
| 计数器 | 自定义 sum | 数字累加 |
7.5 Reducer 不是语法糖
很多初学者把 Reducer 当成「让代码更优雅的装饰器」。实际上:
- 没有 Reducer 时,多个节点写同一字段,LangGraph 可能只保留最后一个值
- 有 Reducer 时,并行节点的更新被正确合并
- 自定义 Reducer 可以实现去重、求和、字典合并等复杂逻辑
Reducer 是 LangGraph 支持并行节点的基础设施。
8. 完整案例:智能客服意图分类
流程设计:
START -> classify_intent
|
|-- intent="order" -> query_order -> final_answer -> END
|-- intent="logistics" -> query_logistics -> final_answer -> END
|-- intent="other" -> human_handoff -> END
python
from typing import TypedDict, Literal, Annotated
from langgraph.graph import StateGraph
from langgraph.constants import START, END
import operator
# ========== State 定义 ==========
class State(TypedDict):
question: str
intent: Literal["order", "logistics", "other"]
order_id: str
answer: str
audit_log: Annotated[list[str], operator.add]
# ========== Node 函数 ==========
def classify_intent(state: State):
"""节点1:意图分类"""
q = state["question"].lower()
if "订单" in q or "买" in q:
intent = "order"
elif "物流" in q or "快递" in q or "发货" in q:
intent = "logistics"
else:
intent = "other"
return {
"intent": intent,
"audit_log": [f"意图分类结果: {intent}"]
}
def query_order(state: State):
"""节点2a:查询订单"""
return {
"answer": "订单 1001 已发货,预计明天送达。",
"audit_log": ["执行订单查询"]
}
def query_logistics(state: State):
"""节点2b:查询物流"""
return {
"answer": "顺丰快递,单号 SF1234567890,当前位置:北京转运中心。",
"audit_log": ["执行物流查询"]
}
def human_handoff(state: State):
"""节点2c:转人工"""
return {
"answer": "已为您转接人工客服,请稍候。",
"audit_log": ["转人工处理"]
}
def final_answer(state: State):
"""节点3:生成最终回答"""
return {
"answer": f"【客服回复】{state['answer']}",
"audit_log": ["生成最终回答"]
}
# ========== 路由函数 ==========
def route_by_intent(state: State) -> str:
"""根据意图选择分支"""
intent = state["intent"]
if intent == "order":
return "query_order"
elif intent == "logistics":
return "query_logistics"
else:
return "human_handoff"
# ========== 构建图 ==========
builder = StateGraph(State)
builder.add_node("classify_intent", classify_intent)
builder.add_node("query_order", query_order)
builder.add_node("query_logistics", query_logistics)
builder.add_node("human_handoff", human_handoff)
builder.add_node("final_answer", final_answer)
builder.add_edge(START, "classify_intent")
builder.add_conditional_edges("classify_intent", route_by_intent)
builder.add_edge("query_order", "final_answer")
builder.add_edge("query_logistics", "final_answer")
builder.add_edge("human_handoff", "final_answer")
builder.add_edge("final_answer", END)
graph = builder.compile()
# ========== 运行 ==========
result = graph.invoke({"question": "我的订单什么时候到?"})
print(result["answer"])
print(result["audit_log"])
# 输出:
# 【客服回复】订单 1001 已发货,预计明天送达。
# ['意图分类结果: order', '执行订单查询', '生成最终回答']
思考题 2
上面的案例中,如果用户说「我想退货」,当前会被分类为 "other" 转人工。如果要支持退货流程,需要修改哪些部分?退货流程是否应该有独立的子图?
9. 踩坑清单
| 序号 | 坑点 | 现象 | 正确做法 |
|---|---|---|---|
| 1 | 忘记 compile() | AttributeError: 'StateGraph' object has no attribute 'invoke' |
构建完后必须调用 compile() |
| 2 | 节点返回完整 state | 编译时报类型错误 | 只返回需要更新的字段字典 |
| 3 | 原地修改 state | LangGraph 无法追踪变化 | 返回新字典,不要修改入参 |
| 4 | 忘记指定 START | 编译报错:没有入口节点 | 必须 add_edge(START, "...") |
| 5 | 条件边返回非法节点名 | ValueError: Node ... not found |
返回的字符串必须是已添加的节点名 |
| 6 | 并行节点没有 reducer | 后执行的覆盖先执行的 | 用 Annotated..., operator.add 定义合并规则 |
| 7 | 节点里藏副作用 | 调试困难,复现不了 | 副作用显式通过 state 传递或拆成独立节点 |
| 8 | 节点名和函数名不一致 | 日志里看不懂 | 显式命名,让节点名表达业务含义 |
10. 面试速答版
Q1:LangGraph 的核心三要素是什么?
A:State(共享数据快照)、Node(执行逻辑)、Edge(流程控制)。Node 负责做事,Edge 负责决定下一步,State 负责保存过程中的数据。
Q2:StateGraph 和 CompiledGraph 的区别?
A:StateGraph 是构建器,只声明结构和连接关系,不能直接执行。compile() 后得到 CompiledGraph,验证完整性并生成可执行的图对象。
Q3:Conditional Edge 怎么写?
A:定义一个路由函数,接收 state 返回下一个节点名的字符串。用 builder.add_conditional_edges("node_name", route_fn) 添加。
Q4:Reducer 的作用是什么?
A:当多个并行节点都更新同一个字段时,Reducer 定义如何合并这些更新。没有 Reducer 时可能只保留最后一个值。
Q5:Node 为什么不能原地修改 state?
A:LangGraph 需要追踪每次更新的来源,用于 checkpoint、streaming 和调试。原地修改破坏了不可变性,导致无法正确追踪状态变化。
11. 总结与延伸
本章核心要点
- LangGraph 用有向图建模 Agent 工作流,解决循环、分支、持久化、并行四大问题
- StateGraph -> compile() -> invoke() 是标准生命周期
- State 是共享快照,节点返回局部更新,LangGraph 自动合并
- Node 职责单一,不显式命名,不原地修改 state
- Edge 分普通边(固定流转)和条件边(动态路由)
- Reducer 是并行节点的基础设施,不是语法糖
延伸思考
- 并行节点:LangGraph 支持多个节点同时执行,需要 Reducer 合并结果
- Checkpointer:为图配置 checkpointer 后,支持中断、恢复和多轮对话
- Subgraph:复杂流程可以拆成子图,提高复用性(见第十章)
- Streaming:stream() 和 astream() 可以实时获取图的执行进度(见第八章)
12. 思考题
-
State 设计:设计一个「电商退货 Agent」的 State 结构,包含退货原因、订单信息、审批状态等字段。哪些字段需要 Reducer?
-
条件边路由:如果用户输入同时包含「订单」和「物流」关键词,当前案例会走订单分支。如何设计更智能的路由逻辑?
-
并行查询:假设查询订单和查询物流可以并行执行(不互相依赖),如何修改图结构让它们并行?需要注意什么?
-
错误处理 :如果
query_order节点查询外部 API 失败,图应该如何处理?是走到错误处理节点,还是让条件边支持异常路由? -
状态持久化 :把当前的
InMemorySaver换成 Postgres 持久化存储后,用户隔一天再问「刚才的订单查得怎么样了」,系统该如何恢复之前的对话状态?
如果本文对你有帮助,欢迎点赞 + 收藏 + 关注!有任何问题欢迎在评论区交流。
参考资料:LangGraph 官方文档 Core Concepts 章节