第七章:记忆与上下文管理 —— 让 Agent 拥有持久记忆
7.1 为什么 Agent 需要记忆?
没有记忆的 Agent 就像一条金鱼——每次对话都从零开始。
没有记忆:
用户:我喜欢用 React
Agent:好的
----- 新会话 -----
用户:帮我写一个组件
Agent:你想用什么框架?(不记得上次对话了)
有记忆:
用户:帮我写一个组件
Agent:好的,我知道你偏好 React,这是一个 React 组件...(记住了你的偏好)7.2 Agent 记忆体系
┌───────────────────────────────────────────────────────────┐
│ Agent 记忆体系 │
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ 短期记忆 │ │ 工作记忆 │ │ 长期记忆 │ │
│ │ Short-term │ │ Working │ │ Long-term │ │
│ │ │ │ │ │ │ │
│ │ 当前对话上下文│ │ 当前任务状态 │ │ 用户偏好 │ │
│ │ 即 messages │ │ 临时变量 │ │ 历史摘要 │ │
│ │ 数组 │ │ 中间结果 │ │ 学到的知识 │ │
│ │ │ │ │ │ │ │
│ │ 生命周期: │ │ 生命周期: │ │ 生命周期: │ │
│ │ 单次对话 │ │ 单个任务 │ │ 永久持久化 │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
└───────────────────────────────────────────────────────────┘7.3 短期记忆:对话上下文管理
短期记忆就是 messages 数组。核心问题是:上下文窗口有限,无法无限增长。
问题:对话太长怎么办?
javascript
// 如果每轮对话加 500 tokens,聊了 200 轮 = 100,000 tokens
// 即使是 128K 上下文窗口也会溢出,而且消耗大量费用方案一:滑动窗口
只保留最近 N 条消息:
javascript
class SlidingWindowMemory {
constructor(maxMessages = 20) {
this.maxMessages = maxMessages;
this.messages = [];
this.systemMessage = null;
}
setSystemMessage(content) {
this.systemMessage = { role: 'system', content };
}
addMessage(message) {
this.messages.push(message);
// 超出限制时,删掉最早的消息
while (this.messages.length > this.maxMessages) {
this.messages.shift();
}
}
getMessages() {
// System message 始终保留在最前面
return this.systemMessage
? [this.systemMessage, ...this.messages]
: [...this.messages];
}
}方案二:Token 预算管理
更精确地控制 Token 使用量:
javascript
class TokenBudgetMemory {
constructor(maxTokens = 4000) {
this.maxTokens = maxTokens;
this.messages = [];
this.systemMessage = null;
}
// 粗略估算 Token 数(实际项目用 tiktoken 库精确计算)
estimateTokens(text) {
// 英文约 4 字符 = 1 token,中文约 2 字符 = 1 token
return Math.ceil(text.length / 3);
}
addMessage(message) {
this.messages.push(message);
this.trimToFitBudget();
}
trimToFitBudget() {
let totalTokens = this.messages.reduce(
(sum, m) => sum + this.estimateTokens(m.content || ''),
0
);
// 从最早的消息开始删除,直到在预算内
while (totalTokens > this.maxTokens && this.messages.length > 2) {
const removed = this.messages.shift();
totalTokens -= this.estimateTokens(removed.content || '');
}
}
getMessages() {
return this.systemMessage
? [this.systemMessage, ...this.messages]
: [...this.messages];
}
}方案三:摘要压缩(最推荐)
当对话太长时,让 LLM 把旧对话压缩成摘要:
javascript
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
class SummaryMemory {
constructor(options = {}) {
this.maxMessages = options.maxMessages || 20;
this.summaryThreshold = options.summaryThreshold || 15;
this.messages = [];
this.summary = ''; // 历史对话摘要
this.systemMessage = null;
}
setSystemMessage(content) {
this.systemMessage = { role: 'system', content };
}
async addMessage(message) {
this.messages.push(message);
// 当消息数超过阈值,触发摘要
if (this.messages.length >= this.summaryThreshold) {
await this.compressHistory();
}
}
async compressHistory() {
// 取出前面的旧消息进行压缩,保留最近几条
const keepRecent = 5;
const oldMessages = this.messages.slice(0, -keepRecent);
const recentMessages = this.messages.slice(-keepRecent);
if (oldMessages.length === 0) return;
// 让 LLM 生成摘要
const conversation = oldMessages
.map(m => `${m.role}: ${m.content}`)
.join('\n');
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini', // 摘要用小模型就够了,省钱
messages: [
{
role: 'system',
content: '将以下对话浓缩为简洁的摘要,保留关键信息、用户偏好、已完成的任务和重要决策。用中文输出。'
},
{
role: 'user',
content: `之前的摘要:${this.summary || '无'}\n\n新的对话:\n${conversation}`
}
],
});
this.summary = response.choices[0].message.content;
this.messages = recentMessages;
console.log('📝 对话已压缩,当前摘要:', this.summary.substring(0, 100) + '...');
}
getMessages() {
const msgs = [];
// 1. System message
if (this.systemMessage) {
let sysContent = this.systemMessage.content;
// 将摘要嵌入 system message
if (this.summary) {
sysContent += `\n\n## 之前的对话摘要\n${this.summary}`;
}
msgs.push({ role: 'system', content: sysContent });
}
// 2. 最近的消息
msgs.push(...this.messages);
return msgs;
}
}
// 使用示例
const memory = new SummaryMemory({ maxMessages: 20, summaryThreshold: 10 });
memory.setSystemMessage('你是一个编程助手。');
await memory.addMessage({ role: 'user', content: '我在用 React 开发一个 Todo 应用' });
await memory.addMessage({ role: 'assistant', content: '好的!我来帮你...' });
// ... 多轮对话后,旧消息会被自动压缩为摘要7.4 长期记忆:跨会话持久化
长期记忆让 Agent 记住用户偏好和历史信息,会话断开后依然保留。
简单文件存储方案
javascript
// long-term-memory.mjs
import fs from 'fs/promises';
class LongTermMemory {
constructor(storagePath = './agent-memory.json') {
this.storagePath = storagePath;
this.data = {
userPreferences: {}, // 用户偏好
facts: [], // 学到的事实
taskHistory: [], // 任务历史
};
}
async load() {
try {
const raw = await fs.readFile(this.storagePath, 'utf-8');
this.data = JSON.parse(raw);
} catch {
// 文件不存在,用默认值
}
}
async save() {
await fs.writeFile(this.storagePath, JSON.stringify(this.data, null, 2));
}
// 记住用户偏好
async setPreference(key, value) {
this.data.userPreferences[key] = value;
await this.save();
}
getPreference(key) {
return this.data.userPreferences[key];
}
getAllPreferences() {
return this.data.userPreferences;
}
// 记住事实
async addFact(fact) {
this.data.facts.push({
content: fact,
timestamp: new Date().toISOString(),
});
// 限制存储量
if (this.data.facts.length > 1000) {
this.data.facts = this.data.facts.slice(-1000);
}
await this.save();
}
// 搜索相关事实(简单关键词搜索)
searchFacts(query) {
const keywords = query.toLowerCase().split(/\s+/);
return this.data.facts.filter(f =>
keywords.some(kw => f.content.toLowerCase().includes(kw))
);
}
// 记录任务历史
async logTask(task) {
this.data.taskHistory.push({
...task,
timestamp: new Date().toISOString(),
});
if (this.data.taskHistory.length > 500) {
this.data.taskHistory = this.data.taskHistory.slice(-500);
}
await this.save();
}
}
export { LongTermMemory };让 Agent 自动提取和存储记忆
javascript
// memory-agent.mjs
import OpenAI from 'openai';
import { LongTermMemory } from './long-term-memory.mjs';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const longMemory = new LongTermMemory();
await longMemory.load();
// 让 LLM 从对话中自动提取值得记住的信息
async function extractMemories(userMessage, assistantReply) {
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
response_format: { type: 'json_object' },
messages: [
{
role: 'system',
content: `分析以下用户和助手的对话,提取值得长期记忆的信息。
输出 JSON:
{
"preferences": [
{ "key": "偏好名称", "value": "偏好值" }
],
"facts": [
"需要记住的事实"
],
"nothing_to_remember": true/false
}
示例:
用户说"我用 TypeScript 比较多" → preferences: [{"key": "编程语言", "value": "TypeScript"}]
用户说"帮我写个排序" → nothing_to_remember: true(这是一次性请求,不需要记忆)`
},
{
role: 'user',
content: `用户: ${userMessage}\n助手: ${assistantReply}`
}
],
});
return JSON.parse(response.choices[0].message.content);
}
// 在每轮对话后调用
async function afterChat(userMessage, assistantReply) {
const memories = await extractMemories(userMessage, assistantReply);
if (!memories.nothing_to_remember) {
// 存储偏好
for (const pref of (memories.preferences || [])) {
await longMemory.setPreference(pref.key, pref.value);
console.log(`💾 记住偏好: ${pref.key} = ${pref.value}`);
}
// 存储事实
for (const fact of (memories.facts || [])) {
await longMemory.addFact(fact);
console.log(`💾 记住事实: ${fact}`);
}
}
}在 System Prompt 中注入长期记忆
javascript
function buildSystemPromptWithMemory(basePrompt) {
const prefs = longMemory.getAllPreferences();
const prefStr = Object.entries(prefs)
.map(([k, v]) => `- ${k}: ${v}`)
.join('\n');
return `${basePrompt}
## 用户画像(长期记忆)
已知的用户偏好:
${prefStr || '暂无'}
请根据用户偏好个性化你的回答。例如,如果用户偏好 TypeScript,代码示例就用 TypeScript。`;
}7.5 工作记忆:任务执行过程中的状态
工作记忆用于跟踪当前任务的中间状态:
javascript
class WorkingMemory {
constructor() {
this.currentTask = null;
this.plan = []; // 任务计划
this.completedSteps = []; // 已完成步骤
this.variables = {}; // 中间变量
this.notes = []; // Agent 的笔记
}
// 设置当前任务
setTask(task) {
this.currentTask = task;
this.plan = [];
this.completedSteps = [];
this.variables = {};
this.notes = [];
}
// 添加计划步骤
setPlan(steps) {
this.plan = steps.map((step, i) => ({
id: i + 1,
description: step,
status: 'pending',
}));
}
// 标记步骤完成
completeStep(stepId, result) {
const step = this.plan.find(s => s.id === stepId);
if (step) {
step.status = 'completed';
step.result = result;
this.completedSteps.push(step);
}
}
// 存储中间变量(如搜索结果、计算结果等)
setVariable(key, value) {
this.variables[key] = value;
}
// Agent 给自己写笔记
addNote(note) {
this.notes.push({ content: note, time: new Date().toISOString() });
}
// 生成状态摘要,供 LLM 参考
getSummary() {
return `
## 当前任务状态
任务: ${this.currentTask || '无'}
### 执行计划
${this.plan.map(s => `${s.status === 'completed' ? '✅' : '⬜'} ${s.id}. ${s.description}`).join('\n')}
### 中间结果
${Object.entries(this.variables).map(([k, v]) => `- ${k}: ${JSON.stringify(v).substring(0, 200)}`).join('\n') || '无'}
### Agent 笔记
${this.notes.map(n => `- ${n.content}`).join('\n') || '无'}
`;
}
}7.6 完整记忆系统集成
把三种记忆整合到一起:
javascript
// memory-system.mjs
import { SummaryMemory } from './summary-memory.mjs';
import { LongTermMemory } from './long-term-memory.mjs';
class AgentMemorySystem {
constructor(options = {}) {
// 短期记忆(对话上下文)
this.shortTerm = new SummaryMemory({
maxMessages: options.maxMessages || 20,
summaryThreshold: options.summaryThreshold || 15,
});
// 长期记忆(持久化)
this.longTerm = new LongTermMemory(options.storagePath);
// 工作记忆(当前任务)
this.working = new WorkingMemory();
}
async initialize() {
await this.longTerm.load();
}
// 构建完整的 system prompt
buildContext(baseSystemPrompt) {
const prefs = this.longTerm.getAllPreferences();
const prefStr = Object.entries(prefs)
.map(([k, v]) => `- ${k}: ${v}`)
.join('\n');
const workingState = this.working.getSummary();
let fullPrompt = baseSystemPrompt;
if (prefStr) {
fullPrompt += `\n\n## 用户偏好\n${prefStr}`;
}
if (this.working.currentTask) {
fullPrompt += `\n\n${workingState}`;
}
this.shortTerm.setSystemMessage(fullPrompt);
return this.shortTerm.getMessages();
}
// 添加对话消息
async addUserMessage(content) {
await this.shortTerm.addMessage({ role: 'user', content });
}
async addAssistantMessage(content) {
await this.shortTerm.addMessage({ role: 'assistant', content });
}
}
export { AgentMemorySystem };7.7 上下文窗口最佳策略
┌──────────────────────────────────────────┐
│ Context Window 分配 │
│ │
│ ┌──────────────────────────┐ │
│ │ System Prompt (固定) │ ~15% │
│ │ + 用户偏好 │ │
│ │ + 工作记忆状态 │ │
│ ├──────────────────────────┤ │
│ │ 历史对话摘要 │ ~10% │
│ ├──────────────────────────┤ │
│ │ RAG 检索结果 │ ~25% │
│ ├──────────────────────────┤ │
│ │ 最近对话消息 │ ~30% │
│ ├──────────────────────────┤ │
│ │ 预留给输出 │ ~20% │
│ └──────────────────────────┘ │
└──────────────────────────────────────────┘javascript
// 上下文预算管理器
class ContextBudgetManager {
constructor(maxTokens = 16000) {
this.maxTokens = maxTokens;
this.budget = {
system: Math.floor(maxTokens * 0.15),
summary: Math.floor(maxTokens * 0.10),
rag: Math.floor(maxTokens * 0.25),
conversation: Math.floor(maxTokens * 0.30),
output: Math.floor(maxTokens * 0.20),
};
}
estimateTokens(text) {
return Math.ceil((text || '').length / 3);
}
// 智能裁剪各部分内容以适应预算
fitToBudget({ systemPrompt, summary, ragResults, messages }) {
const result = [];
// 1. System prompt(必须保留,但可能需要截断)
let sysContent = systemPrompt;
if (summary) {
sysContent += `\n\n## 对话摘要\n${summary}`;
}
if (this.estimateTokens(sysContent) > this.budget.system + this.budget.summary) {
sysContent = sysContent.substring(0, (this.budget.system + this.budget.summary) * 3);
}
result.push({ role: 'system', content: sysContent });
// 2. RAG 结果作为独立消息
if (ragResults && ragResults.length > 0) {
let ragContent = ragResults.map(r => r.text).join('\n\n---\n\n');
if (this.estimateTokens(ragContent) > this.budget.rag) {
ragContent = ragContent.substring(0, this.budget.rag * 3);
}
result.push({
role: 'user',
content: `[参考知识]\n${ragContent}`
});
}
// 3. 对话历史(从最新的开始保留)
let tokenCount = 0;
const conversationMessages = [];
for (let i = messages.length - 1; i >= 0; i--) {
const msgTokens = this.estimateTokens(messages[i].content);
if (tokenCount + msgTokens > this.budget.conversation) break;
conversationMessages.unshift(messages[i]);
tokenCount += msgTokens;
}
result.push(...conversationMessages);
return result;
}
}7.8 Electron 应用中的记忆存储
在 Electron 桌面应用中,你有更多本地存储选项:
javascript
// electron-memory-store.mjs
import { app } from 'electron';
import path from 'path';
import fs from 'fs/promises';
import Database from 'better-sqlite3';
class ElectronMemoryStore {
constructor() {
// Electron 的用户数据目录
const userDataPath = app.getPath('userData');
this.dbPath = path.join(userDataPath, 'agent-memory.db');
}
init() {
this.db = new Database(this.dbPath);
this.db.exec(`
-- 用户偏好表
CREATE TABLE IF NOT EXISTS preferences (
key TEXT PRIMARY KEY,
value TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 对话历史表
CREATE TABLE IF NOT EXISTS conversations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 事实记忆表
CREATE TABLE IF NOT EXISTS facts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
category TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 会话摘要表
CREATE TABLE IF NOT EXISTS session_summaries (
session_id TEXT PRIMARY KEY,
summary TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
}
// 偏好操作
setPreference(key, value) {
this.db.prepare(
'INSERT OR REPLACE INTO preferences (key, value, updated_at) VALUES (?, ?, datetime("now"))'
).run(key, JSON.stringify(value));
}
getPreference(key) {
const row = this.db.prepare('SELECT value FROM preferences WHERE key = ?').get(key);
return row ? JSON.parse(row.value) : null;
}
getAllPreferences() {
return this.db.prepare('SELECT key, value FROM preferences').all()
.reduce((acc, row) => ({ ...acc, [row.key]: JSON.parse(row.value) }), {});
}
// 对话历史操作
addMessage(sessionId, role, content) {
this.db.prepare(
'INSERT INTO conversations (session_id, role, content) VALUES (?, ?, ?)'
).run(sessionId, role, content);
}
getConversation(sessionId, limit = 50) {
return this.db.prepare(
'SELECT role, content FROM conversations WHERE session_id = ? ORDER BY id DESC LIMIT ?'
).all(sessionId, limit).reverse();
}
// 保存会话摘要
saveSessionSummary(sessionId, summary) {
this.db.prepare(
'INSERT OR REPLACE INTO session_summaries (session_id, summary) VALUES (?, ?)'
).run(sessionId, summary);
}
}
export { ElectronMemoryStore };7.9 小结
本章你掌握了:
- ✅ Agent 的三种记忆:短期、工作、长期
- ✅ 短期记忆管理:滑动窗口、Token 预算、摘要压缩
- ✅ 长期记忆:用户偏好、事实记忆、自动提取
- ✅ 工作记忆:任务状态跟踪
- ✅ 上下文窗口的预算分配策略
- ✅ Electron 应用中的本地存储方案
练习
- 实现一个
SummaryMemory,测试对话 20 轮后的摘要质量 - 把长期记忆功能加入你的 Agent,让它记住用户偏好
- 设计一个上下文预算管理器,自动平衡各部分内容
下一章我们将学习多 Agent 协作,让多个 Agent 组队完成复杂任务。