高级功能

时间旅行

使用 Time Travel 回放历史、从过去分叉并定位复杂问题

📚 学习目标

学完这篇文章后,你将能够:

  • 理解 Time Travel 在 LangGraph 里的真正含义
  • getStateHistory / getState / updateState 进行回放与分叉
  • 设计可复现、可比较、可回滚的调试流程

前置知识

在开始学习之前,建议先阅读:


1 什么是时间旅行?

在 LangGraph 中,“时间旅行”并不是魔法功能,而是基于 checkpoint 历史 的三类能力:

  1. 回看过去:查看某次执行在某一步的状态。
  2. 从过去继续:以旧 checkpoint 为起点继续执行。
  3. 从过去分叉:修改旧状态,走出一条新路径并与原路径对比。

可以把它理解成 Git:

  • checkpoint ≈ commit
  • thread_id ≈ 分支名
  • 从 checkpoint 继续执行 ≈ 从历史 commit 开新分支继续开发

2 使用前提

想用时间旅行,必须同时满足:

  1. 编译图时配置 checkpointer
  2. 调用图时始终携带同一个 thread_id
  3. 需要回看/分叉时,提供目标 checkpoint_id

如果缺少任意一项,时间线就不完整或不可恢复。


3 核心 API 速览

API作用常见用途
getStateHistory(config)读取该线程的历史快照排障、定位问题步骤
getState(config)读取某个 checkpoint 的状态精准检查某一步输入输出
updateState(config, patch)修改某个 checkpoint 对应状态人工修正、A/B 分叉起点
invoke(input, config)从当前或指定 checkpoint 继续执行回放、分叉后重跑

推荐把这四个 API 当成一组使用,而不是单独使用。


4 最小可运行示例

下面示例展示完整基础链路:执行一次图,拿到 checkpoint 历史。

import {
  Annotation,
  StateGraph,
  START,
  END,
  MemorySaver,
} from '@langchain/langgraph';
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';

const State = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (a, b) => a.concat(b),
    default: () => [],
  }),
  step: Annotation<number>({
    reducer: (_prev, next) => next,
    default: () => 0,
  }),
});

const step1 = async (state: typeof State.State) => ({
  step: state.step + 1,
  messages: [new AIMessage('执行 step1')],
});

const step2 = async (state: typeof State.State) => ({
  step: state.step + 1,
  messages: [new AIMessage('执行 step2')],
});

const step3 = async (state: typeof State.State) => ({
  step: state.step + 1,
  messages: [new AIMessage('执行 step3')],
});

const app = new StateGraph(State)
  .addNode('step1', step1)
  .addNode('step2', step2)
  .addNode('step3', step3)
  .addEdge(START, 'step1')
  .addEdge('step1', 'step2')
  .addEdge('step2', 'step3')
  .addEdge('step3', END)
  .compile({ checkpointer: new MemorySaver() });

const config = { configurable: { thread_id: 'demo-thread' } };

// 先跑一次,生成时间线
await app.invoke({ messages: [new HumanMessage('开始')] }, config);

5 定位关键 checkpoint

你通常不会用全部历史,而是先找“问题发生前后”的关键点。

const checkpointIds: string[] = [];

for await (const snap of await app.getStateHistory(config)) {
  const id = snap.config?.configurable?.checkpoint_id;
  if (id) checkpointIds.push(id);
}

// 例如拿倒数第二个点做分析
const target = checkpointIds.at(-2);
if (target) {
  const snapshot = await app.getState({
    configurable: {
      thread_id: 'demo-thread',
      checkpoint_id: target,
    },
  });

  console.log('step:', snapshot.values.step);
  console.log(
    'messages:',
    snapshot.values.messages.map((m) => String(m.content)),
  );
}

实践建议:

  • 出问题时,先记录“用户输入 + thread_id + checkpoint_id”三元组。
  • 不要只看最终报错,要看报错前一跳的状态。

6 从过去分叉一条新路径

时间旅行真正有价值的地方是“同起点比较不同策略”。

import { HumanMessage } from '@langchain/core/messages';

const pastConfig = {
  configurable: {
    thread_id: 'demo-thread',
    checkpoint_id: 'kp_xxx',
  },
};

// 1) 改写过去状态(例如改用户意图)
await app.updateState(pastConfig, {
  messages: [new HumanMessage('我真正想问的是天气,不是股票')],
});

// 2) 从这个点继续执行
const forkResult = await app.invoke(null, pastConfig);
console.log(forkResult);

分叉对比流程


7 典型场景

7.1 问题定位

  • 现象:同一个问题有时回答正常,有时突然发散,或工具调用次数异常增多。
  • 操作步骤
    1. 先用 getStateHistory 找到“第一次出现异常输出”对应的 checkpoint。
    2. 再读取它前一个 checkpoint(异常前一跳),比较关键字段(messages、路由字段、工具结果)。
    3. 确认是“输入状态污染”还是“当前节点逻辑问题”。
  • 成功判定
    • 你能明确指出“从哪一个 checkpoint 开始偏离预期”。
    • 你能给出具体原因,例如“messages 长度膨胀导致模型偏航”或“工具返回结构变化导致路由错误”。

7.2 A/B 实验

  • 现象:你想比较两种 prompt、两套工具参数,或两种路由策略到底哪个更好。
  • 操作步骤
    1. 选择同一个基准 checkpoint 作为实验起点。
    2. 分叉出 A 路径和 B 路径(可通过 updateState 写入不同配置字段)。
    3. 分别 invoke 跑完两条路径,记录结果质量、耗时、工具调用次数。
  • 成功判定
    • 两组实验起点完全一致(同一个 checkpoint),保证对比公平。
    • 最终有可量化结论,而不是“感觉 A 更好”。

7.3 人工修正后重跑

  • 现象:用户反馈“中间理解错了”,但你不想整条线程从头重跑。
  • 操作步骤
    1. 定位到“理解错误发生前”的 checkpoint。
    2. updateState 修正关键状态(例如用户意图、工具输入、中间结论)。
    3. 从该点继续 invoke,生成修正后的新结果。
  • 成功判定
    • 修复只影响后续路径,不破坏原始历史。
    • 你可以同时保留“原始输出”和“修正后输出”用于回溯与审计。

8 与 Studio 协作

Studio 非常适合“看全局 + 精准回放”:

  1. 在 Studio 找到目标 thread_id
  2. 用时间线定位可疑步骤。
  3. 记录对应 checkpoint_id
  4. 在代码侧用 getState/updateState 做精准验证。

职责分工建议:

  • Studio:定位“问题在哪一步”。
  • 代码:验证“这一步为什么错、怎么修”。

9 性能与治理建议

  1. 控制状态体积:messages 不做裁剪会让历史查询越来越慢。
  2. 保留关键字段:checkpoint 是排障证据,不要只存最终答案。
  3. 建立命名规范:为 thread 加业务前缀,方便检索与审计。
  4. 限制分叉数量:无管理的分叉会让调试成本上升。
  5. 把回放流程产品化:给团队沉淀固定排障 SOP。

💡 练习题

  1. 操作题:- 运行一个 3 步图(A -> B -> C)。- 找到 B 后的 checkpoint_id。- 修改状态中的一个关键字段。- 从该点继续执行,并与原始 C 对比。

    点击查看答案

    核心链路是:getStateHistory -> getState -> updateState -> invoke。 对比时至少记录两项:最终输出差异、执行路径差异。

  2. 调试题:某节点偶发报错,你如何确保团队能复现?

    点击查看答案

    记录并共享 thread_id + checkpoint_id + 输入样本 + 代码版本。 没有这四项,复现通常不稳定。

  3. 设计题:什么时候不应该用时间旅行?

    点击查看答案

    对一次性、无状态、低价值请求,不必保存完整历史。 时间旅行适合高价值流程和难复现问题,不适合所有请求默认开启重度历史追踪。


📚 参考资源

官方文档

本项目相关内容


✅ 总结

本章要点

  • 时间旅行本质是“基于 checkpoint 的回放与分叉”。
  • 四个核心 API 需要配合使用,而不是单点调用。
  • 真正的价值是:可复现、可比较、可回滚。

下一步:进入部署章节,学习如何把这些调试能力带到生产环境。

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page