
这次我们看一个非常实操的问题同样的 36 次工具调用能不能把 Token 消耗砍掉一半跑过 AI Coding Agent 的人都有这种经验任务本身不复杂就是让 Agent 读取代码、定位问题、改文件、跑测试、再来一轮。可每次打开账单Token 消耗都比预期高一大截。问题出在哪里大多数时候不是模型输出太多而是输入侧在反复烧 Token工具定义、历史对话、工具结果、文件内容每一轮都在累加。这篇文章不聊概念直接拆一个优化案例36 次 Tool Calls 保持不变通过上下文压缩、工具定义精简、工具结果裁剪和多 Agent 协同把整体 Token 消耗降低约一半。文章会给出消耗结构分析、具体优化策略、代码示例和验证方法适合正在做 AI 编程工具、Agent 框架或接口调优的读者。先放结论如果你的 Coding Agent 单次任务超过 15 次工具调用Token 消耗的重灾区基本都在输入侧。优化输入侧是性价比最高的路径。1. 核心优化目标与速览在展开细节之前先用一张表把这次优化的目标、手段和适用边界说清楚。项目说明优化对象Coding Agent 单次任务中的 Token 消耗核心目标36 次 Tool Calls 保持不变Token 总消耗降低约 50%主要手段工具定义精简、历史消息压缩、工具结果结构化裁剪、多 Agent 摘要复用关键指标输入 Token 总量、输出 Token 总量、平均每次 Tool Call 的 Token 消耗、TPM适用场景代码生成、文件修改、命令执行、多轮调试、批量代码审查不适用场景单次短对话、输出质量严重依赖全文上下文的复杂重构任务主要风险裁剪过度导致模型上下文缺失、工具调用失败、关键信息在手写摘要时失真优化的核心思路只有一个输出侧 Token 尽量少砍输入侧 Token 尽量多压。因为 Coding Agent 的工具调用次数一旦多起来输入 Token 的累积速度远快于输出 Token而且模型输出里必然包含工具调用参数这些内容很难再压缩。2. Coding Agent 的 Token 消耗结构分析要优化 Token先要搞清楚 Token 都花在了哪里。Coding Agent 的每次模型请求输入侧通常由四部分组成系统提示词定义 Agent 的角色、任务目标和行为规范。工具定义模型可用的工具列表、参数 Schema 和说明。历史消息之前所有轮次的用户消息、助手消息和系统消息。工具调用结果每次工具执行后返回的文件内容、命令输出、错误信息。输出侧相对简单包含自然语言回复和工具调用参数。工具调用参数是结构化 JSON比如{tool: read_file, path: src/main.py}长度有限优化空间很小。问题在于输入侧的累积效应。假设一个 Coding Agent 执行一个修复 Bug 的任务过程是读取项目结构调用 3 次工具。读取相关文件调用 8 次工具。修改代码调用 5 次工具。执行测试调用 6 次工具。根据测试结果继续调试再调用 14 次工具。总共 36 次 Tool Calls。每一次工具调用完成后工具结果都要追加到消息列表中。到了第 20 次调用时模型不仅要读当前的工具结果还要重新处理前面 19 轮的完整历史。上下文越来越长后面的每一轮输入 Token 都在膨胀。这里要特别强调一个容易被忽略的点“什么任务消耗的 Tokens 大”这个问题答案往往不是“输出长文本”而是“反复读取大文件”和“多轮工具调用后历史累积”。一次read_file返回一个 2000 行的源码文件如果这个文件内容没有及时处理后面每一轮请求都会重复携带这 2000 行内容。即使只多带 5 轮就相当于白白多读了 5 次文件。所以Coding Agent 的 Token 消耗是超线性增长的。36 次工具调用看起来不多但如果每次工具结果都不做裁剪总输入 Token 会非常可观。3. 36 次 Tool Calls 场景拆解为了把优化点讲清楚这里把 36 次工具调用的典型场景拆开看。假设任务目标是“修复项目中的一个 bug”工具调用可以分成四个阶段。阶段工具调用次数主要动作Token 消耗特征阶段一项目探查3列出目录、读取 README、搜索关键词小文件Token 占用低阶段二定位问题8读取源码文件、查看配置文件、搜索符号高风险区文件内容大阶段三修改代码5编辑文件、创建补丁、格式化输出侧工具调用参数较多阶段四验证与调试20执行测试、查看日志、继续读文件、再次编辑高风险区历史累积严重从这张表可以看出阶段二是第一波 Token 高峰阶段四是第二波高峰而且阶段四的高峰叠加了前面的历史通常是最烧 Token 的地方。再拆细一点一次典型的工具调用循环是模型请求输入系统提示 工具定义 历史消息 最新工具结果 - 模型输出输出工具调用参数 - 执行工具 - 返回工具结果 - 追加到历史消息 - 下一轮模型请求执行 36 次工具调用就意味着上述循环执行 36 次。每一轮输入侧都要处理越来越长的历史。如果第 1 轮输入是 3K Token到第 15 轮可能已经涨到 20K到第 36 轮可能到了 50K 以上。这里的优化方向非常明确把重复携带的内容压下去把不需要的历史摘掉把工具结果控制在小体积内。4. 优化策略一精简系统提示词与工具定义很多 Coding Agent 框架默认会给模型注入一大段系统提示词再加上每个工具的详细描述。工具定义如果不做控制一次请求可能就占掉 2000 到 5000 Token。如果工具数量多比如接入文件操作、命令执行、网络请求、数据库查询等 20 多个工具工具定义就可能接近上万 Token。这个数字很吓人但它会在每一轮请求中重复出现。36 次工具调用如果工具定义是 5000 Token光工具定义就消耗了 18 万 Token。优化方法是两件事工具定义瘦身和按需加载工具。4.1 工具定义瘦身先看一个反例一个文件读取工具的定义{ name: read_file, description: 读取指定路径的文件内容并返回给模型用于查看源代码、配置文件、文档等内容。该工具支持任意文本文件返回内容为文件的原始文本。如果文件过大建议结合 grep 或 head 命令先定位关键内容。, parameters: { type: object, properties: { path: { type: string, description: 要读取的文件路径可以是绝对路径或相对于项目根目录的相对路径 }, offset: { type: integer, description: 从文件的第几行开始读取默认为 0 }, limit: { type: integer, description: 最多读取多少行默认为 200 } }, required: [path] } }这段定义本身不算特别长但描述部分有很多模型不需要的冗余词。精简后{ name: read_file, description: 读取文件path 必填支持 offset 和 limit 控制行数。, parameters: { type: object, properties: { path: { type: string }, offset: { type: integer }, limit: { type: integer } }, required: [path] } }描述从 80 个字压缩到 30 个字参数的 description 全部删除。对于 20 个工具的场景每个工具省几十 Token总共就能省 1000 到 2000 Token。而且模型理解工具用途主要靠 description 和参数名过长的描述反而会干扰分类和选择。4.2 按需加载工具第二个做法是按需加载工具。不需要在一开始就把所有工具都塞给模型而是根据任务阶段动态决定加载哪些工具。def get_tools_for_stage(stage): if stage explore: return [list_dir, read_file, search_symbol] elif stage edit: return [read_file, write_file, apply_patch] elif stage test: return [run_command, read_file] else: return []这里的原则是模型看不到它不需要的工具。阶段一只需要探查类工具就不必让模型看到apply_patch和run_command。等到需要修改代码时再注入编辑类工具。这个策略能把每轮请求中的工具定义压缩到 1000 Token 以内。5. 优化策略二历史消息压缩与上下文窗口管理历史消息是 Coding Agent Token 膨胀的最大来源。36 次工具调用如果每次工具结果平均 1000 Token历史累积就有 36K Token。再加上之前轮次里模型输出的自然语言、用户输入最终可能逼近上下文窗口上限。这里有一套组合拳滑动窗口、摘要折叠和关键消息保留。5.1 滑动窗口最简单的方式是只保留最近 N 轮消息更早的消息直接丢弃。实现方式如下def trim_history(messages, max_messages20): if len(messages) max_messages: return messages return messages[-max_messages:]这个方案的问题是丢弃早期消息后模型可能忘记任务最初的目标。所以通常只用于非常长的会话而且要配合下面的摘要策略一起用。5.2 摘要折叠更稳妥的做法是设置一个阈值超过阈值后将早期消息折叠成一段摘要。摘要保留任务目标、已做的操作、当前状态而不保留每一轮的原始内容。def compress_history(messages, summary, max_messages20): if len(messages) max_messages: return messages, summary current messages[:-max_messages] remaining messages[-max_messages:] new_summary summarize_messages(summary, current) return [{ role: system, content: f【历史摘要】{new_summary} }] remaining, new_summary注意这里summarize_messages本身会调用一次模型消耗 Token。表面上看是增加了开销但如果它能用 500 Token 代替 5000 Token 的早期消息并且这个摘要会在后续多次请求中被反复携带那收益是非常明显的。通常跑 3 到 5 轮之后就能回本。5.3 关键消息保留还有一种更精确的做法标记哪些消息必须保留。比如初始用户指示、用户最后一次反馈、最近一次工具结果这些信息对任务连续性至关重要。其他消息可以压缩或丢弃。def is_critical(message): if message.get(role) user: return True if message.get(critical, False): return True return False def smart_trim(messages, keep_latest10): critical [m for m in messages if is_critical(m)] latest messages[-keep_latest:] merged {id(m): m for m in critical latest} return list(merged.values())实践中最有效的方法是 5.2 和 5.3 的组合用摘要折叠早期内容同时保留初始任务指令和用户最新反馈。6. 优化策略三工具结果的结构化与裁剪工具结果对 Token 的影响往往被低估。一个run_command返回 500 行日志一个read_file返回 800 行代码这些内容会全部塞进上下文。如果工具结果不做裁剪无论你怎么压缩系统提示词和历史消息都挡不住数据灌入。工具结果裁剪有四个原则只保留完成任务所需的关键字段。长输出按行数截断超出部分提示模型使用新的工具继续读取。错误日志和测试输出做摘要而不是原样返回。文件读取尽量用 diff、符号表、函数签名代替完整文件内容。6.1 命令输出截断def format_command_result(stdout, stderr, exit_code, max_lines100): lines stdout.splitlines() if len(lines) max_lines: skipped len(lines) - max_lines stdout \n.join(lines[:max_lines]) stdout f\n... [输出已截断共省略 {skipped} 行] return { stdout: stdout, stderr: stderr[:2000], exit_code: exit_code }截断时要保留最后几行因为测试失败的错误信息通常在末尾。下面是改进版def format_command_result(stdout, stderr, exit_code, max_lines100): lines stdout.splitlines() if len(lines) max_lines: head lines[: int(max_lines * 0.7)] tail lines[-int(max_lines * 0.3):] result \n.join(head) f\n... [省略 {len(lines) - max_lines} 行] ...\n \n.join(tail) else: result stdout return { stdout: result, stderr: stderr[-1500:], exit_code: exit_code }6.2 文件内容按需提取文件读取不要一股脑全读。允许模型先用read_file读前 100 行再根据需求用read_file指定行号继续读。这样既省 Token又能让模型更有效地定位问题。在工具端还可以加一个自动限制当文件超过一定行数时默认只返回文件头部、尾部和关键符号列表并在结果末尾提示模型可以使用read_file_range读取指定区间。def read_file_with_budget(path, max_preview_lines80): with open(path, r) as f: lines f.readlines() if len(lines) max_preview_lines: return .join(lines) head .join(lines[:max_preview_lines]) tail .join(lines[-20:]) return ( f文件共 {len(lines)} 行以下为前 {max_preview_lines} 行预览\n f{head}\n f...\n f文件末尾 20 行\n f{tail} )这样做之后一个 2000 行的文件每次最多只消耗 100 行左右的 Token而不是完整地消耗 2000 行。6.3 测试日志压缩测试执行结果往往会输出大量日志。直接全部塞给模型会让 Token 瞬间飙升。更合理的做法是把测试输出压缩成三条信息通过的测试数、失败的测试数、失败测试的关键错误信息。def summarize_test_result(test_output): # 假设 test_output 是 pytest 或 jest 风格的输出 lines test_output.splitlines() failed_lines [line for line in lines if FAILED in line or ✗ in line] summary f测试输出共 {len(lines)} 行失败相关行 {len(failed_lines)} 行。\n for line in failed_lines[:20]: summary line \n return summary[:1500]这样一套组合下来36 次工具调用中每次工具结果的平均 Token 可以从 1000 压缩到 300 左右。以 36 次调用计算仅这一步就能省下约 2.5 万 Token。7. 优化策略四多 Agent 协同中的 Token 复用与摘要如果你的 Coding Agent 不是单 Agent而是一个多 Agent 协同系统Token 优化还有一层空间。多 Agent 系统里最常见的问题就是每个子 Agent 都携带完整的系统提示词和任务上下文甚至并行任务之间还会重复读取相同的文件。这里的核心原则是子 Agent 向主 Agent 汇报时只返回结果摘要不返回完整工作过程。文件内容、搜索日志、调试过程在子 Agent 内部消化主 Agent 只接收最终状态和需要决策的信息。def run_sub_agent(task, context): # 子 Agent 内部会读取大量文件、执行多次工具调用 raw_result sub_agent_execute(task, context) return { task: task, status: raw_result[status], summary: raw_result[summary], changed_files: raw_result[changed_files], need_decision: raw_result.get(need_decision, False) }如果多个子 Agent 需要读取同一份项目配置可以考虑在子 Agent 之间共享一个只读的全局上下文避免每个子 Agent 都重新读取同文件。但要谨慎处理共享上下文的并发写问题。在多 Agent 协同中Token 优化的效果最明显。假设有三个子 Agent 并行工作每个内部要执行 20 次工具调用如果它们各自独立读取相同的大文件总 Token 就是三份。如果共享上下文总 Token 可以压缩到一份半。不过要注意多 Agent 摘要也可能引入信息丢失。子 Agent 返回的摘要必须包含明确的状态标记任务是否完成、阻塞点是什么、需要什么决策。否则主 Agent 收到含糊摘要后只能重新打开子 Agent 的完整上下文Token 反而浪费。8. 优化效果验证Token 计数、TPM 与成本测算优化做完之后需要一套可复现的验证方法。否则你无法确认 Token 真的减半也无法判断是哪个策略带来了主要收益。8.1 从 API 响应中读取 Token 用量目前主流大模型 API 都会在响应中返回 usage 字段包含输入 Token 数和输出 Token 数。实测时可以直接读取这个字段。import requests response requests.post( https://api.example.com/v1/chat/completions, json{ model: your-coding-agent-model, messages: messages, tools: tools }, timeout120 ) data response.json() usage data.get(usage, {}) print(输入 Token:, usage.get(prompt_tokens)) print(输出 Token:, usage.get(completion_tokens)) print(总 Token:, usage.get(total_tokens))注意不同平台的 API 返回结构可能不一样实际使用时需要按对接平台的文档调整。可以在 Agent 框架中加一个中间层把每次请求的 usage 记录到日志中。8.2 统计每个工具调用的平均消耗单看总量不够还需要按轮次分析。建议在日志中记录每一轮请求的输入 Token、输出 Token、工具名和工具结果长度。import json from datetime import datetime def log_request(stage, tool_name, prompt_tokens, completion_tokens, tool_result_length): entry { timestamp: datetime.now().isoformat(), stage: stage, tool: tool_name, prompt_tokens: prompt_tokens, completion_tokens: completion_tokens, tool_result_length: tool_result_length } with open(token_log.jsonl, a) as f: f.write(json.dumps(entry) \n)跑完同一个任务对比优化前后的日志重点看三个指标累计输入 Token 总量。前 10 次工具调用的平均输入 Token。最后 5 次工具调用的平均输入 Token。前 10 次和最后 5 次的差异能直接反映历史累积效应是否被压缩。如果优化前最后 5 次平均输入 Token 是 30K优化后降到 12K说明历史压缩生效了。8.3 TPM 与请求速率在批量任务场景中还要关注 TPM也就是每分钟 Token 数。TPM 的计算公式是TPM 输入 Token 总量 输出 Token 总量这个指标决定了一个平台在一分钟内能处理多少请求。如果单个请求从 50K Token 压到 25K Token在 TPM 限制不变的前提下每分钟能跑的请求数就翻倍。对需要批量处理代码仓库的团队来说这个收益比单次任务的成本节省更明显。8.4 成本测算模板假设模型按输入和输出分开计费可以做一个简单估算def estimate_cost(input_tokens, output_tokens, input_price_per_1k, output_price_per_1k): cost input_tokens / 1000 * input_price_per_1k cost output_tokens / 1000 * output_price_per_1k return cost实际计算时用平台的价格替换参数即可。需要注意的是不同平台的 Token 计量方式可能不同有的平台会为工具定义和缓存单独计费做成本对比时要站在同一个口径上。9. 常见问题与排查方法优化过程并不是一帆风顺的。实际执行中会遇到几个典型问题这里整理成排查表。问题现象可能原因排查方式解决方案压缩后模型忘记任务目标初始用户指令被过早折叠进摘要检查摘要中是否保留了“原始用户目标”字段将第一轮用户消息标记为 critical强制保留工具调用次数没变但失败率上升工具结果截断太狠模型缺少判断依据对比截断前后的工具调用成功率适当提高截断行数失败日志保留末尾 30%单轮 Token 下降但总 Token 没降摘要本身太频繁摘要消耗抵消了节省统计每次摘要的 Token 消耗降低摘要触发频率由每 5 轮一次改为每 10 轮一次请求报 Token 超限单次工具结果过大检查最近一次工具结果长度对工具结果增加强制截断设置单次结果上限TPM 超限批量任务并发过高或单请求 Token 过大查看平台返回的限流错误码降低并发数或继续压缩输入 Token 来提升单分钟请求量多 Agent 摘要信息缺失子 Agent 返回的摘要缺少状态字段检查子 Agent 返回的 JSON 是否包含 status、need_decision给子 Agent 定义强制返回的摘要 Schema优化后代码修改质量下降裁剪掉的文件内容中包含关键逻辑对比优化前后修改的文件 diff文件读取按需加载保留函数签名区工具定义少了但模型选错工具删除 description 后模型无法区分相似工具检查工具名称是否语义清晰保留一句一句话的差异化描述10. 最佳实践与总结回到最初的问题36 次 Tool Calls 不变Token 消耗却减半到底做对了什么从整个优化路径看收益最大的三个动作是工具结果裁剪、历史消息压缩和工具定义按需加载。这三个动作都在削减“每一轮请求都会重复携带”的内容因此收益会随着工具调用次数放大。分阶段落地的顺序可以这样安排第一阶段先做工具结果裁剪。如果你的 Agent 现在跑 36 次工具调用很多工具结果都是原样返回这一步的收益最大改动也最小。第二阶段再做历史消息压缩。加一个摘要折叠逻辑并保留初始化用户指令和最近工具结果。第三阶段精简系统提示词和工具定义。删掉冗余描述改成按任务阶段动态注入工具。第四阶段如果还觉得 Token 消耗不够低再考虑多 Agent 协同中的摘要复用和共享上下文。最后提醒一下安全与合规边界Coding Agent 可能会读取源码、修改文件、执行命令在真实项目中上线前必须做好权限控制限定可访问目录和可执行命令。涉及企业内部代码、用户数据或个人信息的任务要确认数据流向符合隐私与版权要求。压缩和摘要过程中如果调用的是云端模型也要确认数据是否出了本机。Token 优化不是一个一次性工程。每换一个模型、每增加一个新工具都可能改变 Token 的消耗结构。把 usage 日志、TPM 统计和成本估算脚本沉淀到项目里后续每次迭代都能用数据说话。这篇文章的核心就一句话Coding Agent 的 Token 消耗大头在输入侧而输入侧的大头在重复携带。把重复携带的内容压下去36 次工具调用不变Token 减半完全可行。