Skip to content

第七章:记忆与上下文管理 —— 让 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 应用中的本地存储方案

练习 ​

  1. 实现一个 SummaryMemory,测试对话 20 轮后的摘要质量
  2. 把长期记忆功能加入你的 Agent,让它记住用户偏好
  3. 设计一个上下文预算管理器,自动平衡各部分内容

下一章我们将学习多 Agent 协作,让多个 Agent 组队完成复杂任务。

📖本文阅读--次|📊全站访问--次|👥访客--人