Prhub

#21593 Fix tool call constrained decoding and parsing for models with native formats

sgl-project/sglang · 作者 JustinTong0323 · 合并时间 2026-04-11 11:37

分析状态 已生成
文件变更 9提交数 19 · 评论 33
代码增减 +306 / -61
bugfix deepseek run-ci feature

执行摘要

修复工具调用约束解码与解析,确保原生格式模型在 required 模式下正确使用 structural_tag 并强制至少一个调用。

根据PR body描述,当模型配置了特定--tool-call-parser(如kimi_k2deepseekv3qwen25)时,tool_choice="required"此前强制使用通用JSON模式约束,与模型的原生工具调用格式(如特殊标记<|tool_calls_section_begin|>)冲突,并尝试将模型输出解析为纯JSON,导致解析失败并返回tool_calls: null

该PR值得精读,特别是function_call_parser.py中的get_structure_constraint()方法设计,展示了如何权衡模型原生格式与OpenAI协议要求。关注at_least_one标志的引入和supports_structural_tag()检查的逻辑,这对理解约束解码机制有重要价值。

讨论亮点

Review中核心讨论来自AgainstEntropy,建议在非流式_process_tool_calls方法中显式检查supports_structural_tag(),以与流式路径保持一致。JustinTong0323采纳该建议,修改代码添加守卫,确保逻辑一致性。讨论焦点在于正确性,未解决其他重大疑虑。

实现拆解

实现拆解为以下模块:1) 协议层(protocol.py):添加at_least_one: bool = False字段到LegacyStructuralTagResponseFormat,支持强制工具调用。2) 文法后端(xgrammar_backend.py):传递at_least_one字段到xgrammar的StructuralTag API。3) 功能调用解析器(function_call_parser.py):修改get_structure_constraint()方法,为required或命名工具选择使用structural_tag约束并设置at_least_one=True;同时添加supports_structural_tag()检查。4) 服务层(serving_chat.py):更新非流式工具调用处理逻辑,镜像流式路径,使用supports_structural_tag()守卫以确保正确解析。5) 检测器修复(base_format_detector.pydeepseekv3_detector.py):修复流式解析中的空参数处理和deepseekv3格式前缀。6) 测试更新:添加单元测试和集成测试覆盖新逻辑。

文件 模块 状态 重要度
python/sglang/srt/function_call/function_call_parser.py function call parsing modified 9.0
python/sglang/srt/entrypoints/openai/serving_chat.py OpenAI serving modified 8.0
python/sglang/srt/constrained/xgrammar_backend.py constrained decoding modified 6.0
python/sglang/srt/entrypoints/openai/protocol.py protocol definitions modified 5.0
test/registered/unit/function_call/test_function_call_parser.py testing modified 7.0

分析完成后,这里会展示 LLM 生成的相对完整源码片段和详细注释。

关键符号

get_structure_constraint _process_tool_calls parse_streaming_increment structure_info

评论区精华

检查 supports_structural_tag() 在非流式路径 正确性

AgainstEntropy 建议在非流式 _serving_chat.py 的 _process_tool_calls 方法中显式检查 supports_structural_tag(),以与流式路径保持一致。

结论:JustinTong0323 采纳建议并修改代码添加守卫,确保逻辑一致性。 · 已解决

风险与影响

技术风险包括:1) 回归风险:修改核心解析逻辑(serving_chat.py)可能影响现有工具调用功能,需通过测试覆盖验证。2) 兼容性风险:对多种模型原生格式的支持需确保所有检测器正确处理at_least_one标志,特别是那些不支持structural_tag的模型(如lfm2)。3) 性能风险:添加额外检查和解析步骤可能轻微影响性能,但预计可忽略。4) 安全风险:无显著安全风险,但解析错误可能导致数据不一致。

影响范围:1) 用户影响:修复了使用原生格式模型(如DeepSeek、Kimi、Qwen等)时tool_choice="required"失败的问题,提升工具调用可靠性和用户体验。2) 系统影响:改进解析准确性和OpenAI协议合规性,确保strictrequired标志正确交互。3) 团队影响:代码更一致,测试覆盖增加,便于未来维护和扩展。

核心解析路径变更 跨模型兼容性风险 测试覆盖依赖

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

执行摘要

本PR修复了当模型配置了原生工具调用格式时,tool_choice="required"模式下约束解码与解析失败的问题。通过引入at_least_one标志强制至少一个工具调用,并调整解析逻辑以支持模型原生格式(如DeepSeek、Kimi、Qwen等),提升了OpenAI协议合规性和用户体验。该变更影响多个核心模块,经过充分测试和review讨论,是一个重要的bugfix。

功能与动机

为什么做? 当使用模型特定的--tool-call-parser(例如kimi_k2deepseekv3qwen25)时,tool_choice="required"此前强制使用通用JSON模式约束,与模型的原生工具调用格式(如特殊标记<|tool_calls_section_begin|>)冲突,导致解析失败并返回tool_calls: null。这违反了OpenAI协议中required必须调用工具的要求。

关键引用: PR body明确指出:“tool_choice="required" previously forced a generic JSON schema constraint that conflicted with the model's native tool call format... resulting in tool_calls: null。”

实现拆解

按模块梳理关键改动点:
| 模块 | 关键改动 | 影响 |
|------|----------|------|
| 协议层 (protocol.py) | 添加 at_least_one: bool = False 字段到 LegacyStructuralTagResponseFormat | 支持强制工具调用的语义传递 |
| 文法后端 (xgrammar_backend.py) | 传递 at_least_one 字段到 xgrammar 的 StructuralTag API | 实现解码时强制至少一个工具调用 |
| 功能调用解析器 (function_call_parser.py) | 修改 get_structure_constraint(),为 required/命名工具选择使用 structural_tag 并设置 at_least_one=True;添加 supports_structural_tag() 检查 | 核心约束逻辑调整,确保原生格式优先 |
| 服务层 (serving_chat.py) | 更新 _process_tool_calls() 方法,镜像流式路径逻辑,使用 supports_structural_tag() 守卫 | 修复非流式解析失败,提升一致性 |
| 检测器修复 (base_format_detector.py, deepseekv3_detector.py) | 修复 parse_streaming_increment() 中空参数检查;修复 structure_info() 格式前缀 | 确保流式解析正确性和模型识别 |
| 测试更新 | 新增 TestProcessToolCallsWithRequiredToolChoiceTestGetStructureConstraint 等测试类 | 覆盖新逻辑,防止回归 |

关键代码示例(来自 function_call_parser.py):

def get_structure_constraint(self, tool_choice, parallel_tool_calls=True):
    if self.detector.supports_structural_tag():
        is_required = tool_choice == "required" or isinstance(tool_choice, ToolChoice)
        if is_required or (tool_choice == "auto" and strict_condition):
            tag = self.get_structure_tag(at_least_one=is_required) # 设置 at_least_one
            return ("structural_tag", tag)

评论区精华

Review讨论聚焦于逻辑一致性:

  • AgainstEntropy 提问:“Should we also explicitly check supports_structural_tag() in _process_tool_calls just as here?”
  • JustinTong0323 回应:“Good catch! Added supports_structural_tag() guard in the non-streaming _process_tool_calls path to mirror the streaming logic.”

这确保了非流式与流式路径行为一致,避免了潜在解析错误。

风险与影响

具体风险:

  1. 回归风险serving_chat.py 的解析路径变更可能影响现有工具调用功能,但通过新增单元测试(如 TestProcessToolCallsWithRequiredToolChoice)已覆盖。
  2. 兼容性风险:对于不支持 structural_tag 的模型(如 lfm2),PR 通过回退到 JSON 数组解析处理,但需确保所有检测器正确实现。
  3. 性能影响:添加 supports_structural_tag() 检查可能增加轻微开销,但在工具调用场景中可忽略。

影响范围:

  • 用户:修复了 deepseekv3、kimi_k2、qwen25 等模型在 required 模式下的工具调用失败,提升可靠性。
  • 系统:改进 OpenAPI 协议合规性,正确分离 strictrequired 语义。
  • 团队:代码更清晰,测试增强,为后续工具调用功能扩展奠定基础。

关联脉络

与历史 PR 的关联:

  • PR #20310(tokenizer 改进):同样涉及解析逻辑优化,但专注于非流式请求处理;本 PR 在工具调用解析方面延续了类似的设计模式,强调性能与正确性权衡。

从近期 PR 趋势看,仓库在持续优化解析和约束解码机制(如 #22404 修复 CUDA Graph 捕获),本 PR 是这一方向的重要补充,专注于提升多模型工具调用支持。

参与讨论