ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Agent 项目 token 消耗优化指南:Skills、子代理与工具调用的降本实战

Agent 项目 token 消耗优化指南:Skills、子代理与工具调用的降本实战 做过 Agent 项目的同学月底看到用量账单的时候估计都有同样的感受明明没跑几个任务额度却莫名其妙见底了。我之前接手一个基于 Claude 系 API 的自动化运维项目时单次完整跑一遍“日志分析 根因定位 修复建议”流程要烧掉接近 18 万 tokens一个月没到额度就报警。后来痛定思痛专门花了两周时间从Skills 配置、子代理调度、工具暴露粒度三个层面做优化同一个流程降到不到 7 万 tokens整体成本下降了 60% 以上额度利用效率提升了至少 20%。这篇文章就把我实际踩过的坑、试过有效的方法、以及背后的原理一次说清楚。注意本文讨论的是通用 Agent 开发中的额度与上下文优化思路以 Anthropic 系 API 体系为示例但方法论同样适用于 OpenAI、Gemini 或其他兼容接口的 Agent 工程。1. 别急着骂模型贵先看看 Token 都烧在哪了很多团队一觉得成本高第一反应就是换更便宜的模型或者粗暴地砍掉功能。但我的经验是先别急着动模型花点时间搞清楚 tokens 到底消耗在哪些环节往往能找到比换模型更有效的优化点。1.1 上下文膨胀才是头号杀手大部分 Agent 框架比如 Claude Code、OpenCode或者你自己基于 API 手写的调度器默认会把整个对话历史、工具返回结果、系统提示词全部塞给模型。问题就出在这里一次任务执行 20 个步骤每一步的工具返回值动辄几千 tokens代码类任务里读一个文件内容可能就 3000 tokens而真正用到的只有其中 40 行Agent 每走一步都要把之前所有步骤的历史重新发送给模型。20 步跑下来输入 tokens 总量 单步平均输入 × 步数是线性增长的。如果中间有几次工具返回异常的大块报错增长还会更夸张。我见过最极端的例子是一个简单的“帮我改一下 README 里的安装命令”任务跑出了 5 万 tokens——因为 Agent 把整个项目结构扫描了一遍又读了好几个无关文件历史一步步累积最后全成了输入开销。1.2 工具返工是沉默的吞金兽除了上下文膨胀“工具调用→失败→重试→再失败”的循环同样吃额度。就拿我自己的项目举例当时封装了一个bash_execute工具权限给得太宽Agent 经常用它执行各种探测命令结果有些命令在目标机器上根本不存在报错回来后模型又没理解透换了个参数继续试连续失败三次才放弃。每一次失败重试不只是单次工具的 tokens整段错误历史还会一直留在上下文里影响后续每一步的输入大小。一次失败的代价远远大于一次成功的代价。1.3 无差别的子代理调用很多 Agent 框架支持“子代理”能力也就是主 Agent 发现任务复杂后起一个新的子 Agent 去处理特定子任务。这是个好功能但用不好就是灾难给子代理传入了完整的项目上下文而不是只传相关片段子代理任务是“总结一下这个目录的代码”结果它逐个文件读过去子代理输出没有结构化限制返回一份几百行的自由文本报告主 Agent 又得全部“咽下去”再筛选。还有个隐藏问题在某些 headless 运行模式下子代理异常退出会造成主进程跟着挂掉。这个我后面会专门讲。1.4 系统提示词和 Skills 里的隐性成本很多人不知道Skills 的描述本身也是要算进输入 tokens 的。系统提示词 所有可用工具的描述 所有 Skills 的描述会在每一轮请求时都发给模型。如果这些描述写得冗长、含糊、重复那每次请求都要多花几百甚至上千 tokens。跑 20 步就是 20 倍。这块虽然单次看起来不多但胜在积少成多优化之后对额度的提升非常可观。2. 从系统提示词到 Skills把“常驻记忆”压到最小理解完成本构成就可以按“发生频率 × 单次消耗”的优先级排序逐个击破。2.1 精简系统提示词能挪走的全挪走系统提示词是每一轮都固定发送的所以它的每个字都在为你的额度“持续扣款”。我给自己定了个原则系统提示词只放“绝对不会变”的内容比如角色定位、输出协议、安全边界。至于领域知识、项目背景、代码规范这类本质上是“参考资料”的内容不要塞进系统提示词里要么放到文件里让 Agent 需要时读要么做成只有特定任务才加载的 Skill。举个例子我最初给 Agent 写的系统提示词里有这样一段你是 DevOps 助手。你熟悉 Kubernetes、Docker、Prometheus、Grafana、 ELK、CI/CD、Terraform....后面还列了十几个这段看起来没啥问题但实际上模型本来就知道这些工具没必要在提示词里重复罗列占了几百 tokens。把它删掉之后任务效果没有任何变化但每次请求省了 300 多 tokens。跑 30 步的任务光这一项就省了将近 1 万 tokens。2.2 Skill 不是文档库是“缩小版任务指令”很多开源仓库里的 Skills本质上就是一份打包好的“提示词 脚本 示例”。这个思路本身没错但大家在落地上很容易偏把 Skill 当成知识库恨不得把整个领域的手册都写进描述里。实际高效的 Skill 应该满足三个条件描述精准让模型一眼就能判断“我这个任务该不该用这个 Skill”。描述写得太含糊模型就会在无关任务上反复加载和尝试白白消耗 tokens内容精简真正干活的指令放在 Skill 文件里不要写在描述里按需触发不要把 Skill 设为常驻加载只有任务匹配了才让模型去读取。我参照社区里比较火的superpower skills的做法把每个 Skill 拆成两部分一小段摘要给模型判断用 一份操作指南实际干活用。因此模型在决策阶段只需要读摘要不需要把完整指南读进来额度自然就降了。2.3 写 Skill 时的防烧钱设计结合我自己的项目我总结了一套写 Skill 的“防烧钱”要点第一明确输入输出边界。在 Skill 指南里直接写明“输入应该是什么格式”“输出应该控制在多少字以内”。我写文档生成类 Skill 时强行规定“输出摘要不超过 200 字且必须使用 JSON 格式”。这样做表面上看是限制自由度实际上避免了模型生成大段废话输出 tokens 直接砍半。第二内置“全局约束提醒”。很多任务失控是因为模型中途忘了优化目标。我会在 Skill 里固定一句“执行过程中仅在必要时读取文件优先使用 grep/ripgrep 定位关键内容禁止无差别读取整个目录结构。”这句话能显著降低模型乱翻文件的概率。第三给 Skill 配一个“最小示例”。别小看这个动作模型在 Few-shot 场景下参考最小示例时执行路径会稳定很多不太会进行大量试错性的工具调用。我对比过同任务在没有示例和有示例两种情况前者平均多消耗了 23% 的工具调用 tokens。提示Skill 里的示例不要多一两个最小可行示例就够了。示例太多反而会占用上下文空间并引入噪音。3. 子代理的正确打开方式隔离上下文而不是叠加上下文子代理这个功能设计初衷是很好的把一个大任务拆成多个小任务每个子代理只关心自己的局部上下文最后把结果汇总给主代理。这样一来主代理的上下文不会被无关信息污染整体步数也能降下来。但在实际项目里我看到最多的问题是——子代理没有起到“隔离”作用反而变成了“放大”作用。3.1 给子代理的范围画出硬边界我们团队后来定了一条铁律子代理只能拿到完成该子任务所需的最小信息集其他一概不给。比如说“分析 error.log 文件的报错模式”这个子任务传给子代理的信息集应该是error.log 的路径期望输出格式JSON含 error_type、frequency、suggested_action 三个字段一个 100 tokens 以内的任务说明一个硬性截止条件比如“最多读取文件前 500 行”。而不是把整个项目目录结构、主 Agent 之前的十几轮历史、或者其他无关任务的结果都一起传过去。不然子代理光是“理解背景”就要消耗大量 tokens而且它还会因为上下文噪声太大而做出错误判断。3.2 headless 运行子代理导致主进程退出的问题这个话题在社区里已经有大量讨论了我自己也踩过在某些 headless无头运行模式下子代理异常退出会导致主 Agent 进程直接终止。典型的表现是Agent execution terminated due to error. Main process exited with code 137/1排查下来根因通常是两类一是子代理的超时或资源限制没有配置好二是子代理与主进程共用一套进程调度子代理一崩就把主进程拖下水。我试过的有效解法是给子代理加独立超时时间和错误吞掉机制// 伪代码示例给子代理设置独立超时并捕获错误 const timeoutMs 60_000; const childTask runSubAgent({ task: analyze_error_pattern, context: minimalContext, timeoutMs, }).catch((err) { // 吞掉错误不让它向上抛导致主进程退出 return JSON.stringify({ status: failed, reason: err.message, partialResult: null, }); }); const result await Promise.race([ childTask, timeoutWrapper(timeoutMs), ]);这样处理后子代理万一崩了返回的也只是一个 JSON 错误对象主进程照常运行。当然具体 API 取决于你用的 Agent 框架但核心思路是一样的对子代理做边界保护避免它成为主进程的“故障点”。3.3 子代理返回结果要做结构化压缩子代理跑完之后返回给主代理的内容也要控制。我之前项目里一个子代理负责“扫描配置文件”它返回了一份有 300 多行的 Markdown 报告主代理为了提取几个关键参数不得不把 300 多行报告全部读进上下文。这个成本完全可以避免。我的做法是在子代理的任务说明里就明确要求“只返回结构化 JSON不得包含解释性文字、不得输出完整原文、不得附带分析过程”。然后再在主代理侧加一个“接收结果后先用一次 100 tokens 内的 parse 校验再决定是否继续”这样一来返回内容从几百行压缩到二三十行上下文占用直接下降。注意子代理也不是越少越好。如果一个子任务的推理复杂到需要多次往返与其让主代理和子代理之间反复通信不如让子代理内部完整跑完再汇报。通信次数越多信息往返损耗越大这一点在长流程任务里尤其明显。4. 工具优化的核心原则给模型越少工具它越不容易闯祸工具优化是很多团队忽略的重灾区。大家总觉得“工具越全Agent 越牛”但真实情况恰恰相反工具暴露得越多模型的选择负担越重错误调用和试探性调用的概率越高。4.1 “按需装载”而不是“全量装载”在 Anthropic 的 API 体系里你可以通过tools参数指定这次请求能使用哪些工具。很多框架也支持按条件挂载工具。我的做法是做一个工具注册表每个工具都带一个match条件Agent 拿到任务后先做一次“意图匹配”再把匹配到的工具列表传给模型。比如TOOL_REGISTRY [ { name: read_log_file, description: 读取指定日志文件的前 N 行用于快速定位错误。, input_schema: { type: object, properties: { path: {type: string}, max_lines: {type: integer, default: 200} } }, match: [日志, log, error, 报错] }, { name: run_sql_query, description: 对分析数据库执行 SQL 查询。, input_schema: {...}, match: [数据库, SQL, 查询, table] } ] def load_tools_for_task(task_description: str) - list: matched [] for tool in TOOL_REGISTRY: if any(kw in task_description.lower() for kw in tool[match]): matched.append({ name: tool[name], description: tool[description], input_schema: tool[input_schema], }) return matched实际使用下来效果很明显工具列表从 15 个减到 3~5 个模型调用工具的准确率大幅上升因为它的“决策空间”小了瞎猜的概率自然下降试探性工具调用的 tokens 消耗也降了不少。4.2 工具描述要短、要带“使用前提”工具描述看起来很不起眼但它是模型决定“要不要用这个工具”的唯一依据。描述写得太长、太复杂模型理解成本高写得太宽泛模型会过度使用。我给工具描述定的格式是一句话说明用途 一句话说明什么时候不用它。举个例子我有个search_code工具最初的描述是在代码库中执行正则表达式搜索返回匹配的文件与行号支持多个正则同时匹配 支持排除指定目录支持忽略文件大小写返回值包含文件路径、匹配行内容及上下文。后来精简成搜索代码库中的匹配内容。仅在需要定位代码位置时使用。如果只是想读文件内容请直接用 read_file不要用本工具。听起来简单但加了一句“什么时候不用”模型误用率直接下来一大截。少了误用也就少了错误返回和无意义的后续请求。4.3 对高消耗工具设置硬性防护有些工具天然就是“吃 tokens 大户”比如read_file、list_directory、execute_bash。对这类工具要给它内置一些约束参数。以read_file为例我给它加了一个max_lines参数默认值设置成 100。如果模型真的需要读完整文件得显式把max_lines调大——这个过程会迫使模型思考“我是不是真的要读这么多”。从结果看很多情况下它读 100 行就够用了直接省掉了后面 2900 行的 tokens。execute_bash更加要小心。我的约束是禁止执行交互式命令所有命令必须带--no-color之类去掉多余输出的参数限制命令执行超时时间默认 10 秒对cat、find .、ls -R这类高输出命令增加提示引导模型优先用grep/head/tail定向读取。还有一个我在社区里学到的技巧给“查看文件列表”这个动作设置成本提示。例如在工具描述里写明“该操作返回约 200~1000 tokens请确认确实需要列出该目录再调用”。模型看到这个消耗提示后调用频率会明显下降。4.4 用“最小化计划”约束 Agent 的工具调用顺序工具调用次数越多累积 tokens 越高。一个特别有效的优化方式是在任务开始时要求 Agent 先给出一个“最小化计划”并且框定计划里的步骤数量上限。比如在系统提示词里加一句在执行前先列出不超过 5 步的执行计划。计划中每个步骤必须说明 “为什么这一步是必要的”以及“预计读取多少 tokens”。如果某个步骤 可以通过前序步骤的结果完成请合并该步骤。这段提示词在初期会稍微增加一点点输出 tokens但它带来的收益是巨大的模型在后续执行中会明显克制住“走一步看一步”的坏习惯不再频繁地扫描文件、试探性调用工具。我实测下来加了这句之后全程工具调用次数平均减少了 30%~40%整体 tokens 反而大幅下降。5. 综合实战把优化方案一次性落地到这里理论讲得差不多了。下面用一个我实际做过的任务来做一次全流程优化前后的对比方便你直接照着抄。5.1 场景描述任务目标是排查一个 Node.js 服务某天凌晨的报错日志定位最可能的根因并给出修复建议。优化前我直接给 Agent 挂上了全部 14 个工具系统提示词里有大段的背景说明还加载了 5 个常驻 Skill没有启用子代理隔离。任务执行结束后统计数据显示指标优化前总 tokens186,540其中输入 tokens172,240工具调用次数28平均每步上下文大小12,300失败重试次数35.2 实施优化的步骤第一步精简系统提示词。把“熟悉各种技术”“你是一个高级工程师”这类人设描述全删掉只保留角色定位、输出协议、安全边界系统提示词从 1,800 tokens 降到 600 tokens 左右。第二步Skill 按需加载。把 5 个常驻 Skill 改为仅任务匹配时加载5 个 Skill 描述合计从 2,400 tokens 降到每次请求只消耗匹配到的那个 Skill 的约 400 tokens。第三步工具按需装载。日志排查任务匹配到 4 个工具read_log_file、grep_log、analyze_stack_trace、propose_fix。工具描述总长度从 3,200 tokens 降到 1,100 tokens。第四步启用子代理隔离。把“读取日志文件、提取关键错误特征”和“根据错误特征搜索代码库”这两步拆成子代理执行子代理只拿最小上下文返回结构化 JSON。第五步工具返回结果压缩。给read_log_file加max_lines200默认值给grep_log加tail参数约束避免一次性返回超大块日志内容。5.3 优化前后对比指标优化前优化后变化幅度总 tokens186,54069,820-62.6%工具调用次数2814-50%失败重试次数30-100%单次请求平均输入12,3005,400-56.1%额度的提升非常直观同样的一个任务原来能跑 5 次现在能跑 13 次左右换算下来额度利用效率提升了 160%。就算按最保守的估计也能稳定提升至少 20% 的有效执行次数。5.4 优化后仍然要监控的指标别以为落地完就一劳永逸了。我建议至少持续监控以下三个指标怎么强调都不过分平均每步上下文大小如果发现某个任务里这个值超过 8,000 tokens就要回去检查是不是又有什么工具返回了不该返回的大块内容工具调用失败率失败率超过 10% 就必须排查要么是工具描述有歧义要么是工具本身有问题子代理返回内容大小子代理返回超过 1,500 tokens 就要告警说明结构化约束没起作用。6. 高频问题排查与避坑实录最后整理一下我在优化过程中遇到过的典型问题和对应的排查思路如果你也踩到类似的坑可以按表对照排查。现象可能原因解决思路任务跑着跑着主进程退出headless 模式下子代理异常未被捕获给子代理包一层 try/catch设置独立超时返回结构化错误对象不让错误向上抛工具报错后 Agent 反复重试同一命令工具描述里没有说明“什么情况下不要用”在工具描述中补充使用前提和禁用场景Agent 频繁读入整个文件导致上下文爆炸read_file 没有限制最大行数给 read_file 加 max_lines 参数默认 100需要更多时显式传入Skill 永远不触发Skill 描述与任务意图的匹配度不够重写 Skill 描述用更接近用户口语化的说法描述触发场景子代理返回内容太多子代理任务指令没有限定输出格式在子代理指令中明确要求“只返回结构化 JSON不包含解释和过程”同一步骤反复调用工具但只是换个参数“最小化计划”约束缺失在系统提示词中加入计划约束并在每步执行前要求说明“为什么这步是必要的”总 tokens 不高但单次请求输入非常大上下文里积压了大量历史步骤考虑启用上下文压缩或让子代理把局部任务消化掉避免主上下文膨胀优化 Agent 的额度利用这件事本质上不是在“委屈”Agent而是在帮它做减法减少无关信息的干扰减少错误试探的空间减少不必要的历史负担。Agent 的推理效果不但没有下降反而因为上下文更干净、决策路径更清晰表现得更加可控。如果你手头也有一个 Agent 项目正在烧钱我的建议是别急着换模型先按这个顺序排查一遍系统提示词里有没有冗余内容技能是不是全部常驻加载了工具列表是不是过宽子代理有没有做好隔离和输出压缩这四步做完你会发现省下来的额度远比想象中多。
返回列表