记忆压缩模式
记忆压缩模式:Claude-Mem 的具体实现
学习目标
本章聚焦 Claude-Mem 如何将 Claude Code 的工具使用事件转化为结构化记忆。建议先阅读前置知识,下文直接聚焦具体落地方案。
前置知识
本章涉及记忆压缩的通用原理,建议先阅读:
- 记忆压缩模式 — 两阶段压缩管线与 XML 结构化输出
项目实践
XML 解析器架构
Claude-Mem 使用自定义 XML 解析器(src/sdk/parser.ts)处理 LLM 生成的观察和摘要。解析器的工作流程:
解析器的关键设计:
- 代码围栏兼容:LLM 可能将 XML 输出包裹在
xml ...围栏中,解析器首先检测并剥离 - 多 observation 支持:使用全局正则
/g匹配多个<observation>块 - 概念去重:自动从
concepts数组中移除与type相同的值,避免冗余 - 空观察过滤:当 title、narrative、facts、concepts 全部为空时,跳过该观察
观察类型验证
Claude-Mem 通过 ModeManager 动态管理有效的观察类型列表。每种 mode(如 code、code--zh、code--ja)定义了自己支持的观察类型集合。解析器在提取到 type 字段后:
- 若 type 在当前 mode 的有效列表中 → 使用
- 若 type 不在有效列表中 → 记录错误日志,回退到默认类型
- 若缺少 type 字段 → 记录错误日志,使用默认类型
这种设计避免了硬编码类型列表,允许通过 mode 配置灵活扩展。
上下文注入管线
ContextBuilder(src/services/context/ContextBuilder.ts)负责将数据库中的观察和摘要组装为可注入的上下文文本:
每个渲染器是独立的模块,输出字符串数组,最终通过 \n 拼接。这种分节渲染的好处是:
- 每节可以独立启用/禁用(通过 ContextConfig)
- 每节有自己的 token 预算计算
- 便于单元测试和替换
Token 经济学计算
TokenCalculator(src/services/context/TokenCalculator.ts)统计观察的 token 消耗:
- 每条观察的
discovery_tokens记录生成时的消耗 - 上下文输出中显示总计和平均每观察的 token 数
- 帮助 Agent 判断注入成本是否合理
问题与规避
解析器误匹配
问题:正则表达式 /\<observation\>([\s\S]*?)\<\/observation\>/g 可能在用户代码中包含 <observation> 标签时误匹配。
规避:解析器在提取后检测字段完整性,若关键字段(type)缺失或所有内容字段为空,跳过该块。同时,观察类型验证进一步降低误匹配的影响。
去重窗口竞争
问题:在高并发场景下,同一工具使用事件可能被多个 Hook 捕获,导致重复写入。
规避:数据库 schema 中 observations 表设置了 UNIQUE(memory_session_id, content_hash) 约束,写入时使用 INSERT ... ON CONFLICT DO NOTHING,天然吸收重复。
设计取舍
为什么用 XML 而非 JSON?
Claude-Mem 选择 XML 作为 LLM 结构化输出格式,而非 JSON:
- 优势:XML 标签天然支持嵌套和数组(
<facts><fact>...</fact><fact>...</fact></facts>),LLM 更容易生成正确的嵌套结构。JSON 需要精确的引号和逗号,LLM 容易出错。 - 代价:解析复杂度高于 JSON,需要自定义正则解析器。
- 权衡:代码注释中标注了 TODO,计划迁移到 Anthropic tool-use API 的结构化输出,届时将不再需要 XML 解析。
为什么同步解析而非异步?
XML 解析是纯正则操作,不涉及 I/O 或模型调用,同步执行即可。这确保了解析器可以在 Hook 层和 Worker 层复用,无需异步包装。