执行摘要
- 一句话:修复GLM等模型在tool_choice='required'或命名函数时因XML工具调用输出导致的解析失败。
- 推荐动作:该PR值得精读,展示了通过类级标志扩展工具解析器支持不同输出格式的设计模式,关注
supports_required_and_named的引入和请求调整逻辑,以学习如何优雅地处理模型特定行为,同时注意风险点如响应格式清理和测试覆盖。
功能与动机
根据PR body,GLM-5.1等模型使用XML工具调用语法定义在聊天模板中,而引导解码强制JSON输出,导致解析失败。讨论中sfeng33指出'required/named tool choice generally don't work well with models that generate xml style tool call',因此需要修复以支持自定义工具解析器处理这类场景。
实现拆解
- 添加类级标志:在
vllm/tool_parsers/abstract_tool_parser.py的ToolParser类中添加supports_required_and_named: bool = True属性,作为子类覆盖的开关,默认使用标准JSON解析。
- GLM解析器设置标志并调整请求:在
vllm/tool_parsers/glm4_moe_tool_parser.py和glm47_moe_tool_parser.py中设置supports_required_and_named = False,并重写adjust_request方法,当tool_choice为'required'或命名函数时跳过super().adjust_request()以避免设置structured_outputs,从而允许XML输出。
- 非流式请求处理:在
vllm/entrypoints/openai/engine/serving.py的_parse_tool_calls_from_content函数中,检查tool_parser_cls.supports_required_and_named标志,如果为False,则将required/named路由到工具解析器的extract_tool_calls方法;否则保持标准JSON解析逻辑。
- 流式请求处理:在
vllm/entrypoints/openai/chat_completion/serving.py的chat_completion_stream_generator中,添加tool_choice_uses_parser逻辑,当标志为False时,将required/named视为自动工具选择路径,使用工具解析器的extract_tool_calls_streaming方法处理流式输出。
- 测试配套:PR body提到手动测试GLM-4.7、GLM-5、GLM-5.1和Kimi-K2.5模型,但代码变更未包含测试文件,需注意测试覆盖不足的风险。
关键文件:
vllm/entrypoints/openai/engine/serving.py(模块 请求处理;类别 source;类型 core-logic;符号 _parse_tool_calls_from_content): 处理非流式请求的工具调用解析,是核心路由逻辑的关键文件。
vllm/tool_parsers/abstract_tool_parser.py(模块 抽象解析器;类别 source;类型 core-logic;符号 ToolParser): 定义工具解析器基类,新增supports_required_and_named标志,是所有解析器的行为开关。
vllm/tool_parsers/glm4_moe_tool_parser.py(模块 GLM解析器;类别 source;类型 core-logic;符号 Glm4MoeModelToolParser, adjust_request): GLM模型的具体工具解析器,设置supports_required_and_named为False并重写adjust_request以支持XML输出。
vllm/entrypoints/openai/chat_completion/serving.py(模块 流式生成;类别 source;类型 core-logic;符号 chat_completion_stream_generator): 处理流式请求的工具调用解析,新增tool_choice_uses_parser逻辑以支持回退到工具解析器。
vllm/tool_parsers/glm47_moe_tool_parser.py(模块 GLM47解析器;类别 source;类型 core-logic;符号 Glm47MoeModelToolParser): GLM-4.7模型的具体工具解析器,继承自Glm4MoeModelToolParser并设置相同标志。
关键符号:_parse_tool_calls_from_content, adjust_request, chat_completion_stream_generator
关键源码片段
vllm/entrypoints/openai/engine/serving.py
处理非流式请求的工具调用解析,是核心路由逻辑的关键文件。
def _parse_tool_calls_from_content(
request: ChatCompletionRequest,
tokenizer: TokenizerLike | None,
enable_auto_tools: bool,
tool_parser_cls: type[ToolParser] | None,
content: str | None = None,
) -> tuple[list[FunctionCall] | None, str | None]:
# 当工具解析器类存在且不支持 required/named 时,回退到工具解析器路径
if tool_parser_cls and (
enable_auto_tools
and (
request.tool_choice == "auto"
or request.tool_choice is None
or (
not tool_parser_cls.supports_required_and_named # 关键检查:如果为 False,则路由到解析器
and request.tools
and (
request.tool_choice == "required"
or isinstance(request.tool_choice, ChatCompletionNamedToolChoiceParam)
)
)
)
):
# 自动工具调用解析(也作为 required/named 的回退路径)
try:
tool_parser = tool_parser_cls(tokenizer, request.tools)
except RuntimeError as e:
logger.exception("Error in tool parser creation.")
raise e
tool_call_info = tool_parser.extract_tool_calls(
content if content is not None else "",
request=request,
)
if tool_call_info is not None and tool_call_info.tools_called:
function_calls = [
FunctionCall(
id=tool_call.id,
name=tool_call.function.name,
arguments=tool_call.function.arguments,
)
for tool_call in tool_call_info.tool_calls
]
content = tool_call_info.content
return function_calls, content
# 其他情况保持原有逻辑,如标准 JSON 解析或强制函数调用
# ...
vllm/tool_parsers/abstract_tool_parser.py
定义工具解析器基类,新增supports_required_and_named标志,是所有解析器的行为开关。
class ToolParser:
"""
抽象ToolParser类,不应直接使用。提供的属性和方法应在派生类中使用。
"""
# 当为 True(默认)时,服务层对 tool_choice="required" 和命名函数工具选择使用标准 JSON 解析,
# 适用于引导解码产生良好 JSON 输出的模型(如 Hermes)。
# 子类设为 False 当标准解析不适用于其模型输出格式时(如使用 XML 的 GLM 模型)。
# 当为 False 时,服务层回退到工具解析器的 extract_tool_calls / extract_tool_calls_streaming 方法,
# 将 required/named 视为与 "auto" 相同。
supports_required_and_named: bool = True
def __init__(
self,
tokenizer: TokenizerLike,
tools: list[Tool] | None = None,
):
self.prev_tool_call_arr: list[dict] = []
self.current_tool_id: int = -1
self.current_tool_name_sent: bool = False
self.streamed_args_for_tool: list[str] = []
self.model_tokenizer = tokenizer
if tools:
self.tools: list[ChatCompletionToolsParam | FunctionTool] = [
tool
for tool in tools
if isinstance(tool, (ChatCompletionToolsParam, FunctionTool))
]
else:
self.tools = []
# ... 其他方法
vllm/tool_parsers/glm4_moe_tool_parser.py
GLM模型的具体工具解析器,设置supports_required_and_named为False并重写adjust_request以支持XML输出。
class Glm4MoeModelToolParser(ToolParser):
"""GLM-4模型的工具解析器,支持增量字符串流式处理。"""
supports_required_and_named = False # 关键设置:禁用标准 JSON 解析
def adjust_request(
self, request: ChatCompletionRequest | ResponsesRequest
) -> ChatCompletionRequest | ResponsesRequest:
"""调整请求参数以处理工具调用令牌。
对于required/named tool_choice,跳过设置structured_outputs,
因为GLM模型以XML格式输出工具调用(根据聊天模板)。
引导解码会强制JSON输出,与XML格式冲突导致解析失败。
"""
if request.tools:
tc = request.tool_choice
if tc == "required" or isinstance(tc, ChatCompletionNamedToolChoiceParam):
# 不要为 required/named 调用 super().adjust_request(),
# 因为它会设置 structured_outputs 并通过引导解码强制 JSON 输出。
# GLM 模型使用 XML 工具调用语法,必须跳过引导解码以让模型自由输出 XML。
# 工具解析器从 XML 输出中提取工具调用。
if request.tool_choice != "none":
request.skip_special_tokens = False
return request
request = super().adjust_request(request)
if request.tools and request.tool_choice != "none":
request.skip_special_tokens = False
return request
# ... 其他方法
评论区精华
Copilot指出在adjust_request中可能未清理response_format,存在引导解码冲突风险;gemini-code-assist提示流式生成器中tc.function访问可能类型不安全;sfeng33确认supports_required_and_named标志默认应为True,GLM解析器设为False,并建议支持ResponsesRequest;JaredforReal相应修复;chaunceyjiang询问是否通用功能,结论是临时修复而非通用特性。
- supports_required_and_named默认值 (design): 已采纳,在abstract_tool_parser.py中设置supports_required_and_named: bool = True,GLM解析器设为False。
- 响应格式清理风险 (correctness): 未明确解决,代码中仅跳过super().adjust_request(),但未显式清理这些字段,存在潜在风险。
- 流式生成器类型安全 (correctness): 代码通过isinstance检查处理,但风险仍存,需注意未来维护。
- 功能通用性 (question): 确认是针对GLM模型的临时修复,但设计上通过标志允许其他解析器扩展。
风险与影响
- 风险:技术风险包括:
- 如果
response_format未清理,引导解码可能仍被启用,导致XML输出解析失败(Copilot评论)。
- 流式生成器中
tc.function的字典访问可能导致运行时类型错误(gemini-code-assist评论)。
- 新增的opt-in路径缺少集成测试,可能引入回归。
- 默认行为改变可能影响其他自定义工具解析器,若未设置标志可能意外使用JSON解析。
- 影响:对用户:GLM模型用户现在可以在tool_choice='required'或命名函数时正常使用工具调用,提升体验和功能完整性。对系统:增加了工具解析器的灵活性和可扩展性,支持更多模型输出格式,但代码复杂性增加。对团队:需要理解新标志的逻辑,并在未来开发中注意类似模型特定行为的处理方式。
- 风险标记:响应格式未清理, 类型安全风险, 缺少测试覆盖
关联脉络
- PR #39892 [Bugfix][Responses API] Fix streaming tool calls on /v1/responses: 同样涉及工具调用解析的修复,特别是在Responses API流式处理中,与本PR的XML输出支持有技术关联。
- PR #38579 [Bugfix] Kimi-K2 tool parser streaming - fix token leakage, argument truncation, and content dropping: 涉及工具解析器的流式处理改进,与本PR中对GLM解析器的调整类似,都属于工具调用模块的bug修复。
参与讨论