NestJS + LangChain:会话状态保存器checkpointer
NestJS + LangChain:checkpointer 详解,从原理到代码落地
一、先说结论:checkpointer 到底是什么
一句话:
checkpointer 就是 Agent 的“会话状态保存器”。
它负责把当前对话过程中的状态保存下来,并在下一次相同线程调用时再读回来。
在 LangChain / LangGraph 体系里,短期记忆的本质并不是“模型自己记住了什么”,而是:
- 当前 Agent 运行过程中的状态会被保存
- 下一次调用时,再根据线程标识把状态读回来
- 从而形成“多轮会话”的效果
你可以把它理解成下面这组关系:
- Agent:会思考、会调用工具的执行体
- State:当前会话里的消息、工具调用结果、摘要等状态
- Checkpointer:负责把 State 保存和恢复
- thread_id:告诉 checkpointer,这轮调用属于哪条会话
所以:
- 没有 checkpointer,Agent 每次都是“单轮”的
- 有了 checkpointer,但没有 thread_id,也不知道该接哪条会话
二、checkpointer 和普通“聊天上下文”有什么区别
很多人第一次接触时,容易把 checkpointer 和“自己手动传历史消息”混在一起。
1. 普通聊天上下文
最直接的做法是,自己维护完整消息数组:
messages: [
new HumanMessage("我叫张三"),
new AIMessage("你好张三"),
new HumanMessage("我刚才说我叫什么"),
]
这种方式虽然也能让模型看到历史,但问题很多:
- 需要自己维护历史消息
- controller / service 会越来越乱
- 多用户会话隔离麻烦
- 后续做裁剪、摘要、删除记忆会很难扩展
2. 使用 checkpointer
使用 checkpointer 时,你通常只需要传当前这一轮输入:
messages: [new HumanMessage("我刚才说我叫什么")]
然后框架会根据 thread_id 自动找到这一条线程之前保存过的状态,再继续执行。
也就是说,普通上下文是 你手动维护历史,而 checkpointer 是 框架帮你管理线程级状态。
这也是为什么在 Agent 场景里,checkpointer 比“手动堆历史消息”更适合做真正的多轮会话。
三、checkpointer 的核心工作流
理解 checkpointer,最简单的方式就是看它的完整执行流程。
整个过程可以拆成 4 步。
第 1 步:创建 Agent 时挂上 checkpointer
createAgent({
model,
tools,
checkpointer: new MemorySaver(),
})
这一步的含义是:
以后这个 Agent 每次运行时,它的状态都交给这个 checkpointer 管理。
这里的 MemorySaver() 是最常见的开发阶段方案,表示把状态先存在内存里。
第 2 步:调用时传入 thread_id
agent.invoke(
{
messages: [new HumanMessage(question)],
},
{
configurable: {
thread_id: "user-001",
},
},
);
这一步的含义是:
当前这次调用,属于
user-001这条会话线程。
也就是说,checkpointer 不是“全局存一份状态”,而是按 thread_id 来区分不同会话。
第 3 步:运行前读取历史状态
如果 user-001 这条线程以前已经保存过状态,那么本轮执行开始前,checkpointer 会先把这条线程对应的状态读出来。
这一层通常是框架内部自动完成的,业务代码里看不到,但它确实在发生。
第 4 步:执行结束后保存新状态
本轮执行结束后,新状态会再次保存回 user-001 这条线程下。
比如这一轮发生了:
- 用户问了一句天气
- Agent 调用了天气工具
- Agent 返回了最终答案
那么这些新的状态就会继续被保存下来,供下一轮继续使用。
这就是为什么同一个 thread_id 下,多轮会话能串起来。
四、最常见的两种 checkpointer
在实际开发里,最常见的是下面两种。
1. MemorySaver
这是开发阶段最常用的方案。
特点:
- 数据保存在内存中
- 接入非常简单
- 适合本地调试
- 服务一重启,状态就丢失
适用场景:
- 本地开发
- Demo 演示
- 先验证多轮记忆链路是否正确
2. PostgresSaver
这是更适合生产环境的方案。
特点:
- 数据保存在数据库中
- 服务重启不会丢失
- 多实例部署时可共享状态
- 更适合正式业务场景
适用场景:
- 正式项目
- 真实用户会话
- 需要持久化历史
- 需要排查某些线程状态
所以一般建议是:
- 开发阶段先用
MemorySaver - 确认逻辑稳定后,再升级到
PostgresSaver
五、在 Nest 项目里,checkpointer 应该放在哪?
这部分是最容易让人困惑的地方。
在我的这个 Nest 项目里,Agent 是在 tools.service.ts 里创建的,所以 checkpointer 也应该放在这里。
原因其实很简单:
checkpointer 不是一个独立的外挂配置,而是 Agent 状态管理能力的一部分。
而 Agent 是通过 createAgent() 创建出来的,所以谁负责创建 Agent,谁就应该决定:
- model 是谁
- tools 是哪些
- systemPrompt 是什么
- middleware 用哪些
- checkpointer 用哪个
比如我当前项目里的核心结构大致是这样:
this.agent = createAgent({
model: this.getModel(),
tools: [weatherNowTool, weather3DTool, weather7dTool],
checkpointer: new MemorySaver(),
systemPrompt: '...',
});
从这段代码就可以看出,createAgent() 这一层其实已经在做 Agent 的完整装配。
所以虽然文件名叫 tools.service.ts,但在职责上,它实际上已经不只是“工具服务”,而更像一个 AgentService。
为什么不能随便放到别的地方?
很多人会下意识觉得 checkpointer 可以放在:
controllerchat.service.ts- 某个全局配置文件
但这些位置其实都不太合理。
1. 不能放在 Controller
Controller 的职责是接收请求参数,比如:
- 用户输入的问题
threadIduserName
它不应该关心 Agent 内部是怎么构造的。
如果把 checkpointer 放在 Controller,就会让接口层和 AI 运行机制耦合在一起。
2. 不适合优先放在 ChatService
在当前这个项目里,ChatService 更适合做业务转发,比如:
- 接收参数
- 调用
ToolsService.ask() - 返回结果
但真正决定 Agent 长什么样的,不是 ChatService,而是 createAgent() 那一层。
3. 最合理的位置就是创建 Agent 的地方
因为从本质上讲,checkpointer 就是 Agent 的“记忆系统”。
你可以把它理解成:
model是大脑tools是能力systemPrompt是规则checkpointer是记忆系统
这些都应该在 Agent 创建时统一定义,而不是分散在其它层里。
六、在当前项目里,checkpointer 的实际作用是什么?
在没有加 checkpointer 之前,我的 Agent 每次调用其实都是“单轮”的。
也就是说:
- 这一轮回答完就结束
- 下一轮进来时,它并不知道上一轮聊过什么
但当我在 createAgent() 里加上:
checkpointer: new MemorySaver()
含义就变成了:
这个 Agent 具备了“保存和恢复会话状态”的能力。
不过要注意:
只有 checkpointer 还不够,还必须配合 thread_id 一起使用。
因为 checkpointer 只负责“记住状态”,但它不知道这份状态属于谁。
真正用来区分不同会话的,是调用 invoke() 时传入的:
configurable: {
thread_id: threadId,
}
所以这两者的关系可以概括成:
checkpointer:负责保存和恢复记忆thread_id:负责区分记忆属于哪条会话
少了任意一个,都无法形成真正的多轮会话能力。
七、为什么开发阶段先用 MemorySaver?
在当前这个 Nest 项目里,我优先使用的是:
checkpointer: new MemorySaver()
原因是它最适合开发阶段快速验证功能。
它的优点很明显:
- 接入简单
- 不需要额外数据库
- 本地调试方便
- 能快速验证多轮记忆是否生效
但它也有很明确的限制:
- 数据只存在内存里
- 服务一重启就会丢失
- 不适合正式线上环境
所以更合理的做法是:
- 先用
MemorySaver跑通整条链路 - 确认多轮对话逻辑没问题
- 后续再替换成
PostgresSaver
这样开发成本最低,也最稳。
八、基于当前项目结构,checkpointer 的位置意味着什么?
如果从代码结构来看,我当前项目大致形成了这样的分层:
chat.controller.ts:接收 HTTP 请求chat.service.ts:做业务转发tools.service.ts:真正创建和调用 Agentqweather.service.ts:负责对接外部天气 API
所以 tools.service.ts 这一层,本质上已经不是单纯的“工具管理器”了,而是整个 AI Agent 的装配中心。
也正因为如此,下面这些东西都应该放在这一层统一处理:
- 模型实例
- tools 注册
- systemPrompt
- middleware
- checkpointer
这也是为什么我会说:
在当前这个 Nest 项目里,
ToolsService实际上已经承担了AgentService的角色。
九、thread_id 到底是什么?
这是 checkpointer 相关内容里最容易被问到的问题。
你可以把 thread_id 理解成:
“会话编号”
比如:
- 用户 A:
thread_id = user-a - 用户 B:
thread_id = user-b
或者更细一点:
- 张三第一次会话:
thread_id = zhangsan-chat-1 - 张三第二次会话:
thread_id = zhangsan-chat-2
它的作用不是身份认证,而是:
告诉 checkpointer,这次调用应该去读写哪条会话状态。
同一个 thread_id
会继续接之前的对话。
不同的 thread_id
会话完全隔离。
所以从本质上讲,thread_id 就是 checkpointer 的“索引键”。
十、一个最小测试例子
第一次请求
GET /chat/ask?question=我叫张三&threadId=user-1
第二次请求
GET /chat/ask?question=我刚才说我叫什么&threadId=user-1
因为两次请求使用的是同一个 threadId=user-1,所以第二次调用时,checkpointer 会先把第一次的状态读回来,然后再继续回答。
如果第二次你改成:
GET /chat/ask?question=我刚才说我叫什么&threadId=user-2
那对框架来说,这就是一条新的线程,它不会继承 user-1 的历史状态。
十一、checkpointer 的几个常见误区
误区 1:加了 MemorySaver 就自动有记忆
不对。
你还必须在调用时传:
configurable: {
thread_id: 'xxx'
}
没有 thread_id,checkpointer 根本不知道该把状态存到哪一条线程下。
误区 2:thread_id 可以一直写死
技术上可以,业务上通常不行。
如果你写死:
thread_id: 'default-thread'
那所有用户都会共用同一条会话,最终一定会串话。
误区 3:checkpointer 只保存聊天记录
不完全对。
checkpointer 保存的是 Agent State,而不只是纯文本聊天记录。
常见内容包括:
- 消息历史
- 工具调用结果
- 摘要内容
- 其它中间状态
所以它更准确的理解应该是:
它保存的是整条线程的运行状态,而不是简单的聊天文本。
误区 4:MemorySaver 适合直接上生产
通常不建议。
因为它只存在于进程内存中:
- 服务重启就丢
- 多实例之间不同步
- 不适合正式线上环境
正式生产里,更常见的是数据库型 saver。
十二、什么时候该从 MemorySaver 升级到 PostgresSaver?
当你出现下面这些需求时,就该考虑升级了:
- 服务重启后还要保留会话
- 多台服务器要共享记忆
- 用户下次回来还能继续之前的聊天
- 需要排查某些线程历史状态
这个时候,通常只需要把:
checkpointer: new MemorySaver()
替换成数据库版 saver,其余 thread_id 的调用方式一般不需要大改。
十三、这篇文章最重要的 3 个结论
结论 1
checkpointer = Agent 的状态保存器
它保存的不是单纯聊天记录,而是整条线程的运行状态。
结论 2
thread_id 是 checkpointer 正常工作的关键
没有 thread_id,就没有真正的线程级记忆。
十四、总结
在 NestJS + LangChain 项目里,想让 Agent 具备多轮记忆,核心并不是手动维护消息数组,而是:
- 在
createAgent()时配置checkpointer - 在
invoke()时传入configurable.thread_id
其中:
MemorySaver适合本地开发PostgresSaver更适合生产持久化thread_id决定会话隔离checkpointer决定状态如何保存和恢复
只有这两者真正配合起来,Agent 才能从“单轮问答”升级为“有状态的会话系统”。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)