
本文是「从零理解 Claude Code20 个 Agent Harness 机制」系列的第 8 篇。源码仓库shareAI-lab/learn-claude-code本文基于开源仓库学习整理具体实现以仓库代码为准。我第一次碰到上下文超限时任务正在排查一组认证测试。Agent 先读了失败日志又读了认证服务、令牌处理、配置文件和几个测试用例。前面的过程没有任何异常命令能执行文件也能正常读取。等它准备发起下一次模型调用时接口直接返回了prompt_too_long。回头看消息历史问题就很清楚了。前面读过的完整文件、几轮测试输出、搜索结果和修改记录都还留在messages里。Agent 已经不需要其中的大部分内容但接口不会替它判断哪些结果已经过期。只要这些内容还在历史中下一次调用就得继续携带。上下文压缩解决的是长任务进行到中途以后如何让 Agent 丢掉已经用完的信息同时保留继续完成任务所需的线索。一次请求失败前messages里到底堆了什么一开始很容易盯着消息数量。例如一段任务跑了几十轮messages长度看起来已经很大第一反应通常是删掉早期对话。这个判断只解决了一部分问题。实际占空间的往往是工具结果。read_file可能返回完整源码bash可能返回数百行测试日志load_skill可能带回完整的技能说明。消息只有十几条时只要其中有两三段大输出上下文照样会接近限制。以认证测试这个场景为例真正值得长期保留的信息很少当前目标修复认证测试失败 已确认失败与 token 配置有关 已修改tests/auth/test_token.py 待完成重新运行相关测试检查回归影响而历史中可能还塞着第一次 pytest 的完整输出 多个已经排除的搜索结果 几份后续不再使用的文件全文 旧版本的配置内容这些内容在当时都有用后面却变成了负担。因此压缩不能只看消息出现得早不早还要看它是任务目标、近期结果还是已经失效的大段输出。压缩顺序里有一个位置不能换这一章在每次调用模型前都会先处理消息历史。执行顺序是大工具结果转存到磁盘 → 裁掉过远的中间消息 → 将较早工具结果替换为占位信息 → 上下文仍然过大时生成历史摘要看上去只是几个预处理函数顺序却不能随便调整。先说最容易被忽略的一层大工具结果转存。tool_result_budget()会检查最近一条工具结果消息的总大小。超过阈值后程序从最大的结果开始处理把完整内容写进.task_outputs/tool-results/上下文中只留下文件路径和前 2000 个字符预览。persisted-output Full output: .task_outputs/tool-results/toolu_123.txt Preview: ... /persisted-output这样Agent 后续发现预览不够时还可以通过文件读取工具重新查看完整结果。接下来才是micro_compact()。它保留最近 3 条工具结果将更早的结果替换成一行说明[Earlier tool result compacted. Re-run if needed.]如果这两步顺序反过来问题就出现了。较早的大结果会先被替换成占位文本后面的转存逻辑再也拿不到完整内容也就无法写入磁盘。Agent 看到的只剩一行提示原始输出没有保存入口。这也是这章最值得注意的设计细节压缩过程本身也会丢信息必须先处理可恢复的大内容再处理可以丢弃的旧内容。前面的tool_result_budget()关注单条或单轮结果是否过大snip_compact()处理的是另一类问题一段任务运行太久消息数量本身已经很长。教学代码超过 50 条消息后会保留最开始的 3 条和最近的 47 条中间部分用一条简短标记代替。开头通常保留了用户最初提出的任务和限制条件最近的消息则对应 Agent 当前正在处理的文件、报错或测试结果。中间的搜索过程虽然曾经有用距离当前任务已经比较远优先缩短的风险相对较低。裁剪时有一个边界需要额外处理模型发起工具调用和程序返回工具结果必须作为一组消息保留。例如Agent 想确认测试环境的数据库配置于是先请求读取配置文件助手调用 read_file参数为 config.py程序执行后将结果交回模型工具结果config.py 中 DATABASE_URL 为空这两条消息合在一起才构成一次完整的信息交换。模型知道自己读了哪个文件也知道从文件里得到了什么结论。如果压缩时只留下前一条调用记录模型会知道自己曾经读过config.py却不知道读取结果。它后续可能重复读取文件也可能在缺少依据的情况下继续推断。如果只留下后一条工具结果模型虽然看到了DATABASE_URL 为空却无法确认这段内容来自哪个文件、对应什么操作更难判断它是否仍然与当前任务有关。因此snip_compact()在确定裁剪边界时会检查边界附近是否存在这类调用与结果的配对关系。边界刚好落在两者之间时程序会向前或向后调整保证这组消息一起留下或者一起进入被压缩的历史。摘要能让任务继续丢掉的细节不会自动回来前三层压缩都不需要再调用模型。它们只是移动、删除或替换文本成本低也不会引入新的推理过程。经过这些处理后如果上下文仍然超过阈值程序才会调用compact_history()。这一步先把完整消息写入.transcripts/再请求模型生成摘要。摘要需要保留当前目标、重要发现、已修改文件、剩余工作和用户限制。随后原来的消息历史会被替换成一条摘要消息。defcompact_history(messages):write_transcript(messages)summarysummarize_history(messages)return[{role:user,content:f[Compacted]\n\n{summary},}]压缩后认证测试任务可能只剩下这样的上下文当前目标修复认证测试失败。 已确认token 配置缺少测试环境变量。 已修改tests/auth/test_token.py。 待完成运行 pytest tests/auth并检查相关配置是否影响其他测试。 用户限制不要修改生产环境配置。这足以支撑后续任务继续执行。不过完整历史已经不在模型可见的上下文里了。教学代码虽然保存了转录文件但没有提供让模型重新读取.transcripts/的工具。摘要漏掉的某个细节Agent 不会自动找回来。这也是摘要压缩的边界。摘要适合保留任务主线和关键结论无法替代完整证据。涉及文件内容、测试日志、接口返回值等细节时Agent 后续仍然可能需要重新读取文件、重新运行命令或者由系统额外恢复近期的关键内容。接口已经拒绝请求后系统还剩一次补救机会上下文阈值通常是估算值。教学代码通过字符数量估算消息体积它无法完全等同于模型实际计算的 token 数。再加上一轮工具调用可能突然返回大量内容程序有机会在压缩前就收到prompt_too_long。这时会进入reactive_compact()。它会保留最近几条消息把更早的历史交给模型总结然后重试请求。近期消息通常包含正在执行的工具调用和最新结果直接丢掉会让当前任务失去上下文。紧急压缩只允许有限次数重试。如果压缩后仍然超限继续重试没有意义只会不断消耗调用次数。仓库为这条路径设置了重试上限达到限制后抛出异常留给后续的错误恢复机制处理。这一点看起来很普通却是长任务系统必须具备的失败出口。压缩逻辑如果永远相信下一次会成功最终只会把一次接口错误变成循环调用。这套方案适合解决什么问题上下文压缩不会提升模型对代码的理解能力也不会让 Agent 永远记住所有细节。它解决的是长任务运行过程中的容量管理问题。对于持续读文件、反复运行测试、逐步修改代码的任务早期工具结果会不断失去价值。压缩机制负责回收这些内容占用的空间并尽可能留下任务目标、当前进度和可恢复入口。运行这一章的代码时可以重点观察三个现象python s08_context_compact/code.py先让 Agent 连续读取多个文件观察较早的工具结果是否被替换为占位信息。接着读取较多内容检查.task_outputs/tool-results/是否出现转存文件。最后进行一段较长对话观察终端是否出现[auto compact]或[reactive compact]。前者表示程序在调用模型前完成了主动压缩后者说明接口已经拒绝当前请求程序开始执行紧急处理。下一篇会讨论 Memory。上下文压缩会主动丢弃过程内容用户偏好、项目约束和重要决策也可能随着历史一起被缩短。Memory 要解决的是另一类问题哪些信息应该在压缩后继续保留哪些信息值得跨会话保存。