执行摘要
- 一句话:新增 Kimi K2 流式解析引擎和 Parser
- 推荐动作:值得精读。该 PR 展示了如何基于 ParserEngine 构建新模型解析器,并成功将旧的独立解析器迁移到统一框架,对后续模型支持有示范作用。
功能与动机
为了支持 Kimi K2 模型系列(k2.5/k2.6/k2.7)的工具调用和推理内容解析,并统一到新的 ParserEngine 框架中,避免重复实现。如 PR body 所述,需要兼容 tool_choice=required 的流式与非流式调用。
实现拆解
- 新增
vllm/parser/kimi_k2.py,定义 kimi_k2_config 函数返回 ParserEngineConfig,配置有限状态机,包括 reasoning 和 tool call 标记的转换;定义 KimiK2Parser 类继承自 ParserEngine,提供 _emit_name_delta、_handle_tool_end、_handle_arg_chunk 等方法。
- 重写
vllm/tool_parsers/kimi_k2_tool_parser.py,改为继承 KimiK2ParserToolAdapter(由 make_adapters 生成),仅保留 adjust_request 方法,复用 ParserEngine 逻辑。
- 重写
vllm/reasoning/kimi_k2_reasoning_parser.py,直接设为 KimiK2ParserReasoningAdapter 别名,所有接口委托给 ParserEngine。
- 在
vllm/parser/engine/registered_adapters.py 中通过 make_adapters(KimiK2Parser) 生成适配器并导出。
- 测试配套:在
tests/parser/engine/trace_builder.py 中添加 Kimi K2 的词汇表和场景生成函数,注册到构建器字典;修改 tests/reasoning/test_kimi_k2_reasoning_parser.py 适配新的 parser 属性访问方式。
关键文件:
vllm/parser/kimi_k2.py(模块 解析器;类别 source;类型 core-logic;符号 kimi_k2_config, KimiK2Parser, init, _extract_tool_id_and_name): 核心新增文件,定义 Kimi K2 流式解析逻辑
vllm/tool_parsers/kimi_k2_tool_parser.py(模块 工具解析器;类别 source;类型 dependency-wiring;符号 KimiK2ToolParser, init, extract_tool_calls, _extract_content): 大幅简化,复用 ParserEngine 适配器
vllm/reasoning/kimi_k2_reasoning_parser.py(模块 推理解析器;类别 source;类型 dependency-wiring;符号 KimiK2ReasoningParser, init, reasoning_start_str, reasoning_end_str): 改为别名,委托给 ParserEngine
vllm/parser/engine/registered_adapters.py(模块 解析器注册;类别 source;类型 dependency-wiring): 注册 KimiK2Parser 适配器
tests/parser/engine/trace_builder.py(模块 测试生成器;类别 test;类型 test-coverage;符号 _kimi_k2_tool_segments, _kimi_k2_segments, _build_kimi_k2): 添加 Kimi K2 测试场景与词汇表
tests/reasoning/test_kimi_k2_reasoning_parser.py(模块 推理解析测试;类别 test;类型 test-coverage;符号 test_parser_selection_thinking_enabled, test_parser_selection_thinking_disabled, test_streaming_end_token_id_buffered, test_streaming_tool_section_id_buffered): 适配新的 ParserEngine 结构
关键符号: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
大幅简化,复用 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 KimiK2ParserToolAdapter
class 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
添加 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 segs
def _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
评论区精华
在 review 中,sfeng33 指出 tool_id 解析时应使用 removeprefix('functions.') 以兼容不同前缀的 functions.,建议仅在 functions. 开头时去除。sfeng33 还建议同时支持 enable_thinking 和 thinking 参数以兼容上游配置。chaunceyjiang 回复已修改(Done)。
- tool_id 解析去除 functions. 前缀 (correctness): chaunceyjiang 回复 Done,已修改。
- 同时支持 enable_thinking 和 thinking 参数 (design): 未收到明确回复,但 PR 已合并,可能已处理或计划后续跟进。
风险与影响
- 风险:主要风险:
- 旧版 KimiK2ToolParser 和 KimiK2ReasoningParser 的完整实现被删除,如果存在外部直接引用内部符号(如
_start_token_id)可能破坏兼容性;测试中已经适配新属性。
- ParserEngine 框架本身较新,可能存在边缘情况处理不完善。
- 配置键名称与旧版不一致可能导致迁移困惑。
- 影响:影响范围:仅影响 Kimi K2 模型用户,且改进了解析可靠性;对系统影响较小,因为基于已有的 ParserEngine 扩展。开发团队因此获得了可复用的 Kimi 模型解析器模板。
- 风险标记:旧代码完全删除, 测试覆盖变更, 配置键兼容性, ParserEngine 框架依赖
关联脉络
- PR #44135 Unknown (mentioned as bug report for tool_id parsing): 该 PR 的 review 中提及此 issue,与 tool_id 解析修复相关。
参与讨论