Appearance
课 1 · 记忆管理
本课目标
实现短期/长期记忆架构,让 Agent 记住用户信息、跨会话保留上下文。课后你会拿到一个具备记忆能力的 Agent。
这一课先掌握短期记忆、长期记忆各自解决什么问题,不要求一开始就把完整记忆系统一次做满。
上一模块的 Agent 有一个明显的短板:没有记忆。每次对话都从零开始,不记得之前聊过什么、做过什么。
这一课解决核心问题:怎么让 Agent 记住东西。
你可以把它理解成两层能力:一层是“当前对话别忘”,另一层是“跨会话还能想起用户说过的重要事实和偏好”。
Agent 记忆的两层结构
用户当前问题
→
短期记忆当前会话历史,滑动窗口 / Token 预算
+
长期记忆跨会话存储,fact / preference / task
→
拼入本轮上下文并回答
短期记忆解决“当前对话别忘”,长期记忆解决“下次重启还能想起”。
短期记忆
短期记忆就是对话历史——前面模块已经用过的 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- 第一次运行时告诉 Agent 你的偏好(如「我喜欢简洁回答」),然后退出
- 重新启动后继续对话,验证偏好是否被持久化记住
- 问「你对我了解什么?」——检查记忆模块存储了哪些内容
面试追问
Q:Agent 的短期记忆和长期记忆有什么区别?
短期记忆是对话历史 messages 数组,在一次会话中有效,窗口滑动后早期内容丢失。长期记忆把重要信息持久化存储(通常用向量数据库),跨会话保留,用时按语义检索取回。短期靠窗口管理,长期靠 Embedding + 检索。
Q:上下文压缩会不会丢信息?
会。压缩本质是有损的——LLM 生成摘要时会判断什么"重要",可能丢掉后续需要的细节。缓解方式:保留最近 N 轮完整对话不压缩(只压缩更早的)、压缩时强调保留特定类型的信息(如用户偏好、任务状态)、长期记忆作为压缩的补充。