首页 / 资讯中心 / 文章详情

Coding Agent Token消耗太猛?终端输出剪枝把上下文成本砍掉六成

Coding Agent Token消耗太猛?终端输出剪枝把上下文成本砍掉六成 ★ FEATURED ARTICLE
最近帮一个团队排查Coding Agent的Token消耗账单一个月烧掉近百万Token账面上有一大半都浪费在终端输出上面。一个构建命令几十KB的日志、一段测试输出里99%的无用行、一条报错堆栈带上十几层无关调用帧这些东西原封不动塞进上下文Token刺客就是这么掏空预算的。更麻烦的是上下文爆仓之后Agent会开始“忘事”早期对话里交代的约束全部失效生成的代码越来越离谱。这周我把终端输出剪枝这套方案完整落地实测单次任务的平均Token消耗降了六成以上终端输出整体剪枝率稳定在98%上下。这篇文章就聊聊这个思路怎么从原理走到落地给还在被Token账单折磨的人一个可以直接抄的作业。先说清楚一件事终端输出剪枝不是简单地把输出截断它是一套围绕“什么信息值得进上下文”的取舍逻辑。Coding Agent每执行一条命令终端输出就会变成上下文的一部分这些内容会跟着后续所有对话一起被重复计费还会挤占上下文窗口。所以剪枝不只是省当下这一笔Token更是在保护后续步骤的执行质量。1. 先搞清楚钱去哪了Coding Agent的Token消费特征1.1 Token刺客的三种典型作案方式我观察过不少Coding Agent的使用记录Token消耗异常基本逃不出下面三种情况。第一种是终端输出原样进上下文。Agent执行npm run build终端刷出几千行webpack编译日志这些日志从Module not found到Compilation success全都被塞进上下文。更离谱的是git diff一次大变更的输出可能上万行Agent可能只需要其中几处关键函数的修改但整份diff全进去了。第二种是失败重试的滚雪球效应。Agent执行命令失败后会把错误堆栈完整带回对话然后尝试修复。第一次修复后又失败新的错误堆栈又叠加进来。三次重试之后光错误堆栈就占了几千Token这还没算每次重试生成的修复代码和重复执行的输出。我见过最夸张的一次Agent为了解决一个路由配置错误重试了7次上下文里堆了接近3万Token的错误日志最终还因为上下文太长导致后续指令执行错乱。第三种是多轮对话的累积计费。Coding Agent的任务通常要经历多个步骤分析需求、读取文件、修改代码、运行测试、再次修复。每一步的终端输出、Agent的工具调用结果、历史对话记录都会在后续每一次请求中重新计费。也就是说一个步骤的输出不只是消耗一次Token而是会在之后的每一次请求里反复消耗。这就是为什么很多团队明明任务数量不多Token账单却高得吓人。以我手头一个真实项目为例框架是NestJS TypeScript每次npm run build终端输出大约12000到15000 Token一次完整的修复任务平均要执行5到8次构建光构建日志就吃掉接近10万Token。这还只是构建如果把测试输出、git diff、日志查询加进来单任务Token消耗超过20万很正常。而上面这些场景里真正对Agent决策有用的信息往往不超过总输出的2%。这就给剪枝留下了巨大的空间。1.2 上下文爆仓不是“慢一点”那么简单很多人以为上下文爆仓只是让Agent变慢、变贵实际上它的危害远不止如此。模型在上下文长度逼近上限时注意力会被大量无关信息稀释出现一个非常典型的现象早期指令被“遗忘”。举个例子你最初告诉Agent“项目使用pnpm不要使用npm”Agent在执行前几个步骤时遵守得很好。但当成千上万的终端输出堆进上下文之后模型会逐渐把注意力集中到最新的内容上早期约束的权重被稀释最后它可能无意识地调用了npm命令。我遇到过的实际情况是Agent在一个长任务后期报出“Im sorry, I seem to have lost the context about yarn restrictions”——这已经不是工作失误而是上下文管理失控的结果。还有一层隐蔽的坑是大模型超出它训练时见过的上下文长度输出质量会断崖式下降。术语上叫“超出有效上下文范围”表现就是胡言乱语、重复内容、格式错乱。这个问题在很多开源模型上尤其明显号称128K上下文实际上超过32K就已经明显劣化。也就是说你为了增加上下文容量花了更多Token结果模型的输出质量反而更差这是一个双重惩罚。所以我们真正要做的是把宝贵的上下文窗口留给最有价值的信息。上下文工程的核心不是提示词写得有多花哨而是搞清楚“什么信息可以不进上下文”。提示词工程解决的是“话怎么说”上下文工程解决的是“什么信息配被记住”终端输出剪枝就是上下文工程里最关键的一环。2. 核心原理终端输出剪枝到底在剪什么2.1 剪枝的四个维度终端输出剪枝不是一个单一操作我把它拆成四个可以独立实施的维度。第一个维度是长度控制这是最简单的。给输出设置一个行数或Token数的天花板超过的部分直接截断。但这个做法容易误伤如果错误恰好出现在截断点之后Agent就会完全看不到关键信息。所以长度控制只能作为兜底不能作为唯一手段。第二个维度是内容过滤这才是真正的核心。要对终端输出做语义级别的筛选只保留有信息量的内容。不同场景的筛选逻辑完全不同构建日志要看ERROR和WARNING测试输出要看FAILED和AssertionErrorgit diff要看文件路径和变更行日志查询要看Exception和Traceback。内容过滤的逻辑写好了一个上万Token的输出能被压缩到几百Token且不丢失任何决策所需的关键信息。第三个维度是格式清洗。终端输出里有大量ANSI颜色码、制表符、重复的分隔线这些内容对模型理解毫无帮助却会白白消耗Token。清洗ANSI转义序列是最常见的一项尤其是带颜色输出的构建工具和测试框架这些转义码每个都要占用若干Token。第四个维度是语义压缩。用大模型对输出做摘要把一大段报错压缩成两行提炼后的要点。这个维度最强大但也最需要谨慎使用因为摘要本身也会消耗Token而且模型可能会在摘要过程中丢失细节。我的一般做法是前三个维度能解决的场景绝不动用第四个维度只有当前三个维度处理完后内容仍然过长时才考虑用摘要。2.2 98%剪枝率的目标设定先定义清楚剪枝率剪枝率 (原始Token数 - 剪后Token数) / 原始Token数 × 100%。98%意味着一个原始消耗10000 Token的输出剪完后只剩200 Token。这个目标激进吗一开始我也觉得激进但实测下来确实可以做到原因是终端输出里绝大多数信息本身就是冗余的。以最常见的构建场景为例一次NestJS构建输出里90%以上的内容是webpack的编译进度、模块打包清单、静态资源列表这些信息Agent根本用不到。真正有用的是最后的编译结果、错误代码和错误文件位置。测试场景也一样几十个测试用例的输出里只有失败的几个用例名字和断言信息是有价值的。拿一个真实数据做参考。某次项目跑npm run build原始输出刷了18460 Token其中大部分是webpack的编译日志。经过内容过滤之后只保留了ERROR级别的报错、出错文件路径以及对应的代码片段最后剩下356 Token剪枝率98.1%。这356 Token里包含了错误所在的文件、具体的报错原因、堆栈里关键的几帧Agent拿到这些信息就能做出正确的修复决策和拿到完整输出时的效果没有区别。当然了不是所有场景都能做到98%。比如某些测试失败场景断言信息特别分散剪枝率可能只有90%到95%但依然比不剪强得多。设定98%这个目标更多是为了逼自己把过滤逻辑写得足够精细而不是追求每个场景的数字都好看。3. 实操我落地的一套终端输出剪枝方案3.1 工具链选型有几种方案可以实现终端输出剪枝我先后试过三条不同的路线。第一条路线是完全依赖Coding Agent自带能力。Claude Code有hooks机制可以在命令执行后处理输出Codex也提供了一些输出样式上的配置。这条路线门槛最低不用自己写代码但问题是可定制性差内置的钩子只能做简单的截断没法对输出做语义过滤。当时实测效果不理想剪枝率大概只能做到40%到50%而且配置方式比较固定。第二条路线是自建一个命令包装层把Agent执行的所有终端命令都包装起来先由这个包装层执行真实命令拿到完整输出后做剪枝处理再把剪枝后的结果返回给Agent。这个方案的好处是可定制性极强想怎么剪就怎么剪坏处是需要侵入式地改造Agent的命令执行流程。目前大部分Coding Agent都支持自定义工具或命令前缀所以这个方案是可行的。第三条路线是在Agent内部套提示词要求Agent不要查看完整输出、只关注摘要信息。这个方案实施成本最低但稳定性最差它对模型的遵循能力要求太高。实测下来效果波动极大很难作为一个可靠的生产方案。我最后采用的是第二路线为主、第三条路线为辅的组合方案用命令包装层对终端输出做硬性过滤同时用系统提示词引导Agent依赖过滤后的摘要信息。这样的好处很明显即使提示词约束失效了底层的输出过滤依然是可控的即使过滤逻辑有疏漏提示词也能让Agent更倾向于关注摘要而不是要求查看完整日志。3.2 核心实现终端输出剪枝脚本这里给出一个可复用的Python实现核心逻辑分三步清洗ANSI、按内容过滤、按场景截断。import re import subprocess import sys ANSI_RE re.compile(r\x1b\[[0-9;]*[a-zA-Z]) def strip_ansi(text: str) - str: 去掉ANSI颜色码和转义序列 return ANSI_RE.sub(, text) def filter_build_output(text: str, max_lines: int 80) - str: 构建场景的过滤逻辑 保留ERROR、WARNING级别及附近的内容忽略编译进度和模块列表 lines text.splitlines() important_lines [] error_indices [] for idx, line in enumerate(lines): upper line.upper() if ERROR in upper or FAILED in upper or EXCEPTION in upper: error_indices.append(idx) elif WARNING in upper: error_indices.append(idx) if error_indices: # 保留每个错误点前3行、后12行的上下文 begin max(0, min(error_indices) - 3) end min(len(lines), max(error_indices) 12) important_lines lines[begin:end] else: # 没有错误的情况下默认只保留最后10行 important_lines lines[-10:] if len(important_lines) max_lines: important_lines important_lines[-max_lines:] return \n.join(important_lines) def run_with_pruning(cmd: str, cwd: str None, max_tokens: int 2000) - dict: 执行命令并对输出做剪枝 1. 先取完整输出 2. 清洗ANSI 3. 按场景过滤 4. 如果仍然超过token上限再做摘要 proc subprocess.run( cmd, shellTrue, cwdcwd, capture_outputTrue, textTrue, encodingutf-8, errorsreplace ) full_stdout proc.stdout or full_stderr proc.stderr or cleaned_stdout strip_ansi(full_stdout) cleaned_stderr strip_ansi(full_stderr) filtered_stdout filter_build_output(cleaned_stdout, max_lines80) filtered_stderr filter_build_output(cleaned_stderr, max_lines60) pruned_output f[EXIT_CODE] {proc.returncode}\n if filtered_stderr: pruned_output f[STDERR]\n{filtered_stderr}\n if filtered_stdout: pruned_output f[STDOUT]\n{filtered_stdout}\n return { cmd: cmd, returncode: proc.returncode, pruned_output: pruned_output[:max_tokens], full_output_size: len(full_stdout) len(full_stderr), pruned_output_size: len(pruned_output) } if __name__ __main__: _, command sys.argv[0], .join(sys.argv[1:]) result run_with_pruning(command) print(result[pruned_output]) print(f\n[剪枝统计] 原始输出 {result[full_output_size]} 字符剪后 {result[pruned_output_size]} 字符)这段脚本有几点值得说清楚。filter_build_output里的error_indices机制是踩着坑调出来的最开始我用的是“错误行向前向后各取N行”的简单逻辑效果很差因为一个真实报错往往跨越多个层级错误信息分散在不同区块。后来改成“以错误行为锚点整体截取错误点的上下文区间”效果才稳定下来。另外一个坑是打印那一行“[剪枝统计]”会让Agent误以为这是任务的一部分所以实际生产环境建议把统计信息写到日志文件而不是混在输出里。另外注意errorsreplace这个参数终端输出经常夹杂非UTF-8字符特别是Windows环境下的GBK编码不处理的话decode阶段就会抛异常。用errorsreplace保证任何情况下都能拿到可用的字符串。3.3 接入Coding Agent的关键配置剪枝脚本本身只是第一步怎么让Coding Agent用上它才是关键。我用的方式是给Agent定义一个自定义工具把真实的命令执行替换成剪枝后的输出。以Claude Code为例可以通过在settings.json中注册一个自定义的shell包装命令让Agent执行命令时默认调用剪枝脚本。代码库里的配置大致是这样{ customShellExecution: { command: python3 /opt/agent-prune/run.py } }实际配置项因工具而异但思路是统一的把命令执行路径指向你的剪枝层而不是让Agent直接调用bash。除了工具层面的接入提示词也同步做了调整。我在系统提示词里加了一段约束终端输出已经经过压缩过滤[EXIT_CODE]为命令退出码[STDERR]为错误输出[STDOUT]为过滤后的标准输出。优先依赖这些内容做判断。如果输出中没有出现错误信息不要自行猜测存在隐藏错误。不要在未收到明确错误的情况下请求重新执行完整命令。这段提示词的作用是防止Agent做出两个坏行为一个是在没有错误时怀疑脚本剪掉了一半内容非要自己再跑一遍完整命令另一个是在看到剪枝后的摘要后自作主张地推断出错误码。两个坏行为都会把Token成本拉回去。接入之后我做了一组前后对比选取20个真实的修复任务统计每个任务的Token消耗。结果是场景剪枝前平均Token消耗剪枝后平均Token消耗降低幅度构建错误修复5个任务18.6万6.8万63.4%测试失败定位8个任务22.4万8.1万63.8%多文件重构7个任务35.2万13.5万61.6%单看终端输出部分的剪枝率基本稳定在95%到98%。整任务Token消耗降幅在60%以上没有达到终端输出剪枝率那种水平是因为对话历史、代码读取等其他部分的Token没动。但终端输出这一块本来就是大头砍掉之后整体账单下降了六成多这个结果已经非常可观。4. 常见问题与排查技巧实录4.1 剪过分了怎么办关键信息被剪掉最常遇到的问题就是剪枝过度。有一回Agent在处理一个测试失败时反复尝试修复同一个无关的warning我检查剪枝日志才发现测试输出里真正的失败信息被截断了唯独把那个无关的warning留了下来。Agent基于错误的信息做决策自然就绕了远路。排查思路是这样的我给剪枝层加了一个“原始输出存档”功能每次剪完都把原始输出按日期存一份。当Agent行为异常时翻一下存档对比原始输出和剪后输出就能很快定位是不是剪枝逻辑出了问题。那次的问题根源是过滤逻辑里对FAILED关键字的匹配太严格实际用例输出用的是FAILED加数字编号的格式正则没匹配上。解决方法是给过滤逻辑增加优先级规则错误级别的关键字优先于警告级别如果同时存在多个错误关键字取最早出现的错误为锚点而不是简单取所有错误行的并集。另外设置一个保底行数无论过滤逻辑怎么走每条命令的输出至少保留最后20行防止完全丢失信息。4.2 剪枝本身消耗Token怎么办剪枝逻辑本身如果引入大模型做摘要就会产生二次消耗。我在早期版本里对每一条输出都强制跑摘要结果剪枝省下的Token又有一部分被摘要模型吃了回去。后来改成两级结构第一级是正则和规则过滤这部分零成本只有当过滤后的内容依然超过阈值时才用便宜的小模型做摘要。实测下来90%的命令输出在第一级就被处理完了需要动用摘要模型的比例很低。选择摘要模型也有讲究。不要用任务主模型做摘要太贵。我用的是一个参数量很小的开源模型单独部署在本地用GPU跑摘要质量对终端日志这种格式化的文本完全够用。还有一个优化点是加缓存同一个命令的完整输出在短时间内重复出现时直接复用之前的剪枝结果不再重新处理。这个优化在构建场景特别有效因为Agent经常会在修复过程中反复执行同一个构建命令。4.3 其他容易踩的坑把这段时间踩过的坑整理成一张速查表遇到类似问题可以对照着查。问题原因解决方案剪枝后出现乱码ANSI颜色码清洗不彻底部分扩展颜色码格式没覆盖增加\x1b\][0-9;]*[a-zA-Z]和\x1b\][^\x07]*\x07规则Agent反复请求“查看完整输出”提示词没有明确说明输出已经被压缩在系统提示词里显式声明“输出已经过过滤禁止自行重跑命令”exit code为0但实际有warning被忽略过滤逻辑只看错误不看警告对warning做单独标记只在没有error时保留warning输出量特别大但仍超限制过滤后的内容量还是超过上下文预算对输出做分页处理每页固定Token上限Windows系统下编码报错终端输出使用GBK编码Python默认UTF-8解码失败设置encodinggbk或errorsreplaceGit diff场景剪不掉diff的统计信息和文件路径在开头错误信息在结尾对diff场景做专门的解析保留文件路径变更行摘要统计跨平台换行符不一致Windows的\r\n混入输出统一用splitlines()而不是split(\n)还有一个比较隐蔽的问题Agent在输出被剪枝后偶尔请求读取原始日志文件。它会尝试用cat命令打开日志文件拿到完整内容这样前面做的剪枝工作就白费了。我的应对方案是在剪枝层拦截对日志文件的cat和tail命令同样经过剪枝处理后再返回。本质上思路是一致的不管Agent用什么方式读取终端相关的内容这个路径都必须经过剪枝层。4.4 Token相关报错的连带排查说到Token还有一类问题和终端输出剪枝无关但同样让很多人头疼就是登录态的Token失效和续签问题。我在日常使用Coding Agent时遇到过一个典型的报错sign-in could not be completed, token exchange failed这类登录态失效的消息表面上是认证问题实际上如果发生在任务执行到一半的时候也会造成上下文丢失和多次重试间接推高Token消耗。排查这一类问题建议先看本地的凭据缓存文件大部分Agent会把登录态存成一个本地文件里面有缓存的refresh token。如果发现refresh token已经过期就需要重新走登录流程。一个常见的坑是存在多个版本的凭据缓存Agent用的是旧的、已经过期的那个导致反复触发token exchange失败。处理方式是彻底清掉旧的凭据缓存重新登录一遍确保Agent拿到的是最新的refresh token。另外有些Agent用的是JWT格式的token这时候需要检查JWT的exp字段如果剩余时间太短就要配置自动续签的逻辑。这些问题和输出剪枝不是一回事但都属于日常使用Coding Agent时“Token成本失控”的常见诱因。任务执行到一半登录态失效意味着之前消耗的Token全部白费下一轮还要重新来一遍。所以我把登录态的巡检也写进了日常维护脚本里每天开工前检查一次凭据状态避免中途罢工。最后再补一个实战细节还有一个我觉得特别值得分享的配置是关于上下文窗口的超载护栏。尽管我把终端输出剪枝做得很彻底但有时候一个任务自身就会产生很长的对话历史尤其是有大量代码读取的场景。这时候我额外加了一个“窗口翻页”机制当对话历史的Token量超过主模型有效上下文的一半时自动把早期的对话摘要成一段几百Token的要点放进后续上下文的开头。这个逻辑和终端输出剪枝是同一个思路——不是所有的历史信息都值得原样保留。我个人在整个方案落地过程中最大的体会是Coding Agent的Token消耗问题不是一个单点问题它是一整套管理策略。终端输出剪枝能砍掉最大的一块浪费但如果不配合对话历史的摘要压缩、登录态的稳定性维护、提示词层面的行为约束Token账单还是会被其他漏洞悄悄吃掉。这套方案上线之后我们团队的单任务平均Token成本降了一半以上Agent的修复成功率反而还提了几个点因为上下文干净了模型做决策的准确率自然就上来了。如果你也在被Token账单追着跑先从终端输出剪枝入手这是性价比最高的一步。
阅读完成 · 觉得有帮助?
咨询建站