上下文管理全景:Agentic Coding 工具操纵 Messages 数组的六种策略
一个常见的直觉是:agentic coding 工具会"偷偷"管理对话历史。MCP 工具在第一轮取回了一大段数据库查询结果,模型分析完毕,过了十轮对话之后,工具应该已经悄悄把那段原始数据替换成了一句"此前查询了一段数据"。输入 token 因此减少,成本随之下降。
实际情况并非如此。不开 compact 或 prune,messages 数组就原封不动地增长。那段数据库查询结果会从第一轮一直带到第一百轮,每次 API 调用都完整发送,按全价计费。
这引出了一个工程问题:面对一个只增不减的数组,各家 agentic coding 工具到底有哪些操纵手段?这些手段如何与 prompt cache 机制交互?不同选择会导致怎样的成本和信息保真度差异?
本文是对 Claude Code 源码深度解析 和 Agentic Coding 深度解析 的差异化补位。前两篇分别覆盖了 Claude Code 的五级压缩流水线和 messages 数组的五维操纵模型(注入 / 定位 / 保护 / 清理 / 重复)。本文专注于跨工具的策略全景,以 prompt cache 的前缀匹配原理为统一约束条件,建立一套不漏不重的分类体系。
两个前提
本文的分析建立在两个前提之上。两者的详细论述分别在前两篇文章中,此处只做最简回顾。
前提一:messages 数组是 Agent 的唯一状态载体。 所有主流 LLM API 的对话状态都存储在一个 messages 数组里。历史对话、推理过程、工具调用请求、工具返回结果,全部是这个数组的元素。Agent 框架在做的核心事情,就是决定往这个数组里追加什么、保留什么、丢弃什么。
前提二:prompt cache 基于前缀匹配。 以 Anthropic 为例,缓存按 tools → system → messages 的顺序做字节级前缀匹配。增长型多轮对话里,只要旧的 message blocks 保持不变,新一轮请求就能从缓存中读取前面所有轮次的内容,只为新增部分付费。缓存读取成本是正常输入的 10%。反过来说,任何打破前缀的操作都会导致缓存失效,下次请求要重新全价写入。
三家主流 LLM 提供商的缓存机制存在根本性差异:
| 维度 | Anthropic | OpenAI | Google Gemini |
|---|---|---|---|
| 控制方式 | 显式断点 + 自动 | 全自动 | 隐式 + 显式 |
| 读取折扣 | 90%(0.1× 正价) | 50% | 按模型浮动 |
| 写入成本 | 1.25×(5min TTL)/ 2×(1h TTL) | 无额外成本 | 按存储时长计费 |
| 最低 token 阈值 | 512-4096(按模型) | 1024 | 2048-4096 |
| 默认 TTL | 5 分钟 | 5-10 分钟至 24 小时 | 1 小时 |
| 开发者可控性 | 高(最多 4 个断点) | 低 | 中 |
Anthropic 的缓存机制给了开发者最大的控制空间,代价是操纵 messages 数组的每一步都需要考虑缓存影响。OpenAI 的全自动方案省心但不可调优。Gemini 的按时长计费模型引入了一个额外维度——缓存不仅有命中率问题,还有存储成本问题。
六种操纵策略的分类学
对 messages 数组的操纵,按照干预强度从弱到强,可以归纳为六种基本操作。这不是某个工具的专有概念,而是所有 agentic coding 工具在工程实践中收敛出的通用模式。
1 | |
追加(Append)
追加是默认行为。每一轮对话在数组末尾添加新的 user / assistant / tool 消息,不对已有内容做任何修改。
这是对 prompt cache 最友好的操作。前缀单调增长,第 n+1 轮请求天然命中前 n 轮的缓存。以 Anthropic 的机制为例,第 10 轮请求只需为第 10 轮新增的消息付全价,前 9 轮的内容以 0.1× 的缓存价读取。
所有工具在 context window 未打顶之前都采用这个策略。问题在于,追加是单调的——数组只增不减,终会撞上 context window 的天花板。一个典型的 agentic coding 会话,每轮工具调用可能产生数千到数万 token 的返回内容(读文件、grep 结果、编译输出),十几轮之后 context window 就开始吃紧。
追加策略的有效距离取决于两个因素:模型的 context window 大小,以及每轮的 token 产出密度。context window 越大(如 Claude 的 200K)、工具调用越轻量,追加策略的有效期就越长。
截断(Truncate)
截断的作用对象是单条消息。当某一条 tool result 的内容超过预设的 token 预算时,只保留前 N 个 token,丢弃其余部分。
Claude Code 的 Tool Result Budget 是这种策略的典型实现。一次 Read 工具调用返回了一个 10000 行的文件,Tool Result Budget 会将其截断到预算范围内。被截断的是这一条 tool result 消息的尾部,数组中的其他消息不受影响。
截断对 prompt cache 的影响取决于被截断消息在数组中的位置。实践中,截断几乎总是作用于当轮的新消息(即数组末尾),因此前缀不变,缓存完全不受影响。如果某种实现对历史消息做回溯截断,则被截断位置之后的缓存都会失效——但这种做法在主流工具中并不常见。
截断的局限在于它只能限制增量。十轮之前的一条 5000 token 的 tool result,已经进入数组、已经被缓存前缀覆盖,截断机制管不着它。
丢弃(Prune / Drop)
丢弃是从数组中移除整条消息。根据移除策略的不同,分为两种模式。
选择性丢弃只移除特定类型的消息,通常是旧的 tool result。opencode 的 compaction.prune = true 就是这种模式——启用后,compaction 过程中会移除旧的 tool output 来节省 token,但保留 user / assistant 消息的对话骨架。Claude Code 的 Microcompact 层(由 API 原生的 context_management 触发)也属于这一类,它会裁掉最旧的若干 tool_result。
选择性丢弃的逻辑是:工具返回的原始数据(文件内容、grep 结果)通常可以重新获取,丢掉原始数据但保留"调用了什么工具、在哪个文件上操作"的记录,信息损失相对可控。
滑动窗口丢弃的做法更简单粗暴:当 context window 满时,直接丢弃数组头部最老的消息,不区分类型。Cursor 和 Windsurf 在 context 满时都采用这种方式。Windsurf 的文档明确记载了这一行为:“when a model’s context window grows too long in Cascade sessions, earlier contexts can be dropped”——没有摘要,没有选择性保留,直接从头部截掉。
两种模式对 prompt cache 的影响是相同的:被移除位置之后的所有缓存失效。从数组头部移除一条消息,等于改变了整个前缀,全部缓存要重建。
两种模式的信息损失则差异很大。选择性丢弃保留了对话骨架(谁问了什么、模型答了什么、调用了哪些工具),只丢掉工具返回的原始数据。滑动窗口丢弃则可能把早期的关键决策连同上下文一起丢掉,导致模型在后续轮次"忘记"早期的架构约定或设计选择。
摘要(Compact / Summarize)
摘要是用 LLM 生成一段凝练的概述,替换数组中的原始消息。这是对 messages 数组最强力的干预,也是工程上最复杂的操作。
操作的核心逻辑:
1 | |
各家工具的实现差异集中在触发方式、摘要执行者、摘要内容的可见性三个维度。
Claude Code 的 Autocompact 是三段式流程:剥离非文本内容(图片、大型 trace)→ 调用 LLM 生成历史摘要 → 恢复关键文件引用和 skill 状态。摘要后保留最近 N 轮(社区估计 5-10 轮)的原文(preservedSegment),更早的内容被摘要替换。用户可以通过 /compact 命令手动触发,也可以等系统在 context 使用率接近阈值时自动触发。整个过程对用户不可见。
Aider 的 ChatSummary 采用递归摘要策略。它将消息历史按 token 预算切成 head(旧)和 tail(新)两段,在 assistant 消息边界切割,避免切断一组配对的 tool_call / tool_result。head 部分交给 LLM 摘要,如果摘要 + tail 仍然超限,则递归处理(深度上限 3 层)。摘要失败时会尝试多个备选模型作为 fallback。Aider 没有手动触发命令,摘要完全自动,阈值由 --max-chat-history-tokens 控制。
opencode 提供了两个层面的摘要能力。内置 compaction(compaction.auto = true)会在 context 接近上限时自动触发,compaction.reserved = 30000 指定了 compaction 后为新内容保留的 token 预算。此外,opencode Server 暴露了 /session/:id/summarize HTTP 端点,供平台层在 token 超阈值或 phase 结束时主动调用。这个设计反映了 opencode 作为可嵌入后端的定位——上下文管理的决策权可以交给调用方。
Cursor 的 /compress 命令存在但几乎没有文档。社区中更常见的做法是直接开一个新对话。Cursor 的上下文管理哲学更偏向"短对话 + 强检索",而不是"长对话 + 强压缩"。
摘要对 prompt cache 的影响是最大的:数组被重写,前缀完全改变,所有缓存失效,下一次请求要从零开始付 cache write 成本。这就是为什么 Claude Code 的 Autocompact 触发阈值设在较高的位置(社区分析约 75-80% context 容量)而不是 50%——过早压缩意味着更频繁地为缓存重建付费,总成本反而更高。
摘要还有一个根本局限:信息损失不可控。LLM 生成的摘要无法保证保留所有关键信息。它可能漏掉某个微妙的类型约束、某个隐含的并发假设、某段 SDD 流程中的必需字段。在实际项目中观察到的现象是:compact 能减少旧 tool output 和历史对话的体积,但不能保证把 37 万 token 压到 7 万,也不能保证摘要保留了业务流程的必需字段。
摘要只能作为兜底手段,不能当作主要的上下文管理策略。
外化(Externalize)
外化是把信息从 messages 数组搬到外部存储,在需要时通过工具调用或系统注入重新取回。这不是对数组的直接操纵,而是绕过数组限制的间接策略。
Claude Code 的 memory tool 是典型的外化机制。Agent 在对话过程中把关键决策、架构约定、进度状态写入文件系统(CLAUDE.md、memory 文件、TODO list)。即使上下文被压缩甚至清空,这些外化的信息可以通过文件读取重新注入。
Aider 的 repo map 是另一种外化形态。它用 tree-sitter 解析整个代码仓库的结构(类、函数、类型、调用签名),生成一份 token 预算可控的结构摘要(默认约 1K token,通过 --map-tokens 调节)。repo map 作为独立的系统级上下文随每次请求发送,不是 chat history 的一部分——chat history 被摘要压缩时,repo map 不受影响。repo map 使用图排序算法(文件为节点、依赖为边),优先保留被引用最多的标识符,在固定 token 预算内最大化信息密度。
Windsurf 的 Memories 系统把自动生成的上下文笔记存储在 ~/.codeium/windsurf/memories/ 目录下,按工作区隔离。这些 memory 跨对话持久存在,Cascade 判断相关时自动注入。Windsurf 没有 compaction 机制,它的上下文管理哲学就是:短对话 + 外化记忆 + 强检索。
外化对 prompt cache 完全无影响,因为它不修改 messages 数组的内容。外化的代价在别处:外化的信息需要通过工具调用取回,每次取回都会产生新的 token 消耗,且取回的内容会追加到数组中、占据 context window 空间。这是一种"把持久存储成本转换为按需检索成本"的策略。
重放(Replay / Reconstruct)
重放是每次请求时从头重建 messages 数组,而不是在已有数组末尾追加。这是六种策略中对 prompt cache 最不友好的一种。
opencode 的 run 模式就是重放策略的实例。每次执行 opencode run -- "$PROMPT" 时,如果平台层想实现多轮对话,需要将历史消息 replay 成一个文本 prompt:
1 | |
这段文本作为一个单独的 user message 发送给 opencode。表面上看,文本的前半部分每次都一样,"应该"能命中缓存。但 prompt cache 的前缀匹配是结构化的——它匹配的是 tools → system → messages 中每个 block 的内容和边界,不是一个大文本字符串内部的子串前缀。每次 run 都构造一个新的 message block(内容持续变长),从缓存角度看,每次都是一个全新的请求前缀。
与之对比,opencode 的 serve 模式(opencode serve)维护一个持久的 session,每轮通过 /session/{id}/prompt_async 发送新增的 user message。历史上下文由 session 内部维护,messages 数组单调增长——这就是追加策略,天然适配 prompt cache 的前缀匹配。
run 模式和 serve 模式在功能上等价(都能完成多轮对话),但在 prompt cache 效率上差距巨大。假设每轮产生 3000 token,对话进行到第 20 轮时,run 模式每次请求都要全价发送 60000 token 的历史;serve 模式则只需为最新一轮的 3000 token 付全价,前 57000 token 以 0.1× 读取。在 Anthropic 的定价模型下,run 模式的输入成本可以达到 serve 模式的 5-8 倍。
缓存影响矩阵
六种操纵策略对 prompt cache 的影响汇总:
| 策略 | 缓存前缀影响 | 缓存重建成本 | 适用场景 |
|---|---|---|---|
| 追加 | 无(前缀只增不减) | 无 | context window 未打顶的常规对话 |
| 截断 | 通常无(只改最新消息尾部) | 无 | 单条 tool result 过大 |
| 选择性丢弃 | 被移除位置之后全部失效 | 一次性 cache write | context 接近上限且需保留对话骨架 |
| 滑动窗口丢弃 | 全部失效(从头部移除) | 一次性 cache write | context 打顶且无摘要能力 |
| 摘要 | 全部失效(数组重写) | cache write + 摘要 LLM 调用 | context 打顶且需保留语义连续性 |
| 外化 | 无(不改数组) | 无 | 跨对话持久化关键信息 |
| 重放 | 完全无缓存(每次全新构造) | 每次请求都是全价 | 无 session 管理能力的 run 模式 |
核心权衡是:干预强度与缓存友好度负相关。越强力的操纵(摘要、重放),对缓存的破坏越大。而缓存命中带来的成本节省(Anthropic 场景下高达 90%)往往超过操纵本身节省的 token 量。
Claude Code 宁可让 messages 数组增长到接近 context window 上限才触发 Autocompact,而不是在 50% 时就积极压缩。在 50% 到触发阈值这个区间里,每一轮请求都在享受缓存命中的 90% 折扣。过早压缩意味着放弃这段"缓存有效期",反而推高了总成本。
跨工具对比
五个主流 agentic coding 工具的策略选择:
| 维度 | Claude Code | Aider | opencode | Cursor | Windsurf |
|---|---|---|---|---|---|
| 默认策略 | 追加 → 自动摘要 | 追加 → 自动摘要 | 追加 → 可配置 compact | 追加 → 截断 | 追加 → 截断 |
| 触发方式 | 自动 + 手动 /compact |
自动 | 自动 + API 端点 | 手动 /compress + 自动截断 |
自动截断 |
| 摘要能力 | Autocompact 三段式 + 五级降级 | 递归摘要(深度上限 3) | 内置 compact + /summarize 端点 |
/compress(文档极少) |
无 |
| 选择性丢弃 | Microcompact(丢旧 tool_result) | 无 | prune=true(丢旧 tool output) |
无 | 无 |
| 外化机制 | memory + CLAUDE.md + TODO | repo map(tree-sitter 图排序) | session 持久化 | .cursorrules + notepads | Memories 系统 |
| 缓存感知 | 高(围绕 cache_control 构建) | 中(--cache-prompts 可选) |
取决于底层模型和集成方 | 依赖提供商侧缓存 | 依赖提供商侧缓存 |
五个工具的策略选择分成两条路线。
Claude Code 和 Aider 走"长对话 + 强压缩"路线——允许对话持续很长,通过摘要在 context window 内腾挪空间。代价是摘要机制的工程复杂度高,且每次摘要都会击穿缓存。
Cursor 和 Windsurf 走"短对话 + 强检索"路线——不投入工程复杂度做摘要,而是通过 codebase 索引(Cursor 的 Merkle tree + 双索引架构、Windsurf 的语义知识图谱)确保每次新对话都能快速检索到相关上下文。这条路线把"记住历史"的责任从 messages 数组转移到了外部索引系统,用检索成本替代压缩成本。
opencode 的定位比较特殊。它既支持 serve 模式下的长 session(追加 + 自动 compact),也支持 run 模式下的短 session(重放)。compaction 配置的三个参数给了平台层灵活的调优空间:
1 | |
auto 控制是否自动触发压缩,prune 控制是否在压缩过程中移除旧的 tool output(默认关闭),reserved 指定压缩后为新内容保留的 token 预算。平台层还可以通过 /session/:id/summarize 端点在 phase 切换时主动触发一次摘要。
但 compact 本身的局限——信息损失不可控、无法保证压缩到目标大小——意味着 compaction 只能是长 session 管理中的兜底手段。真正的主解是在架构层面让对话走 serve 模式的增量追加路径,充分利用 prompt cache 的前缀命中。
决策指南
选择哪种策略组合,取决于三个参数:对话预期长度、context window 大小、是否需要跨对话保持状态。
1 | |
一条贯穿所有场景的原则:尽量推迟对 messages 数组的破坏性操作。每一次丢弃或摘要都会击穿 prompt cache,引发一次缓存重建的成本。在缓存带来 90% 输入折扣的定价模型下,保持数组的前缀稳定性,往往比积极压缩更省钱。
另一个容易忽视的决策点是会话模式的选择。如果底层工具支持 serve 模式(如 opencode serve),走 serve 模式的增量追加路径远优于 run 模式的历史重放。这个选择发生在架构层面,效果却比 compaction 配置的调优大一个数量级。


