上下文管理全景:Agentic Coding 工具操纵 Messages 数组的六种策略
一次工具调用返回了几万 token 的日志。十轮之后,这段日志是否还在上下文里?如果还在,是否每轮都按普通输入价格重新结算?如果触发 compact,工具定义、Skill 和缓存又会发生什么?
这三个问题经常被混成一个问题。实际上,它们分别属于逻辑上下文、请求结构和计费统计。旧日志仍在有效上下文里,不代表它每轮都按未缓存输入计费;一次请求命中了 prompt cache,也不代表这些 token 不占 context window。
本文保留原来的六种 Messages 操纵策略,但把分析边界收紧到可验证的结构:稳定前缀、可变尾部、渐进式能力加载,以及 API usage 中可以实际测量的缓存结果。未经公开文档或请求轨迹验证的产品排名,不再作为结论。
先分清三本账
讨论上下文成本前,需要同时记三本账。
| 账本 | 记录什么 | 常见观测方式 |
|---|---|---|
| 上下文占用 | 当前请求中模型可见的 token | 工具的 /context、token 估算或请求追踪 |
| 请求变动 | 本轮新增、删除或重写了哪些 segment | 对 tools、system、messages 分段哈希 |
| 计费输入 | 未缓存输入、cache write、cache read 各有多少 | Provider 返回的原始 usage 字段 |
messages 中的一段旧 tool result 只要没有被 prune、compact 或重建移除,通常仍属于模型的逻辑工作集,也继续占用上下文窗口。启用 prompt caching 后,未变化的前缀可能按 cache read 处理;新增或变化的后缀则落入普通输入或 cache write。三者不能用一个“输入 token”数字替代。
不同 Provider 的字段名、TTL、阈值和价格会变化。可靠判断来自当次响应的 usage,而不是根据产品名猜测命中率。下文用 Anthropic 的公开机制解释前缀结构,不把其价格参数外推到其他 Provider。
稳定前缀与可变尾部
一个 coding agent 的有效输入不只有 messages。以 Anthropic Messages API 的公开顺序为例,prompt cache 覆盖 tools → system → messages。工程上可以把整个请求拆成稳定前缀和可变尾部:
“稳定”不是指永远不变,而是指跨相邻请求尽量保持内容和顺序一致。工具 schema 的增删、system prompt 中的时间戳、规则文件顺序变化,都可能让差异点提前。差异越靠前,后面可复用的连续前缀越短。
Anthropic 文档把 cache prefix 定义在 content block 和 token 序列上,不是对序列化 JSON 做逐字节比较。相同语义也不保证相同 token 前缀;空格、标点、排序和动态字段都可能改变前缀。反过来,一个持续增长的文本 block 也不必然全量 miss:如果 Provider 能在既有断点或自动查找范围内找到相同 token 前缀,前半段仍可能复用。能否命中要看具体缓存机制和 usage 结果。
Anthropic 当前公开的计费乘数是:5 分钟 cache write 为基础输入价的 1.25 倍,1 小时 cache write 为 2 倍,cache read 为 0.1 倍。这些数字是厂商当前合同,不是 prompt cache 的通用定义。文章中的结构结论不依赖这些具体价格。
两条渐进式能力加载通道
Messages 六策略描述的是会话历史怎样变化。现代 agent 还有两条与之正交的上下文通道:Skill 加载和工具定义加载。它们管理的是“能力说明何时进入上下文”,不应硬塞成第七种 Messages 策略。
Skill:目录、正文与资源分层
Skill 常见的渐进式披露有三层:
- 启动或扫描阶段只加载名称、描述等目录信息,把可用能力目录放入模型上下文。
- Skill 被调用后,
SKILL.md正文进入当前上下文。 - 正文引用的脚本、模板和参考资料在实际需要时再读取。
因此,“已安装一个 Skill”和“完整 Skill 正文常驻上下文”不是一回事。目录信息适合留在稳定前缀;正文和资源更接近按需换入的可变材料。不同实现可能把它们放进 system、message 或内部包装层,公开文档未承诺的具体 role 不宜当成跨产品事实。
Tool Search:先暴露目录,再加载 schema
工具数量变多时,完整 schema 会挤占稳定前缀。Tool Search 的做法是先暴露命名空间或工具的概要,需要调用时再加载细节。
OpenAI 的公开文档说明:命名空间或 MCP server 在请求开始时只向模型展示名称和描述,内部函数细节通过 Tool Search 按需加载;单个 deferred function 仍会暴露名称和描述,主要延迟参数 schema。Anthropic 的公开文档进一步区分了“客户端发送什么”和“什么进入 context window”:客户端仍把完整 deferred tool 定义发给 API,服务端搜索时不让这些定义进入模型前缀;发现工具后再通过 tool_reference 展开,而且不改动原有前缀。
这是一种能力平面的上下文管理:
前三步控制工具定义何时进入上下文;工具结果产生后,问题才回到 Messages 数组。将两层分开,才能解释“工具很多但启动上下文不一定等比例膨胀”,也能解释为什么 tool result 仍要交给六种历史策略治理。
六种操纵策略
六种策略按动作对象划分,不按某个产品的命令名划分。
追加 Append
追加只在末尾增加新的 user、assistant、tool call 和 tool result,不改旧 block。它是常规多轮会话的默认路径。
对前缀缓存而言,追加通常提供最高的旧历史复用概率。Anthropic 的自动缓存会随着会话增长向后移动断点,旧 content blocks 可成为 cache read,新追加部分写入新的缓存前缀。不过,这仍受 TTL、最小 token 阈值、断点数量和查找范围约束,不能简化成“旧 token 永远命中”。
追加的限制是 context window 单调增长。大文件、长日志和搜索结果会一直占据工作集,直到被后续策略处理。
截断 Truncate
截断裁剪单条消息,只保留前 N token 或头尾片段。最常见的处理对象是当轮刚产生的超大 tool result。
如果截断在消息首次进入请求前完成,旧前缀没有变化。如果回头改写一条历史消息,差异点之后的缓存复用就会受到影响。因而“截断是否破坏缓存”取决于发生时机,而不是 truncate 这个名字。
截断适合控制单次增量,但不能自动清理已经进入历史的旧输出。头部截断还可能丢失错误总结、文件尾部或最终统计,工程实现通常需要按工具类型选择头、尾或结构化摘要。
丢弃 Prune / Drop
丢弃从历史中移除完整 block。常见形式包括选择性移除旧 tool result,以及从最老位置开始收缩滑动窗口。
选择性 prune 可以保留“调用过哪个工具、得出了什么结论”的对话骨架,只移除可重新获取的原始材料。滑动窗口更简单,但可能连同早期约束和决策一起删除。
无论采用哪种形式,只要历史中间或头部的 block 被移除,请求尾部就不再延续原前缀。缓存需要从新的断点重新建立。若工具调用协议要求 call/result 成对出现,prune 还必须维持消息结构合法,不能单独删除配对的一半。
摘要 Compact / Summarize
Compact 用较短的摘要替换一段历史,通常保留近期原文或重新注入必要规则。它同时改变语义表示和 token 结构。
1 | |
被替换的历史后缀需要重新建立缓存,但更早且未变化的 tools、system 或规则前缀仍可能继续命中。把 compact 描述成“整个请求必然从零失效”不准确;正确说法是:从最早发生变化的位置开始,后续前缀需要重建。
Compact 的成本至少包括一次摘要生成、重写后上下文的输入,以及潜在的信息损失。它适合在窗口压力下保存最小可执行状态,不适合替代外部事实源。摘要里保留文件路径和验证命令,通常比复制长日志更有恢复价值。
外化 Externalize
外化把信息写到 Messages 之外,例如代码仓库、状态文件、spec、测试报告、记忆库或检索索引。后续任务通过指针和查询按需取回。
外化动作本身可以不改既有历史,但“缓存零影响”仍然过于绝对。外部索引的目录、检索指令和返回结果会进入某个上下文 segment;一旦这些内容插入稳定前缀的前部或改写旧历史,仍会影响缓存。设计重点是让小而稳定的指针常驻,让大材料只在需要时进入可变尾部。
外化还需要生命周期治理。旧结论可能过期,检索结果也可能缺少来源。仅仅“写到 memory”没有解决真实性、版本和淘汰问题。
重放 Replay / Reconstruct
重放从持久化事件或文本记录重建本轮请求,而不是沿用进程内的会话对象。CLI 单次运行、队列任务和会话恢复都可能采用这种方式。
重放不等于必然 cache miss。只要重建结果在断点之前保持相同 token 前缀,Provider 仍可能复用缓存。重放的主要风险是确定性:
- 序列化顺序、空白和 wrapper 是否稳定;
- 是否每轮插入当前时间、随机 ID 或动态说明;
- tools 与 system 是否按固定顺序生成;
- 历史是否被合并成不同结构的 block;
- 重放是否发生在缓存 TTL 内。
持久 session 的增量追加天然更容易保持旧 block 不变;重放路径则必须主动保证确定性。两者的成本差异不能凭运行模式推算,只能通过相同负载下的 usage 记录验证。
缓存影响矩阵
| 策略 | 对请求的典型改动 | 可能保留的缓存 | 主要风险 |
|---|---|---|---|
| 追加 | 尾部新增 blocks | 旧前缀 | 窗口持续增长 |
| 截断 | 首次入列前裁剪,或回写旧消息 | 截断点之前 | 丢失尾部信息;回写历史 |
| 丢弃 | 删除旧 blocks | 删除点之前 | 结构配对、早期约束丢失 |
| Compact | 摘要替换历史区段 | 最早改写点之前 | 摘要遗漏、缓存后缀重建 |
| 外化 | 大材料移出,小指针和检索结果按需进入 | 未被插入或改写的前缀 | 过期、检索遗漏、回灌过量 |
| 重放 | 从事件或文本重新组装请求 | 确定性相同的前缀 | 动态字段和排序漂移 |
这张表只描述典型结构,不提供产品排名。相同策略可以有不同实现,相同实现也会因 Provider、模型版本、TTL 和请求包装不同而得到不同 usage。
一套可复现的测量协议
缓存结论应该能由另一台机器复现。最小实验需要锁定以下变量:
- Provider、模型 ID、API 版本与 endpoint;
- system prompt、工具集合及其顺序;
- Skill 目录、正文和调用顺序;
- cache 配置、TTL 与两次请求间隔;
- 请求包装器版本、序列化方式和会话模式;
- 输入文本、工具返回值和输出上限。
然后执行一组有对照的请求:
- 冷启动:发送基线请求,保存完整请求结构、响应 usage 和延迟。
- 热重试:在 TTL 内发送完全相同的请求。
- 尾部追加:只增加一条 user message。
- Skill 调用:保持其他内容不变,让一个 Skill 正文进入上下文。
- Tool Search:先保留 deferred 状态,再加载一个工具 schema。
- 前缀扰动:只修改靠前的 tool description 或 system 字段。
- Compact:用固定摘要替换旧历史,避免摘要内容随机性干扰结构实验。
- 重放:从事件日志重建与增量会话逻辑等价的请求。
每一步至少记录:
1 | |
Provider 字段名不同,应保留原始 usage JSON,再映射到统一表。实验至少重复两轮,并在首个响应完成后、TTL 未过期时发起热请求。若产品只提供汇总账单而不暴露 cache usage,结论只能写成“不可观测”,不能根据延迟或总 token 反推出精确命中。
工程取舍
短会话可先限制单条工具输出,保持追加即可。长会话应把可复现的大材料外化,在窗口压力出现前 prune 或 compact。跨会话恢复要保存事实源、决策依据、当前状态和验证入口,而不是重放全部聊天文本。阶段退出条件和恢复契约见《Agent 会话边界设计》。
缓存优化的关键不是追求某个固定命中率,而是稳定最靠前、最昂贵的 segment,把动态材料留在尾部。Skill listing、工具命名空间和项目规则都适合做小而稳定的目录;Skill body、工具 schema、文件内容和日志则按需进入。
当成本判断影响架构选择时,先跑测量协议。没有 usage 证据,就没有可靠的“哪种工具更省”结论。
系列文章
- AI Coding Agent 的上下文操作系统:组件、生命周期与阅读地图
- AI Coding Agent 的上下文栈:组件边界与装配顺序
- AI Coding Agent 的 Skill 加载机制深度解析
- 一次 AI Coding Agent Turn 的上下文生命周期
- Agent 会话边界设计:Session、Context Window 与工作状态转移
- Context Paging:Compact、外部化、恢复与 Memory 生命周期
- 2026 Coding Harness 实现图谱


