上下文税:为什么你的编码代理把同样的 600 行代码读了 400 遍

·阅读约 13 分钟·Evergreen Tools Team
Developer terminal showing token-heavy agent session

💡 工具推荐优化代理 token 花费?用 AI Token 计数器测量真实用量,用代码转 Markdown 工具为代理准备上下文。 AI Token 计数器, 代码转 Markdown 工具

当编码代理在一个放不进上下文窗口的代码库里工作时,它只能用 shell 的方式导航:grep,读文件,再读更大的文件切片。SonarSource 研究工程师 Antonio Aversa 称之为「上下文税」:那些读取一旦进入对话,就会在之后的每一轮被重复计费。在他自己的仓库里,一个约 800 行的普通 PR 累计消耗了 1.56 亿上下文 token——峰值上下文窗口 458,700 token,花费约 41 美元,而最终 diff 人五分钟就能读完。18 个同类 PR 平均每个约 2.34 亿上下文 token、约 65 美元。这不是一个 PR 运气差,而是一个结构性成本问题。

1. 机制:上下文是每一轮都要交的税

编码代理不会只读一次文件。每走一步,模型都会把「到目前为止的整个对话」作为输入重新发送。提示缓存让重复 token 单价变便宜(约为输入价的 10%),但每一轮你都要为它们付费。所以一个 token 的真实成本不是它的大小,而是它的大小乘以它存活过的轮数:在第 40 轮读入一个 600 行文件,你付的不是 600 行,而是 600 行乘以之后约 470 轮。文件读取和工具结果会一直留在对话里、被反复重发,直到运行结束或被压缩——这就是为什么缓存读取能膨胀到 1.528 亿 token。

// The context tax, in one formula. A token's true cost is not
// its size. It is its size times the number of turns it
// survives in the conversation.
//   cost(token) = size(token) x turns(token)
// SonarSource measured one ~800-line PR in its own repo:
const MEASURED = {
  modelRoundTrips: 512,
  peakContextWindow: "458,700 tokens",
  freshInputTokens: "106k",
  cacheReadTokens: "152.8M",   // the re-billed transcript
  totalBilled: "~156M tokens",
  sessionCost: "~$41",
};
// The line that matters: cache-read = 152.8M tokens. Every
// read stays in the conversation and is re-sent every turn.

2. 一次过度读取的完整账本

在那次 PR 早期,代理需要理解一个约 67 行的辅助函数。为了找到它,代理读了整个 618 行文件(6,472 token),而实际用到的只有约 700 token:立刻浪费约 5,770 token。这段读取大约在第 42 轮进入对话,并在剩余 470 轮里持续存在——以缓存读取价(约每百万 0.20 美元)计算,5,770 乘以 470 约等于 270 万 token,约 0.54 美元,只为一次不必要的文件读取。这个 PR 重复了约 10 次,加上几十次盲目的全树 grep——其中几次一无所获,被迫再 grep 一次更大的范围。可避免的导航开销在单个 41 美元的 PR 上就达到数美元,而且它随仓库规模增长,而不是随你的改动规模增长。

Circuit board representing token flows through context
// The mechanism, traced end to end. Early in that PR the agent
// needed a ~67-line helper inside a 618-line file. It read the
// whole file (6,472 tokens) instead of the ~700 tokens it used.
const ONE_OVER_READ = {
  wastedTokens: "~5,770",
  enteredConversation: "around turn 42",
  survivedTurns: 470,
  rebilledWaste: "5,770 x 470 ~= 2.7M tokens",
  costAt20CentsPerMillion: "~$0.54  // for ONE file read",
};
// The PR did this roughly 10 times, plus blind tree-wide greps,
// several of which returned nothing and forced a wider grep.
// "The cost scales with the size of the repo, not the size of
// your change." -- Antonio Aversa, SonarSource research eng.

3. 为什么 grep 让代理失败:绑定问题

在 SemSitter 的一次重构中,代理需要回答编程里最普通的问题:调用 ctx.method_index.resolve_return_type(...) 时,ctx.method_index 是什么类型、方法定义在哪里、返回什么?诚实答案是 MethodIndex、method_index.rs、Option<&str>。代理没有索引,于是它只能 grep——结果在 Python、TypeScript、Java、Rust、C# 和共享核心中同时返回定义;正则无法告诉它这一次调用绑定到哪个。于是它打开文件、读宽切片、再 grep 辅助函数、再读另一个文件。讽刺的是:代理正在构建的恰好是它自己缺失的能力——调用点解析。

# The old navigation loop: grep, read, read a wider slice.
grep -rn "resolve_return_type" .
# -> a definition in EVERY backend at once (Python, TypeScript,
#    Java, Rust, C#, shared core). The regex cannot say which
#    one this call binds to.
open method_index.rs      # read a generous slice
grep -rn "extract_type_name" .   # and again, and again
# Every one of those reads is now permanently in the
# conversation, re-billed on every later turn.

4. 出路:用图回答导航问题

Sonar 的答案是 Sonar Vortex 与 SemSitter:一个本地统一依赖图(UDG),在每次变更时即时更新。代理不再 grep 后读切片,而是向图查询特定节点,拿回节点及其类型化关系——包括正则永远匹配不到的调用点。可衡量的效果是:每个任务携带的上下文更少、往返次数更少、在大仓库里能找到正则找不到的调用点。对平台团队,这指向一个明确的投资方向:为代理提供语义导航层,而不是让它们用 shell 的方式在代码库里瞎摸。

Code navigation graph replacing blind file reads

5. 先测量你自己的上下文税

优化之前先测量:记录每一轮新增的 token(读入的文件、工具输出)、这一轮计费的缓存读取 token、以及运行中的总额与预估成本。然后按成本从低到高使用杠杆:用签名/头部替代整文件;用定向导航查询替代 grep;保持代理的编辑范围小——每个存活的 token 都是税;当窗口逼近上限时积极压缩。很多团队执着于选模型和写提示词,却忽略了最大的一笔开支藏在代理与工具之间的数据格式与读取策略里。

// The alternative: answer navigation questions from a graph
// instead of from raw file reads. Sonar's SemSitter engine
// keeps a Unified Dependency Graph updated on every change;
// the agent asks for a node and gets the node plus its typed
// relationships -- call sites a regex would never match.
{
  "query": "resolve_call",
  "call": "ctx.method_index.resolve_return_type(owner, method_name)",
  "graph_answer": {
    "receiver_type": "MethodIndex",
    "definition": "method_index.rs",
    "return_type": "Option<&str>",
    "call_sites_in_repo": 3
  }
}
// One targeted query replaces grep + whole-file reads and the
// agent carries far less context per turn.

6. 给团队的行动清单

第一,从真实会话里导出 token 账单,找出缓存读取的占比——如果像 Sonar 那样超过 90%,你有巨大的优化空间。第二,在最大的仓库里试点语义导航或结构化索引,对比启用前后的 token 与往返次数。第三,把「每次读取都要有理由」写进代理的工作规范:grep 前先问自己要找的是定义、调用点还是类型。上下文税不会消失,但通过测量与更好的导航,它可以从「每 PR 65 美元」降到可忽略的水平。

# Measure your own context tax before optimizing. Log per turn:
#   - tokens added this turn (files read, tool output)
#   - cache-read tokens billed this turn
#   - running total and projected session cost
def per_turn_tax(read_tokens: int, turns_remaining: int, cache_price_per_m: float = 0.20):
    return read_tokens * turns_remaining * cache_price_per_m / 1_000_000
# Remediation levers, cheapest first:
# 1. Include signatures/headers instead of whole files.
# 2. Prefer targeted navigation queries over grep.
# 3. Keep agent edits small; each surviving token is a tax.
# 4. Compact aggressively when windows approach the ceiling.

📌 常见问题 FAQ

什么是上下文税?

SonarSource 提出的概念:代理在对话中读入的每个文件与工具输出都会在之后每一轮被重新发送并计费,因此 token 的真实成本是它的大小乘以它存活的轮数。

什么是上下文税?

SonarSource 提出的概念:代理在对话中读入的每个文件与工具输出都会在之后每一轮被重新发送并计费,因此 token 的真实成本是它的大小乘以它存活的轮数。

什么是上下文税?

SonarSource 提出的概念:代理在对话中读入的每个文件与工具输出都会在之后每一轮被重新发送并计费,因此 token 的真实成本是它的大小乘以它存活的轮数。

什么是上下文税?

SonarSource 提出的概念:代理在对话中读入的每个文件与工具输出都会在之后每一轮被重新发送并计费,因此 token 的真实成本是它的大小乘以它存活的轮数。

什么是上下文税?

SonarSource 提出的概念:代理在对话中读入的每个文件与工具输出都会在之后每一轮被重新发送并计费,因此 token 的真实成本是它的大小乘以它存活的轮数。

上下文税有多大?

实测中一个 800 行 PR 消耗约 1.56 亿上下文 token(约 41 美元),其中缓存读取 1.528 亿;18 个同类 PR 平均约 2.34 亿 token、约 65 美元。

上下文税有多大?

实测中一个 800 行 PR 消耗约 1.56 亿上下文 token(约 41 美元),其中缓存读取 1.528 亿;18 个同类 PR 平均约 2.34 亿 token、约 65 美元。

上下文税有多大?

实测中一个 800 行 PR 消耗约 1.56 亿上下文 token(约 41 美元),其中缓存读取 1.528 亿;18 个同类 PR 平均约 2.34 亿 token、约 65 美元。

上下文税有多大?

实测中一个 800 行 PR 消耗约 1.56 亿上下文 token(约 41 美元),其中缓存读取 1.528 亿;18 个同类 PR 平均约 2.34 亿 token、约 65 美元。

上下文税有多大?

实测中一个 800 行 PR 消耗约 1.56 亿上下文 token(约 41 美元),其中缓存读取 1.528 亿;18 个同类 PR 平均约 2.34 亿 token、约 65 美元。

为什么缓存读取这么贵?

提示缓存让重复 token 单价便宜(约输入价 10%),但每一轮都会计费:一次 5,770 token 的多余读取在剩余 470 轮里累计约 270 万 token。

为什么缓存读取这么贵?

提示缓存让重复 token 单价便宜(约输入价 10%),但每一轮都会计费:一次 5,770 token 的多余读取在剩余 470 轮里累计约 270 万 token。

为什么缓存读取这么贵?

提示缓存让重复 token 单价便宜(约输入价 10%),但每一轮都会计费:一次 5,770 token 的多余读取在剩余 470 轮里累计约 270 万 token。

为什么缓存读取这么贵?

提示缓存让重复 token 单价便宜(约输入价 10%),但每一轮都会计费:一次 5,770 token 的多余读取在剩余 470 轮里累计约 270 万 token。

为什么缓存读取这么贵?

提示缓存让重复 token 单价便宜(约输入价 10%),但每一轮都会计费:一次 5,770 token 的多余读取在剩余 470 轮里累计约 270 万 token。

如何降低上下文税?

用签名/头部替代整文件、用语义图或索引做定向导航查询替代 grep、保持编辑范围小、窗口逼近上限时主动压缩;先测量账单再决定优化方向。

如何降低上下文税?

用签名/头部替代整文件、用语义图或索引做定向导航查询替代 grep、保持编辑范围小、窗口逼近上限时主动压缩;先测量账单再决定优化方向。

如何降低上下文税?

用签名/头部替代整文件、用语义图或索引做定向导航查询替代 grep、保持编辑范围小、窗口逼近上限时主动压缩;先测量账单再决定优化方向。

如何降低上下文税?

用签名/头部替代整文件、用语义图或索引做定向导航查询替代 grep、保持编辑范围小、窗口逼近上限时主动压缩;先测量账单再决定优化方向。

如何降低上下文税?

用签名/头部替代整文件、用语义图或索引做定向导航查询替代 grep、保持编辑范围小、窗口逼近上限时主动压缩;先测量账单再决定优化方向。

语义图导航真的有用吗?

Sonar 的 SemSitter 用本地统一依赖图回答导航问题,效果是每任务携带上下文显著减少、往返更少,并能找到正则匹配不到的调用点。

语义图导航真的有用吗?

Sonar 的 SemSitter 用本地统一依赖图回答导航问题,效果是每任务携带上下文显著减少、往返更少,并能找到正则匹配不到的调用点。

语义图导航真的有用吗?

Sonar 的 SemSitter 用本地统一依赖图回答导航问题,效果是每任务携带上下文显著减少、往返更少,并能找到正则匹配不到的调用点。

语义图导航真的有用吗?

Sonar 的 SemSitter 用本地统一依赖图回答导航问题,效果是每任务携带上下文显著减少、往返更少,并能找到正则匹配不到的调用点。

语义图导航真的有用吗?

Sonar 的 SemSitter 用本地统一依赖图回答导航问题,效果是每任务携带上下文显著减少、往返更少,并能找到正则匹配不到的调用点。