Agent Token 消耗优化:Skill、子代理与工具精简
2026/9/17 3:38:03 网站建设 项目流程

1. Agent 复杂度为什么直接吃掉你的额度

1.1 一次请求背后,token 是怎么被算掉的

很多人第一次接触 Agent 开发,注意力全在"能不能跑通"上,等到账单出来才发现,额度掉得比预想快得多。问题不在模型本身贵,而在于你每发一次请求,带上去的东西远超你的直觉。

一个典型的 Agent 请求,实际发送给模型的内容包含这么几块:系统提示词、工具(Tool)定义列表、当前对话历史、检索回来的上下文、以及用户这一轮的输入。系统提示词动辄两三千 token,工具定义如果挂了十几个,每个定义带描述、参数 schema、枚举值,轻松再吃掉两三千。对话历史随着轮次累积线性增长,跑到第十轮,历史本身可能就是前面所有内容的数倍。

真正要命的是这些内容是每一轮都重发的。模型没有记忆,它是无状态的,你看到的"记得前面说过什么",是客户端把历史拼回去再发一遍。所以你以为的一轮对话,实际计费的是"提示词 + 工具定义 + 完整历史 + 新输入"的总和,每一轮都重算一次。

这就解释了一个常见现象:同样问一个问题,单纯聊天消耗可能几百 token,挂上 Agent 之后消耗翻了五到十倍。多出来的部分,大部分不是你真正需要的"智能",而是架构设计带来的固定开销。

1.2 复杂度账单:上下文、工具定义、历史消息的三重叠加

把 Agent 的成本拆开看,主要来自三个叠加项。

第一层是固定开销。系统提示词和工具定义属于这一层,它们和任务难度无关,你哪怕只让它回一个字,这部分也照发不误。很多团队的提示词是从模板或别处抄来的,堆了一大堆通用规则,实际当前任务根本用不上,纯属浪费。

第二层是上下文开销。检索增强也好、文件内容也好、网页抓取结果也好,塞进去多少就按多少计费。常见错误是把整个文件、整篇文档不加裁剪地丢进去,明明只需要其中一小段。

第三层是历史开销。这个最隐蔽,因为它随轮次增长。一个跑二十步的自动化任务,前面积累的历史在中后期会变成大头,而其中大量内容(失败的尝试、冗余的工具返回)对最终结果毫无贡献。

三层叠加之后,你会发现"额度不够用"往往不是模型问题,而是架构膨胀的问题。复杂度越高,固定层越厚;任务越长,历史层越重。你为复杂度买的单,本质上是在为"没必要的上下文"付费。

1.3 20% 是怎么算出来的,不是拍脑袋

我拿几个真实项目做过对比,把优化前后的 token 消耗拉出来看,节省幅度稳定落在 20% 到 45% 之间。为什么差距这么大?取决于你原来的架构有多臃肿。

如果原来的系统提示词和工具定义已经很精简,那你能挤出来的主要是历史层的优化,空间大概在 20% 出头。如果原来是把所有功能塞进一个大 Agent、工具挂了十五六个、上下文全量注入,那砍掉一半都可能。

所以"至少 20%"这个说法,是给一个相对克制但仍有优化空间的架构留的保守估计。它的来源不是玄学,而是三个可量化的动作:Skill 按需加载省掉固定开销、子代理隔离上下文省掉历史开销、工具精简省掉定义开销。三块加起来,20% 是个很稳的底线。

下面我把这三块逐个拆开讲,每一块都会落到具体的写法和数字上。

2. Skills:把臃肿的提示词拆成按需加载的模块

2.1 Skill、Prompt、Agent 三者的边界到底在哪

这三个词现在被混用得很厉害,理清楚对省钱很关键。

Prompt是你发给模型的一段文字指令,它本身没有加载机制,你写多少就发多少。Agent是一个能自己决定调用什么工具、跑多少步的执行体,它有自己的循环。Skill介于两者之间,准确说,它是一个按需加载的能力包,里面通常包含一段指令、可能还有脚本和资源文件,只在被需要时才被读进上下文。

关键就在"按需"两个字。传统的做法是把所有能力都写进系统提示词,反正模型可能用得上。Skill 的做法是:系统提示词里只留一句"你有哪些 Skill 可用,各自一句话描述",具体内容等模型判断需要时再展开。没被触发的 Skill,实际消耗的 token 接近于零。

这就是省钱的第一个杠杆。一个功能齐全的 Agent,如果它有八个能力分支,全写进提示词可能就是四千 token 的固定开销;拆成 Skill 之后,常驻的可能只有三百 token 的索引,真正用到的那个再补上五百到一千。固定开销直接砍掉大半。

注意:Skill 不是"另一种 Prompt 写法",它的价值在于延迟加载。如果你把 Skill 内容又原样塞回系统提示词,那等于没拆。

2.2 一个可用的 Skill 结构长什么样

我现在的习惯是,一个 Skill 目录里至少放三样东西:一个描述文件、一份主指令、以及可选的脚本或模板。

描述文件负责"让人和模型知道这个 Skill 干嘛的",字段精简到几句:名称、一句话用途、触发条件。这部分是常驻的,所以越短越好,通常控制在二三十个 token。

主指令是实际被加载的内容,写的时候按"这个任务该怎么一步步做"来组织,而不是写成百科。我踩过的一个坑是,主指令里塞了大量"背景知识"和"注意事项",后来发现真正被执行到的只有流程部分,那些背景八成用不上。现在我会把背景知识单独放,只在特定分支需要时再引用。

脚本和模板是真正省 token 的地方。凡是能用一段确定性脚本完成的(比如格式转换、字段提取、文件校验),就不要让模型去"思考着做"。把脚本路径写进 Skill,让模型调用脚本,返回结果。这比让模型推理省得多,也更稳定。

一个极简的目录结构大概是这样:

skills/ >## 输入 一份或多份 CSV/TSV 文本,列名可能大小写不一致、含空格或中文。 ## 处理 1. 调用 scripts/normalize.py,参数为输入文件路径。 2. 脚本负责列名规范化、去空、类型推断。 3. 若脚本报错,读取错误信息,判断是编码问题还是结构问题。 ## 输出 按 templates/report.md 的格式输出,包含处理行数和异常行清单。

第三步,写脚本。normalize.py做确定性的活:统一列名、去重、补默认值。这部分是纯代码,不消耗模型额度,只在失败时把错误信息回给模型,让模型决定下一步。

第四步,测触发。这一步很多人偷懒,结果上线后模型该用的 Skill 不触发,不该用的乱触发。我的做法是拿二十来条真实输入跑一遍,看命中率。命中率低于八成,就去改索引里那句话的措辞,通常是把"触发条件"写得更具体,或者加一两个典型例子。

2.4 Skills 用起来最容易踩的几个坑

第一个坑是描述写太长。常驻部分每多一句话,每次请求都多付一次费。我见过有人把 Skill 描述写成一段说明文,八个 Skill 加起来常驻就一千多 token,等于白拆。

第二个坑是把该写进脚本的逻辑写进指令。像"把所有日期格式统一成 YYYY-MM-DD"这种事,让模型逐条判断既慢又费,写个正则十行搞定。凡是规则确定的,都交脚本。

第三个坑是Skill 之间职责重叠。两个 Skill 都能处理"清洗数据",模型就会犹豫,触发不稳定,还会把两个都读进来。我现在维持一个原则:一个能力只在一个 Skill 里出现,重叠就合并。

第四个坑是忘了版本管理。Skill 的主指令是会不停调整的,改之前记得留个记录,不然哪次改坏了触发率都不知道从哪查起。

实操心得:判断一个 Skill 是否值得拆,看它被调用的频率。高频且轻量的放常驻,低频或重量级的做成 Skill。反过来的话,拆了也没用。

3. 子代理编排:把大上下文拆给专注的小角色

3.1 什么时候该上子代理,什么时候纯属折腾

子代理(Sub-agent)最被误解的一点是,很多人以为它是"更高级的写法",逢事就上。实际它解决的是一个非常具体的痛点:单一上下文的膨胀

当一个任务需要翻很多资料、试很多路径,所有中间过程都堆在一个上下文里,历史会迅速变重。子代理的思路是把一段独立的、目标明确的子任务切出去,用一个新的、干净的上下文去跑,跑完只把结论带回来。主上下文里只留一句"子任务结果是什么",不带中间过程。

所以判断标准很干脆:如果一段子任务的中间过程对主任务没用,就值得切出去。比如"从十份文档里各提取一段摘要",中间读文档的过程主任务根本不关心,它只要十段摘要。这种就该用子代理。

反过来,如果子任务的每一步都需要主上下文里的信息来判断,切出去反而要多传一次上下文,得不偿失。我见过有人把"根据前面讨论改代码"这种强依赖上下文的任务拆成子代理,结果每次都要把完整历史传过去,消耗比不拆还高。

3.2 上下文隔离与结果回传的写法

子代理省钱的核心机制是上下文隔离。具体做法是:主代理只传给子代理完成任务所需的最小信息,子代理在自己的上下文里跑完,只返回结构化的结论。

关键在于"最小信息"这四个字。很多人偷懒,直接把主上下文整个传给子代理,那隔离就失效了。我现在的习惯是给子代理定义一个明确的入参结构,只要必要的字段。

{ "task": "提取关键指标", "input": "单份文档内容", "output_schema": { "metric": "string", "value": "number", "source_line": "number" } }

回传的时候也讲究。让子代理按固定 schema 返回,主代理拿到的是精简结构,不是一大段自由文本。自由文本回传看着方便,实际又多了一层要解析的内容,还是费 token。

一个常见误区是子代理的返回还要"解释一下"。不需要。主代理要的是结论,解释属于子代理内部的事,留在它自己的上下文里就行。

3.3 一个编排流程的完整拆解

我拿一个实际做过的任务来拆:批量分析二十份用户反馈,输出一份分类汇总。

不拆的写法是:主代理循环读二十份文件,每读一份就在主上下文里累积,二十份下来历史极长,后期每轮都在重发前面所有内容。

拆成子代理之后是这样:主代理只做三件事——决定要处理哪些文件、把文件逐个派给分析子代理、收集返回的分类结果。分析子代理每次只拿到一份文件和一个固定 schema,跑完返回"分类 + 理由一句"。

主代理的上下文里,每份文件只占一行结果,二十行汇总就是二十行。整个任务跑完,主上下文增长有限,而不是线性叠加二十份原文。实测下来这个场景省了将近四成的 token,主要省在历史层。

这里有个细节要注意:子代理不要嵌套太深。我试过主代理派子代理、子代理再派子代理,结果每一层都要传上下文,中间的传递成本比隔离省下的还多。两层基本够用,三层以上要非常谨慎。

3.4 子代理的常见问题与排查方向

子代理跑不起来,八成是三类问题。

一类是入参没传全。子代理的上下文是干净的,它不知道主代理知道的一切,少传一个字段它就抓瞎。排查方法是把子代理单独提出来跑一遍,看它在缺少什么信息时卡住。

另一类是回传格式不对齐。子代理返回的自由文本和主代理期望的结构不一致,主代理解析失败,就会反复重试,越重试越费。解决办法是回传强制 schema,并在子代理指令里明确"只返回 JSON,不要额外说明"。

还有一类是任务切分粒度不对。切得太粗,子代理内部又变成一个大上下文,隔离没意义;切得太细,通信成本超过收益。经验值是一段子任务的执行步骤在五到十五步之间比较合适,太短不如直接做,太长说明还能再切。

提醒:子代理是省 token 的手段,不是架构洁癖。如果一个任务在主上下文里跑得挺好,别为了"看起来更专业"去拆它。

4. 工具优化:少即是多,把定义数量控制住

4.1 工具定义是怎么悄悄膨胀上下文的

工具(Tool)定义是固定开销里最容易被忽视的一块。每个工具都要附带名称、描述、参数 schema、枚举说明,一个中等复杂度的工具定义吃掉两三百 token 很正常。挂十个就是两三千,而且每轮都发

问题在于很多人加工具的时候完全不考虑成本,觉得"多一个能力总是好的"。结果是模型面前摆着十五个工具,它要花更多注意力去选,选错的概率也上升,然后失败重试又烧一轮。工具多不光费定义的钱,还费决策的钱和试错的钱。

我做过一个粗暴的对比:同一套功能,一个版本挂十二个细粒度工具,另一个版本合并成四个粗粒度工具。后者在固定开销上直接省了两千多 token,任务成功率还高了一点,因为模型不用在一堆相似工具间纠结。

4.2 工具合并与参数收敛的实操方法

合并的原则是按操作对象而不是按动作来组织。比如原来有"读取文件""写入文件""删除文件""列出文件"四个工具,如果它们操作的是同一类对象,可以合并成一个带action参数的文件工具。

{ "name": "file_op", "description": "对文件执行读、写、删、列操作", "parameters": { "action": { "enum": ["read", "write", "delete", "list"] }, "path": "string", "content": "string, 仅 write 时使用" } }

合并之后定义变短,但要注意别过度合并。把毫不相干的工具硬塞一起,模型反而更难判断何时用哪个。我的经验是,合并的前提是这些操作共享同一个对象和同一套参数,否则分开更清晰。

参数也要收敛。常见浪费是把所有可选参数都写进 schema,还配上一大段说明。实际高频用到的参数往往就两三个,其余的可以放默认值,说明能省则省。参数描述写清楚就够了,不用写使用教程。

4.3 工具返回内容的瘦身

工具定义省的是发出去的,工具返回省的是收进来的。返回内容同样进上下文,而且进了之后会随历史一直堆着。

最常见的浪费是工具返回一大堆原始数据,其中只有一小部分被用到,比如查一个字段返回整个记录,读一个配置返回整份文件。我现在的做法是工具层做一次裁剪,只返回调用方明确要的字段。

还有个细节是错误信息。工具报错时如果把完整堆栈抛回来,那一段就永久留在历史里了。合理的做法是返回简短的错误码和一句人话描述,详细诊断信息写到日志,不进上下文。

# 工具内部,返回前做裁剪 def query_user(user_id): raw = db.get(user_id) return {"name": raw["name"], "status": raw["status"]} # 只回必要字段

实操心得:让工具返回结构化的、字段可控的结果,比返回自由文本省得多。自由文本看着"信息全",实际九成内容在后续轮次里都是负担。

5. 额度优化实测与常见问题速查

5.1 优化前后的对比数据

我把一个中等规模的 Agent 项目做了完整优化,前后各跑一轮标准测试集,数据大致是这样。

优化项优化前优化后节省
系统提示词 + 工具定义(常驻)约 5200 token约 1800 token约 65%
单轮平均历史开销约 3400 token约 1900 token约 44%
单任务平均总消耗约 78000 token约 51000 token约 35%
任务成功率82%88%+6 个百分点

注意最后一行:优化之后成功率反而涨了。这不是巧合。上下文越干净,模型注意力越集中,出错概率越低。臃肿不只是费钱,还费准确率。

20% 是针对"已经比较克制"的架构说的,这个项目优化前的基线偏高,所以省得多。如果你的起点本身就不臃肿,别指望砍一半,但稳住 20% 是现实的。

5.2 常见问题速查表

现象可能原因排查方向
额度掉得比预期快固定开销过大看系统提示词和工具定义总长度
任务越跑越慢越费历史线性累积检查是否有长循环未清理中间过程
Skill 不触发索引描述太模糊补充触发条件和典型例子
子代理反复重试回传格式不一致强制 schema,去掉自由文本
工具选错频繁工具数量过多按对象合并工具,减少数量
单轮消耗突然暴涨某工具返回超长内容检查工具返回是否做了裁剪

这张表是我自己排障时最常用的几行,基本能覆盖八成的问题。

5.3 我个人的几条经验

搞 Agent 优化这两年,最深的体会是:省额度不是一个专项任务,而是一种默认习惯。每加一个 Skill、每挂一个工具、每切一个子代理,都要顺手问一句"这会不会让固定开销变大"。养成这个习惯之后,根本不用专门做优化,架构自然就是瘦的。

另一个体会是先测再改。很多人凭直觉去砍,砍错地方反而更差。我的做法是先加一层统计,把每轮的提示词、工具定义、历史的 token 数分别记下来,看看大头在哪,再针对性地动。数据指向哪就改哪,比自己猜靠谱得多。

还有一点是别追求极限压缩。我试过把提示词压到极简,结果模型理解不了任务边界,反而要靠多轮澄清,总体更费。优化的目标是"去掉浪费",不是"越短越好"。留够必要的指令,砍掉确定没用的部分,这个平衡点需要动手测几次才能找准。

最后分享一个小技巧:把每个月的消耗按"固定开销 / 上下文 / 历史"三块记下来,画个趋势。你会发现异常往往出现在某次架构改动之后,有了趋势线,定位问题快到飞起。这个习惯比任何单次优化都值钱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询