学习笔记 · Obsidian

TypeScript 持久化、记忆、容错与时间旅行

LangChainLangGraphTypeScript

两种持久化不要混淆

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

概念主键与范围用途
Checkpointerthread_id + checkpoint namespace保存某个 thread 的图执行状态、任务和恢复位置
Storenamespace 数组 + 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。

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

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

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-threadcheckpointer: true跨调用保留子图 State同一子图不支持并行多次调用
Statelesscheckpointer: 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。