~/octoblog / harness

上下文压缩吃掉了我的约束:一次真实翻车与修法

[series:Harness 深水区] [date:2026-07-09] [read:4min] [words:1.7k] #context-engineering#踩坑#Claude Code

先讲事故。

一个长任务:让 Agent 重构一个模块链路,任务开头我明确写了约束——“不许动 X 模块,它有下游依赖”。前一个小时一切正常。任务跑到两个多小时、几百轮工具调用之后,Agent 把 X 模块改了。

它不是”叛逆”,也不是没看见。回看 trace,根因干净利落:上下文自动压缩(auto-compact)触发时,那条约束被摘要吃掉了。 压缩后的上下文里,“不许动 X”变成了不存在的信息。模型每一步都在忠实地执行它”看得见”的指令——问题是它已经看不见了。

这次翻车给我的教训值得写成一条普适判断:

Demo 和生产的差距,大头不在模型,在”约束持久化”和”验收独立化”这两件 Harness 的事。

Demo 里任务 10 分钟跑完,上下文从不溢出,一切约束都在窗口里。生产里任务跑两小时、几百轮,上下文一定会被压缩——上下文是易失的,而任务约束必须是持久的。这对矛盾不解决,长任务就永远是抽奖。

修法一:规格外置,永不进压缩池

约束不能只活在对话历史里——对话历史是会被压缩的。我的做法是把任务规格外置成结构化文件,四个字段:

[locus:  从哪开始——文件/模块路径]
[signal: 什么证据证明有问题——error log / 测试失败]
[done:   做到什么算完——可验证的验收标准]
[done-v: 完成验证方式——auto | manual]

关键机制有两条:规格文件永远不进压缩池每次 compact 之后,强制重读一遍规格。这样无论对话历史被压成什么样,“不许动 X”始终以原文形态回到模型眼前。

修法二:压缩要分类,不能一锅端

自动压缩的默认行为是”把旧内容统一摘要”。但上下文里的信息重要性天差地别,正确的做法是分类处理:

信息类型压缩策略
任务规格与验收标准无损保留,一个字都不能丢
关键决策及理由结构化保留(决策 + 一句话理由)
中间观察(工具输出、日志)激进摘要,大对象转成索引按需再取
失败的尝试只留结论(“方案 A 不行,因为 B”),过程全丢

失败尝试的处理最反直觉:留太多,残留的错误路径会污染后续判断(模型会被自己的历史带偏);全丢掉,它会再试一遍同样的死路。只留结论是两害相权后的平衡点。

修法三:压缩后跑一次一致性自检

机制很简单:每次 compact 之后,插入一个自检步骤——“当前的执行计划,还满足任务规格吗?逐条对照。”

这一步的成本是几百个 token,但它把”约束漂移”从事后发现(人看到结果傻眼)变成了事中拦截(Agent 自己发现计划和规格对不上)。我把它理解为给长任务加的”重新对准仪式”:

人开三小时的会也会跑偏,所以人发明了议程和纪要。规格外置和一致性自检,就是 Agent 的议程和纪要。

一个附带的观察:上下文是分层预算,不是一个桶

这次翻车之后,我重新梳理了长任务的上下文策略,核心转变是把”一个不断被填满的桶”换成”四层各有纪律的预算”:

  1. 固定前缀(system prompt + 工具说明):稳定不动,吃缓存;
  2. 任务规格:无损驻留,永不压缩;
  3. 工作记忆(当前计划、关键决策):结构化摘要;
  4. 观察流(工具输出):激进裁剪,检索代替驻留——文件不进窗口,进本地索引,用到再取。

顺带说一个选型结论:桌面场景的本地检索,我实测 SQLite FTS5 + BM25 比向量库更稳、更省、且可解释——全文检索的”为什么命中”一目了然,向量检索的”为什么召回这条”经常说不清。不是所有场景都需要 embedding,这个判断帮我省掉了一整套向量库运维。

收个尾

长任务的可靠性,不是靠更大的上下文窗口解决的——窗口再大,该压缩时还是要压缩,该丢的还是会丢。它靠的是一个朴素的设计原则:

分清楚什么是易失的(对话),什么是持久的(规格),然后用机制保证后者永远不被前者的生命周期拖累。


这是「Harness 深水区」系列的第二篇。相关阅读:《Model + Harness = Agent:产品差距的大头,不在模型》。