tool_choice、parallel_tool_calls 与约束降级#
这章解决什么问题#
前面的结构化生成章节已经解释了 response_format、grammar backend、tool parser、SamplingParams 和 Responses 工作流,但还有一层非常工程化的细节值得单独成章:当调用方提供 tools 以后,tool_choice 和 parallel_tool_calls 到底怎样改变请求的约束形状?如果同时又给了 response_format、regex 或 ebnf,系统会怎样处理?
这不是协议边角料,而是结构化输出在真实工程里最容易踩坑的一层。
因为调用方真正关心的不是“系统理论上支持 tool call”,而是:
- 我要求必须调用工具时,输出到底会被约束成什么样
- 我指定某个工具时,系统会不会只保留这一把工具
- 我允许并行工具调用时,约束 schema 怎样变化
- 如果我同时给了 schema 和 tool call,系统会怎样取舍
为什么这一层值得单独讲#
如果只看 tools 字段,你很容易形成一个过度简化的心智模型:模型有工具列表,然后 parser 去解析输出,事情就结束了。实际不是这样。
在 python/sglang/srt/entrypoints/openai/protocol.py、serving_chat.py 和 function_call_parser.py 之间,系统还做了三层决策:
- 协议层先把默认
tool_choice补齐 - serving 层按
tool_choice过滤工具集并生成tool_call_constraint - parser 再根据 detector 能力选择
structural_tag、json_schema或不加额外约束
这说明 tool call 不是单一开关,而是一条多级编译链。
一张图:tools 怎样被编译成约束#
这张图解决的理解障碍是:很多读者会把 tool_choice 理解成“只影响 parser 后处理”,但它实际上会提前改变 sampling constraint。
flowchart LR
Req["Chat/Responses request"] --> Proto["protocol.py defaults"]
Proto --> Serve["serving_chat._process_messages()"]
Serve --> Filter["filter tools / choose parser"]
Filter --> Constraint["tool_call_constraint"]
Constraint --> Samp["to_sampling_params()"]
Samp --> Run["grammar / sampler / parser"]图比纯文字多解释的一点是:tool_choice 不是只在结果阶段生效,它会在生成开始前改写约束。
默认值是怎样决定的#
protocol.py 里,ChatCompletionRequest 的校验逻辑会做一个很关键的默认化:
- 如果没有
tools,默认tool_choice = "none" - 如果有
tools,默认tool_choice = "auto"
这意味着“给了工具但没写 tool_choice”不等于“系统不会走 tool call 路径”,而是会自动进入 auto 模式。很多排障问题都出在这里:调用方以为只是“顺手带了工具定义”,系统却真的把 tool parser 和约束逻辑接进来了。
named tool choice 为什么会先裁剪工具集#
在 serving_chat.py::_process_messages(...) 里,如果 tool_choice 不是字符串,而是命名 ToolChoice,系统会先把 request.tools 过滤成只剩被点名的那一个:
- 不是先让模型面对全部工具,再在结果阶段检查
- 而是先缩小提示和约束空间,再去生成
这很重要,因为它直接影响:
- 提示里出现哪些工具 schema
- parser 用哪组工具做校验
- 结构化约束最终长成什么样
从教学角度看,这一层解释了 named tool choice 的真正价值:它不是后验过滤,而是前置收缩生成空间。
FunctionCallParser.get_structure_constraint(...) 实际在做什么#
python/sglang/srt/function_call/function_call_parser.py 里最关键的分叉有两种:
detector 支持 structural_tag#
如果 detector 声明 supports_structural_tag(),系统更倾向生成 structural_tag 约束。此时:
required或 named tool choice 一定会生成带约束的 tagauto只有在工具或 parser 本身要求 strict 时才会主动加约束
这解释了为什么“同样是 tool call”,不同 parser/模型的约束强度可能不一样。
detector 不支持 structural_tag#
这时在 required 或 named tool choice 下,会退回 json_schema 约束。parallel_tool_calls 也会在这里被带进 get_json_schema_constraint(...),决定 schema 是允许单调用还是数组/多调用形状。
这就是为什么 parallel_tool_calls 不是 UI 级小参数,而是会真正改变约束结构。
parallel_tool_calls 改变了什么#
它最直接改变的是“工具调用结果是否必须是单个 call,还是可以是多个 call 的集合”。这会影响两层:
- 生成时的结构化约束
- parser 解析时允许出现的输出形态
在 required 或 named tool choice 下,这一点尤其关键。因为这时系统通常会生成更强约束,而 parallel_tool_calls 决定了“强约束的目标形状”到底是一项还是多项。
也正因为如此,parallel_tool_calls 不是性能参数,而是语义参数。
为什么会出现“约束降级”#
protocol.py::to_sampling_params(...) 里有一段非常值得写进书里的逻辑:如果当前请求已经有 regex、ebnf、structural_tag 或 json_schema 这类输出约束,再叠加 tool_call_constraint 时,系统不会试图把两套约束强行合并,而是记录 warning:
Constrained decoding is not compatible with tool calls.
这说明系统在这里选择的是保守降级,而不是做一个看上去更“智能”但实际更脆弱的多约束交叉编译器。
这是一处很典型的工程 tradeoff:
- 收益:行为边界更清晰,失败模式更可预测
- 代价:调用方不能同时指望 tool call 和另一套输出约束都被严格执行
response_format 与 tool call 冲突时应当怎样理解#
如果你同时设置:
response_format- 或
regex/ebnf - 又给了
tools
那么系统不会把它们自然地合成为一套统一约束。更稳的理解是:
response_format面向“最终输出形状”tool_choice面向“是否必须走工具调用回路”
这两者在很多场景里代表不同的产品意图。SGLang 当前实现更倾向把它们视作互斥或至少难以可靠组合的约束来源。
文中这里要明确区分 事实 与 推断:
事实:to_sampling_params(...)已明确在“已有约束 + tool_call_constraint”时打 warning,而不是合并推断:上游之所以这么做,是为了避免多约束组合带来的不可维护状态空间
Responses API 为什么更保守#
serving_responses.py 里对工具工作流有额外限制,例如只支持 tool_choice == "auto"。这说明不同 API surface 对工具工作流的容忍度并不完全一致。
如果你遇到“chat/completions 能跑,Responses API 不接受”的情况,不应先怀疑 parser,而应先看 surface 自己的协议限制。
这一层最容易踩的坑#
1. 以为给了 tools 但没写 tool_choice 就不会进 tool 模式#
默认会进入 auto。
2. 以为 named tool choice 只是结果阶段过滤#
实际上 serving 层会先裁剪工具集,再构造约束和模板。
3. 以为 parallel_tool_calls 只是“允许多调一次”#
它会改变约束 schema 的形状,因此会影响生成和解析两侧。
4. 以为 response_format 可以与 tool call 自然叠加#
当前实现会选择保守降级,而不是做强行合并。
如果 tool call 行为不符合预期,先怎么查#
建议按这个顺序:
- 看
protocol.py最终把tool_choice默认成了什么 - 看
serving_chat.py::_process_messages(...)是否过滤了工具集 - 看
FunctionCallParser.get_structure_constraint(...)返回的是structural_tag、json_schema还是None - 看
to_sampling_params(...)是否因为已有response_format/regex/ebnf而压掉了 tool constraint - 最后再看 parser 是否正确解析了模型输出
如果问题出在“模型根本没被约束住”,重点看前四步;如果问题出在“模型输出了,但 parser 解释错了”,重点看 detector 和流式解析。
小结#
这一章真正要补齐的,是结构化生成里最容易被轻描淡写的一层协议编译逻辑:
tool_choice会影响默认模式、工具集裁剪和约束形状parallel_tool_calls会改变工具调用结果的结构目标- parser 会根据 detector 能力选择
structural_tag或json_schema - 当已有其他输出约束时,系统会选择保守降级,而不是强行合并
把这层读清楚之后,你再看 tool parser、Responses API 和 SamplingParams 的交界,就不会再把它们误读成几组互不相关的特性。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。