
在实际编码代理coding agent项目里工具调用是最容易消耗 token 的环节。一个中等难度的代码修复任务可能只触发 36 次工具调用但这 36 次往返会让 prompt 侧 token 不断累积最终账单比预想中高出近一倍。很多团队一开始以为是模型输出太长真正统计 usage 后才发现大头全在每次工具调用前都要重新发送的那段上下文里。这篇文章要解决的是同一类问题当工具调用次数已经固定、无法从业务层面继续减少时如何通过压缩工具定义、精简工具返回结果、合理使用上下文缓存和并行工具调用让总 token 消耗接近原来的一半。文中会先讲清楚 token 在工具调用链路上如何累积再给出可落地的优化步骤、可复现的测算方法以及生产环境里常见的踩坑点。1. 编码代理的工具调用循环为什么 tokens 总是悄悄增加1.1 一次工具调用发生了什么编码代理和普通聊天机器人的最大区别是它需要反复执行工具把执行结果重新交回大模型再决定下一步动作。通用流程如下用户输入任务描述。编码代理把系统提示词、历史消息、工具定义和当前任务组装成请求发送给大模型。大模型返回一段文本或一个结构化的工具调用请求。执行器运行这个工具例如读取文件、执行命令、搜索代码。工具执行结果作为新的消息追加到对话历史中。代理带着更新后的历史再次发送请求直到任务完成。这里的第 6 步是关键每次发起新的请求都要带上之前所有消息。假设一个任务需要 36 次工具调用那么从第 36 次请求的角度看它携带的上下文包含了前 35 次工具调用的输入、输出和大模型中间回复。虽然单个工具结果可能只有几百 token累计到第 36 次就是几万 token。1.2 token 消耗的四个去向要优化先要弄清楚 token 花在哪里。一次工具调用请求的 token 通常包含四个部分去向说明典型特征系统提示词代理角色、行为规范、输出格式固定不变但每次请求都会出现工具定义schema、描述、参数说明固定不变但可能非常长历史消息用户输入、模型回复、工具结果随轮次递增增长最快模型输出推理内容、最终回复、工具调用参数每次轮次独立产生很多团队只关注模型输出也就是 completion_tokens忽略了 prompt_tokens 里反复出现的系统提示词和工具定义。当一个任务需要多次工具调用时prompt_tokens 会成倍放大。1.3 为什么同样 36 次调用消耗可以差一半“同样的 36 次工具调用”并不是说模型输出完全相同。差距主要来自上下文管理策略一种做法是每轮都发送完整的历史记录包括第一次读取到的整份源代码文件。另一种做法是只保留当前任务相关的代码片段、已完成的总结和待办列表。这两者的 token 差可以非常悬殊。工具调用次数相同不代表上下文大小相同。优化的本质是让每一轮请求携带的信息量尽量逼近“当前任务真正需要的最小集合”而不是把已经完成的工作反复重新读一遍。2. 先建立可观测的令牌账本不要凭感觉优化2.1 把每次调用的 usage 记录下来优化 token 消耗之前必须先能准确看到每一轮请求的 token 使用量。大多数大模型 API 会在响应里返回 usage 字段例如{ id: chatcmpl_example, usage: { prompt_tokens: 28421, completion_tokens: 1532, total_tokens: 29953 } }在代理框架里不能只记录最终一次响应的 usage而要按 trace_id 把每一次工具调用的 usage 都写到结构化日志中。建议至少记录以下字段{ trace_id: task_20260801_001, turn_index: 8, tool_count_so_far: 8, model: coding-agent-model, prompt_tokens: 28421, completion_tokens: 1532, total_tokens: 29953, cached_tokens: 0, tool_name: read_file, event_time: 2026-08-01T10:00:00Z }有了这个日志表才能回答“36 次工具调用到底花了多少 token”和“每次增长主要来自哪里”这两个问题。2.2 用累计曲线定位增长源把每轮的 prompt_tokens 画成折线图会看到两种典型形态prompt_tokens 线性快速增长说明历史消息没有做摘要或裁剪每轮都在累积。prompt_tokens 在前几轮暴增后不再下降说明某次工具返回了一大段内容后续每一轮都要重新携带。优化前把 36 次工具调用的 usage 日志导出按以下维度汇总汇总维度作用每轮 prompt_tokens判断增长速度每轮 completion_tokens判断模型推理开销单次工具返回 token 数找出超大返回系统提示词长度判断静态部分是否可压缩工具定义长度判断 schema 是否冗余2.3 建立基线后再动手基线记录建议包含以下信息总工具调用次数例如 36 次。总 prompt_tokens 和总 completion_tokens。每次工具调用的平均 prompt_tokens。top 5 超长工具返回。基线不需要覆盖几百个任务选 10 个有代表性的任务即可。优化后的每一次改动都用同一批任务重新测算。只有同一批任务的前后对比才能证明优化确实起效。注意比较 token 消耗时要让模型参数、温度、top_p 保持一致。模型版本变化会导致 token 统计失真。3. 第一刀压缩系统提示词与工具定义静态内容是最容易省的部分3.1 系统提示词不是越长越好很多编码代理把大量行为规范写在系统提示词里例如“你是资深工程师”、“先阅读文件再提出方案”、“不要删除用户代码”、“输出要简洁”。这些内容不是完全没用但每轮都会重复计费。系统提示词建议分成两层核心身份和绝对红线保留较短版本。可以动态注入的规则只在需要时拼接到当前请求。例如把一个 1200 token 的系统提示词压缩到 400 token 是可行的。压缩的原则是去掉形容词、合并同类规则、删除示例中的长代码只保留判断条件。3.2 工具定义的常见冗余工具定义是 prompt_tokens 里的另一大块。一个工具定义通常包含 name、description、parameters其中 description 很容易被写成一大段话。示例原始定义{ name: read_file, description: Read the content of a file in the repository. This tool is used to read source code, configuration files, markdown documents, log files, and any other text-based file. The file path should be relative to the project root. If the file does not exist, an error will be returned. You should only read files that are relevant to the current task, and avoid reading large binary files., parameters: { type: object, properties: { file_path: { type: string, description: The absolute or relative path to the file to be read. } }, required: [file_path] } }这段定义大约 120 token。如果代理注册了 10 个工具每轮光是工具定义就是 1200 token。36 轮下来就是 43200 token 的固定开销而且在 36 轮中一模一样地重复出现。压缩后的定义{ name: read_file, description: Read a text file by path., parameters: { type: object, properties: { file_path: { type: string } }, required: [file_path] } }只有 30 token 左右。省掉的描述性文字不会影响模型理解工具用途因为有经验的模型可以通过工具名和参数结构推断语义。3.3 工具描述与参数注释的取舍工具描述中的“should only read files that are relevant”这类行为约束更适合放在系统提示词或执行前的规则过滤器里而不是放在每个工具描述里。系统提示词只需要声明一次工具描述则要在每个工具上重复。推荐做法name 用动词加名词的明确格式例如read_file、search_symbol。description 控制在 10 到 20 个英文单词或一句中文。参数只保留类型、必填、简短说明。是否允许修改文件、是否限制路径范围等约束放到执行层校验不放进大模型可见的工具定义。3.4 使用 JSON Schema 的紧凑写法如果工具框架支持 JSON Schema可以进一步压缩{ name: patch_file, description: Apply a diff patch to a file., parameters: { type: object, properties: { path: {type: string}, patch: {type: string} }, required: [path, patch] } }这里要避免把工具的输出示例、常见错误信息、权限说明全部塞进 description。这些内容对模型理解工具没有帮助反而会占用每一轮的 prompt 额度。4. 第二刀工具返回结果瘦身避免每个循环都反复携带整份文件4.1 读全文件为什么最贵编码代理常见的工具调用是read_file。如果每次读取都返回整个文件内容文件越大后续每一轮请求携带的历史消息就越重。假设一个文件有 2000 行约 15000 token。代理第一次读取它需要 15000 token 写入历史。之后如果代理又调用了 5 次工具每次请求都带着这 15000 token光这个文件就会在 6 轮中产生 90000 token。这明显不合理。优化思路有两个方向让工具只返回文件的相关片段。让历史消息在后续轮次中不再携带完整文件内容。4.2 在调用前做预取和裁剪如果代理需要先了解项目结构可以提供一个search_symbol或grep工具而不是直接读取整个文件。示例search_symbol(class UserService)工具内部执行类似 grep 的操作只返回匹配的符号和所在行而不是整个文件内容。这样一次返回可能只有 200 token。当确实需要读取某个文件时先按行号范围读取而不是一次性读取全量{ name: read_file_lines, description: Read a line range of a file., parameters: { type: object, properties: { path: {type: string}, start_line: {type: integer}, end_line: {type: integer} }, required: [path, start_line, end_line] } }模型可以先用 grep 定位函数位置再只读取该函数对应的小范围行号。这个模式在很多编码代理中非常有效。4.3 把工具返回结果压缩成摘要工具执行结果也可能来自命令输出例如npm test的 500 行日志。原始日志不应该直接全部进入上下文。推荐做法是在工具执行器和模型之间加一层“结果处理器”def compact_test_output(raw_output: str, max_tokens: int 600) - str: lines raw_output.splitlines() if total_tokens(raw_output) max_tokens: return raw_output failures [line for line in lines if FAIL in line or Error in line] summary_tail lines[-20:] return \n.join(failures [..., *summary_tail])这个处理器可以放在代理框架的 tool executor 里。模型只看到失败摘要和最后若干行既保留了排错所需的信息又避免把 500 行日志重复发送 10 轮。4.4 对历史消息做分层裁剪当工具调用轮次很多时早期轮次的完整内容往往已不再重要。可以设定规则保留最近 N 轮的原始内容。更早的轮次转成总结文本。已经被应用或验证过的补丁不再保留原始 diff。示例消息结构messages [ 系统提示词, 用户原始任务, turn 1-10 总结: 读取了 config.py发现端口配置在 environment.py已修改 redis 连接参数..., 最近 3 轮原始消息... ]这样既保留任务的连续性又大幅压缩 prompt_tokens。5. 第三刀合并工具调用用更少的轮次完成同样的工作5.1 并行工具调用减少往返开销部分大模型接口支持一个响应里包含多个 tool call。例如代理需要读取三个文件可以一次返回三个read_file请求而不是三次循环。支持并行的请求示例{ tool_calls: [ {id: call_a, type: function, function: {name: read_file, arguments: {\path\:\a.py\}}}, {id: call_b, type: function, function: {name: read_file, arguments: {\path\:\b.py\}}}, {id: call_c, type: function, function: {name: read_file, arguments: {\path\:\c.py\}}} ] }执行器可以并行执行这三个调用并把三个结果统一返回。工具的调用次数如果按“单个工具执行”计数仍然是一次一次算但模型往返次数减少了。Prompt 侧不再需要为这次读取单独产生多轮中间回复。5.2 用执行计划替代盲目试错编码代理经常在“读文件-看报错-再读文件”之间来回切换。这里的工具调用次数并没有减少但每轮之间的信息重复度很高。优化做法是在代理前端增加一个“规划器”在一次请求中输出执行步骤而不是每步都询问模型{ tasks: [ {tool: search_symbol, params: {name: UserService}}, {tool: read_file_lines, params: {path: UserService.java, start_line: 20, end_line: 120}}, {tool: run_test, params: {filter: UserServiceTest}} ] }规划器输出的任务列表会进入工具执行器执行器按顺序执行并把结果汇总。这样原本需要 36 次独立模型往返的工具调用可以压缩成若干批次省去模型在轮与轮之间重复生成“好的我去看看”这类回复。5.3 什么时候不要合并并行和规划并非所有场景都适合。以下情况要谨慎情况风险后一个工具依赖前一个工具的输出必须串行不能并行工具输出非常大并行后一次性返回过多内容会撑爆上下文工具本身有副作用并行执行可能产生重复写入或竞态API 不支持并行 tool call服务端会忽略或报错合并的目的是减少往返开销和 prompt 累积而不是为了追求“模型一次输出多个调用”这个形式。6. 上下文缓存与状态保留让静态内容只计费一次6.1 Prompt Caching 的适用条件很多大模型服务提供 prompt cache 机制对重复前缀按缓存价格计费。编码代理的系统提示词和工具定义通常位于请求的最前面只要保持不变就能命中缓存。要让缓存命中率高应把请求前缀设计成稳定结构系统提示词固定不变。工具定义固定不变。可变内容从用户消息或更靠后的位置开始。如果每次请求都要动态修改系统提示词例如把当前时间、随机批次号塞进去缓存就失去了作用。常见错误是把当前仓库名、分支名、随机 request_id 放到系统提示词顶部造成的后果是每一轮都不同无法复用缓存。6.2 用滑动窗口限制历史长度即使有缓存上下文仍然有长度上限。编码代理运行 36 个轮次后历史消息可能接近上限。滑动窗口策略可以这样设计保留用户原始任务不剪。保留最近 6 轮原始消息。中间的轮次转成压缩摘要。最旧的工具输出直接丢弃。这个过程可以由代理框架在每次请求前执行def slim_context(messages, keep_turns6, max_old_turns_summary20): ...压缩摘要的生成本身也有 token 成本所以不要每轮都重写摘要。可以每 5 轮生成一次或者当总 token 超过阈值时才触发。6.3 把代理状态抽到运行时另一种做法是不把所有状态都放进模型上下文。编码代理可以将“当前任务进度”维护在运行时对象中例如{ task: fix redis connection, completed_steps: [ read config.py, checked env vars, updated timeout settings ], next_steps: [ run test, verify connection ], current_files: [src/config.py] }每次请求时代理只发送这个紧凑状态而不是发送“已经读过的 config.py 全文”。这比把所有工具结果都塞进历史消息更可控。注意只要代理还需要模型回忆起具体报错内容摘要不能把关键报错全部删除。压缩摘要要保留错误关键字、文件路径、行号和修改结论。7. 案例测算同样 36 次工具调用如何接近减半7.1 优化前的基线估算下面用一个常见编码任务做估算。假设某个任务完成需要 36 次工具调用具体构成如下项目优化前估算系统提示词每轮800 token工具定义每轮1500 token平均每轮新增历史消息900 token平均每轮 completion800 token36 次合计 prompt36 × (8001500累积历史) ≈ 72000 token36 次合计 completion36 × 800 28800 token总计约 100800 token之所以 prompt 达到 72000是因为历史消息从第 5 轮开始快速累积到第 36 轮时单轮 prompt 已经接近 4000 token。7.2 优化后的估算采用压缩系统提示词、精简工具定义、工具结果裁剪、滑动窗口摘要和并行工具调用后项目优化后估算系统提示词每轮300 token工具定义每轮600 token被压缩的历史消息平均每轮400 token平均每轮 completion500 token36 次合计 prompt36 × (300600400) ≈ 46800 token36 次合计 completion36 × 500 18000 token总计约 64800 token如果再启用 prompt cache静态部分不计费或只计部分费用总费用还能进一步下降。相同 36 次工具调用总 token 从约 100k 降到约 65k接近减少三分之一到二分之一。实际项目还会因为文件大小、任务复杂度和模型回复长度产生浮动但这个数量级是可信的。7.3 优化后需要观察什么优化不是一步到位。每次改动后除了看总 token还要观察工具调用失败率是否上升。代理完成同样功能所需轮次是否增加。模型是否因为摘要缺失而反复读取同一个文件。有一种副作用要特别注意如果压缩导致模型信息不足它会用更多工具调用去补足上下文。工具调用次数可能从 36 涨到 50总 token 反而没降。因此优化的目标是“同样的 36 次调用更省”而不是“为了省 token 让模型多次返工”。8. 常见坑与排查路径优化后效果不明显时先查这几项8.1 排查表问题现象常见原因检查方式处理建议总 token 没有下降只压缩了 completion没有压缩 prompt查看 usage 中的 prompt_tokens 趋势重点处理工具定义和工具返回prompt_tokens 在后半段仍暴涨某次工具返回了超大文件或完整日志定位 prompt_tokens 最大的 turn给工具返回加截断或摘要配置了 prompt cache 但命中率低系统提示词或工具定义每次请求都有变化对比多轮请求的前缀是否一致把变化字段移到用户消息或独立消息位置工具调用次数增加摘要丢掉了关键信息对比优化前后日志中的工具名序列在摘要中保留错误关键字和文件路径模型开始重复读取同一文件上下文里没有保留读取过的文件信息搜索日志中重复的 read_file 调用增加状态对象记录已读文件和已修改内容并行工具调用没有生效API 不支持或框架配置关闭查看请求响应中的 tool_calls 数量用简单任务验证并行能力再启用8.2 token 计数口径不一致不同服务对 token 的统计口径可能有差异。有的把工具定义算作 prompt_tokens有的把缓存命中的 token 单独列出来有的把模型推理内容分两部分统计。排查时不要跨平台比较原始数值统一用总 token 和计费 token 两个维度记录。如果服务方没有提供 tokenizer可以用字符数估算但要记住中文字符和英文字符的 token 差异。8.3 压缩过度导致模型能力下降工具返回结果压缩过度时模型可能看不到关键的测试失败原因只能靠猜测继续修改于是又触发更多工具调用。这种情况在日志里体现为read_file 被反复调用且读取范围差不多。run_test 连续执行多次但没有任何参数变化。模型在多个轮次中生成相同的修改方案。解决方式是给工具结果处理器设置保底字段至少保留最后一个错误摘要、退出码、最近 10 行输出和涉及文件路径。8.4 只调 token 不调轮次效果被放大或缩小同一个编码任务如果模型从 36 次工具调用变成了 30 次总 token 下降可能来自轮次减少而不是上下文优化。做对比时要控制工具调用次数一致或者分别统计“每次工具调用的平均 token”才能判断优化是否真正有效。建议维护一张对比表任务编号优化前总 token优化后总 token优化前调用次数优化后调用次数每调用平均 token 变化9. 生产环境落地建议从跑通到稳定运行9.1 设置 Prompt 和 Completion 预算编码代理在运行前应该有一个 token 预算。例如agent: max_total_tokens: 120000 max_prompt_tokens_per_turn: 20000 max_completion_tokens_per_turn: 4000 max_tool_calls: 60 context_slim_threshold: 30000当单轮 prompt_tokens 超过阈值时触发上下文裁剪当 completion_tokens 连续超过预算时可以提醒模型“请尽量不要输出长篇计划直接给出修改动作”。9.2 监控指标生产环境至少要监控以下指标每任务总 token。每任务工具调用次数。prompt_tokens 每轮增速。tool call 成功率。prompt cache 命中率。平均完成时长。这些指标可以上报到 Prometheus 或内部日志系统。出现总 token 突增时自动拉取该任务的 usage 日志。9.3 灰度和回退上下文压缩、工具返回截断、并行调用这些改动都可能影响代理行为。建议通过配置开关灰度features: slim_system_prompt: true slim_tool_definition: true compact_tool_result: true parallel_tool_calls: false context_summary: true灰度期间保留优化前的执行路径。如果工具调用失败率超过阈值立即关闭对应开关回到完整上下文模式。生产环境可以同时部署两套代理配置一套保守一套激进按任务目录分配。9.4 用回归任务验证质量优化 token 不能只看数字。每个编码代理项目都应该准备一批回归任务任务完成后检查是否生成了正确补丁。是否运行了测试。是否修改了预期文件。是否出现破坏性改动。只有回归任务通过优化才算有效。如果回归任务不通过优先回退而不是继续调 token 参数。10. 实操清单把这篇文章讲的方法一次性落地下面是一份可直接复制的优化清单记录 10 个典型任务的 usage 日志统计总 token、prompt_tokens、completion_tokens 和工具调用次数。压缩系统提示词删除形容词和重复规则保留身份、输出格式和红线。精简工具定义description 控制在 10 到 20 个词参数只保留必要字段。给工具返回结果增加截断和摘要处理器至少保留最后一个错误摘要、退出码和最近 10 行。增加read_file_lines和search_symbol等细粒度工具避免整文件读入。启用上下文滑动窗口保留最近 6 轮原始消息更早内容转为摘要。检查工具定义和系统提示词前缀是否稳定确保 prompt cache 能命中。在 API 支持时开启并行工具调用把独立读取合并到一次返回。每个优化点都用同一批任务做前后对比控制工具调用次数一致。生产环境通过配置开关灰度保证失败时能快速回退。这 10 条并不需要一次性全部完成。优先做第 3 条和第 4 条因为它们对 prompt_tokens 的影响最直接。做完后再次运行同一批任务观察每轮平均 prompt_tokens 是否下降。只要方向正确36 次工具调用消耗几乎一半 token 是完全可实现的。真正关键的是不要为了省 token 牺牲模型的上下文判断能力让每次工具调用都携带“当前步骤真正需要的信息”而不是把所有历史都原样搬运一遍。