---
type: study
created: 2026-08-11
updated: 2026-08-11
sensitivity: private
status: active
tags:
  - study
  - langchain
  - langgraph
  - typescript
  - persistence
  - resilience
topic: LangGraph TypeScript 持久化、记忆、容错、子图与时间旅行
sources:
  - https://docs.langchain.com/oss/javascript/langgraph/add-memory
  - https://docs.langchain.com/oss/javascript/langgraph/checkpointers
  - https://docs.langchain.com/oss/javascript/langgraph/persistence
  - https://docs.langchain.com/oss/javascript/langgraph/stores
  - https://docs.langchain.com/oss/javascript/langgraph/fault-tolerance
  - https://docs.langchain.com/oss/javascript/langgraph/use-time-travel
  - https://docs.langchain.com/oss/javascript/langgraph/errors/INVALID_CHAT_HISTORY
  - https://docs.langchain.com/oss/javascript/langgraph/errors/MISSING_CHECKPOINTER
  - https://docs.langchain.com/oss/javascript/langgraph/errors/MULTIPLE_SUBGRAPHS
last_verified: 2026-08-11
---
# TypeScript 持久化、记忆、容错与时间旅行

## 两种持久化不要混淆

LangGraph 把状态持久化分成两个不同概念：

| 概念 | 主键与范围 | 用途 |
|---|---|---|
| Checkpointer | thread_id + checkpoint namespace | 保存某个 thread 的图执行状态、任务和恢复位置 |
| Store | namespace 数组 + key | 跨 thread 保存任意长期记忆和业务数据 |

短期记忆通常就是 messages 等 State 随 checkpoint 保存在同一 thread；长期记忆使用 Store，并用经过认证的用户/租户身份构建 namespace。

Agent Server 会管理平台持久化。进程内直接运行图时，必须显式提供 checkpointer；遗漏后使用 interrupt、getState 或 durable execution 会触发 MISSING_CHECKPOINTER。

## thread 与 checkpoint

调用带持久化的图时，需要：

    const config = {
      configurable: { thread_id: "stable-thread-id" },
    };

thread_id 是恢复指针，不是用户 ID。生产设计应区分：

- user/tenant：认证与授权主体；
- conversation/thread：一次持续会话或流程；
- run：thread 上的一次调用；
- checkpoint_id：某个历史状态；
- checkpoint_ns：根图或子图的持久化命名空间。

Postgres 文档提醒 thread_id 长度应受限，建议小于 255；不要把超长业务对象或明文敏感信息直接作为 thread_id。

## StateSnapshot 与查询接口

每个 checkpoint 对应 StateSnapshot，关键字段包括：

- values：当前 channel 值；
- next：下一步将执行的节点；
- config：可用于恢复或 replay 的 checkpoint config；
- metadata：来源、step、writes 等元数据；
- createdAt；
- parentConfig：父 checkpoint；
- tasks：当前 Pregel task、错误和 interrupt 信息。

常用接口：

- getState(config)：读取最新或指定 checkpoint；
- getStateHistory(config)：从新到旧遍历历史；
- updateState(config, values, options)：使用 reducer 写入修订并创建新 checkpoint；
- deleteThread(threadId)：删除 thread 数据。

updateState 不是原地篡改历史。它从指定 checkpoint 创建一个新分支。`asNode` 决定这次 update 被视为由哪个节点产生，也决定后续从哪个 successor 继续。

## superstep、pending writes 与恢复

LangGraph 通常在每个 superstep 形成 checkpoint。若同一 superstep 有多个并行节点，其中一个失败，已经成功分支的写入会作为 pending writes 保存；恢复时不必重新执行成功分支。

这降低了并发图的重复成本，但不替代外部副作用幂等。写入第三方系统发生在 checkpoint 提交前，进程仍可能在两者之间崩溃。

生产节点应使用：

- 稳定业务幂等键；
- 数据库唯一约束或 upsert；
- outbox/inbox；
- 外部请求 ID；
- 可查询的执行状态。

## durability 模式

运行时可在吞吐和崩溃窗口之间选择：

| 模式 | 持久化时机 | 权衡 |
|---|---|---|
| exit | 只在结束、interrupt 或 error 时 | 最快，中间崩溃可能丢失全部进度 |
| async | 与下一步并行写 checkpoint | 通常折中，但存在小的进度丢失窗口 |
| sync | 下一步开始前完成 checkpoint | 最强恢复语义，延迟最高 |

支付、审批、长任务和高成本调用通常更偏向 sync；可重算、无副作用的批处理可评估 async/exit。不能只依据性能选择，必须测进程终止和存储故障。

## checkpointer 实现

内存实现只适合开发与测试。生产可使用官方集成包对应的 Postgres、SQLite、MongoDB 或 Redis 等实现。真实部署要单独完成：

- schema/setup 或 migration；
- 连接池与超时；
- 高可用与备份；
- checkpoint retention 和清理；
- 加密、租户隔离与访问审计；
- 大 State 容量和序列化性能验证。

自定义 checkpointer 需要正确实现 put、putWrites、getTuple、list 等接口，尤其不能丢失 pending writes 和 namespace。

文档的不同页面在内存类名上出现 MemorySaver 与 InMemorySaver 混用。应以当前安装版本的导出和类型检查为准，不要只凭错误页片段复制 import。

## Store 与长期记忆

Store 使用 namespace 数组和 key：

    namespace = ["tenant", tenantId, "users", userId, "memories"]

    key = memoryId

常见操作包括 put、get、delete、search、listNamespaces。

### 普通 search

不带 query 时，可按 namespace 前缀、filter、limit 和 offset 列出记录。要注意：

- limit 截断可能是静默的；
- 不同 backend 默认排序不一定一致；
- 大集合必须分页；
- namespace 必须来自服务端认证 context，不能直接信任模型或浏览器传入的 userId。

### 语义 search

Store 可配置 embeddings、向量维度与需要索引的字段。单条 put 可以 `index: false` 或只索引指定字段。

向量维度必须与 embedding 模型一致；更换模型需要重建索引或版本化 namespace。原文、向量和元数据保留策略也要满足隐私删除要求。

### 文档字段不一致

Store 示例在不同位置混用 userId 与 user_id。它们不是自动等价的协议字段。项目应为 runtime context 定义一个 schema，并在 namespace 构造函数中统一字段名。

## 短期消息记忆

MessagesValue 配合 checkpointer 保存 thread 消息。长对话不能无限增长，常用策略：

- trim：按 token 或消息数保留窗口；
- delete：使用 RemoveMessage 删除指定消息；
- summarize：把旧历史压缩为摘要后保留近期消息；
- 新建 thread：业务上结束后不要永久续用一个 thread。

修剪时必须保持聊天协议完整：AIMessage 中的 tool_calls 与对应 ToolMessage 不能被拆开。

INVALID_CHAT_HISTORY 的典型根因是 AI tool call 没有匹配 ToolMessage，常见于：

- 手工构造输入漏掉工具结果；
- graph 在工具执行中断后，应用直接追加新的 HumanMessage；
- 消息裁剪删除了 tool result；
- 工具节点异常退出但状态被错误写入。

修复方式是补齐匹配 ToolMessage，或用 updateState 修复消息后再恢复原流程。不要在未完成 tool turn 上直接追加普通用户消息。

## retry、timeout 与 errorHandler

### 固定组合顺序

节点执行失败时：

1. timeout 可以产生 NodeTimeoutError；
2. retryPolicy 判断是否重试；
3. 重试耗尽或没有 policy 后，errorHandler 执行；
4. 没有 handler 或 handler 失败时，异常向外冒泡。

per-node timeout、errorHandler、setNodeDefaults 和 cooperative drain 需要 `@langchain/langgraph >= 1.4.0`。

### retryPolicy

重试是 opt-in；只有 node 或 graph default 配置了 policy 才会重试。空对象也会启用内置判断。默认参数：

- maxAttempts：3，含第一次；
- initialInterval：500ms；
- backoffFactor：2；
- maxInterval：128000ms；
- jitter：true；
- logWarning：true。

内置逻辑通常不重试：

- Abort/cancel；
- GraphValueError；
- ECONNABORTED；
- 多数 4xx，包括 400、401、402、403、404、405、406、407、409；
- provider 的 insufficient_quota。

408、5xx 和 NodeTimeoutError 默认可重试。GraphInterrupt 与 Command 等控制流异常会绕过 retry。

TypeScript 没有导出的 Python `defaultRetryOn` 对应 helper。自定义 retryOn 时必须自己明确白名单/黑名单，不要重试权限错误、参数错误和不可幂等写操作。

### timeout

节点 timeout 可写成毫秒数，也可区分：

- runTimeout：单次 attempt 的硬墙钟上限，不因进度刷新；
- idleTimeout：没有可观察进度时触发；
- refreshOn: auto：State 写入、custom stream、子任务调度、LangChain callback 等刷新 idle；
- refreshOn: heartbeat：仅显式 runtime.heartbeat 刷新。

NodeTimeoutError 提供 node、elapsed、kind、timeout、idleTimeout、runTimeout。用 isNodeTimeoutError 做 TypeScript narrowing。

Send 可以为单个动态 dispatch 覆盖目标节点 timeout。超时 attempt 的 buffered writes 会清理，但外部系统已经发生的副作用不会自动回滚。

### errorHandler

StateGraph.addNode 可配置 errorHandler，接收 State 和 NodeError；返回 State update 或 Command 进入补偿路径。它适合 Saga 式退款、释放库存、标记流程失败，而不是把所有异常吞成“成功”。

限制：

- 只支持 StateGraph，不支持低层 Graph；
- Functional task/entrypoint 没有 errorHandler；
- 每个 node 只有一个 handler；
- handler 自身失败会冒泡；
- interrupt 不进入 handler；
- 子图未处理异常会作为父节点异常到达父 handler。

失败来源会 checkpoint；若进程在 node 失败和 handler 完成之间崩溃，恢复后的 handler仍能看到同一 NodeError。

## setNodeDefaults

setNodeDefaults 可以统一 retryPolicy、timeout、errorHandler 与 cachePolicy。单 node 配置优先，defaults 在 compile 时解析，因此调用顺序不影响优先级。

边界：

- defaults 不传给子图；每个子图单独配置；
- handler node 可以继承 retry 和 timeout；
- handler node不能再继承 errorHandler，避免自捕获；
- handler node不继承 cachePolicy，避免缓存补偿结果。

## cooperative graceful shutdown

RunControl.requestDrain 在 superstep 边界请求停止：

- 正在执行的 node 和 retry loop 不被抢占；
- 边界处保存可恢复 checkpoint；
- 若图自然完成，正常返回；
- 若仍有步骤，抛出 GraphDrained；
- 之后用同一 thread config 和 null 输入恢复。

drain 不取消正在运行的异步工作。SIGTERM 处理应同时设置 supervisor 的宽限期，并在需要硬上限时配合 AbortSignal。`runtime.control` 可让 node 感知 drainRequested，主动跳过昂贵工作。

## replay、fork 与 time travel

### Replay

从历史 StateSnapshot.config 调用 invoke(null, checkpointConfig)，checkpoint 之前的节点不执行，之后的节点重新执行。

Replay 不是读取缓存：LLM、API、interrupt 都会再次发生。对最终 checkpoint replay 因 next 为空而没有动作。

### Fork

对历史 checkpoint 调用 updateState，得到新 checkpoint config，再 invoke(null, forkConfig)。原历史保留，新历史从该点分叉。

需要显式 asNode 的情况：

- 同一 step 有多个并行 writer，无法推断最后节点；
- 新 thread 上预置测试 State；
- 希望把 update 视为某个后续节点已经完成。

### Time travel 的副作用边界

回放节点必须安全重复调用。生产界面提供 time travel 前，应标出哪些节点会：

- 发邮件或支付；
- 修改数据库；
- 消耗外部配额；
- 触发审批和通知。

对不可重复动作，使用幂等键、dry-run、人工确认或在 fork 中替换 adapter。

## 子图持久化的三种模式

| 模式 | compile 配置 | 能力 | 主要限制 |
|---|---|---|---|
| Per-invocation | 默认继承 | 当前调用可 interrupt/恢复；每次调用独立 namespace | 不跨调用积累子图记忆 |
| Per-thread | `checkpointer: true` | 跨调用保留子图 State | 同一子图不支持并行多次调用 |
| Stateless | `checkpointer: false` | 无 checkpoint 开销 | 无 interrupt、恢复、state inspection |

MULTIPLE_SUBGRAPHS 通常发生在同一个有 per-thread persistence 的子图被并行或多次调用，多个调用写相同 namespace。

解决顺序：

- 不需要长期子图记忆：使用默认 per-invocation；
- 不需要 interrupt/恢复：显式 stateless；
- 必须 per-thread：禁止同一子图并行调用；
- 多个不同 per-thread 子图：给它们稳定、唯一的 node name，防止调用顺序变化导致 namespace 串线。

官方 TypeScript 页面部分表格和说明仍写 `True`、`False`、`None` 或 `checkpointer=True`，这是 Python 表述残留。TypeScript 实际值是 true、false、undefined/省略。

### 子图 time travel 粒度

- 默认继承 checkpointer 时，父图把整个子图视为一个 parent superstep；父级只能从子图前回放，整个子图重跑。
- 子图 `checkpointer: true` 时，子图内部有自己的 checkpoint history，可以在内部节点之间 fork。
- 读取子图 checkpoint 需 getState(config, { subgraphs: true })，再从 task state 提取子 config。

## 生产验收清单

- thread_id、userId、tenantId、runId 明确分离。
- 内存 saver 仅用于测试；生产 backend 完成 migration、备份和恢复演练。
- 根据业务选择 durability，并测试强制终止窗口。
- State 有大小上限、retention、PII 删除与加密策略。
- Store namespace 只由服务端身份构造。
- message trim 不破坏 tool_call/ToolMessage 配对。
- retry 仅覆盖暂时性且幂等错误；timeout 不替代取消与资源清理。
- handler 记录失败且补偿可重复执行。
- SIGTERM 使用 drain + 合理宽限期，不假设 requestDrain 会中止网络调用。
- replay/fork 前标记外部副作用，避免重复真实操作。
- per-thread 子图禁止同一实例并行调用，并使用稳定 namespace。
