Skip to content

课 1 · 记忆管理

本课目标

实现短期/长期记忆架构,让 Agent 记住用户信息、跨会话保留上下文。课后你会拿到一个具备记忆能力的 Agent。

这一课先掌握短期记忆、长期记忆各自解决什么问题,不要求一开始就把完整记忆系统一次做满。

上一模块的 Agent 有一个明显的短板:没有记忆。每次对话都从零开始,不记得之前聊过什么、做过什么。

这一课解决核心问题:怎么让 Agent 记住东西。

你可以把它理解成两层能力:一层是“当前对话别忘”,另一层是“跨会话还能想起用户说过的重要事实和偏好”。

短期记忆

短期记忆就是对话历史——前面模块已经用过的 messages 数组。

滑动窗口

最简单的策略,保留最近 N 轮对话:

typescript
function slidingWindow(messages: ModelMessage[], maxRounds = 10): ModelMessage[] {
  const max = maxRounds * 2 // 每轮 = 1条 user + 1条 assistant
  return messages.length <= max ? messages : messages.slice(-max)
}

问题:早期的重要对话被丢掉了。

Token 预算

按 Token 总量控制,而不是轮数:

typescript
function tokenBudget(messages: ModelMessage[], maxTokens = 4000): ModelMessage[] {
  let total = 0
  const result: ModelMessage[] = []

  // 从最新的消息开始往前算
  for (let i = messages.length - 1; i >= 0; i--) {
    const content = typeof messages[i].content === 'string'
      ? messages[i].content as string : ''
    const tokens = Math.ceil(content.length / 3) // 粗略估算
    if (total + tokens > maxTokens) break
    total += tokens
    result.unshift(messages[i])
  }

  return result
}

长期记忆

短期记忆随着窗口滑动会丢失。长期记忆把重要信息持久化存储,需要时按需检索。

基于向量数据库的长期记忆

核心思路:像存文档一样存记忆,用语义检索找相关记忆。

如果你前面已经做过知识库 RAG,可以把这里理解成“把用户事实、偏好、任务状态也当成一类可检索的小文档”。区别不是技术路线变了,而是存进去的内容从“产品文档”换成了“用户上下文”。

typescript
// 假设已初始化 const embeddingModel = provider.embedding('text-embedding-v4')
interface Memory {
  content: string
  embedding: number[]
  timestamp: number
  type: 'fact' | 'preference' | 'task'
}

const memoryStore: Memory[] = []

// 存储记忆
async function storeMemory(content: string, type: Memory['type']) {
  const { embedding } = await embed({
    model: embeddingModel,
    value: content,
  })
  memoryStore.push({
    content,
    embedding,
    timestamp: Date.now(),
    type,
  })
}

// 检索相关记忆
async function recallMemories(query: string, topK = 3): Promise<Memory[]> {
  const { embedding } = await embed({
    model: embeddingModel,
    value: query,
  })

  return memoryStore
    .map(m => ({ ...m, similarity: cosineSimilarity(embedding, m.embedding) }))
    .sort((a, b) => b.similarity - a.similarity)
    .slice(0, topK)
}

什么时候存记忆

不能把所有对话都存进长期记忆——太多噪音。需要有策略地提取。

先记住一个原则:不是所有对话都值得长期保存。 “今天天气不错”通常没必要存;“我项目用的是 Next.js App Router”“我更喜欢简洁回答”这种信息才更值得保留。

需要有策略地提取:

typescript
import { generateText, Output, type ModelMessage } from 'ai'
import { z } from 'zod'

const MemoryExtractionSchema = z.object({
  shouldStore: z.boolean().describe('这段对话是否包含值得存储的重要信息'),
  memories: z.array(z.object({
    content: z.string().describe('要存储的信息'),
    type: z.enum(['fact', 'preference', 'task']).describe('记忆类型'),
  })).describe('提取出的记忆'),
})

async function extractMemories(messages: ModelMessage[]) {
  const recent = messages.slice(-4) // 取最近两轮对话
  const { output } = await generateText({
    model,
    output: Output.object({ schema: MemoryExtractionSchema }),
    prompt: `从以下对话中提取值得长期记住的信息:
${recent.map(m => `${m.role}: ${m.content}`).join('\n')}

类型说明:
- fact: 用户提到的事实信息(如"我用的是 React 18")
- preference: 用户偏好(如"我喜欢函数式写法")
- task: 待办或进行中的任务`,
  })

  if (output.shouldStore) {
    for (const mem of output.memories) {
      await storeMemory(mem.content, mem.type)
    }
  }
}

在对话中使用长期记忆

每次用户提问时,先检索相关记忆,拼进 System Prompt:

typescript
async function chatWithMemory(userInput: string, messages: ModelMessage[]) {
  // 检索相关记忆
  const memories = await recallMemories(userInput, 3)
  const memoryContext = memories.length > 0
    ? `\n<memories>\n${memories.map(m => `[${m.type}] ${m.content}`).join('\n')}\n</memories>`
    : ''

  const result = streamText({
    model,
    system: `你是一个有记忆能力的 AI 助手。${memoryContext}`,
    messages,
  })

  // 对话结束后提取记忆
  // await extractMemories(messages)

  return result
}

上下文压缩

当对话很长时,可以用 LLM 把历史消息压缩成摘要:

typescript
async function compressHistory(messages: ModelMessage[]): Promise<string> {
  const { text } = await generateText({
    model,
    prompt: `将以下对话历史压缩成一段简洁的摘要,保留关键信息:

${messages.map(m => `${m.role}: ${typeof m.content === 'string' ? m.content : '[complex]'}`).join('\n')}

要求:
- 保留用户的核心需求和偏好
- 保留已完成的任务
- 保留重要的技术细节
- 控制在 200 字以内`,
  })
  return text
}

然后用摘要替代完整历史:

typescript
const compressed = await compressHistory(oldMessages)
const newMessages: ModelMessage[] = [
  { role: 'system', content: `之前的对话摘要:${compressed}` },
  ...recentMessages,
]

本课产物

  • ✅ 短期记忆(滑动窗口 + Token 预算)
  • ✅ 长期记忆(向量存储 + 语义检索)
  • ✅ 记忆提取策略
  • ✅ 上下文压缩

完整代码在 basic/examples/06-memory/01-memory/index.ts

并入主线项目

这一课的 Demo 用最小可运行代码把记忆管理的完整链路串起来:短期记忆、长期记忆、记忆提取、上下文压缩。

基础项目里,这些能力会收束到 basic/project:Agent 会把值得保留的事实、偏好和任务写入长期记忆,在后续对话中按需检索,并在对话过长时压缩更早的历史消息。

也就是说,这一课不只是讲概念,basic/project 已经真正承接了记忆能力;课内 Demo 则负责把实现思路拆开讲清楚,方便你单独验证每个环节。

试试看

bash
cd daqi-ai-agent
pnpm exec tsx basic/examples/06-memory/01-memory/index.ts
  1. 第一次运行时告诉 Agent 你的偏好(如「我喜欢简洁回答」),然后退出
  2. 重新启动后继续对话,验证偏好是否被持久化记住
  3. 问「你对我了解什么?」——检查记忆模块存储了哪些内容

面试追问

Q:Agent 的短期记忆和长期记忆有什么区别?

短期记忆是对话历史 messages 数组,在一次会话中有效,窗口滑动后早期内容丢失。长期记忆把重要信息持久化存储(通常用向量数据库),跨会话保留,用时按语义检索取回。短期靠窗口管理,长期靠 Embedding + 检索。

Q:上下文压缩会不会丢信息?

会。压缩本质是有损的——LLM 生成摘要时会判断什么"重要",可能丢掉后续需要的细节。缓解方式:保留最近 N 轮完整对话不压缩(只压缩更早的)、压缩时强调保留特定类型的信息(如用户偏好、任务状态)、长期记忆作为压缩的补充。

面向前端工程师和独立开发者的 AI 应用工程课程