---
type: study
created: 2026-08-11
updated: 2026-08-11
sensitivity: private
status: active
tags:
  - study
  - langchain
  - langgraph
  - typescript
  - concurrency
topic: LangGraph TypeScript Graph API、类型系统、并发与错误
sources:
  - https://docs.langchain.com/oss/javascript/langgraph/graph-api
  - https://docs.langchain.com/oss/javascript/langgraph/use-graph-api
  - https://docs.langchain.com/oss/javascript/langgraph/errors/GRAPH_RECURSION_LIMIT
  - https://docs.langchain.com/oss/javascript/langgraph/errors/INVALID_CONCURRENT_GRAPH_UPDATE
  - https://docs.langchain.com/oss/javascript/langgraph/errors/INVALID_GRAPH_NODE_RETURN_VALUE
last_verified: 2026-08-11
---
# TypeScript Graph API：State、并发与错误

## 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_VALUE | node 返回非 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 都通过后再升级。
