🔑 关键洞察
✦ AI·GEN本文作者通过详细的 benchmark 测试,验证了开发一个“tool-calling 修复 proxy”的想法是否可行。测试涵盖 22 个本地 AI 模型,分为 6 类实际应用场景,结果显示新一代模型(如 qwen3.5)已经原生支持高质量的 tool-calling,且性能优异,修复层对这些模型反而是负资产。同时,旧模型的修复需求因新模型的普及而逐渐消失。作者总结,tool-calling 修复 proxy 的市场需求已经被模型迭代自然消解,项目在启动前即被否决。文章还提供了针对本地 AI tool-calling 的实用模型选择建议,并强调验证假设的重要性。
打完跟 xl-ai + 本地 Gemma 的那场硬仗之后,我心里留下一个结论:本地模型根本不会 tool-calling。Gemma 3 27B 连稳定吐个 JSON 都做不到,xl-ai 那种「期待模型乖乖讲 tool-call 格式」的库一接就炸,最后是靠后端 adapter 手工翻译才救回来的。
然后我就灵光一闪:既然每个做本地 AI 的人都会撞这面墙,那做一个「tool-calling 修复 proxy」不就是蓝海?
构想很简单:一个 OpenAI-compatible 的中间层,夹在你的 app 跟本地模型之间。App 照常用标准 tools 格式发请求,proxy 负责把 tool 定义翻成模型啃得动的 prompt、把模型吐的烂 JSON 捞回来修好、用 schema 验证、错了自动重试——让「不会 tool-calling 的模型」对外表现得像会。
听起来很美。每个用 Vercel AI SDK / LangChain 接本地模型的人都在抱怨这件事,而市面上没有一个独立的修复层。
但这次我学乖了。动手做之前,先花一天写个 benchmark 验证假设。
结果这个 benchmark 亲手杀死了我的点子。这篇就是完整的验尸报告——顺便附上你现在(2026 年 6 月)挑本地模型做 tool-calling 的实用对照表。
(会想先验证,就是因为上次那场仗的教训:官方库的美好 demo 背后都假设了一个很强的后端。这次换我自己要当「库作者」了,得先确认我假设的那个「很烂的后端」真的存在。)
Benchmark 怎么设计#
25 个测试案例,六个类别,全部都是真实世界会撞到的形状:
| 类别 | 数量 | 在测什么 |
|---|---|---|
| simple | 5 | 单工具直球:查天气、发信、下 SQL(含中文 prompt) |
| complex | 5 | 嵌套 schema:创建行事历事件,attendees 是对象数组、有 required 字段 |
| html | 4 | HTML-in-JSON:就是 xl-ai 那个 applyDocumentOperations——block 字段塞 HTML 字符串,引号跳脱的地狱 |
| parallel | 3 | 一句话要发两个以上的 call:「比较东京和伦敦的天气」 |
| nocall | 4 | 不该 call 的时候忍不忍得住:「天气跟气候差在哪?」(手上有 get_weather 工具) |
| select | 4 | 四个工具摆一起,挑得对吗 |
每个模型跑两个 condition:
- native:直接走 Ollama 的 OpenAI-compatible API +
tools参数——就是 Vercel AI SDK、LangChain 这些框架今天实际拿到的东西。 - shim:仿真「修复 proxy」——不给
tools,改用 system prompt 要求模型只吐 JSON,收到后做 fence 剥除、<think>区块清理、括号平衡扫描抽 JSON、jsonrepair 修复,再用 ajv 做 schema 验证,错了带着错误消息重试一次。
js// shim 的内核:抽 JSON(剥 think 区块、剥 fence、括号平衡扫描)→ 修复 → 验证 → 重试 function parseShimOutput(text) { const raw = extractJson(text) // 找第一个括号平衡的 {...} let obj try { obj = JSON.parse(raw) } catch { obj = JSON.parse(jsonrepair(raw)) // 烂 JSON 的急救室 } // ...之后还有 ajv schema 验证,失败就带错误消息 retry 一次 }
评分不是「有 call 就算过」:工具名要对、参数要过 ajv schema 验证、内容要过语意检查(信真的寄给 bob@example.com 了吗)、parallel 要真的发满两个、nocall 类发了任何 call 就是死。temperature: 0,全部在一张 RTX 5060 Ti 16GB 上用 Ollama 跑,量化是各模型的预设 Q4。
(先讲清楚:25 题、单次跑、Q4 量化,这是 smoke test 不是学术 paper。但你看完下面的数据会同意——0/25 跟 25/25 的差距,不是 noise 能解释的。)
结果总表#
依 native 分数排序:
| 模型 | native | shim | 备注 |
|---|---|---|---|
| qwen3.5:4b | 25/25 | 22/25 | 满分,而且只有 4B |
| qwen3.5:9b | 25/25 | 25/25 | 满分 |
| granite4.1:8b ✨ | 24/25 | 24/25 | IBM 新世代 |
| ministral-3:8b ✨ | 24/25 | 24/25 | Mistral 边缘线 |
| granite4:tiny-h | 22/25 | 12/25 | shim 反而砍半 |
| lfm2.5:8b ✨ | 22/25 | 5/25 | 自称为 tool calling 而生;shim 崩到 5 |
| mistral-nemo | 22/25 | 25/25 | 2024 的老将,意外能打 |
| nemotron-3-nano:4b ✨ | 21/25 | 23/25 | NVIDIA 代理线 |
| qwen3.6:27b ✨ | 21/25 | 23/25 | 输给自家 4B 小老弟,见下文 |
| gemma4:e4b | 20/25 | 24/25 | |
| gemma4:e2b | 19/25 | 18/25 | |
| gemma4:12b | 18/25 | 20/25 | html 类 0/4,全家死穴 |
| hermes3:8b | 18/25 | 23/25 | |
| qwen3:4b | 16/25 | 17/25 | 跟 qwen3.5:4b 对照:一个世代差 9 题 |
| phi4-mini | 12/25 | 22/25 | |
| llama3.2:3b | 9/25 | 16/25 | |
| functiongemma:270m ✨ | 7/25 | 0/25 | 270M 特化模型,只认原生格式 |
| command-r7b | 4/25 | 11/25 | native 的 4 分全是 nocall「沉默得分」 |
| deepseek-r1:8b | 4/25 | 23/25 | thinking 模型的奇特分裂,见下文 |
| glm4:9b | 4/25 | 13/25 | 同 command-r7b,靠沉默得分 |
| gemma3:12b | 0/25 | 23/25 | API 直接 400:not support tools |
| gemma3:4b | 0/25 | 19/25 | 同上 |
(✨ = 2026 年 5–6 月发布的新模型,写文当周现拉现测)
发现一:上一篇的仗打得不冤,Gemma 3 是真的 0 分#
先看最下面两列。Gemma 3 的 native 是 0/25——而且不是答错,是 Ollama 的 API 直接回 400:
registry.ollama.ai/library/gemma3:12b does not support tools
模型的 chat template 里根本没有 tool 这个概念。所以上一篇我跟 xl-ai 打的那场仗,从第一天就是不可能赢的:不是我接得不好,是那个东西在 API 层就不存在。 看到这个 0/25 的当下有种迟来的平反感。
而 shim 把 gemma3:12b 从 0 分拉到 23/25。换句话说——我的修复 proxy 点子,在 2025 年世代的模型上是完全成立的。prompt 策略 + JSON 修复 + 重试,真的能让一个「API 层不支持 tools」的模型跑出 92% 的成绩。
如果这个 benchmark 是 2025 年中跑的,我大概已经开工了。
发现二:世代断层——窗口在 Qwen3.5 发布那天就关了#
然后看最上面两列。
qwen3.5:4b,native 25/25,满分。 4B 的模型。3.4GB。在我这张中产阶级显卡上随便跑。
不是「大致能用」,是 25 题全对:嵌套 schema 全过、HTML-in-JSON 全过(连 <b>final</b> 这种要保留 tag 的都对)、该平行就平行、该闭嘴就闭嘴、四选一全选对。我盯着结果 JSON 找了半天想抓它一题错的,没有。
世代内对照更残酷:
- qwen3 → qwen3.5(同是 4B):16/25 → 25/25
- gemma3 → gemma4(同是 12B):0/25 → 18/25(从「API 不支持」到「大致能用」)
一年之内,native tool-calling 从「本地模型的集体残疾」变成「新世代的标配能力」。我在 blog 33 里那句「我们本地的 Gemma 根本不稳定 tool-calling」——对 2025 的模型是事实,对 2026 的模型已经是过时情报。
发现三:修复层碰到新模型,反而扣分#
这是对 proxy 点子的第二刀,也是更深的一刀。
看 qwen3.5:4b:native 25/25,shim 22/25。同一个模型,套上我的「修复层」反而掉了三题。granite4:tiny-h 更惨:22/25 被 shim 砍到 12/25。
原因不难理解:这些新模型在训练时就深度对齐了官方的 tool-calling 格式(自家的 chat template、自家的特殊 token)。我的 shim 把工具定义翻成「请只输出 JSON」的土法 prompt,等于强迫一个受过正规训练的人改用我发明的方言讲话——它原生的能力被我的「帮助」干扰了。
这对 proxy 的价值主张是致命的:
- 对旧模型(gemma3):shim 大胜,0 → 23。但你为什么不直接
ollama pull qwen3.5就好? - 对新模型(qwen3.5):shim 是负资产。
修复 proxy 唯一的生存空间,是「被锁死在旧模型上、又需要 tool-calling、又不能换模型」的用户。这个交集小到撑不起一个项目。
发现四:个案观察,各有各的死法(和活法)#
deepseek-r1:8b 的人格分裂:native 4/25(它根本不吐 native tool call,reasoning 完就直接用自然语言回答),shim 23/25(要求它吐 JSON 它就乖乖吐,而且品质很高)。Thinking 模型是 shim 条件下的最大受益者——<think> 区块剥掉之后,里面的 JSON 反而干净。
command-r7b 和 glm4:9b 的「沉默得分」:native 各 4/25,而那 4 分全部来自 nocall 类——也就是说它们从头到尾一个 tool call 都没发过,在「不该 call」的题目上靠沉默白拿四分。挂着 tools 标签但实际上是装饰。
HTML-in-JSON 是 Gemma 全家的死穴:gemma4 三个尺寸在 html 类通通 0/4,典型错误是 schema 验证抓到 operations[0] 缺 type 字段——结构在 HTML 跳脱的压力下散架。这正是 xl-ai 那个场景:所以就算你用 gemma4 接 BlockNote,血泪还是会重演。同场 qwen3.5 是 4/4。模型挑错,框架再怎么接都是白工。
mistral-nemo 的老兵不死:2024 年中的 12B 老模型,native 22/25。当年 Mistral 把 function calling 当卖点认真做了,两年后还能跟新世代掰手腕。训练目标有放 tool-calling 跟没放,差距比参数量大。
2026 年 6 月的新兵:整个世代都站在点子的对立面#
写这篇的当周,我把 ollama library 上 2026 年 5–6 月的新模型(塞得进 16GB 卡的)全部拉下来补测。如果说 qwen3.5 满分是一个点,那新兵们连成的就是一条线:
granite4.1:8b 和 ministral-3:8b 双双 24/25。 IBM 和 Mistral 的新世代,native 都只差一题满分。tool-calling 已经不是 Qwen 的独门绝活,是新世代的入场标配。
lfm2.5:8b 是整张表上最有戏的一列:Liquid AI 的官方文案就写「为消费级硬件上快速可靠的 tool calling 而生」,native 22/25 对得起这句话——但 shim 直接崩到 5/25,全表最大的反向落差。一个为原生格式特训过头的模型,你硬喂它土法 JSON prompt,它反而连话都不会讲了。这比 qwen3.5 掉 3 题更凶狠地证明了同一件事:修复层对新模型不是中性的,是毒药。
qwen3.6:27b,本次最贵的一题,21/25——输给自家 4B 小老弟。 17GB 的模型塞 16GB 的卡,14% 的层被挤到 CPU 上,推论速度掉一个数量级,光跑完 50 次推论就花了我快两小时。然后分数还比 qwen3.5:4b(3.4GB,全进 VRAM,飞快)低四题。给所有在消费卡上做本地 AI 的人一句话:小而新、塞得进 VRAM,赢过大而挤。 参数量的虚荣在 offload 面前一文不值。
functiongemma:270m 是彩蛋:Google 出的 270M function-calling 特化模型,native 7/25(以这个尺寸算能看了),shim 0/25——它整个模型就是围着原生格式蒸馏出来的,离开那个格式连一题都活不下来。特化模型的极端版本,刚好帮「格式锁定」这个现象做了对照组。
结论:点子死了,但死得很值#
回到那个修复 proxy。判决书三条:
- 问题正在自然消失。tool-calling 修复是一个「模型世代问题」,不是「结构性问题」。新一代开源模型(qwen3.5 为首)已经原生解掉它,而且会越来越好。
- 修复层对新模型是负资产。你的目标客群一升级模型,你的产品就从帮助变成干扰。
- 替代方案是一行命令。
ollama pull qwen3.5:4b,3.4GB,免费。没有任何 proxy 的安装成本能赢过这个。
所以这个项目在写下第一行产品 code 之前就被处决了。行刑的是我自己写的 25 个测试案例,全程一天。
如果你今天就要在本地做 tool-calling,我的实用建议浓缩成四行:
- 无脑选:qwen3.5(4b 就满分,VRAM 预算够就上 9b);granite4.1:8b 和 ministral-3:8b 是合格的第二志愿
- 塞不进 VRAM 的大模型不要硬上:qwen3.6:27b 在 16GB 卡上又慢又输 4B
- 要 thinking 模型:deepseek-r1 必须自己包 JSON 输出层,别碰它的 native tool call
- 避雷:gemma 全系列做结构化输出(尤其内容含 HTML/markup 时)、所有 2025 上半年以前的模型
最后想说#
上一篇的结尾我写:「官方库的美好 demo 背后通常假设了一个很强的后端」。这篇算是它的镜像:创业点子的美好蓝图背后,通常假设了一个不会变的世界。 我假设「本地模型不会 tool-calling」是个会持续存在的痛,但模型世代翻新的速度比我写 proxy 的速度快。
一天的 benchmark,杀死一个会吃掉我一两个月的项目。听起来是失败,其实是我今年做过最划算的交易——验证的价值不在于证明你是对的,在于趁便宜的时候发现你是错的。
benchmark 的 code 和全部 22 个模型的原始结果 JSON 都在 GitHub,25 个 case 涵盖你在真实产品里会撞到的形状(包括 xl-ai 那种 HTML-in-JSON 杀手题)。欢迎拿去测你手上的模型——如果你发现哪个模型把我的表打脸了,告诉我,我很乐意被打脸,反正已经习惯了。
还没有留言
✨ 成为第一个留言的人吧