第 25 课: Agent 状态与注解系统

0 阅读5分钟

课程目标

精读 Agent 的状态管理机制:AgentState 的构建方式、Annotation 系统(基于 LangGraph 的 StateSchema)、reducer 函数的消息累积策略,以及状态不可变性原则。


25.1 Agent 状态的核心数据结构

每个 Agent 都维护一个状态对象,在 ReAct 循环中不断更新。内置状态定义如下:

源码位置: libs/langchain/src/agents/runtime.ts:17

export type AgentBuiltInState = {
  /** 对话历史:包含所有消息 */
  messages: BaseMessage[];
  /** 结构化响应(仅在配置了 responseFormat 时存在) */
  structuredResponse?: Record<string, unknown>;
};

messages 是 Agent 状态中最重要的字段——它记录了完整的对话历史,包括用户输入、LLM 响应、工具调用结果。


25.2 BuiltInState 与跳转控制

除了用户可见的状态,Agent 还维护内部控制状态:

源码位置: libs/langchain/src/agents/types.ts:426

export interface BuiltInState<TMessageStructure extends MessageStructure = MessageStructure> {
  messages: BaseMessage<TMessageStructure>[];
  __interrupt__?: Interrupt[];
  /** 用于中间件控制路由跳转 */
  jumpTo?: JumpToTarget;
}

jumpTo 是中间件用来控制执行流程的内部字段。可选的跳转目标:

源码位置: libs/langchain/src/agents/constants.ts

export const JUMP_TO_TARGETS = ["model", "tools", "end"] as const;
export type JumpToTarget = (typeof JUMP_TO_TARGETS)[number];

25.3 createAgentState — 状态 Schema 的构建

createAgentState() 是 Agent 状态系统的核心函数。它将用户自定义 schema、中间件 schema 和内置字段合并为一个完整的状态定义。

源码位置: libs/langchain/src/agents/annotation.ts:24

export function createAgentState<
  TStateSchema extends StateDefinitionInit | undefined = undefined,
  TMiddleware extends readonly AgentMiddleware<any, any, any>[] = [],
>(
  hasStructuredResponse = true,
  stateSchema: TStateSchema,
  middlewareList: TMiddleware = [] as unknown as TMiddleware
) {
  const stateFields: Record<string, any> = {
    // jumpTo 用于内部导航控制,使用 UntrackedValue 不参与 checkpoint
    jumpTo: new UntrackedValue<JumpToTarget>(),
  };

  const inputFields: Record<string, any> = {};
  const outputFields: Record<string, any> = {};

  // 合并用户 schema
  if (stateSchema) { applySchema(stateSchema); }

  // 合并 middleware schema
  for (const middleware of middlewareList) {
    if (middleware.stateSchema) { applySchema(middleware.stateSchema); }
  }

  // 只在有 responseFormat 时才包含 structuredResponse
  if (hasStructuredResponse) {
    outputFields.structuredResponse = new UntrackedValue<any>();
  }

  return {
    state: new StateSchema({ messages: MessagesValue, ...stateFields }),
    input: new StateSchema({ messages: MessagesValue, ...inputFields }),
    output: new StateSchema({ messages: MessagesValue, ...outputFields }),
  };
}

关键设计

  • 三份 Schemastate(完整状态)、input(输入通道)、output(输出通道)
  • MessagesValue:内置的消息 reducer,自动处理消息的追加与合并
  • UntrackedValue:不参与 checkpoint 持久化的临时值
  • 下划线前缀约定:以 _ 开头的字段是私有状态,不暴露到 input/output 通道

25.4 Schema 类型支持

createAgentStateapplySchema 内部函数支持两种 Schema 格式:

25.4.1 Zod Schema

import { z } from "zod";
import { createAgent } from "langchain";

const agent = createAgent({
  model: "openai:gpt-4o",
  tools: [],
  stateSchema: z.object({
    userId: z.string(),
    conversationCount: z.number().default(0),
  }),
});

Zod schema 通过 getInteropZodObjectShape() 提取字段,通过 schemaMetaRegistry 查找 reducer 元数据。

25.4.2 LangGraph StateSchema

import { StateSchema, ReducedValue } from "@langchain/langgraph";
import { z } from "zod";

const agent = createAgent({
  model: "openai:gpt-4o",
  tools: [],
  stateSchema: new StateSchema({
    count: z.number().default(0),
    history: new ReducedValue(
      z.array(z.string()).default(() => []),
      {
        inputSchema: z.string(),
        reducer: (current, next) => [...current, next],
      }
    ),
  }),
});

StateSchema 提供更精细的控制:

  • ReducedValue:定义自定义 reducer 函数
  • UntrackedValue:不持久化的临时状态

25.5 Reducer 函数:状态如何累积

Reducer 是 LangGraph 状态管理的核心概念。它定义了新值如何与旧值合并。

25.5.1 消息的内置 Reducer

messages 字段使用 MessagesValue,这是 LangGraph 提供的内置 reducer。它的行为是追加新消息到列表末尾,而不是替换整个列表。

状态更新前: messages = [HumanMessage("你好")]
节点返回:   messages = [AIMessage("你好!")]
状态更新后: messages = [HumanMessage("你好"), AIMessage("你好!")]

25.5.2 自定义 Reducer

const countingState = new StateSchema({
  // 计数器:每次更新累加
  count: new ReducedValue(
    z.number().default(0),
    {
      reducer: (current: number, next: number) => current + next,
    }
  ),
  // 标签集合:合并去重
  tags: new ReducedValue(
    z.array(z.string()).default(() => []),
    {
      inputSchema: z.string(),
      reducer: (current: string[], next: string) =>
        current.includes(next) ? current : [...current, next],
    }
  ),
});

ReducedValue 的两个关键参数:

  • valueSchema:状态值的完整类型
  • inputSchema(可选):输入值的类型(可以与状态值不同)
  • reducer(current, next) => merged 合并函数

25.6 StateManager — 节点间状态共享

StateManager 负责在不同节点间共享状态,是 ReactAgent 的内部工具。

源码位置: libs/langchain/src/agents/state.ts:16

export class StateManager {
  #nodes = new Map<string, AgentNode[]>();

  addNode(middleware: AgentMiddleware, node: AgentNode) {
    this.#nodes.set(middleware.name, [
      ...(this.#nodes.get(middleware.name) ?? []),
      node,
    ]);
  }

  getState(name: string) {
    const middlewareNodes = this.#nodes.get(name) ?? [];
    const state = middlewareNodes.reduce((prev, node) => {
      return { ...prev, ...((node.getState() as Record<string, unknown>) ?? {}) };
    }, {} as Record<string, unknown>);

    // jumpTo 是内部属性,不暴露给中间件
    delete state.jumpTo;
    return state;
  }
}

不可变性原则:注意 getState 使用 ...prev 展开运算符创建新对象,而非直接修改旧状态。


25.7 用户输入类型

UserInput 定义了 invoke()stream() 方法接受的输入类型:

源码位置: libs/langchain/src/agents/types.ts:445

export type UserInput<TStateSchema extends StateDefinitionInit | undefined = undefined> =
  InferSchemaInput<TStateSchema> & {
    messages: Messages;
  };

messages 是必须的,自定义 schema 的字段按照其定义是否可选而定。


25.8 私有状态约定

以下划线 _ 开头的状态字段被视为私有状态:

// annotation.ts 中的判断逻辑
const isPrivate = key.startsWith("_");

if (!isPrivate) {
  inputFields[key] = fieldSchema;
  outputFields[key] = fieldSchema;
}

私有状态的特性:

  • 保存在图的内部状态中,参与 checkpoint 持久化
  • 暴露为 input 或 output 通道
  • 适合中间件内部使用的状态(如调用计数器、上下文缓存)

25.9 实战练习:自定义状态的 Agent

import { z } from "zod";
import { createAgent, tool } from "langchain";
import { StateSchema, ReducedValue, MemorySaver } from "@langchain/langgraph";

// 定义带 reducer 的自定义状态
const agentState = new StateSchema({
  userId: z.string().optional(),
  // 追踪工具调用次数
  toolCallCount: new ReducedValue(
    z.number().default(0),
    { reducer: (current: number, next: number) => current + next }
  ),
  // 追踪搜索过的查询词
  searchHistory: new ReducedValue(
    z.array(z.string()).default(() => []),
    {
      inputSchema: z.string(),
      reducer: (current: string[], next: string) => [...current, next],
    }
  ),
});

const searchTool = tool(
  async ({ query }) => `搜索结果:关于「${query}」的信息`,
  {
    name: "search",
    description: "搜索信息",
    schema: z.object({ query: z.string() }),
  }
);

const agent = createAgent({
  model: "openai:gpt-4o",
  tools: [searchTool],
  stateSchema: agentState,
  checkpointer: new MemorySaver(),
});

// 第一次调用
const result1 = await agent.invoke(
  {
    messages: [{ role: "user", content: "搜索 LangChain.js" }],
    userId: "user-001",
  },
  { configurable: { thread_id: "thread-1" } }
);

console.log("工具调用次数:", result1.toolCallCount);
console.log("搜索历史:", result1.searchHistory);

// 第二次调用(同一线程,状态持续累积)
const result2 = await agent.invoke(
  { messages: [{ role: "user", content: "再搜索 LangGraph" }] },
  { configurable: { thread_id: "thread-1" } }
);

console.log("累积工具调用次数:", result2.toolCallCount);
console.log("累积搜索历史:", result2.searchHistory);

25.10 源码精读路线

优先级文件关注点
P0agents/annotation.tscreateAgentState() — 状态 Schema 的构建
P0agents/runtime.ts:17-44AgentBuiltInState — 内置状态类型定义
P1agents/state.tsStateManager — 节点间状态共享
P1agents/types.ts:426-450BuiltInState, UserInput — 状态类型
P2agents/constants.tsJumpToTarget — 跳转目标定义
P2agents/middleware/types.ts:97-123InferSchemaValueType — 状态类型推断

本课收获总结

级别你应该掌握的
🟢 基础理解 Agent 状态的核心数据结构:messages 列表 + 自定义字段
🔵 中阶掌握 Zod schema 和 StateSchema 两种状态定义方式
🟡 高阶理解 ReducedValue 的 reducer 机制:消息如何累积和更新
🟠 资深分析 state/input/output 三通道分离设计;理解私有状态约定
🔴 架构设计有状态 Agent 的内存管理策略:短期(UntrackedValue)vs 长期(checkpoint)

下一课预告

第 26 课深入 Agent 中间件与节点系统 —— 理解 Agent 图的中间件钩子、内置节点和 RunnableCallable 的桥接作用。