【工具调用:闭源与开源的天壤之别】
使用闭源模型进行工具调用十分顺畅,向 API 传入一个函数列表,模型调用这些函数后,就能得到结构化的 JSON 数据,且传输格式是透明的。然而,当转向开源模型时,工具调用依赖于引擎必须理解的传输格式,若引擎不支持该模型的格式,输出就会混乱不堪,比如参数中出现推理标记、JSON 格式错误、缺少工具调用等,此时要么等待,要么自己编写解析器。
【“支持一个模型”的真正含义是什么?】
每个模型家族对工具调用的编码方式都不同。以调用函数 `search(query="GPU")` 为例,[gpt - oss](Harmony)、DeepSeek、[GLM5] 这三种模型的传输格式各不相同,同样的操作,传输格式却不兼容,包括不同的标记词汇表、边界标记和参数序列化方案。为了返回包含生成工具调用的 JSON 对象数组,M 个应用程序(如 vLLM、SGLang、TensorRT - LLM、transformers 等)最终都要为其想要支持的每个模型编写自定义解析器,而这只是实现负担的一半。
【问题的复杂性体现在哪?】
Gemma 4 很好地说明了其中的难度。它的 `<|channel|>` 推理标记在解析器处理之前就被解码器剥离了,推理内容可能会泄露到工具调用参数中,该模型的非标准格式差异太大,以至于 llama.cpp 不得不放弃其通用自动解析器,构建一个专门的实现。这些都是训练时的格式选择导致的解析器错误。
【通用解析器为何难以应对?】
自然的做法是构建一个足够通用的解析器来处理所有格式,每个引擎都尝试过。一个合理的启发式方法,比如“找到特殊标记,提取它们之间的 JSON”,对某些格式效果还不错。但 Harmony 通过带有 `to=` 属性的 `<|channel|>` 进行路由,而 GLM5 则将参数序列化为 ``/`` 对,根本不是 JSON 格式。传输格式是训练时的决策,没有任何约束使其遵循统一的约定,可能的格式空间是开放的,因此通用解析器试图预测尚未做出的设计选择。这就是为什么通用解析器能处理常见情况,但无法消除每个模型特有的问题,而这些问题才是棘手的,比如推理标记泄露到参数中、解码器在解析器处理之前剥离特殊标记、生成结束信号与内容冲突等。不仅在解析结果时需要特定模型的格式知识,在生成过程中同样需要。这就是语法引擎发挥作用的地方。
【缺失的分离机制是什么?】
当一个新模型发布时,工作会在两个独立的地方进行。语法引擎,如 Outlines、XGrammar 和 llama.cpp 的语法支持,需要知道在生成过程中何处应用约束;vLLM、SGLang、TensorRT - LLM、transformers 中的输出解析器则需要将原始生成的文本提取为清晰的 API 响应,它们需要相反的格式知识。这些是不同的团队、不同的代码库、不同的发布周期,但他们需要的特定模型知识是相同的。如今,每个团队都要从聊天模板和文档中独立进行逆向工程。结果就是 N 个模型 × M 种实现方式,对相同的格式知识进行并行开发,却没有共享的约定。一个新模型发布,语法引擎维护者和推理引擎维护者都要从头开始进行相同的逆向工程工作。工具调用需要类似 Hugging Face 上共享聊天模板的分离机制,不是统一的传输格式,而是一种共享的声明式方式来描述它们。缺失的分离机制是将共享的格式知识提取到配置中,而不是代码中。模型发生变化时,只需更新规范,语法引擎和解析器无需改动。那么,如何才能尽快建立起这种分离机制呢?