Prhub

#40422 [Feature] add cohere reasoning and tool parsers

原始 PR 作者 walterbm 合并时间 2026-04-29 12:07 文件变更 6 提交数 7 评论 16 代码增减 +700 / -0

执行摘要

新增 Cohere Command A 推理与工具解析器

根据 PR 描述,目的是为 Cohere 模型(如 command-a-reasoning-08-2025)添加推理和工具解析器。依赖 Cohere 的 melody 库完成大部分处理。

本 PR 值得关注设计思路:如何将外部复杂的推理/工具解析器以可选方式集成到 vLLM 插件体系中。尤其是 try/except 动态导入 + 错误提示的实践,以及 PyFilter 有状态对象的管理策略。建议阅读代码中的 collect_tool_schema 使用 xgrammar 从 JSON Schema 生成 EBNF 语法的手段。但注意缺少测试覆盖,使用解析器时需谨慎。

讨论亮点

主要争议点:

  • 外部库依赖必要性(sfeng33, chaunceyjiang): 评审者质疑将解析逻辑外包给 cohere_melody 是否合适,建议使用 out-of-tree 插件。作者回应 out-of-tree 不满足要求,但将依赖改为可选动态导入。
  • 代码质量问题(gemini-code-assist): 指出 EBNF 生成中使用 str.replace 可能导致部分匹配替换,建议改用 re.sub 并已修复;发现调试 print 语句,已删除;指出 PyFilter 状态管理问题,建议存储 unary_opts 并在每次调用时创建新实例,已按建议修改。
  • 推理起始/结束符号(chaunceyjiang): 要求添加 reasoning_start_strreasoning_end_str 属性,已作为属性添加。

实现拆解

  1. 新增推理解析器 (vllm/reasoning/cohere_command_reasoning_parser.py): 定义 CohereTagRegistryCohereTagStyle 数据结构表示结构标签对,实现 collect_tool_schema 函数使用 xgrammar 从工具 Schema 生成 EBNF 语法。提供 CohereCommand3ReasoningParserCohereCommand4ReasoningParser 两个解析器类,继承 ReasoningParser,分别对应不同的模型架构(Cohere2ForCausalLM 等)。

  2. 新增工具解析器 (vllm/tool_parsers/cohere_command_tool_parser.py): 定义 BaseCohereCommandToolParser,内部使用 cohere_melodyPyFilter 对象处理流式和非流式工具调用提取。CohereCommand3ToolParserCohereCommand4ToolParser 继承基类,仅通过 PyFilterOptions().cmd3().cmd4() 区分配置。

  3. 注册解析器 (vllm/reasoning/__init__.pyvllm/tool_parsers/__init__.py): 在 _REASONING_PARSERS_TO_REGISTER_TOOL_PARSERS_TO_REGISTER 字典中分别添加 cohere_command3cohere_command4 条目,映射到实际模块和类名。

  4. 更新文档 (docs/features/tool_calling.mddocs/features/reasoning_outputs.md): 在工具调用和推理输出文档中添加 Cohere 模型的支持说明,包括启动参数和前置条件(需安装 cohere_melody)。

  5. 依赖处理: cohere_melody 不作为强制依赖,在导入位置使用 try/except ImportError 进行动态导入,并在错误提示中指导用户安装。

文件 模块 状态 重要度
vllm/reasoning/cohere_command_reasoning_parser.py 推理解析器 added 8.98
vllm/tool_parsers/cohere_command_tool_parser.py 工具解析器 added 8.5
vllm/reasoning/__init__.py 推理解析器 modified 5.27
vllm/tool_parsers/__init__.py 工具解析器 modified 5.27
docs/features/tool_calling.md 文档 modified 2.34
docs/features/reasoning_outputs.md 文档 modified 1.18

关键符号

collect_tool_schema _tool_definitions_to_schema_list convert_schema_to_structural_tags extract_tool_calls_streaming extract_tool_calls adjust_request

关键源码片段

vllm/reasoning/cohere_command_reasoning_parser.py core-logic

核心推理解析器文件,定义数据结构、EBNF 语法生成和两个解析器类,是整个 PR 的核心。

def collect_tool_schema(tool_schema: list[CohereNormalizedTool]) -> str:
    """Build an xgrammar EBNF grammar that matches a JSON array of tool calls.    The grammar shape is architecture-independent; callers are responsible for
    wrapping it in the correct structural tag (see ``CohereTagStyle.tools``).
    """
    tool_dictionary: dict[str, str] = {}
    for tool in tool_schema:
        tool_name = tool["name"]
        # 构建每个工具的 JSON Schema 字符串
        json_schema = f"""{{
            "type": "object",
            "properties": {{
                "tool_call_id": {{"type": "string", "pattern": "^[0-9]+$"}},
                "tool_name": {{"type": "string", "const": "{tool_name}"}},
                "parameters": {json.dumps(tool["parameters"])}
            }}
        }}"""
        # 使用 xgrammar 将 JSON Schema 转为 EBNF 规则
        tool_grammar = str(xgr.Grammar.from_json_schema(json_schema))
        # 通过正则替换,对规则名称添加 tool_name 前缀以避免冲突
        for match in re.findall(r"\b(\w+)\s*::=", tool_grammar):
            tool_grammar = re.sub(
                rf"\b{re.escape(match)}\b", tool_name + match, tool_grammar
            )
        tool_dictionary[tool_name] = f"{tool_name} ::= {tool_name}root\n{tool_grammar}"
    # 组合最终文法:根规则匹配 JSON 数组,每个数组元素为工具选项
    tool_alternatives = "tool ::= " + " | ".join(tool_dictionary.keys())
    tool_rules = "\n    ".join(tool_dictionary.values())
    grammar = f"""root ::= tools
tools ::= ws "[" ws tool ws ("," ws tool)* ws "]" ws
ws    ::= (" " | "\\t" | "\\n")*
{tool_alternatives}
{tool_rules}
"""
    return grammar
vllm/tool_parsers/cohere_command_tool_parser.py core-logic

核心工具解析器文件,定义基类和两个具体实现,通过 PyFilter 组件处理流式 / 非流式工具调用。

class BaseCohereCommandToolParser(ToolParser):
    def __init__(
        self,
        tokenizer: TokenizerLike,
        streaming_opts: PyFilterOptions,
        unary_opts: PyFilterOptions,
    ):
        super().__init__(tokenizer)
        # 保存初始选项,以便后续为新请求创建新的 PyFilter 实例
        self.unary_opts = unary_opts
        self.melody_streaming = PyFilter(streaming_opts)
        self.melody_unary = PyFilter(unary_opts)
​
    def adjust_request(self, request: ChatCompletionRequest | ResponsesRequest):
        request = super().adjust_request(request)
        # Cohere 模型需要保留特殊标记以识别推理 / 工具边界
        request.skip_special_tokens = False
        return request
​
    def extract_tool_calls_streaming(
        self, previous_text: str, current_text: str, delta_text: str,
        previous_token_ids, current_token_ids, delta_token_ids,
        request: ChatCompletionRequest,
    ) -> DeltaMessage | None:
        # 使用 stream 模式 PyFilter 处理增量文本
        r = self.melody_streaming.write_decoded(delta_text)
        if r.content is not None:
            return DeltaMessage(content=r.content)
        if r.reasoning is not None:
            return DeltaMessage(reasoning=r.reasoning)
        if r.tool_calls:
            return DeltaMessage(
                tool_calls=[DeltaToolCall(
                    id=tc.id, index=tc.index, type="function",
                    function=DeltaFunctionCall(name=tc.name, arguments=tc.arguments),
                ) for tc in r.tool_calls]
            )
        return None
​
    def extract_tool_calls(
        self, model_output: str, request: ChatCompletionRequest,
    ) -> ExtractedToolCallInformation:
        # 使用新的 unary PyFilter 实例处理完整文本,避免状态污染
        result = PyFilter(self.unary_opts).process_full_text(model_output)
        tool_calls = [ToolCall(
            id=tc.id, type="function",
            function=FunctionCall(name=tc.name, arguments=tc.arguments),
        ) for tc in result.tool_calls]
        return ExtractedToolCallInformation(
            tools_called=len(tool_calls) > 0,
            tool_calls=tool_calls,
            content=result.content,
        )

评论区精华

cohere-melody 依赖必要性及动态导入 设计

sfeng33 质疑将解析逻辑外包给外部库是否合适,建议使用 out-of-tree 插件。chaunceyjiang 不建议添加依赖到 common.txt,要求动态导入。

结论:作者改为 try/except 动态导入,保留依赖但设为可选。 · 已解决

EBNF 生成中 str.replace 安全问题 正确性

gemini-code-assist 指出 `str.replace` 可能导致部分匹配替换(如 'ws' 被误替换),建议用 `re.sub` 加边界。

结论:作者采纳建议,已修复为 `re.sub`。 · 已解决

PyFilter 状态管理问题 正确性

gemini-code-assist 指出 PyFilter 是有状态对象,多次调用会导致状态污染,建议存储 unary_opts 并在每次调用时创建新实例。

结论:作者在 reasoning parser 中添加 self.unary_opts 存储,并在 extract_content_ids 中使用新实例。 · 已解决

调试 print 语句残留 style

gemini-code-assist 发现存在调试 print 语句。

结论:作者已删除。 · 已解决

添加 reasoning_start/end_str 属性 feature

chaunceyjiang 要求添加 reasoning_start_str 和 reasoning_end_str 属性。

结论:作者已添加为属性。 · 已解决

风险与影响

  1. 外部库依赖风险: cohere_melody 是 Cohere 提供的库,但不属于 vLLM 核心依赖。虽然采用动态导入,但该库的稳定性和兼容性不在 vLLM 控制范围内,未来可能出现 API 变更或安全问题,需关注外部库升级。
  2. 缺少测试覆盖: PR 最初包含测试但因模型权限问题被移除。当前变更没有任何自动测试,解析器的正确性无法通过 CI 验证,增加了回归风险,特别是 PyFilter 状态管理和 EBNF 生成逻辑的变更容易引入隐蔽错误。
  3. 状态管理复杂度: PyFilter 是有状态对象,在多请求并发场景或重复使用时可能出现数据污染。虽然已通过存储 unary_opts 并创建新实例进行缓解,但未经过充分测试,仍需警惕。

用户影响: Cohere 模型用户可以指定 --tool-call-parser cohere_command3--reasoning-parser cohere_command3 来启用推理和工具调用功能。如果未安装 cohere_melody,vLLM 会给出明确安装提示。对其他模型用户无影响。
系统影响: 无数据面影响,仅在导入和使用第三方库时增加内存占用。控制面新增两个解析器的注册条目。
团队影响: 需要维护与 cohere_melody 库的接口兼容性,但大部分逻辑封装在外部队列中,维护负担较低。

缺少测试覆盖 外部依赖不可控 有状态对象需注意

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论