在前面的章节中,我们赋予了 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 成为无所不知的领域专家。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐