模块 03 - 记忆系统 | 前置知识:createAgent 入门(如果你跳着读,先回去看一眼)
v0.3 的 Memory 类有两个结构性问题
v0.3 有一组叫 Memory 的类——ConversationBufferMemory、ConversationSummaryMemory、VectorStoreRetrieverMemory——它们直接挂在 Chain 上,调用一次自动管理一轮历史。看上去很省心,但生产场景里两个问题让人很痛苦:
- 记忆和具体的 Chain 绑死,换个 Agent 实现就得重写
- 多用户、多会话的隔离要自己外挂一层
sessionId,老 API 没把这件事当成一等公民
1.x 用 checkpointer + store + tool 三件套整个掀翻了这套抽象。Memory 类被推到 @langchain/classic(兼容容身处),主推的概念变成两个:checkpointer(线程级短期记忆)和 store(跨会话长期记忆),再加上把 VectorStore 暴露成 tool 走语义检索。它们都长在 LangGraph 之上,由 createAgent 自动接管。
这一节先把这个新世界的地图画清楚。后面五节按”短期 → 摘要 → 向量 → 自定义后端 → 多用户隔离”的顺序展开。
为什么 Agent 必须有状态
LLM 本身是无状态的。每次调用 API,模型只看到你这一次塞给它的 prompt。下面这个例子你大概看过无数次:
用户:我叫张三,在北京做后端开发
Agent:你好张三!很高兴认识你
(5 分钟后)
用户:帮我推荐些适合我的技术书
Agent:请问你是做什么方向的?在哪个城市? ← 全忘了要让 Agent 记住”张三 / 北京 / 后端”,只能由我们应用层把上一轮的消息重新喂回去。这就是”记忆”在工程上的本质——状态管理。
记忆解决四类问题:
| 问题 | 例子 |
|---|---|
| 多轮上下文 | 「他刚才问的是哪个城市?」 |
| 用户画像 | 「张三的技术栈是 Go + Python」 |
| 任务进度 | 多步操作中断恢复 |
| 跨会话知识 | 「上周三我说过我对加班特别反感」 |
前两类是短期记忆(一次会话内),后两类多半是长期记忆(跨会话、跨天、跨设备)。1.x 把这两件事分得干干净净。
1.x 时代的记忆四层
我把 LangChain.js 1.x 里能放”状态”的位置画成四层。每层对应一类工具:
逐层说明:
1. 工作记忆:当前 prompt 里的 messages
这是 LLM 实际”看到”的东西。在 1.x 里就是你传给 agent.invoke({ messages: [...] }) 的那个 messages 数组。它受 context window 限制,是个有上限的资源。
2. 短期记忆:thread-based checkpointer
一次”会话”在 LangGraph 里叫一个 thread,由 thread_id 标识。给 createAgent 配上 checkpointer,每次 invoke 都会把状态(包括完整 message 列表)按 thread_id 落盘,下次同 thread_id 再来时自动恢复。
import { createAgent } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
const agent = createAgent({
model: "anthropic:claude-sonnet-4-6",
tools: [],
checkpointer: new MemorySaver(),
});
const config = { configurable: { thread_id: "user-001-conv-1" } };
await agent.invoke({ messages: [{ role: "user", content: "我叫张三" }] }, config);
await agent.invoke({ messages: [{ role: "user", content: "我叫什么?" }] }, config);
// → "你叫张三"不用再手写 messages.push(...),也不用自己维护 sessionId 到消息列表的映射。MemorySaver 是 in-process 的内存实现,生产环境换 PostgresSaver / SqliteSaver 即可,下一节会展开。
3. 长期记忆:跨会话的 store
store 是 1.x 新加的接口,专门放跨 thread、跨会话的信息——比如”张三的技术栈”、“用户偏好的回答风格”。它的 API 形态像一个有命名空间的 KV 存储:
import { InMemoryStore } from "@langchain/langgraph";
const store = new InMemoryStore();
// 写:namespace 用来做隔离(这里按 user_id)
await store.put(["user-001", "profile"], "tech_stack", {
languages: ["Go", "Python"],
city: "北京",
});
// 读
const item = await store.get(["user-001", "profile"], "tech_stack");
console.log(item?.value);
// → { languages: ["Go", "Python"], city: "北京" }把 store 一起传给 createAgent,Agent 内部就能在 middleware / tool 里通过 runtime 拿到它。
4. 语义记忆:把检索包装成 Tool
老版本的 VectorStoreRetrieverMemory 在 1.x 不存在了。新的做法是:把”从历史中检索相关片段”做成一个普通的工具,丢进 tools 数组,让 Agent 自己决定什么时候调用。
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const recallMemory = tool(
async ({ query }) => {
const docs = await vectorStore.similaritySearch(query, 3);
return docs.map((d) => d.pageContent).join("\n---\n");
},
{
name: "recall_memory",
description: "在历史对话和用户画像里检索与当前问题最相关的片段",
schema: z.object({ query: z.string() }),
}
);这个改动看着小,意义很大:Agent 不再被动接收”框架塞过来的历史”,而是主动判断什么时候需要回忆。前者像考试时被强行翻开笔记本,后者像考试时自己决定要不要查。第 4 节 VectorStore 记忆作为工具 会把这个模式讲透。
老 API 还能不能用
简短回答:能用,但别用。
ConversationBufferMemory / ConversationSummaryMemory / VectorStoreRetrieverMemory / RunnableWithMessageHistory 在 1.x 里被搬到了 @langchain/classic,从 import { ... } from "langchain/memory" 改成 from "@langchain/classic/memory" 还能跑。但这些 API 不再演进,并且和 createAgent 的 middleware / checkpointer 完全打不通。新写代码请按本模块后续章节的方式走。
这一模块怎么读
| 节 | 主题 | 关键 API |
|---|---|---|
| 1 | 本节,建立全景 | — |
| 2 | 短期记忆:checkpointer | MemorySaver / PostgresSaver |
| 3 | Summary 策略:middleware | createMiddleware({ beforeModel }) |
| 4 | VectorStore 记忆作为工具 | tool() + retriever |
| 5 | 自定义后端:实现自己的 checkpointer / store | BaseCheckpointSaver / BaseStore |
| 6 | 多用户隔离 | thread_id + store.namespace |
建议按顺序读。如果你只想搞清楚”怎么让 Agent 记得住”,看完第 2、6 两节就够用了;如果你在做企业级产品,第 5、6 节是重头戏。
一个完整的最小例子
把这一节讲到的四层在 30 行代码里串起来——一个能记住用户、能跨会话回忆的 Agent:
// memory-demo.ts
import { createAgent } from "langchain";
import { MemorySaver, InMemoryStore } from "@langchain/langgraph";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const store = new InMemoryStore();
// 长期记忆工具:把用户信息持久化
const rememberFact = tool(
async ({ fact }, runtime) => {
const userId = runtime.context.user_id as string;
await store.put([userId, "facts"], `fact-${Date.now()}`, { fact });
return `已记住:${fact}`;
},
{
name: "remember_fact",
description: "把用户告诉你的、值得长期记住的事实保存下来",
schema: z.object({ fact: z.string() }),
}
);
const agent = createAgent({
model: "anthropic:claude-sonnet-4-6",
tools: [rememberFact],
systemPrompt:
"你是一个会主动记忆的助手。听到用户介绍自己的姓名、职业、城市、偏好时,调用 remember_fact 工具保存。",
checkpointer: new MemorySaver(), // 短期:thread 内连续
store, // 长期:跨 thread 共享
});
const config = {
configurable: { thread_id: "conv-1" },
context: { user_id: "user-001" },
};
await agent.invoke(
{ messages: [{ role: "user", content: "我叫张三,在北京做后端" }] },
config
);
// 关掉 conv-1,开 conv-2,但同一个 user_id
const config2 = {
configurable: { thread_id: "conv-2" },
context: { user_id: "user-001" },
};
// 在新 thread 里读取长期记忆
const facts = await store.search(["user-001", "facts"]);
console.log("user-001 的长期记忆:", facts);短期记忆走 checkpointer,按 thread_id 隔离;长期记忆走 store,按 user_id 隔离。两者各干各的,互不打架。这套模型就是后面所有章节的底层。
小结
1.x 把”记忆”从一组 Memory 类拆成了四个层次:工作记忆(messages)、短期记忆(checkpointer)、长期记忆(store)、语义记忆(tool + vector store)。createAgent 是它们的统一入口。
下一节 短期记忆:thread-based checkpointer 进入正题,把 MemorySaver 和 PostgresSaver 的实际用法讲透。
关于 checkpointer 和 store 的官方文档:LangGraph Persistence。
本文摘自《LangChain.js Agent 开发权威指南》,作者递归客。
本书资源
- 源码仓库 · github.com/diguike/book-langchain-agent
- 在线阅读 · inferloop.dev/langchain-agent
- 所有书目 · inferloop.dev
继续阅读 · 同作者其他书
- 《Transformer 工程实战》从注意力机制到生产部署
- 《自己动手写 AI Agent》从 Claude Code 开源架构到你的第一个编程助手
- 《AI 时代的 CLI 工具开发实战》用 TypeScript 构建现代 CLI 工具
- 《LLM Infra 工程实战》从入门到实践
- 《Hermes Agent 实战》构建会成长的个人 AI Agent
- 《OpenClaw 源码解析》现代 Agent 系统的架构设计与工程实践
- 《Agent Memory 工程实战》从 claude-mem 源码到企业级记忆平台
- 《AI Token 中转站实战》从 0 搭建企业级 LLM 网关
- 《百万级 AI Agent 平台架构》智能客服 SaaS 实战
- 《Claude Code Skill 指南》
- 《Claude 插件官方指南》