模块 03 - 记忆系统 | 前置知识:1.x 时代的记忆系统、VectorStore 记忆作为工具、多用户记忆隔离
checkpointer 记不住”跨会话的事”
短期记忆 用 checkpointer 把一次会话的消息历史存下来——同一个 thread_id 里,Agent 记得你三轮前说过什么。但换个 thread,全忘了。
真实产品需要的是另一种记忆:跨会话、跟着用户走的长期记忆。用户上个月说过”我对花生过敏”,这个月开新会话问”推荐个菜谱”,Agent 应该还记得这条。这不是 checkpointer 的活——checkpointer 按 thread 隔离,长期记忆要按用户沉淀。
LangGraph 用 Store 干这件事。1.x 时代的记忆系统 提过 checkpointer 和 store 的分工,多用户记忆隔离 用 store 的 namespace 做过隔离。这一节讲它最有价值的能力:语义召回——存进去的记忆,能按”意思”而不是”关键词”检索回来。
Store 配上 embeddings 就能语义检索
普通 store 是个带 namespace 的键值库,search 只能按前缀和元数据过滤。给它配一个 embeddings 模型,search 就升级成向量相似度检索。如图 3-6 所示,put 写入时自动算 embedding 入库,search 带 query 时把 query 也嵌入,按 cosine 相似度召回——这就是”用户对花生过敏”能被”饮食限制”召回的原理。
图 3-6:Store 语义召回的数据流。写入和检索都经过同一个 embeddings 模型,按向量相似度匹配
import { InMemoryStore } from "@langchain/langgraph";
import { OpenAIEmbeddings } from "@langchain/openai";
const store = new InMemoryStore({
index: {
dims: 1536,
embeddings: new OpenAIEmbeddings({ model: "text-embedding-3-small" }),
fields: ["text"], // 只嵌入 value 的 text 字段,默认 ["$"] 嵌整个对象
},
});index 三个字段:dims(向量维度,跟 embeddings 模型对上)、embeddings(LangChain Embeddings 实例)、fields(从 value 的哪些字段取文本做嵌入)。配了 index,put 进去的数据会自动算 embedding,search 带 query 时就按语义相似度排序。
这里有个配置形态的坑要分清:代码里(
InMemoryStore)传的是index.embeddings(实例)+index.dims;但 LangGraph Platform 部署 的langgraph.json里写的是store.index.embed——一个字符串模型标识(如"openai:text-embedding-3-small"),字段名是embed不是embeddings。两套形态别照搬。
put / search:存记忆、按意思捞回来
namespace 是 string[] 层级路径,按用户隔离的惯例是 ["memories", userId]:
const userId = "u-123";
const namespace = ["memories", userId];
// 存一条记忆
await store.put(namespace, crypto.randomUUID(), { text: "用户对花生过敏" });
await store.put(namespace, crypto.randomUUID(), { text: "用户喜欢川菜" });
await store.put(namespace, crypto.randomUUID(), { text: "用户的回答偏好是简短" });
// 按语义检索——注意 query 才是触发向量检索的开关
const hits = await store.search(namespace, {
query: "这个用户有什么饮食限制",
limit: 3,
});
for (const item of hits) {
console.log(item.value.text, "score:", item.score);
}
// → "用户对花生过敏" score: 0.82
// "用户喜欢川菜" score: 0.61
// ...关键点:
put(namespace, key, value, index?)——第四参index传false可跳过这条的嵌入(纯存储不参与检索)。search(namespacePrefix, { query, limit, offset, filter })——传query才走向量检索并返回相似度score;不传query就退化成按 namespace + filter 的普通分页列举(不排序)。limit默认 10。- 返回的每项带
score(cosine 相似度,仅 query 模式有)、value、key、namespace、时间戳。
“用户对花生过敏”能被”饮食限制”这个完全不同字面的 query 召回——这就是语义检索相对关键词匹配的价值。
接进 createAgent:让工具读写记忆
把 store 传给 createAgent,Agent 的工具和中间件就能在运行时拿到它。一个典型模式:一个工具负责”记住”,靠语义检索把相关记忆喂进上下文。
import { createAgent, tool } from "langchain";
import { InMemoryStore } from "@langchain/langgraph";
import { OpenAIEmbeddings } from "@langchain/openai";
import { z } from "zod";
const store = new InMemoryStore({
index: {
dims: 1536,
embeddings: new OpenAIEmbeddings({ model: "text-embedding-3-small" }),
fields: ["text"],
},
});
// 工具:记住一条用户信息
const saveMemory = tool(
async ({ text }, config) => {
const runtime = config.runtime; // 从 runtime 拿 store,不是引用外部闭包变量
const userId = runtime.context.userId;
await runtime.store?.put(["memories", userId], crypto.randomUUID(), { text });
return "已记住";
},
{
name: "save_memory",
description: "当用户透露了值得长期记住的偏好/事实时调用",
schema: z.object({ text: z.string().describe("要记住的一句话") }),
}
);
// 工具:检索相关记忆
const recallMemory = tool(
async ({ query }, config) => {
const runtime = config.runtime;
const userId = runtime.context.userId;
const hits = await runtime.store?.search(["memories", userId], { query, limit: 3 });
return (hits ?? []).map((h) => h.value.text).join("\n") || "(没有相关记忆)";
},
{
name: "recall_memory",
description: "回答前先检索这个用户的历史偏好/事实",
schema: z.object({ query: z.string() }),
}
);
const agent = createAgent({
model: "openai:gpt-4o",
tools: [saveMemory, recallMemory],
systemPrompt: "回答前先用 recall_memory 查用户偏好;用户透露新信息时用 save_memory 记下。",
store, // 注入后,工具里 runtime.store 可用
});
// 调用时通过 context 注入 userId
await agent.invoke(
{ messages: [{ role: "user", content: "推荐个菜谱" }] },
{ context: { userId: "u-123" } }
);两个容易踩的点:
- 工具里取 store 的入口是
runtime.store(从工具第二参config.runtime,中间件里是request.runtime.store),不要去引用外部那个store变量。runtime.store可能是undefined(没注入时),调用要?.。 runtime.store拿到的其实是一个AsyncBatchedStore包装实例(它把多次读写批量化以提性能),不是你new的那个InMemoryStore原对象。功能完全一致,但instanceof InMemoryStore判断会失败——别依赖它。
userId 通过 context 注入、而不是放进工具 schema,是 多用户记忆隔离 强调过的关键:模型永远看不到也改不了 userId,记忆的隔离边界由代码守住,不交给模型。
从内存到生产
InMemoryStore 顾名思义存在进程内存里,进程一停就没。生产环境换成持久化后端——和 自定义后端 里 checkpointer 的思路一样,store 也有 Postgres 等实现,langgraph.json 部署时配 store.index.embed 即可让 LangGraph Server 托管带语义检索的长期记忆。原型用 InMemoryStore,上线换持久化后端,namespace 和 search 的代码不用改。
小结
长期记忆(跨会话、按用户沉淀)用 Store,不是 checkpointer。给 InMemoryStore 配 index(dims + embeddings 实例 + fields),search 就从前缀过滤升级成语义检索——put(namespace, key, value) 存,search(prefix, { query, limit }) 按意思捞回带 score 的结果。接进 createAgent({ store }) 后,工具通过 runtime.store(注意是 AsyncBatchedStore 包装、可能 undefined)读写,userId 走 context 注入保证隔离。原型用 InMemoryStore,生产换持久化后端、代码不变。注意 langgraph.json 里是字符串 store.index.embed,和代码里的 embeddings 实例是两套形态。
到这里记忆系统的全貌就齐了:短期靠 checkpointer、长期靠 store、语义召回靠 store 的向量索引。下一模块 工具与函数调用 讲 Agent 怎么通过工具触达外部世界——包括把上面这种记忆读写封装成工具。
本文摘自《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 实战
- 《AI Agent 评测工程实战》从 0 用 TypeScript 构建你的评测平台
- 《Agent Harness 评测工程》用评测建设并守护一个 agent harness
- 《源码精读》每章一个开源仓库 · 从架构到品味
- 《Claude Code Skill 指南》
- 《Claude 插件官方指南》