记忆与压缩

专栏:Agent 工程 · 第 7 / 18 篇
Agent记忆压缩Compaction

上一篇的硬约束意味着:不做任何处理,长任务必然撑爆窗口或成本失控。这篇讲”跌”的那一半——**压缩(compaction)**管会话内,**记忆(memory)**管跨会话。

:::info 学习目标 完成本篇后你能够:给 mini-agent 实现一个最小压缩函数;说出两种压缩流派(模型总结 vs 预算换窗)的取舍;为你的场景选择记忆的第 0/1/2 层方案。 前置:第 6 篇完成。预计时长:60 分钟。 :::

:::note 本章术语速查(新手建议先读)

  • Compaction(压缩):把旧的对话历史折叠成一段摘要,腾出窗口空间——像把旧聊天记录整理成备忘录。
  • 记忆(Memory):让 agent 跨会话记住信息。最简单的实现:让它读写一个笔记文件。
  • 摘要提示词:让模型”总结前情”时用的指令——写什么必须保留,直接决定压缩后任务能否继续。
  • 事件边界:压缩这类操作要在日志里留下”开始/结束”记录,方便事后查证。
  • Mem0 / Letta:两个开源的记忆系统产品——前者自动从对话抽取记忆,后者让 agent 自己管理分层记忆。个人项目通常用不上。 :::

压缩的最小实现(15 行)

先动手再谈理论——给 mini-agent 加这个函数,消息超过上限时调用:

def compact(messages: list[dict], keep_recent: int = 6) -> list[dict]:
    """把较旧的历史折叠成一条摘要,保留最近 keep_recent 条原样。"""
    if len(messages) <= keep_recent + 1:
        return messages                          # system + 不足量,不压
    system, rest = messages[0], messages[1:]
    old, recent = rest[:-keep_recent], rest[-keep_recent:]
    transcript = "\n".join(
        f"{m.get('role')}: {str(m.get('content'))[:200]}" for m in old)
    summary = chat([system, {"role": "user", "content":
        f"把以下对话压缩成要点摘要,保留:任务目标、已完成的操作、关键文件/数字、未完成的事。\n\n{transcript}"}])
    return [system, {"role": "user", "content": f"[前期工作摘要]\n{summary['content']}"}] + recent

检查点:跑一个 15 步以上的任务,观察压缩发生后的行为——好的摘要注意三件事:任务目标不丢、已完成的操作不重复做、半成品状态能接续。摘要丢三落四就改 compact 里那段提示词,这是杠杆最大的几行字。

压缩:把历史折叠成摘要

压缩的通用形态:接近阈值时,把较旧的历史替换成一段摘要,保留近期消息原样。但两个系统各自的实现揭示了不同的设计深度。

Codex:压缩是一个任务类型,且有三种流派。 压缩在 Codex 里不是函数而是任务tasks/compact.rs,与 Regular 任务共享生命周期,hooks 可观测):

  1. 模型总结型:用 SUMMARIZATION_PROMPTprompts/templates/compact/prompt.md)让模型总结旧历史;
  2. token 预算型:跳过模型总结,直接安装全新上下文窗口——世界状态重注入(InitialContextInjection::BeforeLastUserMessage),快而省;
  3. 服务端压缩:走 Responses API 让平台侧压缩,附带图片预算管理。

最见功力的是mid-turn 触发:压缩发生在轮次中间(采样请求之间),新窗口接上当前轮次继续跑——长任务不因爆窗而中断。触发逻辑(第 16 篇摘录过)连防死循环都注释了:“只要压缩能可靠把 token 压到远低于上限,就不必担心无限循环”。

dsh:压缩是能力族 + 只遮蔽不删除。 dsh 的 compaction 是标准的能力 seam(Service Definition + basic 提供方 + 命令 Consumer),tool-result-pruner 这类细粒度策略可以单独挂载(如超过 8192 字符的工具结果折叠成头尾摘要)。关键语义:replace遮蔽 surface 上的旧节点——原始日志条目永不删除。压缩后审计与回放依然完整,这正是”仅追加日志”哲学的红利。

两者的共同底线:压缩必须有边界事件(Codex 的 CompactedItem、dsh 的 compaction/start|summary|end 事件族)——压缩了什么、何时压缩,永远可查。

压缩触发时机的三种策略

策略触发点代表取舍
阈值触发逼近窗口上限你要写的 mini-agent简单;但压缩瞬间的延迟毛刺
mid-turn 自动压缩轮次中间采样后检查Codex run_auto_compact任务不断流;实现复杂
手动命令用户 /compact两个系统都有用户掌控;依赖用户警觉

共同的底线:压缩必须有事件边界(dsh 的 compaction/start|summary|end、Codex 的 CompactedItem)——压缩了什么、何时压缩,永远可查。

另一个常被忽略的时机问题:压缩摘要本身的提示词是杠杆最大的几行字。Codex 的 SUMMARIZATION_PROMPT 模板值得抄——它明确要求保留任务目标、已完成操作、关键文件与未竟事项。

记忆:跨会话的三层方案

压缩解决”这一场”的记忆,跨会话靠 memory。按投入产出排序:

第 0 层:文件系统(先做这个)。 给 agent 读写 Markdown 文件的工具(如 NOTES.md / MEMORY.md),让它自己记笔记。写入时机有讲究:完成一个阶段性结论就写(不是任务结束才写)——中途崩溃也不丢。Letta 的基准测试提出了那个本质问题:“一个文件系统是不是就够了?“——多数个人 agent 场景,答案确实是。dsh 的 todo 工具(todo_write + 会话投影)就是同思想的轻量版:模型维护自己的清单,状态从事件流折叠而来。

第 1 层:会话持久化 + 检索。 把历史会话存成结构化日志(两个系统都天然具备:rollout / session JSONL),新会话需要时检索旧会话。dsh 甚至内建了 session-query(SQLite 全文检索会话语料)。你自己的日志就是记忆的第一数据源——第 13 篇的评测也靠它。

第 2 层:专门的记忆系统。 2026 年的两大范式(对比):Mem0 的被动抽取记忆层(从对话抽取事实、两阶段更新,生态最大)与 **Letta(MemGPT)**的自编辑 OS 式记忆运行时(agent 主动管理分层记忆、sleep-time compute)。什么时候才需要它们:多用户产品级的个性化记忆、海量会话的自动沉淀。个人 agent 阶段,跳过。

压缩与记忆的分工

图表(memory-and-compaction.md)

分工原则:压缩是 无损性的妥协(保任务连续),记忆是有选择的沉淀(保长期价值)。两者都要有事件边界,都可审计。

框架对照:LangGraph

from langchain.agents.middleware import SummarizationMiddleware

agent = create_agent(model, tools, middleware=[
    SummarizationMiddleware(
        model="deepseek:deepseek-chat",
        max_tokens_before_summary=4000,   # 逼近阈值自动折叠旧消息
    ),
])
# 长期记忆:Store(跨 thread 的命名空间 KV)——第 4 篇用例 3 演示过

SummarizationMiddleware 就是压缩的框架化形态;Store 对应第 1 层记忆。

本篇产出(带验收标准)

  1. 实装 compact():消息历史超过 12 条时自动触发,记录压缩前后步数与 token 消耗。验收:同一个 15 步任务,压缩版总 token 下降 ≥ 30% 且任务仍完成;
  2. 加一个 write_note 工具(写到本地 notes.md)+ 启动时读取——体验第 0 层记忆。验收:跨两次会话,agent 能复述上次的工作结论;
  3. 踩坑观察:故意在压缩摘要里丢掉”任务目标”,看压缩后的步骤如何跑偏——体会摘要提示词里每一句话存在的理由;
  4. 进阶:翻 codex-rs/prompts/templates/compact/prompt.md,看 OpenAI 的摘要提示词结构,抄它的骨架。

← 返回文章列表