版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式,也不对任何收益结果作承诺。转载请注明出处。

68 个工具的 schema(喂给模型的说明书,含名字、描述、参数定义)序列化成 JSON 是 19,653 个字符,按本文口径合 6,845 token(token 是计费与窗口的最小单位)。塞进 8192 的窗口,光工具定义就吃掉 83.6%。

谁占大头:描述 2,640 token(38.6%),参数定义 2,798(40.9%),JSON 括号引号骨架 1,201(17.5%)。「把描述写短点」这个最常做的动作只省 247 token(3.6%),召回一条没掉——省下的不是你缺的那部分。

读完你能判断三件事:68 个工具该不该全塞;预筛值不值那点误筛;工具数落在哪个区间,做预筛是白忙。

第 1 章 装置:68 个工具、8 个组、40 条请求

1.1 工具集按真实后台的切法造,不是按论文的切法

工具调用(function calling)指模型不自己动手,而是输出一个「调哪个函数、参数填什么」的结构化对象,执行由你的代码完成。要喂给模型的那份说明书就是 schema。它省钱的地方是模型不必生成自然语言动作,费钱的地方是每接一个工具,这份说明书就得常驻一次上下文。

68 个工具分 8 组:文件、数据库、网络、监控、消息、部署、计费、账号,数量是 9/9/9/9/8/8/8/8。这个切法照着一个中等规模研发助手的后台菜单来——读文件、查库、发请求、看监控、发通知、发版、看账单、管账号——不是先定 68 再凑功能。

组工具数代表工具组内平均 token
文件9file_read、archive_zip104.8
数据库9db_explain、db_count102.7
网络9http_get、tls_check99.2
监控9log_tail、alert_list104.8
消息8mail_send、im_send97.2
部署8k8s_rollout、release_rollback102.9
计费8bill_query、quota_set95.1
账号8user_create、audit_log97.5

单条 schema 平均 289.0 个字符、100.7 个 token;最小 227 字符 / 82 token,中位 289 / 100,最大 340 / 120。组与组之间平均 token 差在 10 个以内——token 成本跟工具属于哪一组无关,只跟参数个数和描述长度有关。这句话后面第 4 章要用。

1.2 token 口径:一个汉字算一个,其余四个字符算一个

本文只用一套口径,全篇不换:

  • 一个汉字(Unicode 汉字区)算 1 个 token;
  • 其余所有字符——英文字母、数字、半角标点、全角标点、JSON 的括号与引号——按 4 个字符算 1 个 token,向上取整;
  • 每条工具 schema 单独估算再累加,不把 68 条拼起来算一次。

为什么这么定:主流分词器对常用汉字基本是 1 字 1 token,英文与符号按 4 字符 1 token 是常用粗估。这条口径不是精确分词——三组用的是同一把尺,比值不受尺的误差影响。

举一个能对着验的例子:第一个工具 file_read 的 schema 是 317 个字符,其中汉字 49 个、其余 268 个字符,所以是 49 + ⌈268 ÷ 4⌉ = 49 + 67 = 116 token。68 条累加得 6,845;如果改成先拼接再估算,是 6,816,差 29(0.4%),来源就是每条各自的向上取整。本文一律用 6,845。

1.3 40 条测试集:请求是人话,标注只有一个工具名

测试集是 40 条「用户请求 → 正确工具」的标注对,每组 5 条。请求全部写成真实用户会说的话:带口语、带指代、故意不提工具名里的关键词。标注只有一个工具名,没有「也可以调另一个」的模糊答案——有模糊答案就没法算召回(召回:正确工具有没有落在被交出去的候选集里)。

用户请求标注工具组
帮我把 report 目录下的日志文件打包一下发我archive_zip文件
订单表现在有多少条记录db_count数据库
昨天报错里有没有 connection resetlog_search监控
把订单服务发到 v2.3.1k8s_rollout部署
这个月花了多少钱bill_query计费
查一下谁在什么时候删了这个库audit_log账号

这批请求刻意避开工具名里的关键词:说「花了多少钱」不说「查询费用」,说「发到 v2.3.1」不说「滚动更新」。测试集如果按函数名写,召回率会虚高到自己都不信。

40 条这个样本量也有代价:一条就是 0.025。0.975 与 0.875 差 4 条,能分辨;0.950 与 0.975 只差 1 条,分辨不了。所以后面凡是说「A 和 B 的首选项一样」,都要按「差 1 条以内不算差」来读。

第 2 章 三种注入策略各自动了什么

2.1 A 全量:19,653 个字符一次塞进去

最直白的做法:把 68 条 schema 全部序列化进 tools 数组,一次请求全带上。它的好处是正确工具一定在上下文里,不存在「没被选中」这回事;代价是每次请求都付 6,845 token 的固定开销,且这个开销与用户问什么完全无关。

{"type":"function","function":{"name":"file_read","description":"读取一个文本文件的内容,可按行号范围截取。当用户说看看这个文件写了什么、打开某个配置、读一下某份文本时调用。","parameters":{"type":"object","properties":{"path":{"type":"string"},"start_line":{"type":"integer"},"end_line":{"type":"integer"}},"required":["path","start_line"],"additionalProperties":false}}}

这一条 317 个字符,里面真正描述「干什么」的只有 description 那 54 个字符。剩下 263 个字符全是结构化字段名、类型声明、required 列表和 additionalProperties。

2.2 B 预筛:先用 BM25 选 8 个,再注入

BM25(Robertson & Zaragoza 2009 那篇综述里的公式)是按「词在这条文档里出现几次、这个词在全库有多稀有、这条文档本身有多长」打分的关键词算法,k1 控制词频收益的饱和速度,b 控制长度惩罚。中文不装分词器,用相邻两字拼成二元组代替:「打包发我」切成「打包」「包发」「发我」。

def tok(s):
    s = s.lower()
    out = ASCII_W.findall(s)                      # 英文与数字整体保留
    for run in CJK_RUN.findall(s):                # 中文连续段切二元组
        out.extend([run] if len(run) == 1 else [run[i:i+2] for i in range(len(run)-1)])
    return out

def score(q, i):                                  # i 是第 i 条工具描述
    tf, dl = IDX.tf[i], sum(IDX.tf[i].values())
    s = 0.0
    for w in tok(q):
        f = tf.get(w, 0)
        if f:
            s += IDX.idf[w] * f * (K1 + 1) / (f + K1 * (1 - B + B * dl / IDX.avgdl))
    return s

索引建在 68 条「描述 + 拆开下划线的函数名」上,取分数最高的 8 条注入。选 8 不是拍的:8 条平均 815 token,是 6,845 的 11.9%,也是多数模型在「候选太多就乱选」这条经验曲线上的常用档位。

有人会问为什么不用向量检索。一是本机装不上第三方包,也没有可调的 embedding 接口;二是本文要量的是「描述字符占多少上下文」,用词面法能把变量锁死在「候选集怎么来」这一层,不受 embedding 版本漂移影响。换成向量,参数块照样要进上下文,6,845 那笔账一分不少。

2.3 C 两级:先按组名选组,再在组内选 4 个

两级(先粗后细)的思路是:68 个工具先归到 8 个组,每组给一句 10–16 个关键词的组描述(8 组共 100 个词、314 个字符、242 token),先让模型或打分器在组这一层挑 1 个组,再在组内的 8-9 个工具里挑 4 个。

def two_level(q):
    g = GROUP_IDX[GBM.rank(q, list(range(8)))[0]]      # 第一级:8 个组里挑 1 个
    inner = BM.rank(q, GROUP_TOOLS[g])[:4]             # 第二级:组内挑 4 个
    return inner

两级的账面上更好看:上下文只要 4 条 schema(平均 404.9 token)加 242 token 的组描述,合计 646.9 token,比 B 组再省 20.6%。代价在第 5 章,这里先记着。

策略进上下文的 schema 条数每条请求的平均 token选择器候选从哪来
A 全量686,845无(模型自己挑)全集
B 预筛8815.1BM25 打分68 条描述里取前 8
C 两级4 + 8 组描述646.9BM25 两次先 8 组取 1,再组内取 4

第 3 章 主结果:窗口 83.6%、召回 0.975 与 0.875

3.1 上下文:8k 窗口放不下,32k 窗口只占两成

同一个 6,845 token,落在不同窗口里是完全不同的问题:

窗口A 全量占比B 预筛占比C 两级占比
819283.6%9.9%7.9%
3276820.9%2.5%2.0%
1310725.2%0.6%0.5%

8k 窗口下,工具定义占 83.6%,剩下的 1,347 token 要装系统提示、装对话历史、装工具返回结果——一次查库返回 50 行就能把这点余量吃干净。这不是「有点紧」,是这条链路在 8k 窗口上跑不通。

32k 窗口下是另一回事:20.9%,还有 25,923 token 可用。同一个 68 工具集,在 8k 上是必须先解决的阻塞项,在 32k 上只是一笔要记账的成本。 这一点决定了后面第 6 章的边界怎么画。

3.2 召回:B 掉 1 条,C 掉 5 条

召回@k 的口径是「正确工具有没有落在被交出去的候选集里」,不是「模型有没有挑对」;后者归首选项准确率管——排第一的候选恰好是正确工具的比例。这个区分很重要:预筛这一步的责任只是别把对的漏掉,挑不挑得对是下一步的事。

策略候选集大小召回首选项准确率平均候选数
A 全量681.0000.95068.00
B 预筛80.9750.9508.00
C 两级40.8750.8754.00

A 的召回 1.000 是定义使然,不是优点——候选集就是全集,正确工具不可能不在里面。可比的是首选项准确率:A 和 B 都是 0.950(38/40)。这不是巧合:B 的候选集就是 A 的前 8 条,前 8 条的第一名永远等于全集的第一名。用同一把尺做预筛,第一名不会变,变的是第二名以后还有没有机会。

B 漏掉的那 1 条是「把 id=1024 那行的状态改成已完成」,正确工具 db_exec 在全量排序里排第 17。原因很具体:db_exec 的描述写的是「把这行改掉」,查询说的是「改成已完成」,二元组「改成」对不上,两者共同词 0 个,得分 0.00。而 file_write 的描述里恰好有「改成」两个字,靠这一个词拿到 3.96 分,排第一。一个近义词,决定了 40 条里 1 条的对错。

C 的召回掉到 0.875(35/40),5 条漏单的机制跟 B 不一样,第 5 章单独拆。

3.3 耗时:A 和 B 同价,省的不是 CPU

单次选择耗时(纯 CPU,不含网络与模型推理,三轮各 120 次重复取中位):

策略第一轮第二轮第三轮中位极差
A 全量486.7619.7509.9509.9133.0
B 预筛517.5468.0505.5505.549.6
C 两级136.6165.9156.4156.429.3

单位是微秒。这一轮 A 与 B 的中位差 4.4 微秒,但两者自己的极差分别有 133.0 与 49.6,轮次之间互有先后——A 和 B 做的是同一件事:对 68 条描述逐条打分,B 只在末尾截了前 8 条。预筛没省下一点 CPU。

这张表是全文唯一不会逐位复现的一列:同一台机器三次重跑,A 的中位落在 427~620 微秒、B 在 438~571、C 在 107~166,抖动三到五成;方向结论不变——A≈B,C 快 3.3~4.3 倍。

真正省下来的是 token:6,845 降到 815.1,每 1,000 次请求省 6,029,925 个输入 token,这才是预筛值得做的理由。C 的 156.4 微秒快 3.3 倍,因为它只对 8 条组描述加组内 9 条打分,但 0.16 毫秒在整条链路里看不见——一次模型调用以百毫秒计。

第 4 章 反直觉:吃上下文的不是描述,是参数结构

4.1 大家以为:描述写太长了,把窗口吃光

这个判断有出处,而且来自官方。截至 2026-11-05,Anthropic 官方 SDK 的 ToolParam 里对 description 的说明是:

“Tool descriptions should be as detailed as possible. The more information that the model has about what the tool is and how to use it, the better it will perform.”

翻译过来就是:描述要尽可能详细,模型知道得越多,表现越好。OpenAI 的同名字段说明则是另一面:

“A description of what the function does, used by the model to choose when and how to call the function.”

一边让你把描述写长,一边说描述就是选工具的依据。于是「68 个工具描述太长吃掉了上下文」听起来顺理成章,对策自然就是把描述压到一两句、删掉「当用户说……时调用」。

4.2 实测:描述占 38.6%,参数占 40.9%,骨架占 17.5%

把 6,845 token 按字段拆开:

部分token占比平均每工具
description2,64038.6%38.8
parameters(JSON Schema)2,79840.9%41.1
name2063.0%3.0
JSON 骨架与标点1,20117.5%17.7
合计6,845100.0%100.7

描述看着像第一大头,其实比参数少 158 个 token、2.3 个百分点——它是第二大,不是第一大。把描述砍到零,也还剩 4,205 token 的固定开销。

那 1,201 token 的骨架是什么?每条参数块都要写 "type":"object"、"properties"、"required"、"additionalProperties":false 这几样,加上每条属性的 {"type":"string"} 外壳,折下来 17.7 token 一条。这几样不是哪家 SDK 自己发明的:type 与 required 由 JSON Schema 的 Validation 规范定义,properties 与 additionalProperties 在 Core 规范里;OpenAPI 3.1 的 Schema Object 又声明自己是 JSON Schema 2020-12 的超集。再往下,RFC 8259 规定 JSON 文本由六个结构字符加字符串、数字、字面量组成——括号、冒号、引号是语法本身,不是装饰。

所以这笔一条 17.7 token 的税,只要你还用 schema 这套格式就删不掉——除非换成紧凑写法(比如把属性写成 path:str 这样的简写再在代码里展开),那是另一套取舍,本文没测。

换句话说,「描述太长」最多解释 38.6% 的成本,而精简描述这个动作在真实工程里还要拿召回换,因为它同时是检索信号。

这就解释了开头那组数:把每条描述截到 40 字(原来的六成左右),schema 从 19,653 字符降到 19,369,token 从 6,845 降到 6,598,只省 247 token(3.6%),而召回@8 还是 0.975、首选项还是 0.950,一条没变。省得少、还不掉点,说明这个动作既不解决问题、也没带来伤害——它纯粹是无效功。

4.3 真正省钱的是砍参数,不是砍文字

同一把尺量第二个变体:参数定义只保留 required 里的两个字段,其余属性删掉。

变体schema 字符token相对 68 工具全量召回@8首选项
原样19,6536,845—0.9750.950
描述截到 40 字19,3696,598省 247(3.6%)0.9750.950
参数只留 required16,0945,950省 895(13.1%)0.9750.950

砍参数省下的是砍描述的 3.6 倍,且召回完全不变——因为排序只用了描述和名字,参数从来不是检索信号。这就是本章要回答的那件事:为什么大家以为是描述、实际是参数——大家盯着给人看的文字,而 token 花在给机器校验的结构上。

OpenAI 的字段说明里有一句能解释这个结构从哪来:

“Omitting parameters defines a function with an empty parameter list.”

也就是说参数块本身可以整个省掉,代价是这个函数从此不收参数。反过来读:参数块的 token 不是废话,它是「这个工具能接受什么」的全部信息;想省它,只能靠减少参数或减少工具,不能靠改文风。

第 5 章 两级为什么掉 5 条:组这一层根本没分

5.1 5 条漏单里,4 条是「组层面零分」

两级的召回是 0.875,5 条漏单逐条看:

用户请求正确组选中组该组得分
订单表现在有多少条记录dbuser1.33
把 id=1024 那行的状态改成已完成dbfile0.00
把订单服务发到 v2.3.1deployfile0.00
这个月花了多少钱billingfile0.00
查一下谁在什么时候删了这个库userfile0.00

4 条的选中组得分是 0.00。这不是「选错」,是一个组都没打上分。

零分从哪来,看排序函数就明白了:

def rank(self, q, idx):
    sc = [(self.score(q, i), i) for i in idx]
    sc.sort(key=lambda x: (-x[0], x[1]))    # 分数相同按索引,索引 0 就是 file 组
    return [i for _, i in sc]

idf 是「这个词在全库有多稀有」的权重,一个词在 8 条组描述里一次都没出现,对这 8 条就贡献 0。于是 8 条全是 0 分,sort 退化成按索引排,永远选第 1 个组 file。这不是 bug,是「先粗后细」在粗粒度失配时的必然退化:粗的那层词表覆盖不到,细的那层再好也没机会出场。

组这一级只有 8 条候选,每组 10–16 个关键词,8 组共 100 个词、242 token。40 条请求里 6 条在组描述上一个词都命中不了:4 条的正确组是 db、deploy、billing、user,全被塞进 file 组;另 2 条本就属于文件组,并列规则让它们蒙对。所以 35/40 里有 2 条是蒙的。

5.2 一条被「记录」两个字骗走的请求

唯一一条非零分却选错的:「订单表现在有多少条记录」,正确组 db 得 1.18,选中组 user 得 1.33。

原因在词面重叠:db 组关键词里有「记录」,user 组的「操作记录」也含「记录」。两组打平在一个词上,差异来自长度归一化——user 组描述更短,b 项(长度惩罚)把分数抬了上去。组描述越短,单次命中越值钱:为省那 242 token 把描述写短,反而让噪声词权重变大。

5.3 省 168 token,换来 4 条错

把 B 和 C 摆在一起算这笔账:

  • B:815.1 token,召回 0.975,40 条错 1 条;
  • C:646.9 token,召回 0.875,40 条错 5 条;
  • 差:省 168.2 token(20.6%),多错 4 条。

168 token 不到一条 schema 的两倍。为这点量多错 4 条,等于把错误率从 2.5% 抬到 12.5%。按一次误调用多花 900 token 的往返口径折,4 条摊到 40 条是每条 90 token 期望损失,净赚 78 token——账面上还微赚,但这是拿「偶尔执行错工具」换「每次省 168 token」(这一笔是 C 对 B 的边际账,与 6.2 那条管「全量还是预筛」的 1,000 门槛不是同一笔)。只读工具可以接受;db_exec、quota_set、release_rollback 这类有副作用的,一次错就不是 token 的事。

再往深一步:8 条组描述一共 100 个关键词、314 个字符、242 token,折下来 2.42 token 一个词。按这个密度,每加 10 个词只多 24 token,要涨到 410 token 才把 C 顶到 B 的水平——那是 169 个词上下,比现在多 69 个,每组从 10–16 个加到 19–25 个。「补组描述会把省下的 168 token 花光」这个直觉不成立:到 169 个词时,组描述 410 token,C 合计 814.9,与 B 的 815.1 只差 0.2。换句话说,两级的成本优势能撑到组描述扩 1.7 倍。

但 169 个词不是安全线。5.1 那 6 条零分不是「词太少」,是「词不对」:「改掉」对不上「改成」、「花了多少钱」对不上「费用明细」,这类错位加词只能靠猜。判据不在 token 账上,在词表是否同源:同词则 168 token 是净赚;不同词则扩到 169 个词也只是与 B 齐平,召回还停在 0.875。

所以两级不是不能用,是只有组划分与用户口语严格对齐时才成立:这 8 个组照后台菜单切,用户不按菜单说话,组这一层先漏。

68 条工具 schema 与 40 条测试集原文:三组策略的完整打分、每条请求的候选集、以及 8 条组描述的关键词表都在这一份里,换成你自己的工具集可以直接重跑。放在资料包里,扫码即可获取:

配套资料获取

第 6 章 边界:工具少于 16 个时,预筛是在给自己找事

6.1 预筛的成本是常数,全量是线性

把工具数从 12 加到 68,重跑同一套 top-8 预筛(测试集只保留正确工具在其中的用例):

工具数用例数全量 token占 8k 窗口top-8 token省下 token省的比例召回@8
1271,24915.2%84240732.6%0.857
20112,07225.3%8271,24560.1%0.909
30173,09637.8%8282,26873.3%0.941
45264,58255.9%8233,75982.0%0.962
68406,84583.6%8156,03088.1%0.975

最后一列最有用的地方:top-8 的上下文成本几乎不随工具总数变化——842、827、828、823、815,挤在 27 个 token 的区间里。不管 12 个还是 68 个工具,预筛后进上下文的永远是 8 条,每条体量差不多。全量线性,预筛常数,两条线在 12 个工具附近就拉开 407 token。

横着读还有一层:省下的 token 从 407 涨到 6,030,14.8 倍;召回从 0.857 到 0.975,涨 0.118。工具越多,预筛越划算,方向单调,问题只在「多少才算多」。

6.2 12 个工具时,净收益只有 278 token

看绝对量:12 个工具时全量只占 8k 窗口的 15.2%,1,249 个 token。这时候预筛省 407 token,相当于窗口的 5.0%,同时召回从 1.000 掉到 0.857(7 条里错 1 条)。

一次误调用要多走一轮:重述请求、带上错工具的结果、再选一次,按 900 token 的往返口径,一次就吃掉 407 的 2.2 倍。摊到 7 条是每条 128.6 token 期望损失,净赚 278 token——账面仍是正的,但薄,而你要为此多养一套索引、一套分组、一条排查路径。

所以边界不能只用 token 画。本文给这条线定了个可判定的数:净收益不到 1,000 token/请求就不做。278 不到门槛的三成;工具加到 20 个时净赚 1,163,刚过线。分界因此落在 16 个附近:

全量 schema占 8k 窗口判定
≤ 1,638 token(约 16 个工具)≤ 20%不做预筛,全量注入
1,638 – 3,100(17–30 个)20% – 38%可做一级预筛,别做两级
3,100 – 6,845(31–68 个)38% – 84%必须预筛,且要补组描述
> 6,845(68 个以上)> 84%预筛 + 延迟加载(工具定义先不进上下文,用到再取),或按会话裁剪工具集

1,638 是 8k 窗口的 20%,不是拍的:留 80% 给系统提示、对话历史与工具返回,是这类链路跑通的最低要求。工具越少越如此:12 个工具时全量只占 15.2%,省下的 407 token 要拿 14.3% 的错单率换,净收益 278——门槛不是「有没有收益」,是「收益够不够养这套装置」。

6.3 这套做法在四种情况下不成立

第一,工具数少于 16 个。 6.1 那张表已经给了数:省 407 token、错 1/7,净收益 278 token,不到 1,000 门槛的三成——不是没有收益,是薄到抵不过一套索引、一条分组和一条排查路径的维护。

第二,组划分与用户口语不对齐。 本文 8 个组按后台菜单切,40 条里 6 条在组这一层拿 0 分,全部落到第一个组。组描述那 100 个词是从菜单名里挑的,与用户口语不同源,再加词也只能靠猜。

第三,工具集合是动态的。 MCP(把工具以标准协议暴露给模型的一套规范)的 tools/list 支持分页,服务端还能声明 listChanged,列表一变就发通知。索引没跟着重建,预筛就是在用旧表选新工具——这类错误不报错,只选错。

第四,工具里有写操作。 预筛的代价在查询场景是多花 token,在写操作上是执行了错的写操作。这 68 个工具里,file_write、file_delete、db_exec、quota_set、release_rollback、role_grant、user_disable、mail_send 这 8 个一旦选错,是发出去了、删掉了、改了。token 可以重试,这些不一定能回滚:要么常驻上下文,要么预筛后加一次确认。

第 7 章 判据与复现

7.1 决策表:先量成本结构,再决定砍哪一块

6.2 的四档只回答「要不要预筛」,不回答「省哪一块」。所以这张表只借它一行,其余六行量的是成本结构与风险:先量成本落在哪,再量哪条路不能走。

先量本文实测判据做什么
全量 schema 占窗口83.6%(8k)> 38%上预筛;工具数档位见 6.2 那张四档表
每工具 description token均值 38.8,占 38.6%> 50 且参数占比 < 35%截描述:40 字省 3.6%,并保留「用户可能这样说」
参数块 token 占比40.9%≥ 40%,且第 4 行为否只留 required:省 895(13.1%),召回不变
参数是否承载语义(枚举、单位、范围)db_exec 的 dry_run、k8s_rollout 的 timeout有别砍参数,改砍工具数
工具集是否动态服务端声明 listChanged会变别预筛:索引追不上,只会静默选错
工具里有没有写操作8 个有写工具常驻,只筛读工具
组名与用户口语是否同词40 条里 6 条组层零分不同词别做两级:省 168 token,多错 4 条

第一行决定要不要预筛,第 2–4 行决定砍哪一块,最后三行是「哪条路根本不能走」。最容易搞错的是第 4 行:参数是最大一块成本,但往往不能砍——少一个参数,工具的语义就变了。最后两行最容易被当成「调参能解决」:db_exec 排第 17 不是 k 太小,是描述里没有用户说的那个词。

7.2 复现:先对三个数,再逐行核表

cd "c:/Users/Administrator/WorkBuddy/CSDN"
PYTHONIOENCODING=utf-8 "C:/Users/Administrator/.workbuddy/binaries/python/versions/3.13.12/python.exe" \
  "work/2026-11-05-04_脚本.py"

脚本只用标准库(json、math、re、statistics、time、collections),不装 jieba、numpy 或任何检索库。工具与测试集写死在源码里,重跑时字符数、token 与召回一位不变;只有耗时列会漂,见 3.3。

核对点期望值对不上说明什么
schema 字符 / token19,653 / 6,845token 口径或工具集被改过
单工具均值289.0 字符 / 100.7 token描述或参数被增删
召回 A / B / C1.000 / 0.975 / 0.875测试集标注或分词被改过
组选择正确率0.875(35/40)组描述关键词被改过
组层面零分条数6组描述变长或变短
组描述词数 / 每词 token100 个 / 2.42组描述的关键词被增删
描述/参数 token 占比38.6% / 40.9%字段统计口径被改过

7.3 一手源取用:两条官方文档没取到正文

MCP 规范的 Tools 页(2025-06-18 版)取到了正文,里面一条工具定义一共列了 6 个字段:

"A tool definition includes:

  • name: Unique identifier for the tool
  • title: Optional human-readable name of the tool for display purposes.
  • description: Human-readable description of functionality
  • inputSchema: JSON Schema defining expected parameters
  • outputSchema: Optional JSON Schema defining expected output structure
  • annotations: optional properties describing tool behavior"

本文这套装置只用其中三段:name、description、inputSchema,字符数也按这三段拆。同版本的 TypeScript schema 是协议的真源,Tool 接口里另三个字段都带 ?,生产端可以不给——所以「三段」覆盖的是大多数真实定义,不是漏了三段。

再往下,MCP 的每条消息都按 JSON-RPC 2.0 走(基础协议原文:All messages between MCP clients and servers MUST follow the JSON-RPC 2.0 specification),tools/call 的请求体就是 jsonrpc、method、params、id 四个成员。工具定义不是给人看的文档,它是 tools/list 响应里的一个 JSON 对象——所以必须可序列化,括号与引号一个都省不掉。

OpenAI 与 Anthropic 的文档站没取到:platform.openai.com 返回 403,docs.claude.com 只给一页「App unavailable in region」。所以改引两家 SDK 源码:OpenAI 的 function_definition.py 与 Anthropic 的 tool_param.py(raw 文件,均 200),逐字照录——SDK 注释就是 OpenAPI 生成时的原始说明,比文档页更接近实现。Anthropic 那边还有一条,写在 defer_loading 字段的注释里:

“If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search.”

一边建议「描述尽可能详细」,一边给「先别放进系统提示、用到再加载」的开关——两条同在一份源码里,说明矛盾点不是「描述写多长」,而是「一次放多少个」。6,845 对 815.1,量的就是这「多少个」。

边界判据表与复现核对表:6.2 的四档阈值、6.3 的四种不成立情况、7.2 的七个核对点,加上 68 条 schema 的分组明细,都在这一份里。放在资料包里,扫码即可获取:

配套资料获取

附表 A:本文引用事实与出处对照表

事实出处(文件全名 + 作者/机关 + 编号 + 日期)访问日期链接本文位置
68 工具 / 8 组 / 19,653 字符 / 6,845 token;单工具 289.0 字符、100.7 token;字段拆分 2,640 / 2,798 / 206 / 1,201;变体省 247 与 895;组描述 100 词 / 314 字符 / 242 token本机实测:work/2026-11-05-04_脚本.py(无编号,2026-11-05);日志 work/2026-11-05-04_脚本.log2026-11-05work/2026-11-05-04_脚本.log第 1、4、5 章
三组 6,845 / 815.1 / 646.9;召回 1.000 / 0.975 / 0.875;首选项 0.950 / 0.950 / 0.875;组选择 35/40;组层零分 6 条;C 省 168.2本机实测:work/2026-11-05-04_脚本.py(无编号,2026-11-05);日志 work/2026-11-05-04_脚本.log2026-11-05work/2026-11-05-04_脚本.log第 2、3、5 章
边界 12/20/30/45/68 的全量 token、top-8 与净收益;耗时中位 509.9 / 505.5 / 156.4 微秒(单机单次采样)本机实测:work/2026-11-05-04_脚本.py(无编号,2026-11-05);日志 work/2026-11-05-04_脚本.log2026-11-05work/2026-11-05-04_脚本.log第 3、6 章
工具定义 6 字段(name/title/description/inputSchema/outputSchema/annotations);tools/list 分页;listChangedTools,MCP Specification 2025-06-18(Server 章),Model Context Protocol 组织,无文号,2025-06-182026-11-05(HTTP 200)https://modelcontextprotocol.io/specification/2025-06-18/server/tools第 6、7 章
「All messages between MCP clients and servers MUST follow the JSON-RPC 2.0 specification.」Base Protocol(Overview 章),MCP Specification 2025-06-18,Model Context Protocol 组织,无文号,2025-06-182026-11-05(HTTP 200)https://modelcontextprotocol.io/specification/2025-06-18/basic第 7 章
Tool 接口定义;title/outputSchema/annotations 均带 ?schema.ts(协议唯一真源),MCP Specification 2025-06-18,Model Context Protocol 组织,无文号,2025-06-182026-11-05(HTTP 200)https://raw.githubusercontent.com/modelcontextprotocol/specification/main/schema/2025-06-18/schema.ts第 7 章
请求对象四成员 jsonrpc/method/params/id;id 缺省即通知JSON-RPC 2.0 Specification,JSON-RPC Working Group,2010-03-26(2013-01-04 更新)2026-11-05(HTTP 200)https://www.jsonrpc.org/specification第 7 章
「A JSON text is a sequence of tokens. The set of tokens includes six structural characters, strings, numbers, and three literal names.」RFC 8259 The JavaScript Object Notation (JSON) Data Interchange Format,T. Bray (Ed.),IETF,2017-122026-11-05(HTTP 200)https://www.rfc-editor.org/rfc/rfc8259.txt第 4 章
properties、additionalProperties 属 Core 词汇JSON Schema: A Media Type for Describing JSON Documents,draft-bhutton-json-schema-01,JSON Schema 组织(IETF 草案),2022-06-162026-11-05(HTTP 200)https://json-schema.org/draft/2020-12/json-schema-core.html第 4 章
type、required 的取值约束JSON Schema Validation: A Vocabulary for Structural Validation of JSON,draft-bhutton-json-schema-validation-01,JSON Schema 组织(IETF 草案),2022-06-162026-11-05(HTTP 200)https://json-schema.org/draft/2020-12/json-schema-validation.html第 4 章
「Models are defined using the Schema Object, which is a superset of JSON Schema Specification Draft 2020-12.」OpenAPI Specification v3.1.0,OpenAPI Initiative,2021-02-152026-11-05(HTTP 200)https://spec.openapis.org/oas/v3.1.0.html第 4 章
「A description of what the function does, used by the model to choose when and how to call the function.」「Omitting parameters defines a function with an empty parameter list.」function_definition.py,OpenAI(openai-python 仓库,由 OpenAPI 规范生成),src/openai/types/shared/function_definition.py,末次提交 commit f348ec8,2026-09-092026-11-05(HTTP 200)https://raw.githubusercontent.com/openai/openai-python/main/src/openai/types/shared/function_definition.py第 4 章
「Tool descriptions should be as detailed as possible…」「defer_loading … Only loaded when returned via tool_reference from tool search.」tool_param.py,Anthropic(anthropic-sdk-python 仓库),src/anthropic/types/tool_param.py,末次提交 commit ae671f8,2026-09-042026-11-05(HTTP 200)https://raw.githubusercontent.com/anthropics/anthropic-sdk-python/main/src/anthropic/types/tool_param.py第 4、7 章
BM25 公式:词频饱和 k1、长度惩罚 bThe Probabilistic Relevance Framework: BM25 and Beyond,Robertson & Zaragoza,FnT in IR 3(4):333–389,20092026-11-05(核著录)https://doi.org/10.1561/1500000019第 2 章
两家文档站未取到正文(OpenAI 403 / Anthropic 地区不可用),故不引用其条款抓取失败记录,非一手源,不占名额2026-11-05https://platform.openai.com/docs/guides/function-calling;https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview第 7 章

附表 B:术语速查表

术语一句话解释本文位置
工具调用模型输出「调哪个函数、参数填什么」,执行由代码完成第 1 章
schema喂给模型的工具说明书,本文统计名字、描述、参数三段第 1 章
token计费与窗口的最小单位,本文按 1 汉字 = 1 估第 1 章
BM25按词频、稀有度、长度打分的关键词算法第 2 章
字符二元组中文按相邻两字切开当词用,替代分词器第 2 章
召回@k正确工具是否落在前 k 个候选里第 3 章
首选项准确率排第一的候选恰好是正确工具的比例第 3 章
两级预筛先在组这层选 1 组,再在组内选几个第 2 章
延迟加载工具定义先不进系统提示,用到再取回第 7 章

写在最后:这篇用到的资料

写这篇文章时,把相关的官方文档和源码又翻了一遍,顺手也整理了几份配套的东西:

  • 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
  • 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
  • AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
  • 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
  • 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多

资料是我自己整理的,放在下面这个码上,扫码即可获取:

配套资料获取
课程目录:私有化部署与 LangChain
课程总目录:7 个模块
在线知识库:Agent Skills 创建全流程
课程目录:LangGraph 开发智能体
在线知识库:Claude Skills 完全指南

添加时备注「AI」,优先通过。

资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。

Logo

更多推荐