ai agent --- mem0 外挂记忆系统
英文标题:ai agent --- mem0 外挂记忆系统
一.mem0是什么? 1.概念 核心能力: 三层记忆作用域:user_id(用户级)、agent_id(Agent 级)、run_id(会话级) 自动事实提取:丢一段对话进去,LLM 自动从中抽取关键
67.0 KB
28.6 KB
13.4 KB
85.6 KB
55.5 KB
19.5 KB
62.3 KB
43.5 KB
33.8 KB
5.4 KB
中文
一.mem0是什么?
1.概念
Mem0 是为 AI 应用和 Agent 提供的智能记忆层,负责从对话中识别、保存、更新、隔离和召回有价值的上下文,使无状态模型能够跨会话保持连续性。
Mem0(读作“mem-zero”)是给AI 助手/Agent加的一层 持久化语义记忆层**。它解决的核心问题是:
AI是无状态的,每次对话都从零开始,而Mem0让它能跨会话记住用户偏好、项目决策、学习经验等。
核心能力:
- 三层记忆作用域:
user_id(用户级)、agent_id(Agent 级)、run_id(会话级) - 自动事实提取:丢一段对话进去,
LLM自动从中抽取关键事实 - 多信号检索:语义搜索 +
BM25关键词 + 知识图谱,三路融合后去重,重排。 - 实体链接:自动提取实体(人名、项目名等),跨记忆关联
- 时序推理:时间感知检索,区分"当前状态"和"过去事件"
在aiAgent 里面 mem0 角色
使用 Mem0 之前:
用户提问后,系统并行调用
Milvus进行语义检索、BM25进行关键词检索,并访问知识图谱进行关系检索;三路分别返回结果后,业务层需要完成去重、排序、加权融合和上下文整理,再将筛选后的内容注入提示词,交给大模型生成回答。
使用 Mem0 之后:
用户提问进入应用后,由
Mem0统一管理相关记忆的召回、过滤、排序和整理;应用取得整理后的相关记忆后,再交给大模型。也就是说mem0将原来的向量检索,知识图谱,关键词搜索,去重,重排等一系列复杂的事情全部封装好了你只需要将mem0返回的结果交给大模型处理就好了。
上图非常像RAG的处理流程图,但是这个处理的是会话上下文。RAG 负责“企业文档知识”,他的搜索依旧需要原来的解决方案,不能使用mem0做简化,因为mem0的优势是用户记忆,会话记忆,agent的配置记忆。
绑定userld后,存的是使用人的:姓名、城市、偏好、习惯等。 换会话、换Agent也能读到,适合个性化和跨场景复用。
绑定userld+runld,存的是当前这次对话里的任务上下文:这次要做什么、讨论到哪- 步、临时目标等。 会话结束后可以清理,不会和别的会话混在一起。
绑定agent_id,agent_id是Agent 上下文的隔离与归属标识,存的是Agent自己的设定:角色、语气、回答方式、专业领域等。
同一个用户换不同Agent,各自保持独立人格和风格。
就是说换个大模型以后,我设定的机器人是男的还是女的,声音,回答方式,你的角色是旅游博主等等,都不会随agent的切换而改变。
二.mem0的使用
mem0有2个比较容易混淆的类,如下所示:
MemoryClient 把数据发给 Mem0 托管服务;Memory 在你自己的基础设施中运行。
MemoryClient的创建方式
使用MemoryClient创建的memory,他的数据是存放在app.mem0.ai/dashboard/r… 里面的。mem0是第三方数据管理平台。 它不但帮我们托管数据,还帮我完成会话向量检索,知识图谱搜索,关键词搜索。
安装
pnpm install mem0ai
获取MEM0_API_KEY,登录:app.mem0.ai/login 先要注册用户,才能获取。
import MemoryClient from "mem0ai";
import "dotenv/config";
const client = new MemoryClient({
apiKey: process.env.MEM0_API_KEY,
});
使用Memory创建的memory,他的数据是存放在自己的数据库里面。他会和milvus,BM 25,知识图谱配合完成 完成会话向量检索,知识图谱搜索,关键词搜索,去重,重排等繁琐的事情。
Memory的创建方式
import { Memory } from "mem0ai/oss";
const memory = new Memory({
llm: {
provider: "openai",
config: {
apiKey: process.env.OPENAI_API_KEY,
model: "gpt-5-mini",
},
},
embedder: {
provider: "openai",
config: {
apiKey: process.env.OPENAI_API_KEY,
model: "text-embedding-3-small",
},
},
vectorStore: {
provider: "qdrant",
config: {
host: "localhost",
port: 6333,
collectionName: "memories",
},
},
});
MemoryClient和Memory的区别
如何选择MemoryClient和Memory
- 数据必须完全留在企业内部:选
Memory - 希望快速上线、降低基础设施负担:选
MemoryClient - 需要 Graph Memory、Webhooks 等平台能力:选
MemoryClient - 需要深度改造记忆链路:选
Memory
有数据私有化和平台运维能力的企业,更可能选择 Memory;重视交付速度、成本和托管能力的企业,更可能选择 MemoryClient。
数据必须留在自有基础设施,并且企业具备模型与数据库运维能力时,选
Memory;其余大多数项目,尤其是产品验证和一般业务系统,优先选MemoryClient。MemoryClient 的优势是把基础设施和运维责任交给 Mem0 托管平台,因此交付速度更快、基础设施效率更高
MemoryClient使用案例
import MemoryClient from "mem0ai";
import "dotenv/config";
const client = new MemoryClient({
apiKey: process.env.MEM0_API_KEY,
});
const userId = "user-123";
async function main() {
const messages = [
{
role: "user",
content: "我喜欢弹钢琴,我每周都要弹三次,每次一小时,我很喜欢这个过程。",
},
{
role: "assistant",
content: "好的,我已经记住你的喜好。",
},
];
const addResult = await client.add(messages, {
userId,
metadata: {
category: "travel_preference",
source: "chat",
},
});
console.log("新增结果:", JSON.stringify(addResult));
const searchResult = await client.search("推荐北京餐厅", {
filters: {
user_id: userId,
},
topK: 10,
});
console.log("搜索结果:", JSON.stringify(searchResult, null, 2));
const allMemories = await client.getAll({
filters: {
user_id: userId,
},
});
console.log("用户全部记忆:", JSON.stringify(allMemories, null, 2));
const memoryId = "f6ae414a-aa19-4859-8853-a5ad576caeba";
const singleMemory = await client.get(memoryId);
console.log("单条记忆:", JSON.stringify(singleMemory, null, 2));
}
main().catch((error) => {
console.error("Mem0 操作失败:", error);
});
查看 mem0 的数据
语义检索,我每周都要干嘛?他回复我每周喜欢弹钢琴,一周三次。
这个东西的好处是,他帮我们做好了会话上下文的图谱检索,关键词搜索,语义检索,就不需要我们手动再做。不过这个东西是需要掏钱的。每天都有一些额度你可以使用。
利用redis和mem0存储会话
利用redis做短期记忆,压缩记忆本次会话都说了些什么东西。利用mem0做长期记忆,记录用户信息,agent喜好等。
具体实现代码如下:
代码里面用RedisMessageStore类来装redis的存信息,取信息,清空信息的方法。
Mem0MemoryStore类用来存mem0存取数据的方法。
invokeWithMemory是用来加载对话的,不管是进入长期会话还是进入短期会话,都是靠大模型自己来判断加入的。
-
1. 从 Redis 加载当前会话历史 -
2. 从 Mem0 检索用户长期记忆和会话记忆 -
3. 将 Mem0 记忆转换为 SystemMessage -
4. 调用 Agent -
5. 将新对话写入 Redis -
6. 判断哪些内容需要写入 Mem0 -
7. 返回模型结果和统计信息
invokeWithMemory 负责“取记忆—组上下文—调用模型—存会话—筛选长期事实—写回 Mem0”的完整闭环,是整个应用把 Redis 短期会话与 Mem0 长期记忆连接起来的核心方法。
import "dotenv/config";
import Redis from "ioredis";
import * as readline from "node:readline/promises";
import { stdin, stdout } from "node:process";
import { z } from "zod";
import { MemoryClient } from "mem0ai";
import { ChatOpenAI } from "@langchain/openai";
import {
SystemMessage,
SystemMessageChunk,
HumanMessage,
mapChatMessagesToStoredMessages,
mapStoredMessagesToChatMessages,
} from "@langchain/core/messages";
import { createAgent, summarizationMiddleware } from "langchain";
const REDIS_HOST = process.env.REDIS_HOST ?? "localhost";
const REDIS_PORT = Number(process.env.REDIS_PORT ?? 6379);
const REDIS_DB = Number(process.env.REDIS_DB ?? 0);
const MEMORY_TTL = Number(process.env.MEMORY_TTL_SECONDS ?? 1800);
const KEY_PREFIX = process.env.MEMORY_KEY_PREFIX ?? "agent:short_memory";
const USER_ID = process.env.MEM0_USER_ID ?? "demo_user_001";
const SESSION_ID = "session_002";
const MEM0_TOP_K = Number(process.env.MEM0_TOP_K ?? 5);
const memorySchema = z.object({
write_user: z
.boolean()
.describe(
"写入用户层:换一个新会话仍应保留的长期事实(身份、居住地、长期爱好、饮食禁忌、持久偏好)。不含仅本轮任务。",
),
write_session: z
.boolean()
.describe(
"写入会话层:仅当前会话/thread 有效的任务、大纲、进度、待办、临时决策(如「这次先写…」「数据部分明天补」)。",
),
reason: z.string().describe("分类理由,一句话"),
});
const CLASSIFIER_PROMPT = `你是记忆分层分类器。判断本轮对话是否有「新事实」需写入 Mem0,并分到正确层级。
## user 层(跨会话长期)
- 用户身份与画像:姓名、职业、居住地、长期爱好
- 长期偏好与约束:饮食过敏、回答风格、常用技术栈
- 持续数周以上的个人背景(非单次任务)
## session 层(仅当前会话)
- 当前正在做的任务、目标、文档大纲、方案草稿
- 本会话内的进度、决策、待办、临时约定
- 用户明确用「这次」「本轮」「当前会话」描述的工作上下文
## 均不写入
- 寒暄、致谢、纯确认
- 助手生成的通用内容(攻略、示例代码、建议清单),用户未明确采纳为新事实
- 无信息增量的复述
## 决策原则
1. 「这次我们先写 Q1 总结」「当前在排查 XX」→ 优先 session,不要标成 user
2. user 与 session 可同时为 true(如同时说职业+当前任务),但勿把纯会话任务只标 user
3. 一次性请求(如「帮我做旅行攻略」)且未产生需跨轮记住的约定 → 均为 false`;
const summaryPrompt = `你是对话摘要助手。用中文简洁总结:话题、会话内进度/报错/待办。
用户级长期偏好由外部记忆维护,摘要勿重复堆砌。不要编造。
待摘要的对话:
{messages}
摘要:`;
function messagesForRedis(messages) {
return messages.filter(
(m) => !SystemMessage.isInstance(m) && !SystemMessageChunk.isInstance(m),
);
}
class RedisMessageStore {
constructor({ redis, keyPrefix, ttlSeconds }) {
this.redis = redis;
this.keyPrefix = keyPrefix;
this.ttlSeconds = ttlSeconds;
}
messagesKey(sessionId) {
return `${this.keyPrefix}:${sessionId}:messages`;
}
async loadMessages(sessionId) {
const raw = await this.redis.get(this.messagesKey(sessionId));
if (!raw) return [];
return mapStoredMessagesToChatMessages(JSON.parse(raw));
}
async saveMessages(sessionId, messages) {
const payload = JSON.stringify(mapChatMessagesToStoredMessages(messages));
await this.redis.set(this.messagesKey(sessionId), payload, "EX", this.ttlSeconds);
}
async clear(sessionId) {
await this.redis.del(this.messagesKey(sessionId));
}
async ttl(sessionId) {
return this.redis.ttl(this.messagesKey(sessionId));
}
}
class Mem0MemoryStore {
constructor({ client, userId, sessionId, topK, classifier }) {
this.client = client;
this.userId = userId;
this.sessionId = sessionId;
this.topK = topK;
this.classifier = classifier;
}
async search(query) {
const [userRes, sessionRes] = await Promise.all([
this.client.search(query, {
filters: { user_id: this.userId },
topK: this.topK,
}),
this.client.search(query, {
filters: {
AND: [{ user_id: this.userId }, { run_id: this.sessionId }],
},
topK: this.topK,
}),
]);
return {
user: userRes.results ?? [],
session: sessionRes.results ?? [],
};
}
buildSystemMessage({ user, session }) {
const blocks = [];
if (user.length) {
blocks.push(`【用户长期记忆】\n${user.map((m) => `- ${m.memory}`).join("\n")}`);
}
if (session.length) {
blocks.push(`【当前会话记忆】\n${session.map((m) => `- ${m.memory}`).join("\n")}`);
}
if (!blocks.length) return null;
return new SystemMessage(`${blocks.join("\n\n")}\n\n请结合以上记忆回答,勿编造。`);
}
async classifyAndPersist(userText, assistantText) {
const turn = [
{ role: "user", content: userText },
{ role: "assistant", content: assistantText },
];
const { write_user, write_session, reason } = await this.classifier.invoke([
new SystemMessage(CLASSIFIER_PROMPT),
new HumanMessage(`用户:${userText}\n助手:${assistantText}`),
]);
const written = [];
if (write_user) {
await this.client.add(turn, { userId: this.userId });
written.push("user");
}
if (write_session) {
await this.client.add(turn, { userId: this.userId, runId: this.sessionId });
written.push("session");
}
return { written, reason };
}
async clear() {
await this.client.deleteAll({ userId: this.userId });
await this.client.deleteAll({ userId: this.userId, runId: this.sessionId });
}
}
async function invokeWithMemory(agent, redisStore, mem0Store, sessionId, userText) {
const history = await redisStore.loadMessages(sessionId);
console.log(` ↳ Redis 加载 ${history.length} 条历史`);
const mem = await mem0Store.search(userText);
if (mem.user.length) console.log(` ↳ Mem0 用户层 ${mem.user.length} 条`);
if (mem.session.length) console.log(` ↳ Mem0 会话层 ${mem.session.length} 条`);
const memoryMsg = mem0Store.buildSystemMessage(mem);
const invokeMessages = [
...(memoryMsg ? [memoryMsg] : []),
...history,
new HumanMessage(userText),
];
const result = await agent.invoke(
{ messages: invokeMessages },
{ recursionLimit: 30 },
);
const redisMessages = messagesForRedis(result.messages);
const dropped = result.messages.length - redisMessages.length;
await redisStore.saveMessages(sessionId, redisMessages);
const ttl = await redisStore.ttl(sessionId);
console.log(
` ↳ Redis 写回 ${redisMessages.length} 条` +
(dropped ? `(过滤 ${dropped} 条 SystemMessage)` : "") +
` (TTL ${ttl}s)`,
);
const assistantText = String(result.messages.at(-1)?.content ?? "");
const { written, reason } = await mem0Store.classifyAndPersist(userText, assistantText);
console.log(` ↳ 分类: ${reason}`);
console.log(written.length ? ` ↳ Mem0 写入: ${written.join(", ")}` : " ↳ Mem0 未写入");
return { messages: result.messages, redisMessages, assistantText };
}
if (!process.env.MEM0_API_KEY || !process.env.OPENAI_API_KEY) {
console.error("需要 MEM0_API_KEY 与 OPENAI_API_KEY");
process.exit(1);
}
const redis = new Redis({ host: REDIS_HOST, port: REDIS_PORT, db: REDIS_DB });
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY });
redis.on("connect", () => console.log("✅ Redis 已连接"));
redis.on("error", (err) => console.error("❌ Redis 错误:", err.message));
try {
await redis.ping();
} catch {
console.error("Redis 未连接,请先执行: docker compose up -d redis");
process.exit(1);
}
const redisStore = new RedisMessageStore({
redis,
keyPrefix: KEY_PREFIX,
ttlSeconds: MEMORY_TTL,
});
const llmOpts = {
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_BASE_URL },
temperature: 0,
};
const model = new ChatOpenAI({ model: process.env.MODEL_NAME, ...llmOpts });
const classifier = new ChatOpenAI({
model: process.env.MODEL_NAME,
...llmOpts,
}).withStructuredOutput(memorySchema);
const mem0Store = new Mem0MemoryStore({
client: mem0,
userId: USER_ID,
sessionId: SESSION_ID,
topK: MEM0_TOP_K,
classifier,
});
const agent = createAgent({
model,
tools: [],
systemPrompt:
"你是会话助手。结合系统消息中的长期/会话记忆回答,中文简短。有对话摘要则据此继续。",
middleware: [
summarizationMiddleware({
model,
summaryPrompt,
trigger: { messages: 8 },
keep: { messages: 4 },
}),
],
});
console.log(`用户 ${USER_ID} | 会话 ${SESSION_ID}`);
console.log("输入 exit / quit / :q 退出;:clear 清空 Redis;:clear-mem0 清空 Mem0\n");
const rl = readline.createInterface({ input: stdin, output: stdout });
let prevCount = (await redisStore.loadMessages(SESSION_ID)).length;
try {
while (true) {
const userText = (await rl.question("你: ")).trim();
if (!userText) continue;
if (["exit", "quit", ":q"].includes(userText.toLowerCase())) break;
if (userText === ":clear") {
await redisStore.clear(SESSION_ID);
prevCount = 0;
console.log("已清空 Redis 短期记忆\n");
continue;
}
if (userText === ":clear-mem0") {
await mem0Store.clear();
console.log("已清空 Mem0 用户层与当前会话层\n");
continue;
}
const { redisMessages, assistantText } = await invokeWithMemory(
agent,
redisStore,
mem0Store,
SESSION_ID,
userText,
);
console.log("\n助手:", assistantText);
console.log(`Redis 消息数: ${redisMessages.length}`);
if (redisMessages.length < prevCount + 2) {
console.log(" ⚡ 已触发压缩");
}
prevCount = redisMessages.length;
console.log();
}
} finally {
rl.close();
}
await redis.quit();
三.总结
Mem0 是一层智能记忆系统,通过
user_id、run_id和agent_id管理用户、会话与 Agent 上下文;MemoryClient使用 Mem0 托管服务,Memory在自有基础设施运行;企业文档知识仍由 RAG 负责。
- Mem0 解决 AI 无状态导致的上下文丢失问题,使应用和 Agent 能跨会话记住相关信息。
- 用户记忆绑定
user_id,保存长期有效的事实和偏好。 - 会话记忆绑定
run_id,保存当前任务、进度和临时决策。 - Agent 记忆绑定
agent_id,用于隔离不同 Agent 的上下文,而不是保存角色提示词。 MemoryClient使用托管平台,Memory在自有基础设施运行。- Mem0 负责用户和 Agent 记忆,RAG 负责企业文档知识,两者可以组合使用。
- 生产项目还需增加授权、脱敏、审计、删除、权限控制和数据留存策略。
Mem0 不是通用文档 RAG 的替代品,也不是 Agent 配置中心,而是位于业务应用与模型之间的智能记忆层。它负责事实抽取、记忆存储、更新、冲突处理、范围隔离、检索和结果整理;企业文档检索仍应由专门的 RAG 系统承担。
English / 原文
一.mem0是什么?
1.概念
Mem0 是为 AI 应用和 Agent 提供的智能记忆层,负责从对话中识别、保存、更新、隔离和召回有价值的上下文,使无状态模型能够跨会话保持连续性。
Mem0(读作“mem-zero”)是给AI 助手/Agent加的一层 持久化语义记忆层**。它解决的核心问题是:
AI是无状态的,每次对话都从零开始,而Mem0让它能跨会话记住用户偏好、项目决策、学习经验等。
核心能力:
- 三层记忆作用域:
user_id(用户级)、agent_id(Agent 级)、run_id(会话级) - 自动事实提取:丢一段对话进去,
LLM自动从中抽取关键事实 - 多信号检索:语义搜索 +
BM25关键词 + 知识图谱,三路融合后去重,重排。 - 实体链接:自动提取实体(人名、项目名等),跨记忆关联
- 时序推理:时间感知检索,区分"当前状态"和"过去事件"
在aiAgent 里面 mem0 角色
使用 Mem0 之前:
用户提问后,系统并行调用
Milvus进行语义检索、BM25进行关键词检索,并访问知识图谱进行关系检索;三路分别返回结果后,业务层需要完成去重、排序、加权融合和上下文整理,再将筛选后的内容注入提示词,交给大模型生成回答。
使用 Mem0 之后:
用户提问进入应用后,由
Mem0统一管理相关记忆的召回、过滤、排序和整理;应用取得整理后的相关记忆后,再交给大模型。也就是说mem0将原来的向量检索,知识图谱,关键词搜索,去重,重排等一系列复杂的事情全部封装好了你只需要将mem0返回的结果交给大模型处理就好了。
上图非常像RAG的处理流程图,但是这个处理的是会话上下文。RAG 负责“企业文档知识”,他的搜索依旧需要原来的解决方案,不能使用mem0做简化,因为mem0的优势是用户记忆,会话记忆,agent的配置记忆。
绑定userld后,存的是使用人的:姓名、城市、偏好、习惯等。 换会话、换Agent也能读到,适合个性化和跨场景复用。
绑定userld+runld,存的是当前这次对话里的任务上下文:这次要做什么、讨论到哪- 步、临时目标等。 会话结束后可以清理,不会和别的会话混在一起。
绑定agent_id,agent_id是Agent 上下文的隔离与归属标识,存的是Agent自己的设定:角色、语气、回答方式、专业领域等。
同一个用户换不同Agent,各自保持独立人格和风格。
就是说换个大模型以后,我设定的机器人是男的还是女的,声音,回答方式,你的角色是旅游博主等等,都不会随agent的切换而改变。
二.mem0的使用
mem0有2个比较容易混淆的类,如下所示:
MemoryClient 把数据发给 Mem0 托管服务;Memory 在你自己的基础设施中运行。
MemoryClient的创建方式
使用MemoryClient创建的memory,他的数据是存放在app.mem0.ai/dashboard/r… 里面的。mem0是第三方数据管理平台。 它不但帮我们托管数据,还帮我完成会话向量检索,知识图谱搜索,关键词搜索。
安装
pnpm install mem0ai
获取MEM0_API_KEY,登录:app.mem0.ai/login 先要注册用户,才能获取。
import MemoryClient from "mem0ai";
import "dotenv/config";
const client = new MemoryClient({
apiKey: process.env.MEM0_API_KEY,
});
使用Memory创建的memory,他的数据是存放在自己的数据库里面。他会和milvus,BM 25,知识图谱配合完成 完成会话向量检索,知识图谱搜索,关键词搜索,去重,重排等繁琐的事情。
Memory的创建方式
import { Memory } from "mem0ai/oss";
const memory = new Memory({
llm: {
provider: "openai",
config: {
apiKey: process.env.OPENAI_API_KEY,
model: "gpt-5-mini",
},
},
embedder: {
provider: "openai",
config: {
apiKey: process.env.OPENAI_API_KEY,
model: "text-embedding-3-small",
},
},
vectorStore: {
provider: "qdrant",
config: {
host: "localhost",
port: 6333,
collectionName: "memories",
},
},
});
MemoryClient和Memory的区别
如何选择MemoryClient和Memory
- 数据必须完全留在企业内部:选
Memory - 希望快速上线、降低基础设施负担:选
MemoryClient - 需要 Graph Memory、Webhooks 等平台能力:选
MemoryClient - 需要深度改造记忆链路:选
Memory
有数据私有化和平台运维能力的企业,更可能选择 Memory;重视交付速度、成本和托管能力的企业,更可能选择 MemoryClient。
数据必须留在自有基础设施,并且企业具备模型与数据库运维能力时,选
Memory;其余大多数项目,尤其是产品验证和一般业务系统,优先选MemoryClient。MemoryClient 的优势是把基础设施和运维责任交给 Mem0 托管平台,因此交付速度更快、基础设施效率更高
MemoryClient使用案例
import MemoryClient from "mem0ai";
import "dotenv/config";
const client = new MemoryClient({
apiKey: process.env.MEM0_API_KEY,
});
const userId = "user-123";
async function main() {
const messages = [
{
role: "user",
content: "我喜欢弹钢琴,我每周都要弹三次,每次一小时,我很喜欢这个过程。",
},
{
role: "assistant",
content: "好的,我已经记住你的喜好。",
},
];
const addResult = await client.add(messages, {
userId,
metadata: {
category: "travel_preference",
source: "chat",
},
});
console.log("新增结果:", JSON.stringify(addResult));
const searchResult = await client.search("推荐北京餐厅", {
filters: {
user_id: userId,
},
topK: 10,
});
console.log("搜索结果:", JSON.stringify(searchResult, null, 2));
const allMemories = await client.getAll({
filters: {
user_id: userId,
},
});
console.log("用户全部记忆:", JSON.stringify(allMemories, null, 2));
const memoryId = "f6ae414a-aa19-4859-8853-a5ad576caeba";
const singleMemory = await client.get(memoryId);
console.log("单条记忆:", JSON.stringify(singleMemory, null, 2));
}
main().catch((error) => {
console.error("Mem0 操作失败:", error);
});
查看 mem0 的数据
语义检索,我每周都要干嘛?他回复我每周喜欢弹钢琴,一周三次。
这个东西的好处是,他帮我们做好了会话上下文的图谱检索,关键词搜索,语义检索,就不需要我们手动再做。不过这个东西是需要掏钱的。每天都有一些额度你可以使用。
利用redis和mem0存储会话
利用redis做短期记忆,压缩记忆本次会话都说了些什么东西。利用mem0做长期记忆,记录用户信息,agent喜好等。
具体实现代码如下:
代码里面用RedisMessageStore类来装redis的存信息,取信息,清空信息的方法。
Mem0MemoryStore类用来存mem0存取数据的方法。
invokeWithMemory是用来加载对话的,不管是进入长期会话还是进入短期会话,都是靠大模型自己来判断加入的。
-
1. 从 Redis 加载当前会话历史 -
2. 从 Mem0 检索用户长期记忆和会话记忆 -
3. 将 Mem0 记忆转换为 SystemMessage -
4. 调用 Agent -
5. 将新对话写入 Redis -
6. 判断哪些内容需要写入 Mem0 -
7. 返回模型结果和统计信息
invokeWithMemory 负责“取记忆—组上下文—调用模型—存会话—筛选长期事实—写回 Mem0”的完整闭环,是整个应用把 Redis 短期会话与 Mem0 长期记忆连接起来的核心方法。
import "dotenv/config";
import Redis from "ioredis";
import * as readline from "node:readline/promises";
import { stdin, stdout } from "node:process";
import { z } from "zod";
import { MemoryClient } from "mem0ai";
import { ChatOpenAI } from "@langchain/openai";
import {
SystemMessage,
SystemMessageChunk,
HumanMessage,
mapChatMessagesToStoredMessages,
mapStoredMessagesToChatMessages,
} from "@langchain/core/messages";
import { createAgent, summarizationMiddleware } from "langchain";
const REDIS_HOST = process.env.REDIS_HOST ?? "localhost";
const REDIS_PORT = Number(process.env.REDIS_PORT ?? 6379);
const REDIS_DB = Number(process.env.REDIS_DB ?? 0);
const MEMORY_TTL = Number(process.env.MEMORY_TTL_SECONDS ?? 1800);
const KEY_PREFIX = process.env.MEMORY_KEY_PREFIX ?? "agent:short_memory";
const USER_ID = process.env.MEM0_USER_ID ?? "demo_user_001";
const SESSION_ID = "session_002";
const MEM0_TOP_K = Number(process.env.MEM0_TOP_K ?? 5);
const memorySchema = z.object({
write_user: z
.boolean()
.describe(
"写入用户层:换一个新会话仍应保留的长期事实(身份、居住地、长期爱好、饮食禁忌、持久偏好)。不含仅本轮任务。",
),
write_session: z
.boolean()
.describe(
"写入会话层:仅当前会话/thread 有效的任务、大纲、进度、待办、临时决策(如「这次先写…」「数据部分明天补」)。",
),
reason: z.string().describe("分类理由,一句话"),
});
const CLASSIFIER_PROMPT = `你是记忆分层分类器。判断本轮对话是否有「新事实」需写入 Mem0,并分到正确层级。
## user 层(跨会话长期)
- 用户身份与画像:姓名、职业、居住地、长期爱好
- 长期偏好与约束:饮食过敏、回答风格、常用技术栈
- 持续数周以上的个人背景(非单次任务)
## session 层(仅当前会话)
- 当前正在做的任务、目标、文档大纲、方案草稿
- 本会话内的进度、决策、待办、临时约定
- 用户明确用「这次」「本轮」「当前会话」描述的工作上下文
## 均不写入
- 寒暄、致谢、纯确认
- 助手生成的通用内容(攻略、示例代码、建议清单),用户未明确采纳为新事实
- 无信息增量的复述
## 决策原则
1. 「这次我们先写 Q1 总结」「当前在排查 XX」→ 优先 session,不要标成 user
2. user 与 session 可同时为 true(如同时说职业+当前任务),但勿把纯会话任务只标 user
3. 一次性请求(如「帮我做旅行攻略」)且未产生需跨轮记住的约定 → 均为 false`;
const summaryPrompt = `你是对话摘要助手。用中文简洁总结:话题、会话内进度/报错/待办。
用户级长期偏好由外部记忆维护,摘要勿重复堆砌。不要编造。
待摘要的对话:
{messages}
摘要:`;
function messagesForRedis(messages) {
return messages.filter(
(m) => !SystemMessage.isInstance(m) && !SystemMessageChunk.isInstance(m),
);
}
class RedisMessageStore {
constructor({ redis, keyPrefix, ttlSeconds }) {
this.redis = redis;
this.keyPrefix = keyPrefix;
this.ttlSeconds = ttlSeconds;
}
messagesKey(sessionId) {
return `${this.keyPrefix}:${sessionId}:messages`;
}
async loadMessages(sessionId) {
const raw = await this.redis.get(this.messagesKey(sessionId));
if (!raw) return [];
return mapStoredMessagesToChatMessages(JSON.parse(raw));
}
async saveMessages(sessionId, messages) {
const payload = JSON.stringify(mapChatMessagesToStoredMessages(messages));
await this.redis.set(this.messagesKey(sessionId), payload, "EX", this.ttlSeconds);
}
async clear(sessionId) {
await this.redis.del(this.messagesKey(sessionId));
}
async ttl(sessionId) {
return this.redis.ttl(this.messagesKey(sessionId));
}
}
class Mem0MemoryStore {
constructor({ client, userId, sessionId, topK, classifier }) {
this.client = client;
this.userId = userId;
this.sessionId = sessionId;
this.topK = topK;
this.classifier = classifier;
}
async search(query) {
const [userRes, sessionRes] = await Promise.all([
this.client.search(query, {
filters: { user_id: this.userId },
topK: this.topK,
}),
this.client.search(query, {
filters: {
AND: [{ user_id: this.userId }, { run_id: this.sessionId }],
},
topK: this.topK,
}),
]);
return {
user: userRes.results ?? [],
session: sessionRes.results ?? [],
};
}
buildSystemMessage({ user, session }) {
const blocks = [];
if (user.length) {
blocks.push(`【用户长期记忆】\n${user.map((m) => `- ${m.memory}`).join("\n")}`);
}
if (session.length) {
blocks.push(`【当前会话记忆】\n${session.map((m) => `- ${m.memory}`).join("\n")}`);
}
if (!blocks.length) return null;
return new SystemMessage(`${blocks.join("\n\n")}\n\n请结合以上记忆回答,勿编造。`);
}
async classifyAndPersist(userText, assistantText) {
const turn = [
{ role: "user", content: userText },
{ role: "assistant", content: assistantText },
];
const { write_user, write_session, reason } = await this.classifier.invoke([
new SystemMessage(CLASSIFIER_PROMPT),
new HumanMessage(`用户:${userText}\n助手:${assistantText}`),
]);
const written = [];
if (write_user) {
await this.client.add(turn, { userId: this.userId });
written.push("user");
}
if (write_session) {
await this.client.add(turn, { userId: this.userId, runId: this.sessionId });
written.push("session");
}
return { written, reason };
}
async clear() {
await this.client.deleteAll({ userId: this.userId });
await this.client.deleteAll({ userId: this.userId, runId: this.sessionId });
}
}
async function invokeWithMemory(agent, redisStore, mem0Store, sessionId, userText) {
const history = await redisStore.loadMessages(sessionId);
console.log(` ↳ Redis 加载 ${history.length} 条历史`);
const mem = await mem0Store.search(userText);
if (mem.user.length) console.log(` ↳ Mem0 用户层 ${mem.user.length} 条`);
if (mem.session.length) console.log(` ↳ Mem0 会话层 ${mem.session.length} 条`);
const memoryMsg = mem0Store.buildSystemMessage(mem);
const invokeMessages = [
...(memoryMsg ? [memoryMsg] : []),
...history,
new HumanMessage(userText),
];
const result = await agent.invoke(
{ messages: invokeMessages },
{ recursionLimit: 30 },
);
const redisMessages = messagesForRedis(result.messages);
const dropped = result.messages.length - redisMessages.length;
await redisStore.saveMessages(sessionId, redisMessages);
const ttl = await redisStore.ttl(sessionId);
console.log(
` ↳ Redis 写回 ${redisMessages.length} 条` +
(dropped ? `(过滤 ${dropped} 条 SystemMessage)` : "") +
` (TTL ${ttl}s)`,
);
const assistantText = String(result.messages.at(-1)?.content ?? "");
const { written, reason } = await mem0Store.classifyAndPersist(userText, assistantText);
console.log(` ↳ 分类: ${reason}`);
console.log(written.length ? ` ↳ Mem0 写入: ${written.join(", ")}` : " ↳ Mem0 未写入");
return { messages: result.messages, redisMessages, assistantText };
}
if (!process.env.MEM0_API_KEY || !process.env.OPENAI_API_KEY) {
console.error("需要 MEM0_API_KEY 与 OPENAI_API_KEY");
process.exit(1);
}
const redis = new Redis({ host: REDIS_HOST, port: REDIS_PORT, db: REDIS_DB });
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY });
redis.on("connect", () => console.log("✅ Redis 已连接"));
redis.on("error", (err) => console.error("❌ Redis 错误:", err.message));
try {
await redis.ping();
} catch {
console.error("Redis 未连接,请先执行: docker compose up -d redis");
process.exit(1);
}
const redisStore = new RedisMessageStore({
redis,
keyPrefix: KEY_PREFIX,
ttlSeconds: MEMORY_TTL,
});
const llmOpts = {
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_BASE_URL },
temperature: 0,
};
const model = new ChatOpenAI({ model: process.env.MODEL_NAME, ...llmOpts });
const classifier = new ChatOpenAI({
model: process.env.MODEL_NAME,
...llmOpts,
}).withStructuredOutput(memorySchema);
const mem0Store = new Mem0MemoryStore({
client: mem0,
userId: USER_ID,
sessionId: SESSION_ID,
topK: MEM0_TOP_K,
classifier,
});
const agent = createAgent({
model,
tools: [],
systemPrompt:
"你是会话助手。结合系统消息中的长期/会话记忆回答,中文简短。有对话摘要则据此继续。",
middleware: [
summarizationMiddleware({
model,
summaryPrompt,
trigger: { messages: 8 },
keep: { messages: 4 },
}),
],
});
console.log(`用户 ${USER_ID} | 会话 ${SESSION_ID}`);
console.log("输入 exit / quit / :q 退出;:clear 清空 Redis;:clear-mem0 清空 Mem0\n");
const rl = readline.createInterface({ input: stdin, output: stdout });
let prevCount = (await redisStore.loadMessages(SESSION_ID)).length;
try {
while (true) {
const userText = (await rl.question("你: ")).trim();
if (!userText) continue;
if (["exit", "quit", ":q"].includes(userText.toLowerCase())) break;
if (userText === ":clear") {
await redisStore.clear(SESSION_ID);
prevCount = 0;
console.log("已清空 Redis 短期记忆\n");
continue;
}
if (userText === ":clear-mem0") {
await mem0Store.clear();
console.log("已清空 Mem0 用户层与当前会话层\n");
continue;
}
const { redisMessages, assistantText } = await invokeWithMemory(
agent,
redisStore,
mem0Store,
SESSION_ID,
userText,
);
console.log("\n助手:", assistantText);
console.log(`Redis 消息数: ${redisMessages.length}`);
if (redisMessages.length < prevCount + 2) {
console.log(" ⚡ 已触发压缩");
}
prevCount = redisMessages.length;
console.log();
}
} finally {
rl.close();
}
await redis.quit();
三.总结
Mem0 是一层智能记忆系统,通过
user_id、run_id和agent_id管理用户、会话与 Agent 上下文;MemoryClient使用 Mem0 托管服务,Memory在自有基础设施运行;企业文档知识仍由 RAG 负责。
- Mem0 解决 AI 无状态导致的上下文丢失问题,使应用和 Agent 能跨会话记住相关信息。
- 用户记忆绑定
user_id,保存长期有效的事实和偏好。 - 会话记忆绑定
run_id,保存当前任务、进度和临时决策。 - Agent 记忆绑定
agent_id,用于隔离不同 Agent 的上下文,而不是保存角色提示词。 MemoryClient使用托管平台,Memory在自有基础设施运行。- Mem0 负责用户和 Agent 记忆,RAG 负责企业文档知识,两者可以组合使用。
- 生产项目还需增加授权、脱敏、审计、删除、权限控制和数据留存策略。
Mem0 不是通用文档 RAG 的替代品,也不是 Agent 配置中心,而是位于业务应用与模型之间的智能记忆层。它负责事实抽取、记忆存储、更新、冲突处理、范围隔离、检索和结果整理;企业文档检索仍应由专门的 RAG 系统承担。