上下文压缩吃掉了我的约束:一次真实翻车与修法
[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 的议程和纪要。
一个附带的观察:上下文是分层预算,不是一个桶
这次翻车之后,我重新梳理了长任务的上下文策略,核心转变是把”一个不断被填满的桶”换成”四层各有纪律的预算”:
- 固定前缀(system prompt + 工具说明):稳定不动,吃缓存;
- 任务规格:无损驻留,永不压缩;
- 工作记忆(当前计划、关键决策):结构化摘要;
- 观察流(工具输出):激进裁剪,检索代替驻留——文件不进窗口,进本地索引,用到再取。
顺带说一个选型结论:桌面场景的本地检索,我实测 SQLite FTS5 + BM25 比向量库更稳、更省、且可解释——全文检索的”为什么命中”一目了然,向量检索的”为什么召回这条”经常说不清。不是所有场景都需要 embedding,这个判断帮我省掉了一整套向量库运维。
收个尾
长任务的可靠性,不是靠更大的上下文窗口解决的——窗口再大,该压缩时还是要压缩,该丢的还是会丢。它靠的是一个朴素的设计原则:
分清楚什么是易失的(对话),什么是持久的(规格),然后用机制保证后者永远不被前者的生命周期拖累。
这是「Harness 深水区」系列的第二篇。相关阅读:《Model + Harness = Agent:产品差距的大头,不在模型》。