学习笔记 · Obsidian

TypeScript Graph API:State、并发与错误

LangChainLangGraphTypeScript

Graph API 心智模型

Graph API 由 State、node 和 edge 组成:

  • State 是全图共享的数据契约;
  • node 读取当前 State,返回局部 State update 或 Command;
  • edge 决定下一个节点;
  • reducer 决定一个字段如何接收一次或多次更新;
  • compile 把声明转换成 Pregel 可执行图。

一个节点不应直接修改传入 State。返回局部对象,让运行时按 schema 和 reducer 合并。

推荐 StateSchema

当前推荐使用 StateSchema。它接受符合 Standard Schema 的字段,因此不绑定 Zod;Zod v3/v4、Valibot、ArkType 等都可参与。常见 TypeScript 推断方式:

type State = typeof MyState.State; type Update = typeof MyState.Update; const node: typeof MyState.Node = async (state) => ({ ... });

也可以用 GraphNode 和 ConditionalEdgeRouter 为独立函数声明精确类型。把 node 写在 builder 外时,这些类型可避免节点名、State update 和 config 退化为 any。

特殊字段原语

  • ReducedValue:字段可接收多个 update,由自定义 reducer 累积;输入类型可以不同于最终 State 类型。
  • MessagesValue:预构建的 ReducedValue,使用消息 reducer,支持消息 ID 覆盖、反序列化与删除。
  • UntrackedValue:运行期间可用但不进入 checkpoint,适合连接、缓存和临时对象;恢复后会重置。

UntrackedValue 默认 guard: true,同一 superstep 多次写入会报错;guard: false 允许多个写入并保留最后一个值。它解决的是“不要持久化”,不是“可以随意并发写”。

旧 State API

官方仍保留:

  • Annotation.Root;
  • Zod v3 的 .langgraph.reducer;
  • Zod v4 registry;
  • 低层 Channels。

这些用于兼容旧图或高级 channel 定制。新项目优先 StateSchema;迁移时不要在没有 checkpoint 兼容评估的情况下机械改写。

reducer 与并发写入

普通字段默认是覆盖语义。同一个 superstep 只有一个节点写该字段时没有问题;多个并行节点写同一字段时,运行时无法决定如何合并,会抛出 INVALID_CONCURRENT_GRAPH_UPDATE。

修复方式不是“让最后一个赢”,而是明确业务合并规则:

  • 数组结果:拼接或按业务 key 去重;
  • 计数:求和;
  • 集合:并集;
  • 评分:最大值、最小值或结构化聚合;
  • 真正互斥的输出:改成不同 State key,再由后续节点汇总。

reducer 必须满足可重放要求。最好具备确定性,并尽量满足结合律;否则并发完成顺序或 replay 可能改变结果。

输入、输出与私有 schema

一个图可以分别声明 input、output 和内部/private schema:

  • input schema 限制调用者可以传入的键;
  • output schema 限制 invoke 最终返回的键;
  • 内部 schema 允许节点交换不对外暴露的数据。

但 output schema 不是流式隐私边界。官方明确指出,streamEvents 的 values 投影可能包含全部 channel,包括私有字段。需要对外隐藏字段时,应:

  • 优先使用 updates 投影;
  • 显式配置 outputKeys;
  • 在服务端做响应/事件过滤;
  • 不把密钥、连接、token 等敏感数据写进 State。

节点返回契约

StateGraph node 必须返回:

  1. 与 State schema 相容的局部对象;或
  2. Command,用于同时更新 State 与控制流。

返回数组、字符串、null、任意消息对象,或只在部分分支返回对象,会触发 INVALID_GRAPH_NODE_RETURN_VALUE。多分支函数的每条路径都要满足同一契约。

未知字段也可能因为输出映射失败而落入这一错误路径。TypeScript 编译期检查能减少错误,但外部 JSON、any、动态对象和未经校验的模型输出仍需运行时 schema。

edge、并行与动态 fan-out

静态 edge

一个节点存在多个静态 outgoing edge 时,下游会在下一 superstep 并行执行。它们读取的是同一个提交后 State,而不是彼此中途写入的数据。

conditional edge

router 读取 State,返回下一个节点名或节点名集合。使用 ConditionalEdgeRouter 可让返回值受节点名联合类型约束。

Send

Send 用于运行时 fan-out,例如 orchestrator 根据模型生成的任务列表动态创建 worker。每个 Send 可以携带独立输入;worker 结果通过 reducer 汇总回共享 State。

生产上还要限制:

  • 最大 worker 数;
  • 外部模型与 API 并发;
  • 单 worker 超时与重试;
  • reducer 的内存增长;
  • worker 的稳定任务 ID 与幂等键。

Command

Command 可以在一个返回值中同时完成:

  • update:更新 State;
  • goto:选择下一个 node;
  • graph:跳转到父图;
  • resume:恢复 interrupt。

TypeScript node 返回 Command 且路由目标是动态的时,应在 addNode 选项中声明 ends,让类型、图渲染和部署分析知道潜在目标。

若子图返回 Command.PARENT 并更新父子共享字段,父图该字段需要 reducer,否则并发子图写入会冲突。

每个 node 只应选择一种控制流来源:

  • 静态 edge;
  • conditional edge;
  • 返回 Command.goto。

如果同时给同一 node 配静态 edge 又返回 Command,两个路由都会执行,不是后者覆盖前者。

Command 作为输入的边界

普通新一轮对话应传入普通 input object。作为 invoke/stream 输入的常规 Command 只有 new Command({ resume: ... }),用于恢复 interrupt。

new Command({ update: ... }) 单独作为输入会基于最新 checkpoint 恢复,而不是创建普通新 turn;若图已经结束,它可能表现得像“没有执行”。State 修订应使用 updateState,常规对话应传 input object。

工具与消息完整性

工具返回 Command 时,如果同时更新 messages,必须包含与原 AI tool call ID 匹配的 ToolMessage。否则聊天历史会留下未闭合的 tool call。

ToolNode 会正确传播工具返回的 Command,并处理常见并行工具执行、错误和状态注入。手工工具节点则必须自行维护消息关联。

节点重放与幂等

节点发生 retry、interrupt 或进程恢复时,会从节点函数开头重新执行。节点内部已经发生但未由 checkpoint 记录的副作用可能重复。

推荐策略:

  • 写操作使用稳定幂等键;
  • 把外部副作用拆成独立节点或 task;
  • 写库采用唯一约束、upsert 或 compare-and-set;
  • interrupt 前不做不可重复动作;
  • 把成功外部操作的业务 ID 写入 State 后再进入下一步。

节点内调用可 checkpoint 的 task 时,恢复可复用已完成 task 结果,比把长流程全部写在一个 node 中更细粒度。

节点缓存

缓存需要两端同时配置:

  1. compile 时提供 cache backend;
  2. addNode 时提供 cachePolicy。

缓存只适合纯函数式或以完整输入为 key 的节点。模型调用、用户权限、当前时间、库存、资金和有副作用动作不能只按表面 State 缓存。

循环与 GRAPH_RECURSION_LIMIT

LangGraph 默认 recursionLimit 是 25 个 superstep。它是 invoke/stream config 的顶层选项,不应塞进 configurable。

触发 GRAPH_RECURSION_LIMIT 通常有两类原因:

  • 图的停止条件有 bug,循环永远不结束;
  • 合法复杂图确实需要更多 superstep。

处理顺序:

  1. 先检查 terminal condition、router 返回值和 State 是否真正变化;
  2. 为 evaluator-agent、重写检索等循环增加业务轮次或成本上限;
  3. 记录 runtime.executionInfo 或 metadata 中的 step;
  4. 只有确认流程有界后才提高 recursionLimit。

recursionLimit 是运行时安全网,不是业务终止条件。把它单纯调大只会延后失控、增加费用和延迟。

Runtime context

运行时 context 用于传入不可由模型控制、且不应写入 checkpoint 的依赖与身份,例如 tenantId、userId、数据库 client、feature flags。应为 context 定义 schema,并通过 Runtime 类型访问。

文档的不同段落偶尔把 context 与旧式 configurable 混用。新代码应以当前类型签名为准:业务身份和运行依赖走 context;thread_id 等持久化协议字段仍放 configurable。

runtime.executionInfo、runtime.serverInfo 等较新字段有版本要求;文档指出相应能力需较新的 graph/deepagents 版本。使用前要锁定并测试具体包版本。

三个错误的定位表

错误根因首选修复
INVALID_CONCURRENT_GRAPH_UPDATE并行节点写同一普通字段ReducedValue/reducer 或拆 key
INVALID_GRAPH_NODE_RETURN_VALUEnode 返回非 State update/Command让所有分支返回合法局部对象
GRAPH_RECURSION_LIMIT循环无停止或合法图超过步数先修终止条件,再有界调高 limit

生产检查清单

  • StateSchema 不使用 any,外部输入做运行时校验。
  • 每个并发共享字段都有明确 reducer。
  • reducer 可确定重放,不依赖完成顺序。
  • output schema 不被误当成 streaming 数据脱敏。
  • 一个 node 只有一种路由机制。
  • 动态 Command node 声明 ends。
  • Send fan-out 有数量、并发、成本与超时上限。
  • node 副作用幂等,retry/interrupt 后可安全重放。
  • 循环有业务终止条件,并保留 recursionLimit。
  • TypeScript 编译、单测和真实 checkpoint replay 都通过后再升级。