前言:从上下文压缩到持久化记忆
前代方案通过会话摘要解决了上下文窗口限制,但存在致命缺陷:压缩过程会导致细节丢失(如“用 Tab 不用空格”的偏好被泛化为“有代码风格偏好”),且新会话无法继承这些信息。LLM 缺乏持久状态是架构层面的问题,单纯优化压缩算法无法解决,必须引入一层不参与压缩、可跨会话保留的存储机制。
为什么选择文件系统作为记忆载体
相较于向量数据库(部署重、调试难)和结构化数据库(查询逻辑复杂、LLM 读取不自然),文件系统方案具有显著优势:
- 零依赖与直观性:每个记忆是一个独立的 Markdown 文件,支持人工直接查看、编辑和删除。
- 自然语言交互:LLM 生成和读取均为自然文本,无需额外的序列化层。
- 故障排查便捷:出现问题可直接检查文件内容。
存储格式设计:采用"Markdown + YAML Frontmatter"结构。Frontmatter 存储元数据(name, description, type)用于索引检索,正文存储完整细节,实现按需读取。
---
name: user-preference-tabs
description: User prefers tabs for indentation
type: user
---
User prefers using tabs, not spaces, for indentation.
**Why:** Consistency with existing codebase conventions.
**How to apply:** Always use tabs when writing or editing files.
四种记忆类型分工:
| 类型 | 记录内容 | 示例 |
| user | 用户身份与偏好 | “用 Tab 不用空格” |
| feedback | 做事准则与避坑指南 | “测试中不要 Mock 数据库” |
| project | 当前项目背景与状态 | "Auth 重写是合规驱动” |
| reference | 关键资源位置 | "Pipeline bug 在 Linear INGEST" |
索引机制:常驻 SYSTEM Prompt
随着记忆文件增多,需建立统一目录。MEMORY.md作为索引文件,每行包含一个记忆文件的链接及简述。每次写入新记忆时自动重建该索引。
- [user-preference-tabs](user-preference-tabs.md) — User prefers tabs for indentation
- [no-mock-db](no-mock-db.md) — Never mock the database in tests
- [auth-rewrite-context](auth-rewrite-context.md) — Auth rewrite is compliance-driven
注入策略:将索引内容注入 SYSTEM Prompt。由于索引体积小且稳定,可利用 Prompt Cache 机制,只要记忆未变更,这部分 Token 无需重新计算,极大降低成本。
def build_system() -> str:
index = read_memory_index()
memories_section = f"\n\nMemories available:\n{index}" if index else ""
return (
f"You are a coding agent at {WORKDIR}."
f"{memories_section}\n"
"Relevant memories are injected below. Respect user preferences from memory.\n"
"When the user says 'remember' or expresses a clear preference, extract it as a memory."
)
Agent 仅通过索引知晓“有哪些记忆”,避免将所有内容塞入上下文造成浪费。
加载策略:按需注入 User Turn
知道有哪些记忆后,仅需在每次用户请求开始时,筛选出相关文件并将完整内容注入到当前的 User Turn,而非 SYSTEM Prompt。
设计考量:若将动态变化的记忆内容放入 SYSTEM,会导致 Prompt Cache 失效。将其置于 User Turn 可保持 SYSTEM 稳定,确保缓存命中率。
筛选逻辑:通过一次 Side-query(辅助查询),将最近几轮对话与记忆目录(名称 + 描述)发送给模型,由其返回相关文件的索引列表。若 API 调用失败,则降级为关键词匹配。
def select_relevant_memories(messages: list, max_items: int = 5) -> list[str]:
# 1. 收集最近用户输入作为上下文
# 2. 构建记忆目录 catalog
# 3. 调用 LLM 进行语义匹配,返回 JSON 数组索引
# 4. 异常处理:降级为关键词匹配 name + description
# ... (代码逻辑略,见原文)
return selected_files
选中的文件内容将被包裹在<relevant_memories>标签中,拼接到当前用户输入之前,确保主 Agent 能即时获取相关信息。
写入机制:隐式提取与去重合并
隐式提取
用户偏好通常散落在正常对话中,不会显式要求“记住”。提取逻辑设定在每轮对话结束(Agent 回答完毕且未调用工具)时触发。
关键细节:提取操作基于压缩前的消息快照(pre_compress)。若在压缩后提取,关键细节已丢失,会导致记忆质量下降。
# 在 agent_loop 中
if response.stop_reason != "tool_use":
extract_memories(pre_compress) # 从原始对话提取
consolidate_memories() # 定期合并
提取时会对比现有记忆列表,避免重复写入。
定期合并(Consolidation)
为防止记忆文件冗余、过时或矛盾,当文件数量达到阈值(如 10 个)时触发合并流程。LLM 负责去重、解决冲突并淘汰过时信息,返回精简后的清单覆盖旧文件。
注:生产环境需增加时间间隔、扫描节流、会话数统计及文件锁等多重门控机制,防止频繁合并。
Memory 与 Session Memory 的分工协作
本方案并未取代原有的上下文压缩机制,而是与其形成互补:
| 维度 | Memory (长期记忆) | Session Memory (会话记忆) |
| 持久范围 | 跨会话 | 单会话内 |
| 存储位置 | .memory/*.md 文件 | Compact 摘要 |
| 注入位置 | SYSTEM + User Turn | 替换历史消息 |
| 核心作用 | 长期知识积累(偏好、背景) | 跨压缩点的上下文续接 |
两者配合逻辑:会话内上下文超限触发压缩,由 Session Memory 承接关键信息;会话结束后触发提取,由 Memory 将高价值信息永久保存。
完整执行循环与时序
Agent 的标准执行流程如下:
用户输入
↓
load_memories() ← 筛选相关记忆,注入 User Turn
build_system() ← 读取 MEMORY.md 索引,构建 SYSTEM
↓
pre_compress snapshot ← 创建压缩前快照,供后续提取使用
↓
[上下文管理]: tool_result_budget / snip_compact / micro_compact
↓
LLM 推理
↓
若有工具调用 → 执行工具 → 继续循环
若无工具调用(本轮结束):
extract_memories(pre_compress) ← 从快照提取新记忆
consolidate_memories() ← 若文件过多则合并
return
此设计确保同一轮次内 SYSTEM 不重复构建,且记忆提取基于最完整的原始信息。
记忆内容的边界界定
Memory 并非日志系统,受限于索引行数(≤200)、单次加载量(≤5 条)及文件大小,其设计原则是只存“未来有用”的,不存“曾经发生”的。
- 适合存储:用户偏好、反复出现的约束、项目核心背景(目标、架构、依赖)、常用排查线索。
- 不适合存储:完整对话记录、临时任务状态、仅对当前任务有效的中间信息(这些应交给 Session Memory 或 Transcript)。
下一步优化方向将是 System Prompt 的动态组装,使其能根据不同工具和项目约束实时生成,而非依赖硬编码字符串。

