执行摘要
- 一句话:升级 xgrammar 并引入内置 structural tags 支持更多模型 tool calling
- 推荐动作:建议团队精读此 PR,尤其是
get_structure_constraint 的重构和 base_format_detector.py 中 get_structural_tag 的集中化设计,这为未来添加更多模型的原生结构化约束奠定了良好基础。同时注意监控 Kimi 内置 tag 的 xgrammar 上游修复进度,以便及时重新启用。
功能与动机
PR body 明确指出升级 xgrammar 到 v0.2.0 以利用其 built-in structural_tags 特性,为更多模型提供结构化生成,尤其是 tool calling 场景。性能基准测试(BFCL-v3)显示开启 structural tags 后正确率显著提升。此外,社区反馈也期望支持更多模型(如 minimax)的结构化调用。
实现拆解
- 升级依赖并处理可选导入:在
pyproject.toml 中将 xgrammar 从 0.1.32 升级到 0.2.0,并在 base_format_detector.py 中使用 try/except ImportError 保护 xgrammar.StructuralTag 和 get_model_structural_tag 的导入,避免 XPU 等无 xgrammar 环境启动失败。
- 新增基类方法:在
BaseFormatDetector 中添加 get_structural_tag_name()(子类重写返回模型名称)和 get_structural_tag()(调用 get_model_structural_tag 生成原生 tag),子类只需提供名称即可复用统一逻辑。
- 重构约束生成逻辑:
FunctionCallParser.get_structure_constraint() 优先尝试 detector.get_structural_tag(),若返回非 None 则直接使用原生 tag;否则回退到旧有的 legacy_structural_tag 路径(需要 supports_structural_tag() 为 True);最后才是 JSON schema fallback。同时处理 thinking_mode 参数的传递,并在存在 ReasonerGrammarBackend 时不要求 xgrammar 包含 reasoning prefix。
- 新增模型 detector:添加
deepseekv4_detector.py,继承 DeepSeekV32Detector 但使用 <|DSML|tool_calls> 标签;qwen3_coder_detector.py 开启 supports_structural_tag 并返回 qwen_3_coder 模型名;deepseekv32_detector.py、gpt_oss_detector.py 等也添加了 get_structural_tag_name。
- 修复 speculative decoding 兼容性:
spec_utils.py 中的 traverse_tree 内部循环在调用 xgrammar 前将 0 维 tensor 显式转换为 Python int,以适配 xgrammar 0.2.0 的严格类型签名。
- 测试配套:在
test_function_call_parser.py 中新增 test_get_model_structural_tag 和 TestDeepSeekV4Detector 等测试,覆盖原生 structural tag 的生成与解析;新增 test_spec_utils_traverse_tree.py 回归测试,确保 tensor 转换正确。
关键文件:
python/sglang/srt/function_call/function_call_parser.py(模块 函数调用;类别 source;类型 core-logic;符号 get_structure_constraint, get_legacy_structural_tag, get_structure_tag): 核心解析器:重构 get_structure_constraint 方法,引入基于 get_structural_tag 的三级回退机制;重命名 get_structure_tag 为 get_legacy_structural_tag。
python/sglang/srt/function_call/base_format_detector.py(模块 函数调用;类别 source;类型 dependency-wiring;符号 get_structural_tag_name, get_structural_tag): 基类:添加 get_structural_tag_name 和 get_structural_tag 方法,集中实现 xgrammar 桥接逻辑,所有子类只需返回模型名称。
python/sglang/srt/function_call/deepseekv4_detector.py(模块 函数调用;类别 source;类型 core-logic;符号 DeepSeekV4Detector, init, get_structural_tag_name): 新增 DeepSeek V4 检测器,继承 V3.2 但使用 DSML 标签,支持两种参数格式(XML 或 JSON),并返回模型名 'deepseek_v4'。
test/registered/unit/function_call/test_function_call_parser.py(模块 测试;类别 test;类型 test-coverage;符号 TestDeepSeekV4Detector, test_get_model_structural_tag, test_detect_and_parse_xml_format, test_detect_and_parse_json_format): 大规模测试新增:覆盖 DeepSeek V4 detector 的 XML/JSON 格式解析、streaming 解析、以及 get_structural_tag 生成测试
test/registered/unit/spec/test_spec_utils_traverse_tree.py(模块 推测解码;类别 test;类型 test-coverage;符号 TestTraverseTreePassesIntsToGrammar, _record_grammar, record_accept, record_fill): 回归测试:验证 spec_utils.traverse_tree 在 xgrammar 0.2.0 下传递 int 而非 tensor,避免类型错误
关键符号:BaseFormatDetector.get_structural_tag_name, BaseFormatDetector.get_structural_tag, FunctionCallParser.get_structure_constraint, FunctionCallParser.get_legacy_structural_tag, DeepSeekV4Detector.init, DeepSeekV4Detector.get_structural_tag_name, Qwen3CoderDetector.get_structural_tag_name, DeepSeekV32Detector.get_structural_tag_name, GptOssDetector.get_structural_tag_name
关键源码片段
python/sglang/srt/function_call/function_call_parser.py
核心解析器:重构 get_structure_constraint 方法,引入基于 get_structural_tag 的三级回退机制;重命名 get_structure_tag 为 get_legacy_structural_tag。
# python/sglang/srt/function_call/function_call_parser.py
# 重构后的 get_structure_constraint 方法(部分)
def get_structure_constraint(
self,
tool_choice: Union[ToolChoice, Literal["auto", "required"]],
parallel_tool_calls: bool = True,
thinking_mode: bool = False,
) -> Optional[ToolCallConstraint]:
is_required = tool_choice == "required" or isinstance(tool_choice, ToolChoice)
should_constrain_auto = tool_choice == "auto" and (
any(tool.function.strict for tool in self.tools)
or self.tool_strict_level >= ToolStrictLevel.FUNCTION
)
# 1. 优先使用模型原生 structural_tag(xgrammar 0.2.0 内建)
# 例如 DeepSeek V4 / Qwen3 Coder 可通过 get_structural_tag_name 返回模型名
try:
if is_required or should_constrain_auto:
structural_tag = self.detector.get_structural_tag(
tools=self.tools,
thinking_mode=thinking_mode,
tool_choice=tool_choice,
)
if structural_tag is not None:
return ("structural_tag", structural_tag)
# 2. 回退到 legacy structural_tag(旧的 parser 内建约束)
if self.detector.supports_structural_tag():
tag = self.get_legacy_structural_tag(at_least_one=is_required)
return ("structural_tag", tag)
except Exception:
# 若 xgrammar 调用失败(如模型名不匹配),继续回退
pass
# 3. 最后回退到 JSON schema
if tool_choice == "required" or isinstance(tool_choice, ToolChoice):
json_schema = get_json_schema_constraint(
self.tools, tool_choice, parallel_tool_calls=parallel_tool_calls
)
return ("json_schema", json_schema)
return None
python/sglang/srt/function_call/base_format_detector.py
基类:添加 get_structural_tag_name 和 get_structural_tag 方法,集中实现 xgrammar 桥接逻辑,所有子类只需返回模型名称。
# python/sglang/srt/function_call/base_format_detector.py
# 在文件顶部,使用 try/except 保护 xgrammar 导入
# 使得无 xgrammar 的 XPU 环境也能正常加载模块
try:
from xgrammar import StructuralTag, get_model_structural_tag
except ImportError:
StructuralTag = Any # type: ignore[misc, assignment]
get_model_structural_tag = None
class BaseFormatDetector(ABC):
# ... 其他方法 ...
def get_structural_tag_name(self) -> Optional[str]:
"""子类重写返回 XGrammar 模型名称(如 "deepseek_v4"),
基类默认返回 None,表示不支持原生 structural tag。"""
return None
def get_structural_tag(
self,
tools: Union[List[Tool], None] = None,
tool_choice: Union[ToolChoice, Literal["auto", "required"]] = "auto",
thinking_mode: bool = False,
) -> Optional[StructuralTag]:
"""
生成模型原生 XGrammar structural tag。
当 --reasoning-parser 配置时,SGLang 的 ReasonerGrammarBackend
会处理 <think>...</think> 前缀,因此此处传入 reasoning=thinking_mode
但只在不需要外层推理时才让 xgrammar 包含 reasoning prefix。
"""
structural_tag_name = self.get_structural_tag_name()
# 既没有模型名,也没有 xgrammar 可用时直接返回 None
if not structural_tag_name or get_model_structural_tag is None:
return None
# 将 SGLang 的 Tool 对象转换为 dict
converted_tools = [tool.model_dump() for tool in tools or []]
converted_tool_choice = (
tool_choice.model_dump()
if isinstance(tool_choice, ToolChoice)
else tool_choice
)
return get_model_structural_tag(
model=structural_tag_name,
tools=converted_tools,
tool_choice=converted_tool_choice,
reasoning=thinking_mode,
)
python/sglang/srt/function_call/deepseekv4_detector.py
新增 DeepSeek V4 检测器,继承 V3.2 但使用 DSML 标签,支持两种参数格式(XML 或 JSON),并返回模型名 'deepseek_v4'。
# python/sglang/srt/function_call/deepseekv4_detector.py
import logging
from sglang.srt.function_call.deepseekv32_detector import DeepSeekV32Detector
logger = logging.getLogger(__name__)
class DeepSeekV4Detector(DeepSeekV32Detector):
"""
检测 DeepSeek V4 模型函数调用格式。
使用 DSML 标签(如 <|DSML|tool_calls>)而非 V3.2 的 <|DSML|function_calls>。
支持两种参数格式:
1. XML Parameter Tags: <|DSML|parameter name="...">value</|DSML|parameter>
2. Direct JSON: { "param_name": "value" } 直接放在 <|DSML|invoke> 内
"""
def __init__(self):
super().__init__()
# V4 使用 tool_calls 而非 function_calls
self.bot_token = "<|DSML|tool_calls>"
self.eot_token = "</|DSML|tool_calls>"
self.function_calls_regex = r"<|DSML|tool_calls>(.*?)</|DSML|tool_calls>"
def get_structural_tag_name(self) -> str:
# 返回 xgrammar 内建模型名称,用于 get_model_structural_tag
return "deepseek_v4"
评论区精华
Review 中主要讨论了以下几个关键点:
风险与影响
- 风险:
- 依赖升级风险:xgrammar 0.2.0 可能与其他依赖(如 NPU 工作流使用的
0.1.25)冲突,PR 中已对 NPU 工作流也做了升级(从 0.1.25 跳到 0.2.0),但未充分验证可能引入不兼容。
- XPU 导入失败风险:虽然使用了
try/except 保护导入,但若 xgrammar 的 StructuralTag 被用作类型注解且运行时缺失,仍可能导致某些路径抛出 NameError(当前代码仅在运行时方法中引用,风险较低)。
- Kimii/Kimi 解析弃用:由于 xgrammar 内置 Kimi 语法缺少 section wrapper,当前对 Kimi 禁用了内置 structural tag,但若未来 xgrammar 修复后忘记重新启用,可能导致 Kimi 工具调用约束退化。
- 测试覆盖不全:PR 主要测试了 Qwen3 Coder 和 DeepSeek V4,但 Kimi K2 和 GPT-OSS 的
get_structural_tag 覆盖未添加单元测试,可能隐藏问题。
- 核心解析路径变更:
get_structure_constraint 的重构改变了工具调用约束的优先级,若某个 detector 的 get_structural_tag 抛出异常(如模型名称不匹配),将导致请求 500 错误(已有 try 块但未处理所有异常)。
- 影响:功能影响:为 DeepSeek V4、Qwen3 Coder 等模型启用了原生结构化工具调用,可显著提高工具调用格式正确率(BFCL 测试显示提升 5-10%)。系统影响:依赖升级可能影响安装流程;NPU 工作流可能因版本跳跃需要额外验证。团队影响:函数调用解析器的架构更加统一,后续添加新模型只需在 detector 中实现 get_structural_tag_name 即可。用户影响:使用相关模型时工具调用可靠性提升,但 tool_choice="auto" 且未启用 strict 模式时行为不变。
- 风险标记:依赖升级未完全验证, XPU 兼容性依赖保护, Kimi 内置 tag 暂禁需跟踪上游修复, 核心解析路径新异常未覆盖, 部分 detector 缺少 get_structural_tag 测试
关联脉络
- PR #25600 Add MiniCPM5 tool call parser for XML-style function calls: 同样涉及 tool calling 解析器扩展,修改了 function_call_parser.py 和 template_detection.py,与本 PR 的解析器重构有重叠逻辑。
- PR #26085 drop
FutureIndices wrapper class: 修改了 speculative decoding 相关模块,与本 PR 对 spec_utils.py 的 tensor 类型修复有间接关联。
参与讨论