尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

编码Agent Token优化:不减少工具调用,压缩上下文才是关键

编码Agent Token优化:不减少工具调用,压缩上下文才是关键 前阵子我调一个 coding agent任务不复杂让它在某个仓库里定位一个功能模块补上错误处理再把相关测试跑通。Agent 最终完成得很好全程调用了 36 次工具。但我看 token 账单时愣住了——它花的 token 比我预估的大了一倍多。我的第一反应是减少工具调用次数比如把两个读取合并成一个把多次搜索改成一次。后来发现思路错了。真正吃掉 token 的不是 36 这个数字而是每次调用背后那些重复携带的上下文、冗长的工具描述、全量返回的日志以及越来越膨胀的历史记录。把这些上游因素修掉之后保持同样的 36 次工具调用不变token 总量降到了原来的一半左右。很多人觉得优化 coding agent 的 token 就是要让模型少调用工具这听起来顺理成章但在真实工作流里工具数量往往由任务复杂度决定强行减少调用次数反而会让模型在缺少信息的情况下瞎猜。更有效的做法是从请求的输入输出入手把每一轮模型实际收到和需要返回的内容压缩到刚好够用的水平。1. 先拆开账单36 次工具调用里token 被谁吃掉了优化前必须先回答一个问题一次 coding agent 的工具调用模型到底收到了什么又生成了什么如果你只是看 API 控制台里的“总 token 数”很容易把问题归因到“工具调用太多”。但仔细拆解后会发现工具调用次数只是表面数字。真正决定 token 量级的是每次请求请求里携带了多少历史上下文、多少工具 schema、多少冗余输出。1.1 为什么每次工具调用都会“重复付钱”Coding agent 的典型工作循环是模型决定调用哪个工具 → 执行工具 → 把工具结果返回给模型 → 模型继续推理。很多大模型 API 的设计是每次调用都要把“到目前为止的完整对话”重新送进去这就像一场永远不允许主动遗忘的会议。第一次调用时模型只读了问题描述和仓库结构可能只需要 2k token。但到第 10 次调用时模型需要回顾前 9 轮里所有工具的执行结果包括当时读取过的文件内容、运行测试的完整输出、修改代码的 diff。到第 36 次调用时这个“会议记录”已经积累了非常多的内容。如果每次工具结果都完整保留尤其是“读取整个文件”“输出整个测试日志”“打印整个函数实现”这类结果那 token 就会呈线性甚至超线性增长。1.2 一个工具调用请求的实际载荷我在做优化前把一次典型的请求拆开通常包含四部分系统提示词描述 agent 角色、工作模式、约束条件。工具定义列表每个工具的名称、描述、参数 schema。历史消息过去所有轮次的助手消息、工具调用消息和工具结果。当前任务上下文当前问题、最近观察到的结果、下一步计划。其中最容易失控的是第三部分。工具结果会被当作“消息”塞回历史读取一个几百行文件返回的内容就是几千 token。如果这个文件被读了两次那份费用就会付两次。一个很典型的观察是某次请求里工具输出占了总 token 的 70% 以上。也就是说模型并没有在“思考”上花太多 token而是在不断接收、重放那些已经有过但可能已经不需要的原始数据。这里可以给一个简化模型假设平均每次请求带 4k 的冗余历史 1k 的工具描述 1k 的当前输出那么 36 次调用就是 216k。如果每次请求只带最近 3 轮历史和压缩过的工具结果可能一次请求只有 2k 到 3k总量能直接砍半。所以优化 coding agent 的 token 消耗真正的杠杆不是“少调用工具”而是“让每一轮携带动得更少、更精确”。2. 输入侧压缩不是减少次数而是减少每次请求的“行李”输入侧是最容易看到效果的地方。模型每次请求都要带上工具描述和历史上下文这两部分改起来风险较小收益又很直接。2.1 工具列表和描述能少装就少装很多 agent 框架默认把所有工具全量注册进来。比如读取文件、搜索文件、运行命令、打开网页、调用外部 API 等等一次性塞十几个甚至二十几个工具定义。每个工具定义在每次请求里都会作为 token 被计算。一个工具描述如果写了两三行自然语言加上参数 schema可能占 200 到 400 token。十个工具就是 2k 到 4k token。如果 36 次请求每次都带这些单是工具定义就浪费掉一两万 token。优化手段是动态工具集先根据任务类型判断需要用哪些工具比如“修复 bug”任务只需要读文件、搜索、运行测试、改文件那就不注册外部 API 工具。工具描述里删掉那些模型不需要的背景解释只保留“这个工具做什么、关键参数是什么”。比如“读取文件返回内容。参数 path 为相对仓库根路径的文件路径”就够了。参数 schema 里不要写太多示例也不要把所有可选字段都列出来只保留与工具调用直接相关的字段。这个动作通常不会降低任务成功率因为模型还是能调用足够多的工具完成工作只是它不再被迫“看到”那些用不上的工具。2.2 历史记录从全量回放到滚动窗口加摘要历史记录是全量保留还是截断是 coding agent 优化里最需要权衡的地方。完全截断会丢失前面已经做过的工作模型可能会重复修同一个位置。完全保留又会快速撑大上下文。我建议采用“滚动窗口加摘要”的方案保留最近 N 轮原始消息N 通常取 6 到 12。更早的历史先用一次小模型调用或简单的规则整理成摘要。摘要里只保留“已确认的事实”和“决策依据”比如已经读取了哪些文件、最终决定修改哪个函数、测试报错的关键信息。后续请求不再携带原始早期消息只携带摘要和最近几轮的原始记录。一个简单的伪代码长这样def compact_history(history, max_messages10): recent history[-max_messages:] older history[:-max_messages] if not older: return history # 把 older 压缩成结构化摘要 summary summarize_events(older) return [summary_message(summary)] recent注意summary_message 仍然是一条保留在对话里的消息但它不再有原始文件内容、完整日志只留下关键节点。这样做能显著降低第 20 次以后请求的 token 量。2.3 任务上下文按需注入而不是全仓阅读另一个常见的浪费是agent 一上来就把整个仓库文件、README、目录树全塞进去试图让模型“理解整个项目”。这在短上下文里非常昂贵而且大多数信息在后续任务中用不上。比较好的做法是分层注入第一层仓库结构的精简摘要比如目录树只到二级或三级目录。第二层任务相关文件的片段由搜索或读取工具按需返回。第三层明确的目标描述比如“在src/parser.py的parse_line函数中补上异常处理”。如果 agent 需要了解某个文件不要直接返回整个文件而应该先搜索目标符号再只返回包含该符号的代码区域。这样每次请求的当前上下文就保持在一个很小的范围里。输入侧优化的核心原则是不给模型喂它不需要的事实。每一 token 都应该是当前决策有直接关联的信息。3. 输出侧约束让工具结果变成决策所需的最小集合输入侧压缩做得再好如果工具每次一执行就返回 2000 行文本那么这些文本很快又会成为下一轮请求里的历史。输出侧的目标是让工具结果在执行完之后消耗的 token 本身就少。3.1 读取类工具的输出截断读取文件是 coding agent 最常用的工具之一也是最容易失控的地方。一次read_file返回一个几百行文件就是几千 token。实际上模型在多数场景下只需要知道某个函数、某个类的具体实现。处理方式不是直接砍掉输出而是改变读取工具的行为默认只返回文件前 N 行和后 N 行并在中间标记... truncated ...。如果模型需要看中间部分再按行号范围读取。更智能一点读取前先搜索目标符号定位到具体行号区间只返回区间内容。伪代码示例def read_file(path, startNone, endNone, max_chars2000): content load_file(path) if start and end: content content.splitlines()[start:end] if len(content) max_chars: content content[:max_chars] \n... [truncated] ... return content这样做不会丢失所有信息因为模型还能发起第二次读取但第二次读取的目标会更精确代价也更低。3.2 执行类工具的结构化返回运行测试、执行命令、查看 git diff 这类工具输出通常又长又杂。比如一个测试失败完整 traceback 可能几百行但模型真正需要知道的只有哪个用例失败了。失败类型的名称。最关键的一两行错误信息。出错位置的文件和行号。所以执行类工具应该尽量返回结构化结果而不是原始文本。常见做法是让工具输出 JSON{ success: false, failed_tests: [ { name: test_parse_invalid_syntax, error: SyntaxError: expected :, location: tests/test_parser.py:42 } ] }在这个 JSON 里错误信息被压缩到了极小的篇幅但决策信息全部保留。模型可以直接根据error和location决定下一步修改哪个文件。3.3 错误信息也要“压缩过”错误信息是模型判断下一步行动的重要依据但不要原样塞入完整 stack trace。代码 agent 通常不需要“从第一帧看到最后一帧”它需要的是“根因在哪一层”。一个可用的压缩策略是保留异常类型和消息。保留调用链上最前面的 3 到 5 帧。如果某个帧来自外部依赖库可以省略或标记为external。在末尾保留最后一行提示...中间堆栈省略...。这样错误信息既保留了根因又不会让每次失败的输出占掉大量上下文。更重要的是模型不会因为堆栈太长而迷失在无关帧里。输出侧的优化本质上是在“信息完整”和“信息精简”之间找平衡。最佳状态是模型每读到一个工具结果都能立刻知道发生了什么、下一步可选动作是什么而不用再翻上下文。4. 用数据说话建立 token 基线和回归流程优化 coding agent最容易犯的错是拍脑袋。今天砍历史明天截断输出感觉都有效但没有基线你就不知道哪个改动真正带来了收益哪个改动只是把问题转移到了别处。4.1 核心指标token 与工具调用比值建议把一次任务分解成几个可观测指标总 token 数输入 token 输出 token。工具调用次数。平均每次请求的 token 数总 token / 请求次数。工具调用 token 占比工具结果和工具定义相关的 token 在总 token 中的比例。任务成功率最终是否完成了用户要求。其中“平均每次请求的 token 数”比“总 token 数”更能反映优化效果。因为总 token 会受任务长度影响而每次请求负载直接说明模型每一轮读到的东西是否冗余。一个优化前后的示例表格指标优化前优化后变化工具调用次数36360总 token约 180k约 90k-50%平均每次请求 token约 5k约 2.5k-50%任务成功率通过通过保持这里我用了“约”是因为不同任务、不同模型、不同工具返回格式会有波动建议你自己采集本地数据。4.2 优化节奏一次只改一个变量我把优化过程分成三个独立阶段避免多个改动混在一起导致无法归因。第一阶段只精简工具列表和工具描述跑同一组任务看 token 和成功率变化。第二阶段只改历史压缩引入滚动窗口和摘要再跑同一组任务。第三阶段只改工具输出处理增加截断和结构化返回再跑同一组任务。每个阶段都要保留一个固定测试集。最好选 5 到 10 个真实任务覆盖不同代码库和不同错误类型。不要用同一个任务反复测因为 agent 可能会记忆。如果某个阶段 token 下降了但任务成功率也下降了那说明这个改动过猛。比如历史压缩把关键错误信息丢掉了模型只能凭感觉修改成功率自然下降。4.3 TPM 与并发限制优化带来的额外好处有些 API 限制的是每分钟输入输出 token 总和也就是 TPM。即使总 token 降下来如果单次请求特别大还是容易在峰值时撞到限流。优化后每次请求平均变小最大的好处是降低了撞限流的概率。尤其当 agent 需要在多任务并行时每个任务每次请求 2k token 比 5k token 更容易塞进同一个 TPM 预算里。从成本角度看大多数 API 都按 token 计费总量减半基本等于成本减半。就算模型本身很便宜在长任务场景下这个优化也值得做。我的建议是把“token/工具调用比”写成监控指标。如果某天发现这个比值异常升高说明有新的冗余来源进入了 agent比如某个工具返回了超长内容或者历史压缩逻辑失效了。5. 实际落地时的坑和排查顺序优化方案不复杂但落地时有一堆边界问题。这里列几个我踩过的坑以及一套排查顺序。5.1 常见坑第一坑把工具输出截断后没有明确标记。模型看到一段没有“省略”标记的代码会误以为那就是完整文件从而做出错误判断。所以截断处一定要有显式的... truncated ...标记最好再包含总行数提示。第二坑历史摘要把决策依据也丢了。有些摘要只留“已读取文件 A、B修改了 C 文件”但没保留修改的具体原因、失败测试的名称。这样后续请求里的模型虽然知道“改过文件”但不知道为什么改遇到同样错误时可能反复试探。第三坑动态工具集选型不准。如果一开始判断任务只需要 3 个工具但任务中途需要读取数据库 schema而对应工具没有被注册模型只能用低效方式猜测。因此动态工具集要根据“任务阶段”调整不能只在一开始决定。第四坑只看总 token不看输出 token。在某些模型里输出 token 比输入 token 更贵。如果 agent 喜欢生成很长的中间推理、重复计划、甚至复盘输出 token 同样会失控。优化时要同时关注输入和输出不能只压一边。5.2 排查链路如果 token 还是很高按这个顺序查我一般按下面这个顺序排查每一步都会有对应输出帮助定位先看 token 总量和工具调用次数的趋势。如果工具调用次数本身异常高比如为了改一个变量读同一个文件 5 次先优化任务规划。再看输入 token 分布。找单次请求最大的那几轮看其中系统提示、历史消息、工具结果各占多少。再看工具结果。找到最长的工具输出确认是不是某个read_file或run_tests把超大内容塞了进来。再看工具定义和系统提示词。动态工具集是否生效描述是否还有可以压缩的空间。最后看输出 token。模型是不是在每一轮都长篇大论地“思考”而不是直接给工具调用参数。这个顺序几乎能覆盖大多数 token 异常增长的情况。5.3 适用边界与不适用场景这套优化方法最适合“工具调用密集、上下文较长”的 coding agent。比如自动修 bug、复杂重构、多文件分析、持续集成流程里的 agent。如果你的任务只有一次工具调用比如“把当前目录的文件列表列出来”那不用做复杂优化。省下的 token 还不够你写配置的时间。如果你的任务要求 agent 必须保留全部历史才能做出正确判断比如长文档综述、跨多个版本的状态追踪那么激进的摘要和高频截断会损害结果。这种情况下应该用更大的上下文窗口或更贵的模型而不是强行压缩。另外当优化导致“工具调用次数不变但任务结果变差”时说明当前任务是高敏感性任务。更好的选择可能是使用更小但更精确的中间状态而不是一味减少喂给模型的信息。最后说一句优化 coding agent 的 token 消耗本质上是在重新设计模型的信息入口和出口。36 次工具调用只代表 agent 和外部世界的交互次数真正昂贵的是每次交互时模型不得不多看的那部分内容。同样的任务工具调用次数不变token 减半背后意味着模型每一轮收到的信息都更接近决策本身而不是被历史包袱和冗余输出拖着走。如果你现在也在跑一个 coding agent建议先别急着优化模型或换更贵的服务。先把一次任务的 token 账单拆开看看找到最大的浪费点然后一次只改一个变量用同一组任务去回测。只要开始做测量优化就已经完成了一半。
返回列表