【从0开发一个 Agent】第八章:实现长期记忆
在前面的章节中,我们赋予了 AI 对话的能力、执行工具的能力以及无限扩展的 MCP 能力。但如果你现在关闭浏览器,明天再来问 AI:“我昨天让你查的那个天气是哪个城市?”它一定会一脸茫然。
大语言模型(LLM)天生是“无状态”的,每次对话对它来说都是全新的开始。为了让 AI 从“金鱼记忆”进化为真正懂你的“数字员工”,我们必须为其构建记忆系统(Memory System)。本章我们将实现从短期会话记忆到跨会话长期记忆的完整闭环。
1. 为什么 AI 会“健忘”?
在传统的 Chat 界面中,所谓的“记忆”其实是一种“伪记忆”。它只是前端把过去的对话历史拼接成一个超长的 Prompt 重新发给 LLM。一旦对话长度超出了模型的上下文窗口(Context Window),早期的信息就会被无情截断。
为了构建真正的企业级 Agent,我们需要借鉴人类认知科学,将记忆分为两类:
- Session Memory(短期/工作记忆):维持当前单次会话的连贯性,生命周期随会话结束而销毁。
- Long-term Memory(长期记忆):跨越多个会话持久化存储。它又可以细分为:
– 语义记忆(Semantic Memory):用户的偏好、事实(如“我喜欢吃辣”、“我是前端工程师”)。
– 情景记忆(Episodic Memory):过去的特定事件和经历(如“上周二我们一起排查了一个数据库 Bug”)。
2. 记忆系统架构设计

设计思考:
记忆系统的核心在于读写分离与异步处理。读取记忆(Retrieve)必须在对话前同步完成,以保证 LLM 拥有充足的上下文;而写入记忆(Extract & Store)如果同步执行,会严重拖慢响应速度。因此,我们采用“后台异步抽取”的策略,在流式响应结束后,利用另一个 LLM 调用去提炼有价值的长期记忆。
3. 数据库设计
为了支撑上述架构,我们需要在 Prisma 中设计合理的表结构。将“会话”与“消息”分离,并独立出“长期记忆”表。
// prisma/schema.prisma
model Conversation {
id String @id @default(uuid())
userId String
title String
createdAt DateTime @default(now())
messages Message[]
}
model Message {
id String @id @default(uuid())
conversationId String
role String // user, assistant, system
content String @db.Text
createdAt DateTime @default(now())
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
}
model UserMemory {
id String @id @default(uuid())
userId String
content String @db.Text // 记忆内容,如 "用户偏好使用 TypeScript"
embedding Float32Array @db.Vector(1536) // 用于语义检索
type String // semantic, episodic
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
4. 核心代码实现
4.1 会话的保存与恢复(Session Memory)
在 /api/chat/route.ts 中,我们在流式响应结束后,将完整的对话持久化到 PostgreSQL。
// src/app/api/chat/route.ts
import { streamText } from 'ai';
import { prisma } from '@/lib/db';
export async function POST(req: Request) {
const { messages, conversationId } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
messages,
});
// 异步保存对话,不阻塞流式响应
result.onFinish(async ({ finishReason }) => {
if (finishReason === 'stop') {
await prisma.message.createMany({
data: result.response.messages.map(msg => ({
conversationId,
role: msg.role,
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
}))
});
}
});
return result.toDataStreamResponse();
}
4.2 长期记忆的异步抽取
这是记忆系统最核心的魔法。我们在对话结束后,调用一个轻量级模型来“反思”并提取记忆。
// src/lib/ai/memory-extractor.ts
import { generateText } from 'ai';
import { openai } from '@/lib/ai/config';
import { prisma } from '@/lib/db';
export async function extractAndSaveMemory(conversationId: string, userId: string) {
// 1. 获取最近的对话
const recentMessages = await prisma.message.findMany({
where: { conversationId },
orderBy: { createdAt: 'desc' },
take: 10,
});
const conversationText = recentMessages.reverse().map(m => `${m.role}: ${m.content}`).join('\n');
// 2. 让 LLM 提取有价值的记忆
const { text } = await generateText({
model: openai('gpt-4o-mini'), // 使用便宜且快的模型
system: '你是一个记忆提取器。请从对话中提取用户的长期偏好、事实或重要事件。如果没有,返回空数组。以 JSON 数组格式返回,如 [{"content": "...", "type": "semantic"}]',
prompt: conversationText,
});
try {
const memories = JSON.parse(text);
// 3. 生成 Embedding 并存入数据库
for (const mem of memories) {
const embedding = await openai.embedding('text-embedding-3-small').doEmbed(mem.content);
await prisma.userMemory.create({
data: { userId, content: mem.content, type: mem.type, embedding }
});
}
} catch (e) {
console.error('Memory extraction failed:', e);
}
}
4.3 记忆检索与上下文注入
当用户开启新对话时,我们需要根据当前问题,从 UserMemory 中检索相关的长期记忆。
// 在 /api/chat/route.ts 的请求处理中
async function getContextMemories(userId: string, currentQuery: string) {
// 1. 将当前查询向量化
const queryEmbedding = await openai.embedding('text-embedding-3-small').doEmbed(currentQuery);
// 2. 使用 pgvector 进行相似度检索
const relevantMemories = await prisma.$queryRaw`
SELECT content FROM "UserMemory"
WHERE "userId" = ${userId}
ORDER BY embedding <=> ${queryEmbedding}::vector
LIMIT 5
`;
return relevantMemories.map(m => m.content).join('\n');
}
最后,将检索到的 relevantMemories 拼接到 System Prompt 中:
你是一个 AI 助手。以下是关于用户的历史记忆:\n${relevantMemories}\n请基于这些记忆提供个性化的回答。
5. 测试验证
验证清单:
- 保存与恢复:刷新页面后,重新加载历史对话,UI 能无缝展示。
- 记忆提取:告诉 AI “我是一名 Java 后端工程师,喜欢喝美式咖啡”,结束对话。
- 记忆检索:开启新对话,询问“帮我推荐一杯饮料”,AI 应该能准确推荐“美式咖啡”。
- 上下文压缩:进行超长对话(超过 50 轮),观察 AI 是否依然能记住开头的核心设定(得益于记忆检索机制)
6. 常见问题与踩坑分析
问题 1:记忆无限膨胀,导致检索成本极高
原因:每次对话都提取记忆,很快数据库中就会充满冗余和过时的信息。
解决:引入记忆遗忘与合并机制(Consolidation & Forgetting)。在写入新记忆前,先检索相似记忆。如果相似度极高,则更新旧记忆而不是创建新记忆;对于超过 3 个月未被检索到的记忆,自动标记为归档或删除。
问题 2:检索出的记忆与当前问题无关(幻觉注入)
原因:向量相似度搜索(ANN)有时会返回语义相近但事实错误的记忆。
解决:在检索后增加一层 Rerank(重排序) 模型,或者在 System Prompt 中明确指示:“如果检索到的记忆与当前问题无关,请忽略它,不要强行使用。”
本章总结
- 我们剖析了 LLM 无状态的缺陷,并引入了 Session Memory 与 Long-term Memory 的分层架构。
- 设计了支持向量检索的 PostgreSQL 数据库表结构。
- 实现了对话的持久化保存、异步记忆提取以及基于 pgvector 的语义检索。
- 掌握了通过 System Prompt 注入长期记忆,实现跨会话个性化交互的核心技巧。
至此,你的 AI Agent 已经拥有了跨越时间的“记忆”,它开始真正认识并了解它的用户。
但企业级应用不仅需要记住过去,还需要学习海量的私有文档(如产品手册、API 文档)。从下一章开始,我们将构建 RAG(检索增强生成)知识库,让 Agent 成为无所不知的领域专家。
更多推荐


所有评论(0)