编码 Agent 跑久了一定会撞墙:一个真实的开发任务往往要几十上百轮工具调用,每一轮的 tool result(文件内容、命令输出、堆栈)都被原样塞回消息历史。token 单调累积,迟早顶满上下文窗口。更糟的是,临近窗口上限时模型的注意力会被大量陈旧、无关的内容稀释,表现不降反崩。所以「长会话不爆窗口」不是锦上添花,而是 harness 能不能干完一件复杂任务的底线能力。

围绕这件事,工程上有三件常被混为一谈的工具:context editing(上下文编辑)compaction(压缩)memory(记忆)。它们解决的是不同层面的问题,下面逐个拆开。

context editing:剪枝,不是总结

context editing 的本质是「剪枝」。当历史里的 token 超过某个阈值,harness 把那些陈旧的 tool result 和 thinking 块清理掉——注意,是清理,不是改写成摘要。

为什么优先动这两类内容?因为它们恰好是「体积大、时效短」的。你在第 3 轮读了某个文件的全文塞进了 tool result,到第 40 轮这个文件早就被改过好几遍,那份原始内容已经是噪声;thinking 块同理,是模型当时的中间推理,过后基本不会被再次引用。把它们换成一个占位标记(如 [tool result cleared]),既腾出了空间,又保留了对话结构——哪一轮调用了哪个工具、参数是什么、用户说了什么,这些骨架都还在。模型知道「我当时读过这个文件」,只是看不到内容了,需要时可以重新读。

1
2
3
4
5
6
7
8
9
10
11
# 剪枝前
user: 修一下登录 bug
assistant: [thinking 800 tokens] + tool_use(read login.py)
tool: [login.py 全文 3000 tokens]
...

# 剪枝后
user: 修一下登录 bug
assistant: tool_use(read login.py)
tool: [cleared]
...

剪枝是无损于结构、有损于细节的。配置阈值时要权衡:清得太狠,模型频繁重读文件,反而浪费 token 和轮次;清得太松,省不下空间。一个实用做法是只清理「N 轮之前」的 tool result,保留最近窗口的原始内容。

compaction:把更早的历史在服务端总结掉

剪枝有上限。当对话本身就很长、连骨架加最近内容都快撑满窗口时,就轮到 compaction 上场。它做的是真正的「总结」:把更早的历史在服务端压成一个 compaction 块——一段浓缩了「目前为止干了什么、当前状态如何、还剩什么没做」的结构化摘要,替换掉原来那一大段消息。

这里有一个极易踩的坑,几乎每个自己搓 harness 的人都会中招:

compaction 之后,每一轮你必须把整个 response.content(其中就含 compaction 块)原样回灌到下一轮的 messages 里。如果你图省事只取了 response.content 里的 text 部分,compaction 块就被你丢掉了,压缩状态当场失效,下一轮历史又膨胀回去。

1
2
3
4
5
6
# 错误:只取 text,丢掉了 compaction 块
text = "".join(b.text for b in response.content if b.type == "text")
messages.append({"role": "assistant", "content": text})

# 正确:整个 content 回灌,结构块一个不少
messages.append({"role": "assistant", "content": response.content})

记住一句话:在 harness 里,response.content 是一个异构块列表(text、tool_use、thinking、compaction……),你的回灌逻辑要按列表整体搬运,绝不能只挑 text。这条原则在 thinking、compaction 上都适用。

memory:跨会话才需要的那一层

前两件套都在解决「会话内」的问题——它们的作用域随进程退出而消失。但有些信息你希望跨会话还在:这个项目的构建命令是 make -j2 不能上 -j8 否则 OOM;上次某个 feature 卡在哪一步;用户偏好用中文 A/B/C 选项回答。

memory 就是把这类信息持久化到一个文件目录(约定如 /memories)。它不依赖任何会话历史,进程重启、换一个全新 session,目录还在,Agent 读一下就恢复了长期上下文。形态上它通常就是一组 markdown 文件加一个索引,Agent 通过工具读写。

三者的分工可以这样记:

机制 作用域 动作 触发
context editing 会话内 剪枝 tool result/thinking 超阈值
compaction 会话内 服务端总结更早历史 接近窗口上限
memory 跨会话 持久化到文件目录 显式读写

实践中它们往往一起用:editing 和 compaction 一里一外管住「会话内」不爆窗口,memory 管住「跨会话」的长期知识。一个跑得久的编码 Agent,通常是三者叠加在工作。

安全:memory 是持久化的,更要当心

正因为 memory 落盘且跨会话存活,它的安全边界比会话内机制更需要警惕:

  • 不要往 memory 写密钥、token、密码等敏感信息。会话历史还会被剪枝、被压缩,memory 文件却是你主动持久化的,它会一直躺在那里,泄露面更大。
  • 多用户场景必须按用户隔离目录并加鉴权。A 用户的 memory 目录绝不能被 B 用户读到,否则就是跨用户的信息泄露。隔离要落在目录层级 + 访问控制上,而不是寄希望于 prompt 约束。

小结

context editing 剪枝、compaction 总结、memory 持久化——三件套各管一段,editing 和 compaction 让长会话在窗口内活下去,memory 让知识跨会话活下去,而回灌整个 response.content 与隔离敏感数据,是这套机制能正确运转的两条底线。