第4课:记忆与上下文管理
⭐⭐⭐⭐ 高频↔ Hermes 源码
一、为什么 Agent 需要"记忆"?
LLM 在对话中天然有上下文窗口(context window)——它能"看"到当前对话中所有消息。但这不是真正的记忆:
- 上下文窗口是临时的——新对话开始时一切归零
- 上下文窗口是有限的——GPT-4 128K、Claude 200K,对话长了总会撑爆
- Agent 需要跨会话记忆——上次对话中用户说"我公司用 PostgreSQL",下次对话应该还记得
所以 Agent 的记忆系统本质上要解决的是:在有限的上下文中,保留最有价值的信息,并在需要的时候精准取回。
二、三种记忆层次
┌─────────────────────────────────────────────┐
│ !工作记忆(Working Memory) │
│ 当前对话的全部内容 = 上下文窗口 │
│ 临时、完整、随时可用 │
│ 限制:Token 上限(128K / 200K) │
├─────────────────────────────────────────────┤
│ ★ 短期记忆(Short-term Memory) │
│ 当前 session 的摘要/关键信息 │
│ 通过压缩/summarization 提炼 │
│ 生命周期:一个用户对话session │
├─────────────────────────────────────────────┤
│ ♾ 长期记忆(Long-term Memory) │
│ 跨 session 持久化的知识 │
│ 通过外部存储(向量库、SQLite、文件) │
│ 生命周期:永久(直到被删除) │
└─────────────────────────────────────────────┘
三、核心方案 1:Token Budget 管理
这是每个 Agent 系统首先要解决的问题——别让上下文窗口撑爆。
3.1 滑动窗口(Sliding Window)
最简单的策略:只保留最近的 N 条消息,扔掉最旧的。
完整对话(按时间):
[M1][M2][M3][M4][M5][M6][M7][M8][M9][M10]
↑ 最新
滑动窗口(size=5):
[M6][M7][M8][M9][M10]
← 旧的被丢弃
优点:实现极简单,O(1) 复杂度
缺点:可能丢失重要信息(用户5轮前说的需求)
3.2 上下文压缩 / Summarization(Hermes 的做法)
不是粗暴丢弃,而是用 LLM 把旧内容总结成摘要,塞回系统提示词里。
Hermes 的 compress_context()(conversation_compression.py,840行)就是做这个的:
// Hermes 压缩流程 (conversation_compression.py)
def compress_context(agent, messages, system_message):
// 1. 检查当前消息总量是否接近上下文窗口上限
// 2. 如果是,调用 aux LLM(辅助模型)生成摘要
// 3. 把摘要拼接成新的 system prompt
// "以下是对之前对话的摘要:..."
// 4. 把旧的轮次从 messages 中移除
// 5. 在 SQLite 中 split session(保存旧session记录)
// 6. 返回 (compressed_messages, new_system_prompt)
//
// 失败回退:如果 aux LLM 生成的摘要不合格,
// 返回原始 messages + system_prompt(不丢数据)
和 Hermes 的对照:
当你用 Hermes 跑一个长对话时,如果上下文接近满,你会看到一条提示:
"⚠️ 正在压缩上下文..."
这就是 compress_context() 在工作。
它还支持 focus_topic 参数——指定压缩时要优先保留的主题。
受 Claude Code 的 /compact <focus> 启发。
3.3 Token 预算的计算
Agent 需要在每次 LLM 调用前估算 Token 消耗:
// 估算公式(Hermes 中的 estimate_messages_tokens_rough)
token_count ≈ sum(len(m.content) * charset_ratio)
+ len(tools) * tools_overhead
+ system_prompt_tokens
+ buffer (预留 ~2000 tokens 给输出)
// 预算检查策略
if estimated_tokens > context_limit * 0.8:
触发压缩 // 达到 80% 就压缩
if estimated_tokens > context_limit:
强制压缩或截断 // 到了上限必须处理
四、核心方案 2:长期记忆(Persistence)
4.1 Hermes 的记忆系统架构
Hermes 的记忆系统有清晰的抽象层:
┌─────────────────────────────────────┐
│ MemoryManager │ ← 编排器(单例)
│ - add_provider() │
│ - prefetch_all() → 每次对话前自动取回 │
│ - sync_all() → 每次对话后自动同步 │
├─────────────────────────────────────┤
│ MemoryProvider (ABC) │ ← 抽象基类
│ initialize() │
│ prefetch(query) → recall before turn│
│ sync_turn(user, asst) │
│ get_tool_schemas() → 暴露工具给LLM │
│ handle_tool_call() │
│ shutdown() │
├─────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ │
│ │ Builtin │ │ External │ │ ← 具体实现
│ │ Memory │ │ Provider │ │
│ │ (系统记忆) │ │ (Honcho │ │
│ │ │ │ Mem0...)│ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────┘
设计亮点:
- 单外部 Provider 限制:一次只能注册一个外部记忆后端,防止工具 schema 膨胀
- 抽象基类 + 钩子:MemoryProvider 定义完整的生命周期(init → prefetch → sync → shutdown),插件只需实现这些方法
- 异步 prefetch:在每次 LLM 调用前,后台线程提前取回相关记忆,不阻塞主循环
- 背景审查:Hermes 还有背景记忆审查机制(post-turn reviewer),评估当前记忆是否需要更新
4.2 内置记忆(Built-in Memory)
你正在用的 Hermes 内置记忆就是 memory 工具(你刚才就用了):
- memory(action="add") → 保存一条事实
- memory(action="replace") → 替换一条事实
- memory(action="remove") → 删除一条事实
- 存储在 SQLite 数据库里(
~/.hermes/memories/)
- 每次对话前自动 inject 到 system prompt
五、核心方案 3:RAG(检索增强生成)
RAG = Retrieval-Augmented Generation。本质上是"用搜索来扩展记忆"。
5.1 RAG 工作流程
User: "我们公司去年Q3的营收是多少?"
Step 1: Embedding
"去年Q3的营收" → [0.23, 0.87, -0.12, ...]
Step 2: 向量检索
在向量数据库中搜索相似度 > 0.85 的文档
→ 找到 "2024-Q3-financial-report.pdf"
Step 3: 内容提取
"Q3 营收 1,280 万元,同比增长 23%"
Step 4: 注入上下文
System prompt + [RAG结果] + User message
→ LLM 基于检索到的数据回答
LLM: "根据财报数据,去年Q3营收为1,280万元..."
5.2 RAG vs Fine-tuning 的选择
| 维度 | RAG | Fine-tuning |
| 数据更新 | 秒级(换数据库就行) | 小时~天(重新训练) |
| 幻觉风险 | 低(基于检索结果回答) | 中高(模型记住了但可能记错) |
| 实现复杂度 | 中(需要向量库 + 检索管线) | 高(需要训练数据 + GPU) |
| 适用场景 | Agent 需要访问最新/特定知识 | 模型需要掌握固定领域的"技能" |
| 面试关系 | ⭐⭐⭐⭐⭐ Agent 面试常考 | 🔄 不是 Agent 的核心 |
5.3 RAG 的工程陷阱
面试加分:RAG 不是简单的"搜索 + 拼接"。工程上的 5 个关键陷阱:
1. Chunk 大小:太大(>1000 tokens)→ 噪音多;太小(<100 tokens)→ 上下文不足
2. Embedding 模型选择:通用模型 vs 领域特定模型(差距可能很大)
3. 检索策略:单向量检索 → 混合检索(向量 + BM25 关键词)效果更好
4. Context 窗口污染:检索回来的内容不能全塞进去,需要 reranker 筛选 top-k
5. Agent 场景特殊之处:Agent 需要的不是单次检索,而是迭代检索——先搜一次,根据结果精化 query 再搜
六、面试问答
🎯 Q1:"Agent 的上下文窗口满了怎么办?有哪些优化策略?"
回答框架(从简单到复杂排列):
1. 滑动窗口:只保留最近 N 轮消息,丢弃最旧的。最简单,但可能丢信息
2. 摘要压缩:用 LLM 把旧内容总结成摘要放回 system prompt。信息密度更高(Hermes 的做法)
3. 分层记忆:工作记忆(当前对话)+ 短期记忆(session 摘要)+ 长期记忆(跨 session 持久化)
4. RAG:对需要特定知识的问题,从外部知识库检索相关内容注入上下文
面试时可以补充:实际生产中通常是组合使用——滑动窗口 + 压缩 + RAG 三层同时工作。
🎯 Q2:"Agent 的长期记忆怎么设计?"
框架:
1. 存储:SQLite / 向量数据库 / 文件系统(选型看数据规模和查询模式)
2. 写入时机:每次对话结束时自动 sync(post-turn hook)
3. 读取时机:每次对话开始前 prefetch(pre-turn hook),把相关记忆注入 system prompt
4. 更新策略:覆盖更新(直接替换)/ 版本追加(保留历史)/ 重要性评分(只保留高重要性)
5. 限流:控制注入 system prompt 的记忆大小(一般不超过 2000 tokens)
🎯 Q3:"RAG 在 Agent 里怎么用?和普通的 RAG 有什么区别?"
框架:
Agent 场景下 RAG 的特殊之处在于迭代检索——Agent 不是一次检索就完事,而是:
检索 → LLM 评估是否够用 → 不够就精化 query → 再检索 → 循环
此外,Agent 可以:
- 根据用户意图自动选择检索源(搜 DB?搜网页?搜内部 wiki?)
- 结合工具调用做后处理(搜到数据 → 计算 → 画图 → 回复)
- 多轮对话中记忆"用户问过什么"来优化后续检索
七、课后行动
- ✅ 理解三层记忆模型:工作记忆 vs 短期 vs 长期
- ✅ 记住 Token budget 管理的 3 种策略(滑动窗口/压缩/RAG)
- ✅ 准备 Q1(上下文满怎么办)——这是面试最高频的问题之一
- ⏭ 下节课:多 Agent 系统——编排器 vs 蜂群、任务分发、协作模式
📁 0004-memory-context.html
📁 源码参考:/usr/local/lib/hermes-agent/agent/memory_manager.py (948行)
📁 源码参考:/usr/local/lib/hermes-agent/agent/conversation_compression.py (840行)
📁 源码参考:/usr/local/lib/hermes-agent/agent/memory_provider.py (296行)
📝 有问题随时问。