Prhub

#46610 [Frontend] Add Streaming Parser Engine and new Kimi k2.5/k2.6/k2.7 Parser

原始 PR 作者 chaunceyjiang 合并时间 2026-06-30 15:53 文件变更 6 提交数 6 评论 3 代码增减 +397 / -570

执行摘要

新增 Kimi K2 流式解析引擎和 Parser

为了支持 Kimi K2 模型系列(k2.5/k2.6/k2.7)的工具调用和推理内容解析,并统一到新的 ParserEngine 框架中,避免重复实现。如 PR body 所述,需要兼容 tool_choice=required 的流式与非流式调用。

值得精读。该 PR 展示了如何基于 ParserEngine 构建新模型解析器,并成功将旧的独立解析器迁移到统一框架,对后续模型支持有示范作用。

讨论亮点

在 review 中,sfeng33 指出 tool_id 解析时应使用 removeprefix('functions.') 以兼容不同前缀的 functions.,建议仅在 functions. 开头时去除。sfeng33 还建议同时支持 enable_thinkingthinking 参数以兼容上游配置。chaunceyjiang 回复已修改(Done)。

实现拆解

  1. 新增 vllm/parser/kimi_k2.py,定义 kimi_k2_config 函数返回 ParserEngineConfig,配置有限状态机,包括 reasoning 和 tool call 标记的转换;定义 KimiK2Parser 类继承自 ParserEngine,提供 _emit_name_delta_handle_tool_end_handle_arg_chunk 等方法。
  2. 重写 vllm/tool_parsers/kimi_k2_tool_parser.py,改为继承 KimiK2ParserToolAdapter(由 make_adapters 生成),仅保留 adjust_request 方法,复用 ParserEngine 逻辑。
  3. 重写 vllm/reasoning/kimi_k2_reasoning_parser.py,直接设为 KimiK2ParserReasoningAdapter 别名,所有接口委托给 ParserEngine。
  4. vllm/parser/engine/registered_adapters.py 中通过 make_adapters(KimiK2Parser) 生成适配器并导出。
  5. 测试配套:在 tests/parser/engine/trace_builder.py 中添加 Kimi K2 的词汇表和场景生成函数,注册到构建器字典;修改 tests/reasoning/test_kimi_k2_reasoning_parser.py 适配新的 parser 属性访问方式。
文件 模块 状态 重要度
vllm/parser/kimi_k2.py 解析器 added 9.25
vllm/tool_parsers/kimi_k2_tool_parser.py 工具解析器 modified 8.93
vllm/reasoning/kimi_k2_reasoning_parser.py 推理解析器 modified 8.93
vllm/parser/engine/registered_adapters.py 解析器注册 modified 5.66
tests/parser/engine/trace_builder.py 测试生成器 modified 7.32
tests/reasoning/test_kimi_k2_reasoning_parser.py 推理解析测试 modified 7.2

关键符号

kimi_k2_config KimiK2Parser.__init__ KimiK2Parser._emit_name_delta KimiK2Parser._handle_tool_end KimiK2Parser._handle_arg_chunk KimiK2Parser._extract_tool_id_and_name KimiK2Parser._extract_args_json KimiK2ToolParser.adjust_request KimiK2ReasoningParser (alias) _kimi_k2_tool_segments _kimi_k2_segments _build_kimi_k2

关键源码片段

vllm/tool_parsers/kimi_k2_tool_parser.py dependency-wiring

大幅简化,复用 ParserEngine 适配器

# vllm/tool_parsers/kimi_k2_tool_parser.py
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
from vllm.parser.engine.registered_adapters import KimiK2ParserToolAdapterclass KimiK2ToolParser(KimiK2ParserToolAdapter): # type: ignore[valid-type, misc]
    """Tool parser for Kimi K2 models.    Inherits all streaming logic from the ParserEngine-based adapter.
    """
    structural_tag_model = "kimi"
​
    def adjust_request(
        self,
        request: ChatCompletionRequest | ResponsesRequest,
    ) -> ChatCompletionRequest | ResponsesRequest:
        # When tools are enabled, keep special tokens as literal text
        # so the parser can see markers like <|tool_call_begin|>
        if request.tools and request.tool_choice != "none":
            request.skip_special_tokens = False
        return request
tests/parser/engine/trace_builder.py test-coverage

添加 Kimi K2 测试场景与词汇表

# tests/parser/engine/trace_builder.py# Kimi K2 特殊词汇表,将标记映射为 token ID
_KIMI_K2_VOCAB: dict[str, int] = {
    "<think>": 50,
    "</think>": 51,
    "<|tool_calls_section_begin|>": 60,
    "<|tool_calls_section_end|>": 61,
    "<|tool_call_begin|>": 62,
    "<|tool_call_end|>": 63,
    "<|tool_call_argument_begin|>": 64,
}def _kimi_k2_tool_segments(
    tool_calls: list[ToolCallSpec],
) -> list[tuple[str, bool]]:
    """生成工具调用标记序列,每个标记附带是否为 special token 的布尔值"""
    segs: list[tuple[str, bool]] = [("<|tool_calls_section_begin|>", True)]
    for index, tc in enumerate(tool_calls):
        # 将参数转为紧凑 JSON
        args = json.dumps(tc.arguments, ensure_ascii=False, separators=(",", ":"))
        segs.extend([
            ("<|tool_call_begin|>", True),
            # 函数 ID 格式:functions.<name>:<index>
            (f"functions.{tc.name}:{index}\n", False),
            ("<|tool_call_argument_begin|>", True),
            (args, False),
            ("<|tool_call_end|>", True),
        ])
    segs.append(("<|tool_calls_section_end|>", True))
    return segsdef _kimi_k2_segments(scenario: Scenario) -> list[tuple[str, bool]]:
    """构建完整输出段序列:可选 reasoning + 可选 content + 可选 tool calls"""
    segs: list[tuple[str, bool]] = []
    if scenario.reasoning is not None:
        segs.append(("<think>", True))
        segs.append((scenario.reasoning, False))
    if scenario.content is not None or scenario.tool_calls is not None:
        segs.append(("</think>", True))
    if scenario.content is not None:
        segs.append((scenario.content, False))
    if scenario.tool_calls is not None:
        segs.extend(_kimi_k2_tool_segments(scenario.tool_calls))
    return segs

评论区精华

tool_id 解析去除 functions. 前缀 正确性

sfeng33 建议在 _extract_tool_id_and_name 中使用 removeprefix('functions.') 以兼容不同前缀格式,并引用 PR#44135 的 bug report。

结论:chaunceyjiang 回复 Done,已修改。 · 已解决

同时支持 enable_thinking 和 thinking 参数 设计

sfeng33 建议 kimi_k2_config 同时支持 enable_thinking 和 thinking 两个参数名以兼容上游。

结论:未收到明确回复,但 PR 已合并,可能已处理或计划后续跟进。 · 待处理

风险与影响

主要风险:

  1. 旧版 KimiK2ToolParser 和 KimiK2ReasoningParser 的完整实现被删除,如果存在外部直接引用内部符号(如 _start_token_id)可能破坏兼容性;测试中已经适配新属性。
  2. ParserEngine 框架本身较新,可能存在边缘情况处理不完善。
  3. 配置键名称与旧版不一致可能导致迁移困惑。

影响范围:仅影响 Kimi K2 模型用户,且改进了解析可靠性;对系统影响较小,因为基于已有的 ParserEngine 扩展。开发团队因此获得了可复用的 Kimi 模型解析器模板。

旧代码完全删除 测试覆盖变更 配置键兼容性 ParserEngine 框架依赖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论