核心组件详解

状态管理与 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️⃣ 状态更新流

理解状态是如何在图中流动的至关重要。

  1. 节点执行inputNode 返回 { key: "newValue" }
  2. 应用 Reducer:系统查找 key 对应的 reducer,执行 reducer(oldValue, "newValue")
  3. 生成新快照:更新后的状态成为下一节点的输入。

⚠️ 重要提示:部分更新

节点不需要返回完整的状态对象,只需要返回想更新的字段

// ✅ 正确:只返回变更字段
const node = (state) => {
  return { step: state.step + 1 };
};

// ❌ 错误:手动复制所有字段(除非你想覆盖)
const node = (state) => {
  return { ...state, step: state.step + 1 };
};

💡 练习题

  1. 代码题:定义一个状态,包含一个 logs 字段,要求每次更新都会在前面加上时间戳,并追加到数组中(即 (current, update) => [...current, { time: Date.now(), msg: update }])。
  2. 纠错题:如果一个节点返回 { count: 1 },而 count 字段定义的 reducer 是 (a, b) => a + b,且当前值为 5,那么更新后的值是多少?

📚 参考资源

官方文档


✅ 总结

本章要点

  • Reducer 决定了状态“如何变”。
  • 默认是覆盖,数组通常需要追加,对象通常需要合并。
  • 节点只需返回“增量”更新,LangGraph 会自动处理合并。

下一步:掌握了状态,我们来看看如何编写处理这些状态的节点

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page