tool choice、function calling 与 parser#

上一节讲的是“怎样限制输出空间”,这一节讲的是“怎样把已经生成出来的结构解释成工具调用语义”。这两件事容易被混在一起,但它们不是同一个层次的问题。

这一节解决什么问题#

这一节主要回答三件事:

  1. tool choice 为什么最终还是要落到执行约束里;
  2. function calling parser 为什么不是 grammar backend;
  3. parser 在 streaming 和 non-streaming 场景里分别承担什么角色。

一张图先看 function calling 的两层关系#

flowchart LR
    A["tool_choice / tools"] --> B["get_structure_constraint(...)"]
    B --> C["json_schema / structural_tag constraint"]
    C --> D["SamplingParams"]
    D --> E["model output"]
    E --> F["FunctionCallParser parse"]
    F --> G["tool call objects"]

这张图最重要的一点是:tool choice 先改写执行期约束,parser 再解释输出结果。这两件事都属于 function calling,但不发生在同一层。

parser 解决的不是“限制”,而是“解释”#

FunctionCallParser 的职责从类定义就写得很清楚:它处理的是 function / tool call 的解析,而不是 token 级约束本身。

它和 grammar backend 的边界可以先压成一句话:

  • grammar backend 负责限制生成空间;
  • parser 负责解释已经生成出来的结构。

如果把这两层混在一起,就会把“输出为什么没被限制住”和“输出已经限制住了但解释失败了”看成同一种问题。

tool_choice 怎样进入运行时#

FunctionCallParser.get_structure_constraint(...) 很关键,因为它说明 tool choice 不是“生成以后再决定怎么解释”,而是会先转成结构化约束,再进入采样参数:

if tool_choice == "required" or isinstance(tool_choice, ToolChoice):
    json_schema = get_json_schema_constraint(...)
    return ("json_schema", json_schema)

这说明 tool choice 先改写的是执行期约束,然后 parser 才在输出侧继续解释结果。

这件事在 serving 层里的接法也很直接。OpenAIServingChat tool call constraint setup 会先把 parser 产出的约束编进请求,再由 streaming / non-streaming 路径继续消费。

为什么 parser 不能被省掉#

即使模型已经被约束在“像工具调用”的结构空间里,调用方依然不能直接拿原始输出当成工具调用对象。原因很简单:

  • 不同模型输出 function call 的具体格式并不完全一样;
  • streaming 场景里,调用信息可能是分段到达的;
  • non-streaming 场景里,仍然需要把结果拆成 normal text 和 tool calls。

这正是 parser 存在的必要性:它把“看起来像工具调用的输出”变成真正可消费的调用对象。

streaming 和 non-streaming 为什么都需要 parser#

FunctionCallParser 不是只为非 streaming 服务。它的 streaming 价值在于:每次有新文本增量到来时,系统都可以尝试把其中已经成形的一部分解释成 tool call。

而在 non-streaming 场景里,parser 则更像最后一道解释层:把完整输出一次性拆成:

  • normal text
  • tool call list

所以 parser 的角色不是“有工具时顺便用一下”,而是 function calling 能否真正被工程系统消费的关键一环。

而在 streaming 路径里,OpenAIServingChat tool call streaming branch 还会继续决定:哪些增量文本已经足够被解释成 tool call,哪些还必须暂存在 parser 状态里等待下一段输出。

调试 function calling 时先看哪里#

如果你看到的现象是:

  • 输出已经像函数调用,但 parser 没认出来;
  • parser 认出来了,但 tool choice 行为和预期不符;
  • streaming 下工具参数被拆得很奇怪;

更稳的顺序通常是:

  1. 先确认 tool choice 是否已经正确落到结构化约束;
  2. 再看 parser 是否和当前模型输出格式匹配;
  3. 最后再看 serving 层在 streaming 或 non-streaming 路径里怎样消费 parser 输出。

小结#

这一节真正要建立的是一个判断:

  • tool choice 先改写执行期约束;
  • parser 再解释输出结果;
  • function calling 只有把“限制”和“解释”两层接起来,才算真正能用。

理解了这点,后面再看 Responses API 和 built-in tools 时,就不会把 parser 误看成一个可有可无的边角模块。