首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >LangGraph 是怎么让 Agent "记住"对话的?拆解 thread 与 Checkpoint

LangGraph 是怎么让 Agent "记住"对话的?拆解 thread 与 Checkpoint

作者头像
windealli
发布2026-06-24 20:11:30
发布2026-06-24 20:11:30
1520
举报
文章被收录于专栏:windealliwindealli

❝大模型本身是"过目就忘"的——每次调用都是一张白纸。可你用 LangGraph 搭的 Agent,却能续聊、能恢复、甚至能"回到三步之前重新跑"。这背后靠的是两个核心概念:「thread(一次对话)」「Checkpoint(每一步的状态快照)」。这篇文章用 6 张图把 LangGraph 的会话管理彻底讲透,并在最后引申一下生产级框架 DeepAgents 在它之上做了哪些加固。 ❞

开场:模型没记性,记性是"框架"给的

先说个反直觉的事实:大模型 API 是**无状态(stateless)**的。它每次被调用,只能看到这一次请求里塞进去的内容,「根本不记得」上一轮你说过什么。

那"连续对话"是怎么来的?靠框架在背后维护一份历史,每次调用都重新递给模型。这层"管历史"的基础设施,就是「会话管理(Session Management)」

不同框架给出的答案差异很大。LangGraph 的答案尤其有特点——它没有单独造一个"会话模块",而是把会话管理「收进了一个更通用的抽象」:状态图(StateGraph)+ 检查点(Checkpoint)。理解了这套抽象,你不仅懂了它怎么记对话,还顺带理解了它为什么能做到时间旅行、断点续跑这些别家做不到的事。

我们从最外层的 thread 开始往里剥。


一、一次对话 = 一个 thread

在 LangGraph 里,一段连续对话被称为一个 「thread」(线程,但跟操作系统的线程没关系,理解成"对话线"更准确)。每个 thread 由一个 thread_id 唯一标识。

你调用 Agent 时,thread_id 不是写在请求体里,而是放在一个叫 config.configurable 的配置包里传进去:

代码语言:javascript
复制
config = {"configurable": {"thread_id": "abc-123"}}

# 第一轮
await agent.ainvoke({"messages": [HumanMessage("帮我建个项目")]}, config=config)
# 第二轮——只要带上同一个 thread_id,它就接着上一轮聊
await agent.ainvoke({"messages": [HumanMessage("再加个测试")]}, config=config)

注意第二轮:我们只塞了一句新消息,「完全没有手动把第一轮的历史拼进去」。LangGraph 自己根据 thread_id 把这个 thread 之前的状态捞了出来、接着往后跑。这就是 thread 的意义——「它是"自动续聊"的钥匙」

换一个 thread_id,就是开一段全新的、互不干扰的对话。多用户、多会话的隔离,本质上就是发不同的 thread_id。(DeepAgents 里用 UUID7 来生成新的 thread id。)

那么问题来了:LangGraph 凭什么能根据 thread_id 把历史"捞出来"?答案在下一层——Checkpoint。


二、核心机制:每走一步,拍一张快照

这是 LangGraph 会话管理「最该理解的一点」:它的存储模型不是"一个消息数组",而是「一连串状态快照」

LangGraph 把 Agent 的运行看成一张图,每执行一个节点(比如"调一次模型""跑一个工具"),就把当时的「完整状态」拍一张快照存下来,这张快照就叫一个 「Checkpoint」

每个 Checkpoint 有三个关键标识:

  • thread_id:属于哪段对话;
  • checkpoint_id:这是该对话里的第几张快照(单调递增);
  • parent_checkpoint_id:上一张快照是谁——靠它,所有快照串成一条「链」

落到存储里(以 SQLite 为例),就是这样一张表,主键是三元组 (thread_id, checkpoint_ns, checkpoint_id)

代码语言:javascript
复制
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
-- 还有 parent_checkpoint_id 字段,把快照前后串起来

checkpoint_ns(namespace)是给子图用的命名空间,主 Agent 默认是空串,先不用管它。 ❞

为什么要存"快照链"而不是"消息数组"?因为快照链能干两件数组干不了的事:「精确恢复到任意一步」,以及「从任意一步岔出新分支」(第六节细说)。代价是体积更大——这点 DeepAgents 有专门优化,最后一节会讲。


三、快照里到底装了什么:State 与 channels

既然每个 Checkpoint 是"完整状态的快照",那这个"状态"是什么?

在 LangGraph 里,状态(State)是一个「带类型的字典」,里面是若干个字段,每个字段叫一个 「channel(通道)」。对一个对话 Agent 来说,最核心的 channel 就是 messages——对话历史本身。

channel 有个很关键的特性:「它不是简单赋值,而是带"归并规则(reducer)"的」。比如 messages 这个 channel 的规则是"追加"——你这一步返回几条新消息,框架就把它们「追加」到已有历史后面,而不是覆盖。这就是为什么你每轮只用返回新消息,历史会自动累积。

除了 messages,State 里还能放别的 channel。DeepAgents 就额外塞了几个私有字段,比如:

  • _summarization_event:记录"上次在哪儿压缩过、摘要是什么";
  • _context_tokens:当前上下文用了多少 token。

这些私有 channel 跟着 Checkpoint 一起持久化,但「不会」直接发给模型——它们是会话管理自己的"账本"。

一句话记住:「Checkpoint 存的是整个 State(所有 channel),而不只是聊天记录。」 这也是为什么 LangGraph 能恢复的不仅是"说过的话",还有 Agent 运行到一半的各种中间状态。


四、Checkpoint 存哪儿?可插拔的 Checkpointer

负责"把 Checkpoint 写进去、读出来"的组件,叫 「Checkpointer」。它是一个接口,后端可以随意替换:

Checkpointer

存储介质

典型场景

MemorySaver

进程内存

测试、临时跑、demo(重启即丢)

AsyncSqliteSaver

本地 SQLite 文件

单机持久化(DeepAgents CLI 默认)

AsyncPostgresSaver

PostgreSQL

生产、多实例共享

用法就是编译 graph 时把它传进去:

代码语言:javascript
复制
from langgraph.checkpoint.memory import MemorySaver

agent = create_agent(model, tools, checkpointer=MemorySaver())

「关键点:没有 checkpointer,就没有记忆。」 如果你编译 graph 时不传 checkpointer,那它就是无状态的——每次调用都从头开始,thread_id 也救不了你。这是新手最容易踩的坑:明明传了 thread_id 却发现不记事,十有八九是忘了配 checkpointer。

DeepAgents 的 CLI 在生产模式下默认用 AsyncSqliteSaver,把所有对话落到 ~/.deepagents/sessions.db 这个文件里。它甚至还会直接查这张表来实现"会话列表""删除会话"这类管理功能,绕过 Agent 直接读底层数据,图个快。


五、恢复对话:同一个 thread_id 就够了

有了 thread + checkpointer,"恢复对话"这件听起来很复杂的事,就变得朴素得不像话:

「继续聊」:用同一个 thread_id 再发一次请求就行。LangGraph 自动找到这个 thread 的最新 Checkpoint,把状态恢复出来,接着往后跑。程序重启过?没关系,只要 checkpointer 是持久化的(SQLite/Postgres),历史还在磁盘上。

「把历史读出来」(比如刷新 UI 要重新渲染对话):调 aget_state

代码语言:javascript
复制
state = await agent.aget_state(config)   # config 里带着 thread_id
messages = state.values["messages"]       # 历史消息都在这儿

「手动改状态」(比如插入一条系统提示、或人工干预):调 aupdate_state,它会基于当前快照生成一张新快照。

❝一个工程细节:DeepAgents 在客户端-服务端模式下,服务器重启后可能出现"磁盘上有 Checkpoint、但内存里的 thread 记录没了"的错位,它专门写了个 aensure_thread 来先确保 thread 记录存在,再操作状态。生产级框架的麻烦,往往都藏在这种边角料里。 ❞


六、杀手锏:时间旅行与分叉

快照链相比"消息数组"最大的优势,在这一节兑现。

因为每一步都留了快照、快照之间又有父子链,所以你能做两件很酷的事:

「① 时间旅行(time-travel)」:在 config 里指定一个「历史」checkpoint_id,就能让 Agent"回到那一刻"的状态。配合 get_state_history 把整条链列出来,你可以挑任意一张快照重放。

代码语言:javascript
复制
# 列出这个 thread 的所有历史快照
async for snapshot in agent.aget_state_history(config):
    print(snapshot.config["configurable"]["checkpoint_id"])

# 指定回到某一张
old_config = {"configurable": {"thread_id": "abc-123", "checkpoint_id": "xxx"}}
await agent.ainvoke(new_input, config=old_config)

「② 分叉(fork)」:从历史某张快照重新跑、且给不同的输入,就会「岔出一条新的快照分支」——原来的历史完好无损,新的探索另起一条线。这对"我想试试换个方向会怎样"的场景极其有用。

值得说明的是:「这是 LangGraph 框架层提供的能力」。建立在它之上的 DeepAgents 出于产品简洁性考虑,目前「没有把时间旅行/分叉暴露给终端用户」——它只用 checkpoint_id 做了"会话列表缓存失效"之类的小事。能力在底座,用不用是上层产品的选择。


七、别混淆:Session(短期)vs Store(长期记忆)

聊到这里要澄清一个特别容易搞混的概念。我们前面讲的所有东西——thread、Checkpoint——管的都是「一段对话内部」的记忆,是「短期」的、按 thread 隔离的。

但 Agent 还需要另一种记忆:「跨对话的长期记忆」。比如"这个用户偏好用中文""上个月帮他建过一个电商项目"——这些信息不该绑死在某一个 thread 里,换一段新对话也得记得。

LangGraph 把这两件事「分得很干净」

Checkpointer

Store

管什么

一段对话的状态(messages 等)

跨对话的长期知识

隔离维度

按 thread_id

按命名空间(如按 user)

生命周期

跟随这段会话

长期留存

典型内容

对话历史、运行中间态

用户画像、学到的技能/事实

设计你自己的 Agent 时,先想清楚每条信息该进哪边:「"只在这次对话有用"的进 Checkpoint,"换个对话也得记得"的进 Store。」 想不清楚,后面就是数据串台和隐私泄露的坑。


八、引申:DeepAgents 在 LangGraph 之上做了什么加固

LangGraph 给的是通用底座,但直接拿去跑长任务(比如让 Agent 写一整个项目)会遇到两个现实问题。DeepAgents 作为生产级框架,针对性地做了加固:

[图 6:两种记忆 + DeepAgents 的生产级加固] (看下半部分:DeltaChannel + Summarization)

「加固一:DeltaChannel——给快照"瘦身"。」 前面说快照链体积大,到底有多大?如果每张快照都把全量 messages 拷一遍,存储量会随对话轮数按 「O(N²)」 膨胀(第 N 轮要存前 N 条 × N 张快照)。DeepAgents 用一个叫 DeltaChannel 的机制:平时只存"这一步新增了什么(delta)",每隔 50 步才拍一张全量快照,把增长压回 「O(N)」。对长任务来说,这是从"撑不住"到"扛得住"的区别。

「加固二:SummarizationMiddleware——上下文压缩 + 落盘。」 对话太长撑爆窗口时,它会:

  1. 把要丢弃的旧历史「先归档」到一个 Markdown 文件(按 thread 一个文件,路径形如 /conversation_history/{thread_id}.md);
  2. 用一段 LLM 摘要替换掉模型输入里的旧消息;
  3. 在 State 的 _summarization_event 字段里记下"在哪儿切的、归档到哪了"。

妙处在于:压缩是「非破坏式」的——原始历史落了盘,Agent 随时能 read_file 翻回来;摘要、切点都跟着 Checkpoint 一起持久化,所以「恢复对话时连"压缩状态"也能一并恢复」。(这部分我之前单独写过一篇深读,感兴趣的可以翻文末延伸阅读。)

一句话总结这一节:「LangGraph 负责"能记、能恢复",DeepAgents 负责"长期跑也不崩、不丢"。」


结语:把会话管理"藏"进通用抽象

回头看,LangGraph 处理会话管理的思路很有辨识度:「它不把"对话记忆"当成一个特殊模块,而是当成"状态图 + 检查点"这套通用机制的一个自然结果。」

  • 一次对话 = 一个 thread
  • 记忆 = 每一步的 Checkpoint 快照;
  • 续聊 = 同一个 thread_id
  • 后悔药 = 沿快照链时间旅行 / 分叉;
  • 长期记忆 = 另一套 Store,跟会话分开。

这套设计的好处是「统一而强大」——你为"让 Agent 跑复杂流程"配的检查点机制,顺手就把会话管理给解决了,还白送了时间旅行。代价是抽象层级偏高、快照体积偏大,需要 DeepAgents 这类上层框架再做一层调教,才能稳稳地扛住真实的长任务。

下次你用 LangGraph 搭 Agent,发现"它怎么不记事"时,先别慌——大概率只是忘了给它配一个 checkpointer 而已。


延伸阅读

  • 上下文压缩深读(本文第八节 Summarization 的展开):Agent 聊着聊着就"失忆"?拆解 DeepAgents 的上下文压缩黑科技
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 海天二路搬砖工 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 开场:模型没记性,记性是"框架"给的
  • 一、一次对话 = 一个 thread
  • 二、核心机制:每走一步,拍一张快照
  • 三、快照里到底装了什么:State 与 channels
  • 四、Checkpoint 存哪儿?可插拔的 Checkpointer
  • 五、恢复对话:同一个 thread_id 就够了
  • 六、杀手锏:时间旅行与分叉
  • 七、别混淆:Session(短期)vs Store(长期记忆)
  • 八、引申:DeepAgents 在 LangGraph 之上做了什么加固
  • 结语:把会话管理"藏"进通用抽象
  • 延伸阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档