核心组件详解
状态管理与 Reducers
详解 Annotation 的使用、状态更新机制以及如何编写自定义 Reducer
📚 学习目标
学完这篇文章后,你将能够:
- 熟练使用
Annotation.Root定义复杂状态 - 理解 LangGraph 的状态更新机制(Immutable)
- 编写自定义
Reducer处理特定的状态合并逻辑 - 辨析
messagesStateReducer的特殊用途
前置知识
在开始学习之前,建议先阅读:
你需要了解:
- JavaScript 对象展开语法 (
...obj) - 纯函数(Pure Function)概念
1️⃣ 状态定义:Annotation
Annotation 是定义图状态 Schema 的标准方式。它不仅定义了类型,还定义了如何更新该字段。
基础语法
import { Annotation } from '@langchain/langgraph';
const StateAnnotation = Annotation.Root({
// 简单字段:新值覆盖旧值
query: Annotation<string>(),
// 带默认值的字段
steps: Annotation<number>({
default: () => 0,
}),
});2️⃣ Reducer:控制状态合并
默认情况下,LangGraph 的状态更新是覆盖式的(这也是为什么默认字段叫 Annotation<Type>())。但有时我们需要追加或合并数据,这就需要 Reducer。
什么是 Reducer?
Reducer 是一个函数 (oldValue, newValue) => mergedValue。
常见 Reducer 模式
1. 数组追加(Append)
const StateAnnotation = Annotation.Root({
// 每次节点返回 errors,都会追加到数组末尾
errors: Annotation<string[]>({
reducer: (current, update) => [...current, ...update],
default: () => [],
}),
});2. 对象合并(Merge)
const StateAnnotation = Annotation.Root({
// 合并部分属性,而不是整个对象替换
userProfile: Annotation<Record<string, any>>({
reducer: (current, update) => ({ ...current, ...update }),
default: () => ({}),
}),
});3. 数学运算
const StateAnnotation = Annotation.Root({
// 累加器
totalTokenUsage: Annotation<number>({
reducer: (a, b) => a + b,
default: () => 0,
}),
});3️⃣ 内置工具:messagesStateReducer
处理对话历史是 LangGraph 最常见的场景,因此官方提供了优化过的 messagesStateReducer。
特性
- 自动去重:基于
id属性 - 保持顺序:确保消息按时间排序
- 支持删除:通过特殊标记删除消息
import { Annotation, messagesStateReducer } from '@langchain/langgraph';
import { BaseMessage } from '@langchain/core/messages';
const StateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: messagesStateReducer,
default: () => [],
}),
});4️⃣ 状态结构怎么设计:让状态“最小、清晰、可演进”
状态设计的目标不是“字段越多越好”,而是:
- 图能跑(节点需要的数据都在 state 里)
- 能维护(你能解释每个字段的来源与用途)
- 可扩展(加新节点/新分支时不把 state 搞成一锅粥)
1. 最小化:只存储不可推导的信息
import { Annotation } from '@langchain/langgraph';
import { BaseMessage } from '@langchain/core/messages';
// ❌ 反例:冗余字段很多(可从 messages 推导)
const BadState = Annotation.Root({
messageCount: Annotation<number>(),
lastMessage: Annotation<string>(),
hasMessages: Annotation<boolean>(),
messages: Annotation<BaseMessage[]>({
reducer: (a, b) => a.concat(b),
default: () => [],
}),
});
// ✅ 更好的做法:最小化 + 用工具函数推导
const MinimalState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (a, b) => a.concat(b),
default: () => [],
}),
userId: Annotation<string>(),
status: Annotation<'idle' | 'thinking' | 'tool_calling'>({
default: () => 'idle',
}),
});
export function getLastMessage(state: typeof MinimalState.State) {
return state.messages.length ? state.messages[state.messages.length - 1] : null;
}2. 规范化:把“深层嵌套对象”拆成可合并的数据
如果你在 state 里塞了一个很深的对象(profile/settings/flags...),通常需要一个对象合并 reducer:
const State = Annotation.Root({
profile: Annotation<Record<string, unknown>>({
reducer: (a, b) => ({ ...a, ...b }),
default: () => ({}),
}),
});5️⃣ 状态更新流
5️⃣ 状态更新流
理解状态是如何在图中流动的至关重要。
- 节点执行:
inputNode返回{ key: "newValue" }。 - 应用 Reducer:系统查找
key对应的 reducer,执行reducer(oldValue, "newValue")。 - 生成新快照:更新后的状态成为下一节点的输入。
⚠️ 重要提示:部分更新
节点不需要返回完整的状态对象,只需要返回想更新的字段。
// ✅ 正确:只返回变更字段
const node = (state) => {
return { step: state.step + 1 };
};
// ❌ 错误:手动复制所有字段(除非你想覆盖)
const node = (state) => {
return { ...state, step: state.step + 1 };
};💡 练习题
- 代码题:定义一个状态,包含一个
logs字段,要求每次更新都会在前面加上时间戳,并追加到数组中(即(current, update) => [...current, { time: Date.now(), msg: update }])。 - 纠错题:如果一个节点返回
{ count: 1 },而count字段定义的 reducer 是(a, b) => a + b,且当前值为 5,那么更新后的值是多少?
📚 参考资源
官方文档
✅ 总结
本章要点:
- Reducer 决定了状态“如何变”。
- 默认是覆盖,数组通常需要追加,对象通常需要合并。
- 节点只需返回“增量”更新,LangGraph 会自动处理合并。
下一步:掌握了状态,我们来看看如何编写处理这些状态的节点。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。