上篇咱们把 LangGraph 的骨架(State / Node / Edge)搭起来了。这篇老哥带你把两个最容易被新手忽视、却决定你写得好不好的核心机制啃透:State和Command。
用大白话讲:State 决定了一张图里能流动什么数据、节点之间怎么协作;Command 让节点能在更新状态的同时,顺便决定下一步去哪。
一、State:图里能流动什么数据,全看它
State 不是随便写个 dict,它是图运行过程中的状态快照,有三个作用:定义数据结构、在节点间传递数据、控制执行逻辑(条件边、Command、Send 都靠它)。
先记一个容易踩坑的点:State 是共享快照,不是普通全局变量。节点不要直接原地改 state,而是返回一个更新,让 LangGraph 帮你合并回全局。写法的差别是:
def node(state): # ❌ 不要原地改 # state["messages"].append(...) # ✅ 返回更新 return {"messages": [new_msg]}三种定义方式,怎么选?
表格
| Schema 类型 | 特点 | 适合场景 |
|---|---|---|
| TypedDict | 简单、轻量、最常见 | 大多数项目、学习 |
| dataclass | 支持默认值 | 需要默认值的状态 |
| Pydantic | 运行时校验、限字段范围 | 强约束、外部输入复杂 |
我的建议很直接:先用 TypedDict 把状态结构设计清楚,确实需要运行时校验时再上 Pydantic。别一上来就追求 "高级"。
二、Reducer:多个节点同时写一个字段怎么办?
这是 State 机制里最重要的部分。设想两个节点并行执行,都往reports这个字段里写东西,如果字段没有 reducer,LangGraph 直接报错 —— 因为它不知道该怎么合并。
解决办法就是给字段指定一个Reducer(合并规则)。写法是配合Annotated:
from typing import Annotated import operator class State(TypedDict): reports: Annotated[list, operator.add] # 列表用 add 拼接Reducer 本质上就是一个函数:接收旧值和新值,返回合并后的值。常见套路给你整理好了:
表格
| 字段类型 | 推荐 reducer | 说明 |
|---|---|---|
list[str] | operator.add | 列表拼接 |
messages | add_messages | 聊天消息合并(带 ID 语义) |
set | 自定义 union | 集合合并 |
dict | 自定义 merge | 字典合并 |
| 计数器 | 自定义 sum | 数字累加 |
特别提醒:聊天消息别用operator.add,要用add_messages。因为消息有 ID、类型(Human/AI/Tool/System),add_messages会追加新消息、同 ID 更新原消息,这个语义是普通列表拼接给不了的。
三、Input/Output Schema 和 Private State:两个进阶法宝
Input / Output Schema:默认图的输入、内部状态、输出是同一个 schema,但复杂项目里,你肯定不想让调用方传一堆intent、docs这种内部字段。拆分出入口 schema 能:隐藏内部状态、让 API 边界清晰、保护私有数据(输出里不出现的字段不暴露)、方便测试。
Private State(私有状态):这是理解 LangGraph 状态体系的关键,我给你打个比方 ——**
公共 State 就像班级团建的公共账户,大家兑钱进去,买大家都吃的东西。如果只有两个同学想吃臭豆腐,拿公共账户的钱买就不合适。这时候这两个人就该自己兑钱搞个私有账户。
对应到代码:如果你的图有 100 个 node,每两个 node 之间传的临时变量都塞进公共 State,字段会爆炸,既不便于阅读,也拖累 checkpoint。私有 State 就是节点之间临时的内存管道:不进全局 State、不持久化、不对外暴露,用完就扔,只在声明它的节点之间流转。
四、Command:让节点 "一边更新状态,一边跳转"
前几章里,节点和边是各司其职的:节点更新 State,条件边读 State 决定下一步。但有个别扭的地方 ——分类结果明明是在classify节点里算出来的,却还要再写一个路由函数读一遍。
Command 就是为这个痛点而生的。它的核心是两个参数:
表格
| 参数 | 作用 |
|---|---|
update | 把数据写入 State(和返回字典等价) |
goto | 指定接下来执行哪个节点 |
两者可以一起用,也可以单独用。一个节点可以这样返回:
from langgraph.types import Command def classify(state): category = decide(state["user_input"]) # 算出分类 return Command(update={"category": category}, goto=category)一条 return 同时完成了 "写状态" 和 "选下一站",不用再单独写路由函数,少写一半代码。
五、用 Command 的三个铁律(新手必看)
- 只更新状态、不控制流程时,直接返回字典,别为了用 Command 而用 Command。
goto的目标节点必须真实存在,拼错名字运行时会报 "找不到目标节点"。- ⚠️ 最坑的一条:
goto不会自动取消静态边。如果一个节点用Command(goto=...)负责路由,就不要再给它配其他出边,否则那两个节点会并行执行,容易撞上 "一个 key 一步只能接收一个值" 的报错。规则是:用了 goto 路由的节点,别给它再加边。
六、完整案例:客服问题分流
把这套知识串起来,就是客服分流:用户输入问题 → 分类节点判断是order(订单 / 物流)、refund(退款)还是general(其他)→ 分类节点用Command同时写入category并跳到对应处理节点。
这正是Command(update=..., goto=...)的经典场景:分类结果和路由强相关,用 Command 一气呵成,比 "条件边 + 独立路由函数" 更直观。
七、老哥的收尾
把这套学完,你已经从 "会用 LangGraph 画图" 进阶到 "能设计出好图" 了。给你留个记忆锚点:
State 管数据怎么流动,Reducer 管并行怎么写,Private State 管临时变量放哪,Command 管节点写完状态直接去哪。
下一步,就可以去挑战真正的 Agent 循环了 —— 记住,循环设计必须带退出条件(文本达标或尝试次数到上限),否则你的图会跑到天荒地老。有问题随时回来问老哥,咱们下篇见。