← 返回资讯

工具定义占多少 token:函数调用怎么省钱,从 schema 到返回值裁剪

2026-08-07

给 Agent 挂工具的时候,大家盯着的都是「能不能调对」——参数会不会填错、该调的时候会不会不调、不该调的时候会不会乱调。这些当然重要。但我见过不少团队,Agent 跑通了、上线了、账单出来了,才第一次去问:这些工具定义本身,每一轮要花多少钱?

答案往往让人不太舒服。工具定义不是「注册一次」的东西,它是每一轮请求都要重新发送的输入内容。模型没有记忆,你上一轮告诉过它有哪些工具,这一轮不说,它就不知道。所以工具定义会跟着历史消息一起,一轮一轮地重复计费。

工具挂得多一点、描述写得详细一点,很容易出现这种局面:你的工具定义比用户的问题本身长十倍以上,而用户那句问题只发了一次,工具定义发了八次。

先把这笔钱看见

在动手优化之前,得先知道自己现在在哪。这一步很多人跳过了,直接凭感觉去删描述,结果删了半天省下的还不如一次工具返回值多。

做法很直接:把你实际发出去的完整工具定义,原样复制出来,丢进 token 计数器数一遍。注意几个容易漏的地方:

  • 数的是序列化之后的完整内容,不是你在代码里写的那个漂亮的类型定义。缩进、引号、括号、字段名,这些结构性字符都算 token。
  • 参数 schema 里的嵌套对象、数组、枚举值列表,全都要算进去。一个参数多、层级深的工具,光 schema 就可能比它的说明文字还大。
  • 如果你的框架会在工具定义外面再包一层(比如自动生成的用法提示、格式约束、示例),这部分也是每轮都发的,一并数。

至于具体的字段名和序列化格式,各家平台不一样,以你所用平台的官方文档为准。你只需要拿到你实际发出去的那一坨东西的 token 数。

概念上,一个工具定义大致是这样三块,字段名以官方文档为准:

{
  "名称": "查询订单",
  "说明": "根据订单号查询订单状态。用户问到订单进度、发货情况时调用。",
  "参数": {
    "订单号": { "类型": "字符串", "说明": "18 位订单编号" },
    "是否含明细": { "类型": "布尔", "说明": "是否返回商品明细" }
  }
}

数完之后你会得到一张表:每个工具多少 token,全部工具加起来多少 token。这张表就是后面所有决策的依据。

一个算例:乘性开销长什么样

下面这组数字全部是假设参数,只是为了把「乘性」这件事的量级演示清楚。你要做的是把假设换成你自己数出来的真实数字,算术逻辑不变。

假设:

  • 单个工具定义平均 300 token(名称 + 说明 + 参数 schema,序列化后)
  • 当前挂了 12 个工具
  • 一个典型的 Agent 任务平均跑 8 轮(每轮一次模型请求)

那么:

项目算式结果
单轮工具定义开销12 × 3003,600 token
单个任务累计3,600 × 828,800 token

28,800 token,全部是输入侧计费,而且内容完全一样——同一份工具定义,发了八遍。用户那句真正的问题可能只有几十个 token。

现在把工具数砍到 4 个(按任务只挂必要的):

项目算式结果
单轮4 × 3001,200 token
单任务累计1,200 × 89,600 token
相比原来节省28,800 − 9,60019,200 token(降 66.7%)

再叠加描述精简,把单个工具从 300 压到 180 token:

项目算式结果
单轮4 × 180720 token
单任务累计720 × 85,760 token
相比原来节省28,800 − 5,76023,040 token(降 80%)

放到量上看:假设每天跑 2,000 个这样的任务,

  • 优化前:28,800 × 2,000 = 57,600,000 token/天
  • 优化后:5,760 × 2,000 = 11,520,000 token/天
  • 每天少发:46,080,000 token

按 30 天算,一个月少发约 13.8 亿 token。乘上你实际的输入侧单价(各家不同,以官方价格表为准),就是你省下的钱。

这里的关键不是某个具体数字,而是那个乘号:工具数 × 轮次数。你加一个工具,涨的不是 300 token,是 300 × 平均轮次 × 日任务量。Agent 越能干、跑的轮次越多,这个乘数越吓人。想系统地看 Agent 的成本结构,可以配合 Agent 成本控制 一起看;不熟悉工具调用机制本身的,先补 函数调用怎么接

怎么减,按性价比排序

下面五条按我实际用下来的性价比排序。每条都写清楚做法和代价——没有白拿的优化。

一、按任务只挂必要的工具

这是收益最大的一条,因为它直接砍的是乘号左边的那个数。

很多人的默认做法是「把所有工具都挂上,让模型自己挑」。这在工具只有三五个的时候没问题,工具到二三十个的时候就是纯烧钱:用户问一句订单状态,你把日程管理、文件读写、邮件发送的 schema 全发过去,一轮不落。

动态工具集的做法是:在真正进入 Agent 循环之前,先做一次轻量的意图分类,决定这轮任务该挂哪几个工具。分类可以用一次小模型调用,也可以用规则或向量检索——取决于你的工具目录有多大、意图边界有多清楚。

代价有两个,都要认:

  1. 多一次调用。 但这次调用很便宜:假设分类请求 400 token 左右,一个任务只做一次,对比上面省下的 19,200 token,账很好算。
  2. 分类错了会漏工具。 这是真正的风险。模型手里没有那个工具,就只能编一个理由说做不了,或者用别的工具凑合。缓解办法:分类结果宁可多带一两个相邻工具(宽一点),并且保留一个「工具不够用」的兜底通道,让 Agent 能触发一次重新挂载。

工具目录不大(比如八个以内)、意图又很杂的场景,这条可能不划算,多的那次调用和分类风险不值当。工具超过十几个、任务类型又比较分明的,基本是必做项。

二、描述只写「什么时候用」

工具的说明文字是给模型看的路由信息,不是给人看的使用手册。它只需要回答一个问题:什么情况下该调这个工具,什么情况下不该。

该写的:适用场景、边界条件(比如「只支持近三个月的数据」)、和相邻工具的区分点(「查历史订单用这个,查物流轨迹用另一个」)。

不该写的:实现细节、内部系统名词、参数怎么拼、返回值结构说明、注意事项长列表、举三个例子。这些要么模型用不上,要么应该放在参数说明里用一句话解决。

我见过一个工具的说明写了两百多字,其中一百多字在讲这个接口的历史沿革和为什么字段叫这个名字。那一百多字,每轮都在发。

代价:写得太省会导致选错工具。这一点单独在后面讲,因为它是这套优化里最容易翻车的地方。

三、参数能枚举就别自由文本

参数 schema 常常比说明文字还占地方,尤其是嵌套深、可选参数多的。两个方向:

能枚举就枚举。 一个 状态 参数,写成自由文本,模型可能填「已发货」「发货中」「shipped」三种花样,你还得在实现层做归一化,归一化失败就是一次错误调用加一次重试。写成枚举,token 上未必省很多(枚举值也占地方),但正确率明显更稳,省的是重试的钱。

可选参数减到最少。 每个可选参数都是三重成本:schema 里的定义占 token、模型要花注意力判断填不填、填错了要重试。问自己一句:这个参数有没有一个合理的默认值?如果九成调用都用默认值,就别把它暴露给模型,在实现层写死。

顺带一提,参数结构的复杂度和结构化输出的返工成本是同一个问题的两面,可以对照 结构化输出的返工成本

四、合并同类工具

这条经常被低估。五个查询工具——查订单、查物流、查退款、查发票、查售后——每个都有自己的名称、说明和参数 schema,五份重复的结构性开销。

合成一个带 type 参数的查询工具,往往同时更省也更好用:

  • 更省:一份说明、一份参数 schema,type 用枚举列出五种查询类型。总 token 通常显著低于五个独立工具。
  • 更好用:模型面对五个名字相近的工具时,选择困难是真实存在的,它容易在「查退款」和「查售后」之间反复横跳。收成一个工具加一个枚举参数,等于把「选工具」这个模糊决策变成了「填一个枚举值」这个清晰决策。

什么时候不该合并:五个操作的参数结构差别很大、或者语义上根本不是一类事(一个是查询一个是写入)。硬合并会导致参数 schema 变成一堆条件性可选字段,反而更大更乱,模型也更容易填错。判断标准是——合并后参数 schema 是不是仍然清爽。清爽就合,变成一锅粥就别合。

五、把工具定义放进稳定前缀

工具定义是整个请求里最稳定的部分之一:同一类任务,它每轮一模一样。这正是缓存最喜欢的形态。

做法上的要点只有一条:把稳定的内容排在前面,变化的内容排在后面。系统提示和工具定义放最前,历史消息和当前输入放后面。顺序一旦每轮都在抖(比如工具列表的顺序随机、或者动态插了一个带时间戳的字段),前缀就对不上,缓存也就没了。

代价:动态工具集和缓存前缀之间有张力——你按任务挂不同的工具,前缀就不一样了。实践中的折中是按任务类型分组:每一类任务用一套固定的工具组合,组内的前缀是稳定的,跨组不共享缓存。这样两头的收益都能吃到大部分。

别砍过头:选错工具比多花 token 贵

上面这些优化里,最容易翻车的是精简描述。

原因很简单:工具说明是模型选工具的唯一依据。砍得太狠,模型分不清两个相邻工具的边界,就会选错。而一次选错的代价是:

  1. 一次无效的工具调用(花了输入 token,也花了输出 token)
  2. 一次无效的工具执行(可能还打了你的后端接口)
  3. 一次带着错误结果的追加轮次
  4. 运气不好的话,模型基于错误结果继续往下走,整条链路都歪了,最后靠用户投诉才发现

前三项加起来,通常远超你在描述上省下的那几十个 token。第四项没法用 token 算,它是产品事故。

所以判断标准不能是「感觉这段话有点啰嗦」,而应该是:用你的评测集验证

具体做法:

  • 先攒一个工具选择的评测集。不需要很大,几十条覆盖主要意图和易混淆的边界情况就够用。重点是要包含那些「两个工具看起来都能用」的坑位。
  • 每改一版描述,跑一遍评测集,记录工具选择准确率平均输入 token这两个数。
  • 准确率不掉、token 下来了,就留下;准确率掉了,就回退。别靠感觉。

没有评测集就开始砍描述,等于闭着眼睛做优化。这个评测集攒起来的成本,通常在你第二次改描述的时候就回本了。

更大的那块:工具返回值

讲了这么多 schema,但说实话——很多 Agent 的上下文膨胀,主因不是工具定义,是工具返回值。

工具的执行结果会作为观察结果进入下一轮上下文,而且留在那里,被后面每一轮反复重发。如果你的工具直接把整个 API 响应 JSON 原样丢回去,那一大坨字段——分页信息、时间戳、内部 ID、你根本用不到的嵌套对象——就会一路跟到任务结束。

还是用假设参数算一笔(数字仅作演示):

  • 假设一个任务跑 8 轮,其中第 1 到第 5 轮各发生一次工具调用
  • 单次工具返回原始 JSON 约 4,000 token
  • 第 i 轮的结果会出现在第 i+1 到第 8 轮的输入里,即被重发 8 − i 次

累计重发次数:(8−1) + (8−2) + (8−3) + (8−4) + (8−5) = 7 + 6 + 5 + 4 + 3 = 25 次

情况算式结果
不裁剪25 × 4,000100,000 token
裁剪到 200 token25 × 2005,000 token
节省100,000 − 5,00095,000 token

对照前面精简 schema 省下的 23,040 token——裁剪返回值这一条的收益是它的四倍多。这就是为什么我把它单列一节:很多团队在 schema 上抠了半天,返回值那边还在原样往回灌。

做法上,裁剪要放在工具实现层,不要放在提示词里。别指望写一句「请只关注返回值中的关键字段」就能省钱——那坨 JSON 已经发出去了,钱已经花了。正确的做法是在你的工具函数里就把它处理掉:

  • 只返回模型需要的字段。 明确列出白名单,而不是排除黑名单——接口一升级加了新字段,黑名单就漏了。
  • 列表类结果做截断。 返回前 N 条 + 一句「共 X 条,还有更多可翻页」。让模型知道有更多,而不是把全部塞进去。
  • 长文本做摘要。 如果工具返回的是一大段文档,在实现层先摘要(可以用小模型),只把摘要和定位信息回传。
  • 大对象换成引用。 把完整结果存起来,只回传一个 ID 和几个关键字段。模型真的需要细节时,让它再调一次「取详情」的工具。
  • 错误信息也要裁。 完整堆栈对模型没用,它需要的是「失败了,原因是参数不合法,哪个参数不合法」。堆栈留给你的日志。

有一条要提醒:裁剪的依据必须是模型的实际需要,不是你的直觉。哪些字段模型真的会用到,看几十条真实轨迹就知道了。裁掉了模型确实需要的字段,它会反复调同一个工具去找,那比不裁还贵。

工具设计层面的更多取舍,给 Agent 挂什么工具 那篇讲得更细。

监控:把输入 token 拆开看

前面所有的优化都建立在一个前提上:你知道钱花在哪一块。

只看「本月总 token 消耗」这个数,你什么也优化不了。要做的是把每一轮请求的输入 token 按来源拆开,至少分成四块:

构成特征优化手段
系统提示每轮固定精简 + 吃缓存
工具定义每轮固定,随工具数线性增长动态工具集 + 精简 + 合并 + 吃缓存
历史消息(含工具返回值)随轮次累积增长裁剪返回值 + 历史压缩/截断
当前输入通常最小一般不用管

这四块的占比会直接告诉你该优化哪儿:

  • 工具定义占比高(比如超过输入的三成)→ 优先做动态工具集和工具合并。
  • 历史增长快、后半段轮次的输入明显比前半段大 → 问题在返回值,去实现层裁剪。
  • 系统提示占大头 → 那是另一个话题,先看它是不是塞了一堆用不上的规则。
  • 总量正常但账单高 → 可能是轮次太多,去看 Agent 是不是在原地打转。

实现上不复杂:在你的网关或者 SDK 封装层,请求发出前把各部分分别做一次 token 计数,连同任务 ID、轮次序号一起打点。加个按任务类型的维度,你就能看到「哪类任务的工具定义最臃肿」。

还有两个值得单独看的指标:

  • 每任务平均轮次。 这是乘号右边那个数,它涨一点,所有固定开销一起涨。
  • 工具调用的成功率 / 重试率。 如果你刚精简过描述,这个数掉了,说明砍过头了,赶紧回退。

自查清单

  1. 把你实际发出去的完整工具定义序列化后丢进 token 计数器,逐个工具记下数字,做成一张表。
  2. 用「工具数 × 单个 token 数 × 平均轮次 × 日任务量」算出工具定义的日开销,看它占输入总量的百分比。
  3. 工具超过十几个的,评估动态工具集:先做一次轻量意图分类,只挂本次任务需要的工具,并保留「工具不够用」的兜底重挂通道。
  4. 逐个审工具说明,只留「什么时候用 / 什么时候不用 / 和哪个工具的区别」,删掉实现细节、历史沿革和长示例。
  5. 参数 schema 里,自由文本能改枚举的改枚举,九成调用都用默认值的可选参数直接在实现层写死、不暴露给模型。
  6. 找出名字相近的同类工具,尝试合并成一个带枚举 type 的工具——前提是合并后参数 schema 仍然清爽。
  7. 把系统提示和工具定义固定排在请求最前面,保证顺序稳定,让这段前缀能吃到缓存;动态挂载按任务类型分组,组内前缀保持一致。
  8. 在工具实现层做返回值裁剪:字段白名单、列表截断、长文本摘要、大对象换引用、错误只留原因——这一条通常比精简 schema 省得更多。
  9. 攒一个几十条的工具选择评测集,每次改描述都跑一遍,准确率掉了就回退,别凭感觉砍。
  10. 在网关层把每轮输入 token 按「系统提示 / 工具定义 / 历史 / 当前输入」四块分别打点,按任务类型看占比,用数据决定下一步优化哪儿。

相关阅读