用大半年Claude Code和Codex这类AI编程代理之后,我最深的感受不是它写代码有多快,而是Token烧得有多快。很多人以为Token消耗的大头是对话和代码生成,其实真正吃量的,反而是平时不太注意的“工具输出”。跑一次测试、翻一次日志、列一次文件,成千上万个Token就没了,而且这些Token会留在上下文里,后面每一轮对话都得重新“背着”它们。这篇文章就围绕“优化工具输出减少AI编程代理的Token使用”这个话题,把我踩过的坑、用过的方案、验证过的参数一次性讲清楚。如果你也在为额度焦虑,或者想提高编程代理的使用效率,这篇内容应该能帮你省下不少成本。
1. 工具输出吃Token的真相:为什么你的额度总是不够用
1.1 Token都被悄悄烧在了哪里
先看三类最常见的Token黑洞,它们几乎存在于每个AI编程代理的日常会话里。
第一类是命令执行的返回结果。比如你在项目里跑一个grep -r "TODO" src/,看起来只是一行命令,但输出可能带上几十个文件的匹配内容,每个匹配行又带着绝对路径、缩进和上下文。如果一个文件被匹配到5行,20个文件就是100行输出,轻松几千Token。
第二类是日志和报错堆栈。写代码过程中让代理帮忙排查测试失败,一个完整的Java或Python堆栈可能有三四十层,每层一行类名加行号。在吞吐量大的时候再配几条Error日志,一次工具返回三四千Token很正常。更麻烦的是这些内容语义密度很低,真正有用就一句话“NullPointerException at line 42”,剩下全是噪音。
第三类是文件列表和目录扫描。代理要理解项目结构时经常执行find . -type f或直接看ls -R,遇到node_modules、dist、.next、build这类目录,返回的就是几百上千个路径。路径名之间几乎没有语义信息,但Token量是实打实的。
有人会问:“返回多就多呗,反正只算一次。”这是一个很大的误区。工具输出一旦进入上下文,之后每一次模型推理都要把整段上下文重新读一遍。也就是说,这一条5千Token的工具输出,如果后续你还要让代理继续改代码、继续调用工具,它会在每一轮里至少再被计算一次。如果你一次会话有20轮,那么这条输出的实际成本可能接近2万Token。这才是“工具输出导致Token爆掉”的核心机制。
1.2 一次工具调用的Token账本
我用一个具体场景算一笔账:让代理“检查一下登录接口为什么超时”,假设代理决定先读配置文件,再看路由文件,最后跑一次带详细日志的测试。
- 第一步读取配置文件:命令约150 Token,返回200行配置约2000 Token,随后模型总结约300 Token,共约2450 Token。
- 第二步读取路由文件:命令约120 Token,返回150行代码约1800 Token,模型总结约250 Token,共约2170 Token。
- 第三步跑测试并抓日志:命令约200 Token,返回300行日志约3500 Token,模型分析约500 Token,共约4200 Token。
三个步骤加起来就花了近9000 Token,而真正有价值的中间结论可能只有“超时发生在发送HTTP请求之后”。如果开发者在设计工具输出时做一点控制,比如限制配置文件只返回变更行、日志只返回最近50行里带关键字的行,这三步的Token用量能压到2500左右,节省超过70%。
从成本角度算一下:按市面上常见的编码类模型定价粗估,不同服务商每百万Token价格从几美元到几十美元不等。按相对便宜的价位、3美元/百万Token算,一次会话因为工具输出浪费8000 Token,约合人民币不到两毛钱,看着不多。但一个重度用户一天有20个会话,一个月就白白烧掉一两百块的额度和大量的时间窗口。如果是团队采购、多人同时使用,这就是一笔不小的开销。
1.3 影响范围:不只是钱的问题
Token成本高只是表层,工具输出过长还会带来几个更难察觉的问题。
响应变慢。模型处理超长上下文的时间随Token数近似线性增长,你让代理多看了5000行日志,它每一步思考都要慢两秒。本来30秒能完成的任务,拖到一两分钟,实际体验非常糟糕。
准确性下降。上下文越长,模型越容易在无关信息中迷失,产生幻觉或漏掉关键约束。有同行反馈过,测试日志太多的时候,Claude Code偶尔会“虚构”一个不存在的报错原因,依据就是日志里某些边缘内容。工具输出越精简,结论就越聚焦。
超出输出上限。不少模型有32k、64k或128k的输出Token上限,一旦上下文过长,再叠加一次大段的工具返回,模型会直接中断,出现“claude's response exceeded the 32000 output token maximum”这类报错。这个不是配置问题,而是你把模型“喂太饱”了。
所以,优化工具输出不是扣扣搜搜省几毛钱,而是在提升整个工作流的稳定性、速度和可维护性。
2. 工具输出瘦身的核心设计思路
2.1 最小化原则:够用就行,别给模型带饭
做AI编程代理的工具输出,第一原则是“最小够用”,不是“完整全面”。
你在写一个工具时,先问自己:模型拿到这个结果之后要做什么?如果只是判断文件是否存在,就返回exists: true/false加上文件大小,不用把整个文件内容塞给它。如果只是判断接口是否注册成功,就返回状态码和耗时,不用把HTTP响应全文拿回来。
实操中最见效的一个改动:所有执行类工具默认关闭完整输出,只在显式要求时才开启全量返回。比如命令工具规定:默认返回最后50行,超过部分用“已截断,共342行,如需完整输出请指定--full”来代替。模型很聪明,它看到截断标记一般会主动决定是否需要补全。
还有一个容易忽略的点:错误信息也要裁剪。很多时候工具执行失败,stderr里的原始错误长达上百行,其实就第一行“Permission denied”有用。给工具统一加一个错误处理逻辑,只返回错误类型和核心信息,比如ERROR[3] Permission denied (path: /root/config.yaml),其他堆栈细节作为可选参数再读取。
2.2 结构化优先:让模型少做阅读理解
模型处理结构化的输出远比处理自由文本高效,token消耗也更低。同样是返回10个搜索结果,下面两种格式的Token差很多:
# 低效格式 文件 src/utils/auth.js 的第42行有一个函数 validateToken,这个函数接收一个参数 token,返回一个布尔值,用于校验token是否过期。文件中还有其他内容,包括错误处理和其他工具函数。{"file":"src/utils/auth.js","line":42,"symbol":"validateToken","type":"function","args":["token"],"return":"boolean","summary":"校验token是否过期"}第一种自然语言描述容易让模型反复重读才能提取关键字段,第二种JSON格式模型一眼就能定位。实际中,只要工具能改造成JSON行(JSON Lines)或紧凑的表格形式输出,整体效果都会有明显提升。
但要注意,结构化不代表无脑堆字段。一个检索文件内容的工具返回结果时带上file_path、line_start、line_end、matched_line就够了,没必要把last_modified、owner、permissions这些与当前任务无关的元数据全部带上。
2.3 分层摘要:先给结论,再按需下钻
这一个策略是我自己最推荐的,也是投入产出比最高的一招:把工具输出设计成分层结构。
第一层返回摘要,控制在500 Token以内,只告诉模型“有哪些候选、各自的关键信息是什么”。第二层返回局部详情,比如某个文件的前100行、某个日志区间。第三层才是完整内容,是模型在明确判断“不看完无法继续”时才会去拉的。
以日志分析为例,第一层可以只返回各个级别日志的数量统计和出现异常的位置:
ERROR 12次, WARN 35次, INFO 240次 ERROR常见位置: - src/auth/jwt.ts:78 (5次) - src/services/order.ts:210 (3次)第二层再返回某个具体位置的详细错误信息。第一层最多300 Token,第二层500 Token,第三层才放完整日志。比起一口气把3000行日志全塞给模型,这种设计至少能节约80%的Token。
我自己在自定义工具时一般会留一个开关:detail=summary|local|full。默认是summary,模型如果觉得不够,可以带detail=local再调一次。既保留了工具的灵活性,又控制了默认成本。
3. 实操:高频工具的具体优化方案
3.1 Shell执行类:命令、日志、测试输出
Shell类工具是Token消耗的重灾区,因为它太灵活,任何输出都可能被直接返回。我的做法是把常用命令封装成固定工具,而不是让模型自己裸跑任意shell命令。
输出截断:给每个命令结果设置硬性上限。比如Pipeline里自动追加| head -n 100,日志文件读取一律走tail -n 50,超过就标记截断。这样即使模型写了cat server.log,最终返回的也只是一部分,而不是整个文件。
一个我在Claude Code的hooks配置里实践过的方式,给工具执行加一层包装脚本:
#!/bin/bash output_file=$(mktemp) timeout 30s "$@" > "$output_file" 2>&1 exit_code=$? line_count=$(wc -l < "$output_file") if [ "$line_count" -gt 100 ]; then echo "--- 输出过长,共 ${line_count} 行,仅显示前 100 行 ---" head -n 100 "$output_file" else cat "$output_file" fi exit $exit_code从系统提示词层面也可以约束:要求编程代理优先使用带过滤的查询命令,而不是直接读取整个文件。例如对日志文件只做关键词过滤和尾部截断。
日志级别过滤:排查问题时明确告诉代理:生产环境先看ERROR和WARN,不要一上来就拉INFO和DEBUG。在实际工具设计上,可以为日志文件专门提供--level参数,封装在配置里。
测试输出:跑测试时建议使用--fail-fast,在第一个失败点停下;同时用JUnit/XML格式或精简的-q模式,而不是完整输出。失败时再单独收集失败用例的堆栈,不要所有用例全部打印。
3.2 文件读写类:按需读取,拒绝整读
文件读写是另一个大头。很多代理默认会把整个文件读进上下文,一个500行的组件文件就有近5000 Token,项目中五六个文件一读,上下文就满了。
优化方向很明确:提供支持行号区间的读取工具。让模型先通过grep或符号索引定位目标函数,再按行号片段读取。设计上参考:
{"tool":"read_file_lines","file":"src/api/user.ts","start":120,"end":160,"truncate":true}中间没有输出的行用...占位,避免每行都重复路径前缀。尤其是TypeScript/Java这类带长签名的语言,一个方法动辄占40行,按需读取能省一半。
文件排除配置:直接在编程代理的配置文件里把不重要的目录排除掉。比如.gitignore思路的扩展,单独设置一个扫描黑名单:node_modules、dist、build、.next、coverage、vendor、__pycache__、*.lock。目录树和全局搜索里直接跳过这些目录,避免模型被海量无意义路径淹没。
自动跳过二进制文件:读取工具默认检测文件类型,图片、压缩包、SQLite数据库这些一律不读,只返回binary file, skipped。这一点对防止“模型突然开始分析一张图片”的情况非常有效。
3.3 搜索与检索类:限制数量,精简字段
搜索工具是调优的重点。语义检索和代码搜索工具只要稍微改几个参数,就能从“一次返回100条”变成“一次返回5条高相关结果”。
- 默认
limit设置小一点。全局代码搜索默认10条以内,需要更多再让模型自己加参数。 - 过滤条件前置。指定文件类型,比如只搜
*.ts或*.py,排除测试目录,减少无关命中。 - 结果去重。多个分支文件里常见的同名符号,按文件路径聚合成一条。
- 字段裁剪。检索结果默认只返回文件路径、行号和一行摘要,完整内容留给后续读取工具。
我建议给搜索工具增加top_k参数,默认值3到5。大部分编程任务,模型其实只需要看最相关的三五个位置就能解答问题,给100条反而会增加它判断的负担。
还有一个好用的设计:检索结果里加入score字段。当多个结果都匹配时,模型可以优先看score最高的部分,不需要全部扫描。实测下来,这个字段能明显减少模型在多结果之间犹豫的次数。
3.4 自定义MCP工具:把截断逻辑写进协议
如果你在用MCP(Model Context Protocol)或者自有Agent框架,工具输出优化更是从源头就要做的事情。
一个常见误区是“工具数量尽量多,每个功能一个工具”。实际上每个工具的描述文本也占用系统提示词;工具一多,还没开始干活,几千Token就没了。我把同类型的操作用参数区分合并,比如文件类工具就保留read_file和edit_file两个,搜索类工具统一成一个search_code,描述写的准确且精简,整体Token开销瞬间降下来。
工具返回的内容需要支持“软截断”和“硬截断”。硬截断是超过某个阈值直接切断;软截断是还在运行时就持续发送中间摘要,直到拿到完整结果再合并成最终返回。硬截断适合日志和文件内容,软截断适合流式输出。
下面是我在自定义MCP工具中的一份精简返回逻辑:
def summarize_output(output: str, limit: int = 100) -> str: lines = output.splitlines() if len(lines) <= limit: return output head = lines[:limit] tail_count = len(lines) - limit return "\n".join(head) + f"\n... 已省略 {tail_count} 行,可用 detail=full 获取完整输出"这段逻辑看着简单,但在一个实际项目中帮我把工具输出从平均2500 Token压到了平均600 Token。省略提示本身是关键,模型看到提示后如果确实需要完整内容,会自己决定额外调用一次,不会影响任务准确性。
4. 常见问题与排查技巧实录
4.1 不要把“认证Token报错”和“模型Token消耗”搞混
网上关于Token话题的讨论很杂,最近搜“token”的热词里有一大半其实是登录认证问题,比如token exchange failed: token endpoint returned status 403、sign-in could not be completed、your access token could not be refreshed。这些跟本文讨论的上下文Token完全是两回事。
如果遇到这类报错,排查思路应该是:本地凭证是否过期、当前登录状态是否失效、用户身份提供方(IdP)配置是否正确、时区或时间是否偏差导致JWT校验失败。有一回我本地电脑系统时间差了三分钟,JWT直接校验不过,折腾了半个小时。先把时间同步对、再重新登录一次,绝大多数会话类报错都能解决。
编程代理里出现的“access token could not be refreshed”,一般是因为登出后重新登录时旧凭证还在本地,或凭证存储文件损坏。处理方式是彻底登出、删除本地凭证缓存目录、重新登录。不要把时间和精力浪费在调整工具输出上,那解决不了认证问题。
4.2 “输出Token上限被截断”的处理思路
不少人在用长上下文模型时会遇到“claude's response exceeded the 32000 output token maximum”或“已达到输出Token上限,回答被截断”,然后点击“继续”让模型往下接。这个做法只能救急,不能根治。
出现这类报错的触发条件通常是:上下文太长,模型需要在极长上下文中生成完整回复,或者单次工具返回的内容太大。按“继续”确实可以补全,但每继续一次都要重新计算前面的全部上下文,成本反而更高。
更推荐的做法是:先把当前任务拆小,一次让模型只做一个完整的小模块,而不是让它一口气写800行代码。重点在于控制上下文,可以把已完成的部分保存到临时文件里,然后开一个新会话,把临时文件路径作为上下文传入,清空旧的历史累积。
“输出Token上限”不是配置项,而是模型架构的硬约束。面对它,只能做减法。
4.3 关于“便宜Token”“3亿Token”这类说法的个人判断
热词里还出现了一些类似“zcode 3亿token”“便宜token”“免费token”的说法。我的观点是,应该理性看待。
Token单价便宜,不一定代表总成本低。很可能便宜的是输入Token,但输出Token和工具调用额外计费,或者限制并发、限制模型版本。更关键的是,第三方“Token中转站”类服务要谨慎使用。一旦你的代码、业务数据、私有仓库内容经过别人的平台,安全边界就是最大的问题。对我而言,与其纠结单价,不如先优化自己的用法,把整体消耗降下来;用量降低之后再考虑哪家方案更划算。
如果真的要用第三方Token服务,至少要确认:数据是否加密、是否记录日志、模型输出是否被用于训练、服务商是否有明确的隐私协议。这些比单纯的Token单价重要得多。
4.4 一个很实用的会话管理习惯
工具输出优化完了,还有一个配套动作:定期换新会话。
编程代理的一个缺点是上下文会随着对话轮次不断膨胀,早期工具输出的历史内容即使已经被“遗忘”在业务逻辑里,仍然会被重新计算。所以我在实际操作中会养成两个习惯。
一是每完成一个可验证的功能点(比如测试通过、代码编译通过),就把关键信息写进项目根目录的规范文件,比如Claude Code支持的CLAUDE.md或通用的AGENTS.md,记录当前项目结构、模块边界、命令用法。然后果断开新会话。新会话只需要读取这一份摘要,历史垃圾全部丢掉,Token用量能大幅下降,而且回答质量反而更好。
二是大型重构任务强制拆分成多个阶段,一个阶段一个会话。第一阶段只分析和设计,第二阶段只实现某个具体模块,第三阶段统一跑测试。这样每个会话的上下文都相对干净,工具输出的累积效应也不会太严重。
最后再说一点个人体会
我自己的项目在做了上述优化之后,同一个任务的Token消耗明显下降。原来的长会话前后对比下来,单轮工具调用的Token量下降了大概四成,会话总成本能省将近一半。代价是前期需要花时间设计工具、写截断逻辑、做排除配置,这部分投入是值得的。
优化AI编程代理的Token使用,核心不是“省”,而是“让模型的注意力集中在真正重要的信息上”。工具输出越短,模型越能快速找到关键问题;上下文越干净,回答越稳定。这套方法无论你用的是Claude Code、Codex还是其他编程代理,都适用。如果你也一直在为Token“跑得飞快”而头疼,建议先从工具输出下手,这是性价比最高的一步。