版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式,也不对任何收益结果作承诺。转载请注明出处。
一、先看现场:任务跑到第 60 轮,它突然开始"忘规矩"
现场不是一次崩溃,是一行没人报警的日志。
上线第 9 天的凌晨,我回翻一个运营 Agent 的会话记录。第 60 轮,它调用了发件工具,把一封邮件发了出去——正文里还带着客户手机号。而它接到的第一批指令里,三条硬约束写得清清楚楚:
预算上限 8000 元,超过必须停下来问我只允许用企业微信发通知,不要发邮件客户名单里的手机号不能出现在任何对外文本里
第 60 轮,这三条它一起破了:用邮件(违反第 2 条)、正文里带手机号(违反第 3 条)、给一笔超预算的单子放行且没有回来问(违反第 1 条)。没有报错,没有异常堆栈,也没有一句"我忘了规矩"。它不是崩溃了,它是把规矩当成了从来没存在过。
1.1 三条约束,是我在第 1–3 轮亲手交代的
这三条不是比喻,是原样贴进首轮消息里的字符串。之所以写成这么死的样子——带具体数字、带"必须停下来问我"这种动作指令——就是因为它们属于违规之后不可逆的那一类:邮件发出去收不回,手机号泄露收不回,钱花出去收不回。
1.2 故障不是一步到的:五个阶段
复盘日志后会发现,整个塌陷是有台阶的:
| 阶段 | 轮次 | 触发条件 | 表面上看到的现象 |
|---|---|---|---|
| 教规矩 | 第 1–3 轮 | 任务开始,贴入三条硬约束 | 模型逐条复述,包含"超过必须停下来问我" |
| 稳定期 | 第 4–40 轮 | 正常工具循环 | 行为全部合规,无需人工干预 |
| 第一次压缩 | 第 41 轮 | 上下文逼近窗口上限,平台自动压缩 | 措辞略变,行为无异常 |
| 第二次压缩 | 第 58 轮 | 再次触顶,又压一次 | 开始漏问,偶发"这个我按默认处理了" |
| 塌陷 | 第 60 轮 | 第三次触发 | 发邮件 + 带手机号 + 超预算放行 |
注意第 3 行:第一次压缩之后,一切看起来都正常。这是这类故障最阴的地方——它不在压缩那一刻爆,它在压缩把最早的几轮挤掉之后,隔了十几轮才爆。
1.3 我先排除了三个"背锅侠"
动手改之前,我先把最容易怪的三样排掉:模型没换、提示词没改、工具没动。日志里唯一变化过的,是上下文本身被压过三次。所以问题不在"模型不听话",而在"模型已经看不到规矩了"。
二、三种压缩方式,谁把硬约束保住了
压不压得住约束,取决于压缩动作"先动哪一段",而不是压掉了多少 token。
大多数人调压缩,调的是一个百分比:窗口 200K,我压到 50K,应该够了吧?这个思路默认"压缩是均匀的",而真实压缩几乎都是有偏向的——它总要先选一个"先动哪一段"。三种最常见的做法,选择完全不同。
2.1 三种做法,先丢的东西不一样
| 压缩方式 | 典型实现 | 先丢什么 | 硬约束(最早 3 轮)的下场 |
|---|---|---|---|
| 滑动窗口截断 | 只保留最后 N 个 token | 最早的内容,不看是不是约束 | 最先被丢 |
| 清工具结果 | 只清工具返回,其余保留 | 中间那批体量最大的工具输出 | 保留(约束不在工具结果里) |
| 丢弃最早 N 轮 | 从头砍掉 N 轮 | 最早的轮次,包括约束所在的前 3 轮 | 最先被丢 |
看最后一列:三种做法对"最早那几轮"的态度,直接决定了硬约束的死活。而硬约束天然就写在最前面——你在任务开头交代规矩,天经地义。
2.2 官方机制里,"按类型清理"是现成的
Anthropic 的上下文清理(Context editing)给了策略类型clear_tool_uses_20250919,参数形态大致是这样:
{"type":"clear_tool_uses_20250919","trigger":{"type":"input_tokens","value":30000},"keep":{"type":"tool_uses","value":3},"clear_at_least":{"type":"input_tokens","value":"<每次至少清掉的 token 数>"},"exclude_tools":["<不想被清的工具体类型>"]}trigger设{"type":"input_tokens","value":30000},表示输入 token 到 3 万才触发;keep设{"type":"tool_uses","value":3},表示保留最近 3 条工具调用。官方对keep的原话是:
The API removes the oldest tool interactions first, preserving the most recent ones.
翻译过来:先清最老的工具交互,保留最近的若干条。这一句就是"只清工具结果、保留最近若干条"的官方版本——它把清理范围限定在"工具交互"这一类型里,不会顺手把你最早写的约束一起带走。exclude_tools还能把某些工具整个排除在清理之外;clear_at_least则用来保证每次至少清掉多少 token(官方说明清缓存会让前缀缓存失效,所以要清够量才划算)。
2.3 但官方压缩本身,也保不住"字面约束"
Anthropic 的压缩(Compaction)是另一套东西。官方定义原文是:
Compaction replaces the older turns of a conversation with a summary that Claude writes on the server, so you need no summarization code of your own.
它把较早的轮次换成一个摘要,并把压缩区分为「compact on demand」(按需手动触发)与「threshold compaction」(到阈值自动触发),同时说明Recent turns stay word for word——最近若干轮逐字保留。
问题就出在"较早的轮次换成一个摘要":摘要是有损的。摘要会写"用户要求控制预算、不要发邮件",但很可能不会逐字保留"预算上限 8000 元"这种精确数字,也不会保留"超过必须停下来问我"这种动作指令。所以哪怕你用的是官方压缩,硬约束也未必安全——它只是从"被截掉"换成了"被摘要成一句大意"。
三、同样压到 2000 token,结局完全相反
同样是留 2000 token 上下,滑动窗口的约束存活是 0/3,只清工具结果是 3/3——不是压得少就安全,是压对位置才安全。
我写了个脚本,构造一个 203 轮、4756 token 的长任务:三条硬约束放在最前面 3 轮,后面是大量工具返回和业务追问。然后分别用三种压缩方式压,看那三条约束的字面还在不在。
🧪 实测环境:Python 3.13.12 / macOS / tiktoken 0.14.0
python work/exp1011/exp04_compaction.pyimporttiktoken enc=tiktoken.get_encoding("cl100k_base")nt=lambdas:len(enc.encode(s))constraints=["预算上限 8000 元,超过必须停下来问我","只允许用企业微信发通知,不要发邮件","客户名单里的手机号不能出现在任何对外文本里",]# 24 条「工具返回 / 追问」短句 × 8 轮业务循环 + 8 行循环标记 = 200 轮,加 3 轮约束共 203 轮turns=constraints+build_long_middle()full="\n".join(turns)print("完整对话:%d 轮,%d 个 token"%(len(turns),nt(full)))# 策略 A:滑动窗口,只保留最后 K 个 tokenforkin[2000,1000,500,300]:keep=enc.decode(enc.encode(full)[-k:])alive=[cforcinconstraintsifcinkeep]print(" 保留 %4d token(占原文 %4.0f%%)-> 约束存活 %d/3"%(k,100.0*k/nt(full),len(alive)))# 策略 B:只清工具结果(保留最近 N 条工具输出)tool_like=[ifori,tinenumerate(turns)ift.startswith("工具返回")]forkeep_nin[8,4,2]:dropped=set(tool_like[:-keep_n])kept=[tfori,tinenumerate(turns)ifinotindropped]alive=[cforcinconstraintsifcin"\n".join(kept)]print(" 保留最近 %d 条工具结果 -> %d token,约束存活 %d/3"%(keep_n,nt("\n".join(kept)),len(alive)))# 策略 C:丢弃最早 N 轮fornin[3,6,12]:text="\n".join(turns[n:])alive=[cforcinconstraintsifcintext]print(" 丢掉最早 %2d 轮 -> %d token,约束存活 %d/3"%(n,nt(text),len(alive)))(build_long_middle()是生成那 200 轮业务循环的辅助函数,完整脚本随本文一起放在资料包里。)
原样输出:
完整对话:203 轮,4756 个 token === 策略 A:滑动窗口(按 token 截断,保留尾部)=== 保留 2000 token(占原文 42%)-> 约束存活 0/3 保留 1000 token(占原文 21%)-> 约束存活 0/3 保留 500 token(占原文 11%)-> 约束存活 0/3 保留 300 token(占原文 6%)-> 约束存活 0/3 === 策略 B:只清工具结果(模拟按条数保留最近 N 条工具输出)=== 保留最近 8 条工具结果 -> 2142 token,约束存活 3/3 保留最近 4 条工具结果 -> 2015 token,约束存活 3/3 保留最近 2 条工具结果 -> 1956 token,约束存活 3/3 === 策略 C:丢弃最早 N 轮 === 丢掉最早 3 轮 -> 4696 token,约束存活 0/3 丢掉最早 6 轮 -> 4646 token,约束存活 0/3 丢掉最早 12 轮 -> 4492 token,约束存活 0/33.1 把最扎眼的对比挑出来
🧪 实测环境:Python 3.13.12 / macOS / tiktoken 0.14.0 (数据实测块,口径见下)
| 压缩策略 | 压缩动作 | 压缩后 token | 占原文 | 三条硬约束存活 |
|---|---|---|---|---|
| A 滑动窗口 | 按 token 截断,保留尾部 | 2000 | 42% | 0/3 |
| A 滑动窗口 | 按 token 截断,保留尾部 | 1000 | 21% | 0/3 |
| A 滑动窗口 | 按 token 截断,保留尾部 | 500 | 11% | 0/3 |
| A 滑动窗口 | 按 token 截断,保留尾部 | 300 | 6% | 0/3 |
| B 只清工具结果 | 保留最近 8 条工具结果 | 2142 | 45% | 3/3 |
| B 只清工具结果 | 保留最近 4 条工具结果 | 2015 | 42% | 3/3 |
| B 只清工具结果 | 保留最近 2 条工具结果 | 1956 | 41% | 3/3 |
| C 丢弃最早 N 轮 | 丢掉最早 3 轮 | 4696 | 99% | 0/3 |
| C 丢弃最早 N 轮 | 丢掉最早 6 轮 | 4646 | 98% | 0/3 |
| C 丢弃最早 N 轮 | 丢掉最早 12 轮 | 4492 | 94% | 0/3 |
表格口径:合成任务 203 轮 / 完全态 4756 token,token 由 tiktoken(cl100k_base)计数;「占原文」= 压缩后 token ÷ 4756;「约束存活」= 三条约束字符串在压缩后文本里字面完整命中的条数。取数日 2026-10-11,脚本 exp04_compaction.py。
3.2 两处反直觉的地方
第一,策略 A 压到 2000 token(保留 42%)就已经全丢,不是压狠了才丢——只要保留方式是"从尾部截",最前面那 3 轮必然在第一批被扔掉。
第二,策略 C 压得最"轻"(丢 12 轮还剩 94%),约束照样 0/3。因为它砍的不是量,是位置:最早那几轮恰好就是约束所在。
反过来,策略 B 压到 1956 token(只剩 41%),三条约束一条不少。它压掉的 token 比策略 A 只多不少,却把约束完整留住了。原因就是第 2 章那句:它清的是"工具结果"这个类型,而约束不在这类里。
四、我改了什么:四条硬规矩
这四条不是我拍脑袋定的,每一条都能对上第 3 章翻车的那组数字。
4.1 硬约束不做"历史",做"常驻槽"
把三条约束从对话历史里挪走,放进每轮都重新注入的系统提示(或独立约束槽)。这样无论压缩怎么动历史,约束都不在它的作用范围内。第 3 章策略 A/C 之所以是 0/3,前提就是"约束在历史里";一旦它不在历史里,这两条路对它的杀伤力直接归零。
4.2 压缩按类型清,不按位置截
只用"清工具结果"这一类(对应官方clear_tool_uses+keep),明确禁止两件事:禁止"丢最早 N 轮",禁止"只留尾部 token"。前者是第 3 章的策略 C,后者是策略 A,两条都是 0/3。
4.3 每次压缩后做约束存活自检
压缩完立刻断言:三条约束字符串必须在当前上下文里字面命中,少一条就回滚本次压缩并告警。这一步成本很低,但能把"压缩悄悄吃掉约束"从"攒到第 60 轮才爆"变成"压缩当场就被发现"。
4.4 不可逆动作加代码闸门
对外发送、付款这类动作,在工具层拦截:命中金额上限或渠道白名单就直接拦下,不依赖模型记不记得。模型可以忘,代码不会忘。这条是最后一道防线——即使前三条都失守,它也能兜住。
一套可以直接照搬的压缩配置:把上面这四条落成配置和代码的写法(清工具结果的参数、约束自检的断言、动作闸门的拦截点),和本文那三条约束样例放在一起。放在资料包里,扫码即可获取:
五、比技术更该先想清楚的事:什么内容有"免压缩"资格
技术手段只能缓解,真正决定生死的是先划清一条线:哪些内容无论如何不能被压。
第 4 章那四条能用,但它们的共同前提是"你已经知道要保护什么"。而大多数翻车,恰恰是压根没想过这个问题——把所有内容一视同仁地塞进历史,然后指望压缩"自己知道轻重"。它不知道。
5.1 用"可逆性"给内容分类
照着"丢了能不能低成本恢复"这一条标准,四类内容各归各位:
- 硬约束(金额上限、渠道白名单、隐私红线)→免压缩,每轮重贴;判据是"违反后不可逆"。
- 不可逆动作的参数(对外发送、付款、删除)→ 常驻 + 代码闸门;判据是"不能只靠模型记得"。
- 可重建的事实(工具返回、中间统计)→ 优先压缩;判据是"丢了能再查一次"。
- 用户临时偏好(“这次用中文”“先别动手”)→ 可压缩;判据是"违反了能重说"。
分完就会发现:真正不能压的,永远是前两类。而它们恰恰排在最前面、最容易在"截尾部"或"丢最早 N 轮"里被第一批扔掉——这正是本文那场故障的完整成因。
5.2 一条可执行的止损线
如果你的长任务同时满足这两个条件——(一)有违反后不可逆的硬约束;(二)当前压缩方式是"截尾部"或"丢最早 N 轮"——那不用等到出事,现在就该改。因为按第 3 章的实测,这两条路对最前面约束的命中是 0/3,跟压多少无关。
反过来说,如果任务里确实没有不可逆动作,全部内容丢了都能重建,那"截尾部"反而是最省事的做法。判据在这里,选择权在你。怕的不是选了哪种压缩,是压根没意识到自己已经选了一种。
一份上下文压缩的判据清单:上面这张"免压缩资格"表、三种压缩方式的取舍,以及第 3 章那组实测数据的可复跑脚本,整理成了同一份清单。放在资料包里,扫码即可获取:
附表 A:本文引用事实与出处对照表
| # | 事实 / 表述 | 出处(文档名 + 发布方 + 链接) | 本文位置 |
|---|---|---|---|
| A1 | 压缩定义原文:「Compaction replaces the older turns of a conversation with a summary that Claude writes on the server, so you need no summarization code of your own.」 | 《Compaction overview》,Anthropic,https://platform.claude.com/docs/en/build-with-claude/compaction | 2.3、5.1 |
| A2 | 压缩区分为「compact on demand」与「threshold compaction」,并说明Recent turns stay word for word | 同上 | 2.3 |
| A3 | 上下文清理的策略类型写作clear_tool_uses_20250919 | 《Context editing》,Anthropic,https://platform.claude.com/docs/en/build-with-claude/context-editing | 2.2、4.2 |
| A4 | trigger可设为{"type":"input_tokens","value":30000} | 同上 | 2.2 |
| A5 | keep可设为{"type":"tool_uses","value":3};keep的官方原句「The API removes the oldest tool interactions first, preserving the most recent ones.」 | 同上 | 2.2 |
| A6 | clear_at_least用于保证每次至少清掉多少 token;官方说明清缓存会让前缀缓存失效,需清够量才值得 | 同上 | 2.2、4.2 |
| A7 | 203 轮 / 4756 token 合成任务的压缩实测:策略 A 约束存活 0/3、策略 B 3/3、策略 C 0/3 | 本文实测,脚本exp04_compaction.py,Python 3.13.12 + tiktoken 0.14.0,取数日 2026-10-11 | 3.1–3.2 |
说明:A1–A6 为 Anthropic 官方文档逐字/参数级命中;A7 为本文真跑实测。社区讨论帖、二手转述一律未用作事实来源。
写在最后:这篇用到的资料
写这篇文章时,把相关的官方文档和源码又翻了一遍,顺手也整理了几份配套的东西:
- 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
- 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
- AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
- 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
- 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多
资料是我自己整理的,放在下面这个码上,扫码即可获取:
添加时备注「AI」,优先通过。
资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。