Coding Agent 实际用起来最容易被低估的成本是 Token。尤其是工具调用Tool Calls一多输入侧会把工具定义、历史消息、中间输出全部塞给模型很快你会发现任务没做几次账单先涨了。这篇文章要讨论的问题很具体同一个编码任务同样是 36 次工具调用如何把 Token 消耗降到原来的一半左右。文章会围绕工具调用链、上下文窗口、多 Agent 协同、批量任务编排这几个环节展开给出可落地的优化手段而不是空谈“少调用几次工具”。1. 先搞清楚36 次 Tool Calls 的 Token 是怎么被烧掉的在优化之前先看一个典型的 Coding Agent 循环。用户输入需求后Agent 通常是这样工作的把系统提示词、工具定义、历史消息拼成一次请求。模型返回一段思考或一个工具调用请求。程序执行工具拿到返回结果。把“上一轮模型输出 工具返回结果”继续拼进下一次请求。反复执行直到任务完成。也就是说整个过程中有两个放大因素最容易忽略工具定义重复计入。每轮请求都会带上全部工具的 schema 和描述。工具越多description 写得越啰嗦每轮重复计的 token 就越多。历史结果重复重放。第 10 次调用时前 9 次工具调用的完整输入输出全部还在上下文里。第 30 次调用时前 29 次的结果全部都要重新计算注意力。所以“同样的 36 次 Tool Calls”并不等于 36 份独立计算它实际是一次比一次重的上下文累积。很多实际项目的 token 消耗大头并不是模型输出而是输入侧反复携带的历史和工具声明。这也是为什么很多编码智能体跑完一个稍复杂的任务Token 消耗会远高于预期。有一个简单的估算方式一次工具调用的真实开销约等于“本轮完整输入 本轮模型输出”而完整输入约等于“系统提示词 所有工具定义 全部历史消息”。只要历史消息里塞了几次大文件读取或长编译日志后面的每一轮都会背上这笔账。2. 核心优化思路速览先给一张总览表方便后面对照检查。优化不是只改一个地方而是把链路里所有“重复计算”的部分都压一压。优化环节做什么影响模块预期效果工具定义瘦身精简 description收紧字段按需挂载工具输入侧每轮固定开销减少每轮基础 token工具返回结果压缩截断、摘要、按需字段返回历史消息膨胀速度减少后续所有轮次的重放开销上下文窗口管理滑动窗口、定期摘要、裁剪历史输入侧历史增长控制上下文线性膨胀任务编排优化合并同类调用、结果缓存、失败重试隔离重复执行次数降低无效 Tool Calls多 Agent 协同子任务上下文隔离只传结构化结果Agent 间通信量避免多路历史互相污染量化监控记录输入/输出 token监控 TPM全链路让优化可验证后面每一节都对应这张表的一行。3. 工具定义瘦身让每轮请求的“固定开销”降下来Coding Agent 的输入侧每轮都要把工具 schema 发送给模型。如果工具定义写得像接口文档全文每个工具描述一两百字挂上十几个工具每轮光是工具定义就几千 token。36 次调用下来这部分会被重复计算 36 次。工具定义瘦身有几个常用手段。第一description 只写必要信息。不要写“该工具用于读取指定路径的文本文件支持 UTF-8 编码支持 Windows、macOS、Linux 跨平台操作文件过大时建议使用 read_file_segment……”这种描述。直接写“读取文件指定行范围”就够了。模型真的需要更多信息时让它先调用工具看返回结果。第二用 JSON Schema 指定 required 字段并给每个字段加上合理约束。这样模型不会乱传参数也不需要在 description 里解释太多。{ type: function, function: { name: read_file_range, description: 读取文件指定行范围, parameters: { type: object, properties: { path: { type: string, description: 文件绝对路径 }, start_line: { type: integer, minimum: 1 }, end_line: { type: integer } }, required: [path, start_line, end_line] } } }第三也是最容易被忽略的一点不是所有工具都必须挂在每个 Agent 上。如果当前任务只涉及代码搜索就不要把“执行 Shell 命令”“修改数据库”“发布部署”这些工具全塞进请求。工具按任务场景动态加载能显著降低每轮的固定 token 开销。4. 工具返回结果压缩别把完整日志塞进历史工具定义瘦身节省的是“每轮固定开销”而工具返回结果决定的是“历史膨胀速度”。很多 Agent 的 Token 爆炸根源是工具把所有完整结果都返回了。比如读取一个 2000 行的文件Agent 第一次只需要了解结构却把 2000 行全部塞进上下文到了第 20 次调用时这 2000 行还在历史里反复参与计算。可以做一个统一的返回结果过滤器。原则很简单先给摘要再按需读细节。def compact_file_result(content: str, max_lines: int 60) - str: lines content.splitlines() if len(lines) max_lines: return content head lines[: max_lines // 2] tail lines[-max_lines // 2:] summary \n.join(head) summary f\n... (共 {len(lines)} 行已截断) ...\n summary \n.join(tail) return summary同样的逻辑可以扩展到其他工具返回运行测试时不要返回完整日志只返回失败用例列表和关键错误片段。执行 Shell 命令时限制返回字节数。比如只保留前 2000 个字符。查看目录结构时只返回文件和目录名不要返回文件内容。搜索代码时返回“文件路径 行号 匹配行”不要返回整个文件。另外可以对返回结果做字段过滤。工具返回的 JSON 里如果含有很多元数据、时间戳、无用字段在进入下一轮上下文之前先剥掉。这样历史里的每一条都更“轻”。5. 上下文窗口管理控制历史消息的线性膨胀就算工具返回都被压缩了36 次工具调用依然会产生 36 组以上的消息片段。如果编码任务很长中间还穿插多次测试、报错、修复历史消息会持续增长。这里建议做一个三层结构第一层精炼系统提示词。System prompt 里不要写大段角色设定和空泛原则可以保留“你是一个编码助手任务完成后给出简洁总结”这类简明信息。更复杂的行为约束可以放在需要时再给。第二层滑动窗口。大多数编码 Agent 其实不需要看到全部历史。较早的“读取文件”“查看目录”这类过程性调用在它们失效之后就可以从历史里移除。只保留最近若干轮对话加上任务目标和当前文件状态。def trim_history(messages, max_messages20): # 保留 system 消息 最近 max_messages 条消息 system_messages [m for m in messages if m[role] system] recent_messages messages[-max_messages:] return system_messages recent_messages第三层定期摘要。对于超过窗口的长任务可以在每个子任务完成后把前面的结果压缩成一段结构化摘要比如“已完成修改了 auth.py 的登录函数通过了 3 个单元测试失败集成测试中 redis 连接超时”。后续只保留这个摘要不保留原始完整记录。这里要注意一个常见误区上下文窗口上限高不等于可以一直往上堆。即使模型能接受很大的上下文长度越长单次请求的处理时间和费用也越高。而且很多 API 服务会有吞吐限制长上下文请求会导致单次耗时增加进而影响整个编码任务的完成时间。6. 针对 36 次 Tool Calls 的编排优化合并、缓存、隔离回到标题里的具体场景36 次 Tool Calls 不变怎么把 Token 减半不改变调用次数就只能降低每次调用的“上下文成本”。这里有三个很有效的做法。第一合并同类读取。Coding Agent 经常出现“先读文件 A、再读文件 A 的另一个片段、再读文件 A 的第三个片段”这种模式。三次调用会产生三条独立历史记录而它们其实可以合并成“一次读取文件 A 的关键部分”。在 Agent 调用工具之前可以加一层简单的工具调用合并检查如果连续两个调用目标是同一个文件并且只是行号范围不同就考虑合并成一次范围更大的读取。第二结果缓存。同一个文件、同一条命令如果在短时间内被多次调用可以直接复用上一次的结果而不是重新执行并重新写入历史。这样能减少重复的工具返回内容进入上下文。tool_cache {} def cached_tool_call(tool_name: str, params: dict): cache_key f{tool_name}:{json.dumps(params, sort_keysTrue)} if cache_key in tool_cache: return tool_cache[cache_key], True # True 表示命中缓存 result execute_tool(tool_name, params) tool_cache[cache_key] result return result, False第三失败重试隔离。Coding Agent 跑测试时一条命令失败了Agent 往往会带着完整历史重新尝试。如果前 30 次工具调用的历史都在那么第 31 次重试时失败的那条命令连同前面 30 次历史一起被重放成本非常高。更合理的做法是重试时把“当前任务目标 失败摘要 需要尝试的新方案”单独组成一个精简上下文而不是把全部历史原样带过去。7. 多 Agent 协同时的 Token 控制上下文隔离只传结论现在很多编码智能体已经不只是单个 Agent 在跑而是由一个主控 Agent 拆任务分给多个子 Agent。任务规划、代码生成、测试执行、代码评审各由一个 Agent 负责最后汇总结果。这种“多 Agent 协同”确实能提升任务并行度但如果实现不好Token 消耗反而会加倍。最常见的坑是主 Agent 把完整历史广播给所有子 Agent子 Agent 之间又把各自的完整记录传回主控。结果每个角色都拿到一份完整上下文Token 消耗成倍放大。优化原则是上下文隔离主控 Agent 只负责任务拆解和结果汇总不保留每个子 Agent 的完整过程历史。子 Agent 只接收完成子任务所需的最小信息文件路径、目标、约束条件。子 Agent 返回结果时优先返回结构化摘要而不是堆砌原始日志。不要在子 Agent 之间直接共享上下文所有通信都通过主控透传“必要字段”。举个例子测试执行子 Agent 完成了测试它应该返回“通过用例 3 个、失败用例 2 个、失败摘要如下”而不是把完整测试日志全部返回给主控。主控如果需要分析失败原因再单独调用一次日志读取这样比全部返回更省。def summarize_test_result(test_output: dict) - dict: return { passed: test_output.get(passed, []), failed: [ { test_name: case[name], error: case[error][:500] } for case in test_output.get(failed, []) ], summary: f{len(test_output.get(passed, []))} passed, {len(test_output.get(failed, []))} failed }这种结构本质上是用“信息筛选”替代“信息搬运”。8. 量化验证怎么确认 Token 真的降了所有优化必须可量化否则就是感觉。做 Coding Agent 优化时建议在调用链路里埋一个 token 计数器。无论是使用模型服务商返回的 usage 字段还是在本地做一次估算都要让每一步 tool call 的 token 消耗可查询。这里给一个建议的最小统计结构class TokenTracker: def __init__(self): self.total_input 0 self.total_output 0 self.tool_calls 0 def record_request(self, input_tokens: int, output_tokens: int, is_tool_call: bool False): self.total_input input_tokens self.total_output output_tokens if is_tool_call: self.tool_calls 1 property def total_tokens(self): return self.total_input self.total_output def report(self): return { input_tokens: self.total_input, output_tokens: self.total_output, total_tokens: self.total_tokens, tool_calls: self.tool_calls, }如果服务端没有返回 usage可以用一个粗略估算函数做“相对优化前后对比”不要求绝对精确只要求口径一致def estimate_tokens(text: str) - int: # 粗略估算实际需以模型服务商返回为准 # 英文约 4 字符/token中文约 1.5 字符/token total_chars len(text) return max(1, int(total_chars / 3.5))还有两个指标需要特别关注输入 token 与输出 token 的占比。如果输入 token 占比超过 80%说明大部分消耗在“重新读取历史”上优先做上下文压缩和工具返回压缩。TPMtokens per minute。TPM 等于“输入 token 输出 token”的总和除以时间。它不只是一个成本指标也是一个性能指标。在编码智能体场景下TPM 太低说明上下文重放太重TPM 接近限流阈值又说明并发或单轮请求体过大需要拆分任务。优化目标应该这样设定在相同任务和相同工具调用次数下对比优化前后的 total_tokens。如果从 100 降到 50那就是达到了“Half the tokens”的效果。9. 常见问题与排查方法优化 Coding Agent 的过程中下面几个问题出现的频率最高。问题现象可能原因排查方式解决方案单轮请求体非常大工具定义过多或 description 过长打印请求体查看输入 token 构成精简工具 schema按任务动态挂载历史消息膨胀很快工具返回了完整文件或完整日志查看历史消息里每一条的字符数统一做返回结果压缩和字段过滤36 次调用后 token 成倍增长历史从不清洗早期调用一直重复参与计算记录每轮 input token 的变化增加滑动窗口和定期摘要重试时 token 消耗异常高失败重试把全部历史原样重放查看重试请求的请求体重试时只保留任务目标和失败摘要多 Agent 协同后总消耗翻倍子 Agent 之间共享了完整历史检查各 Agent 的上下文边界上下文隔离只传结构化结果请求频繁被限流单轮输入 token 过大或并发过高观察 TPM 指标拆分长上下文降低单任务并发10. 最佳实践与总结把“同样的 36 次 Tool CallsToken 消耗减半”当成一个工程目标来推进核心不是压缩模型输出而是降低输入侧的重复计算。通常按照这个顺序做收益最大先量化在链路里埋好 token 统计记录 36 次调用前后的 total_tokens。再压缩工具返回这是见效最快的一步尤其是有文件读取、日志查询、命令执行的编码任务。然后瘦身工具定义去掉啰嗦描述按任务动态加载工具。接着做上下文窗口管理对长任务增加滑动窗口和摘要机制。最后优化多 Agent 协同上下文隔离子 Agent 只返回结论和必要数据。最容易踩的坑有两个。一个是只压缩了当前轮的请求却没有意识到历史里已经积压了大量早期工具返回另一个是重试逻辑不加隔离一旦失败就带着全部上下文重新跑一遍。这两个坑改完Token 往往能肉眼可见地下降。后续如果继续深入可以往这几个方向扩展对不同难度的任务动态调整工具返回长度、把静态工具定义改成按任务生成的最小工具集、在批量任务里对相同文件读取做全局缓存。每做一步都以 token 统计结果为准不要凭感觉判断。建议先保存一份功能正常、上下文精简的最小可运行配置再逐步叠加优化策略。这样即使某个环节改出问题也能快速回退而不是在优化过程中把编码能力一起优化没了。