Prhub

#41068 [Bugfix] KimiK2ReasoningParser: guard against buffered end-token in streaming

原始 PR 作者 JasonKeyiL 合并时间 2026-05-05 01:42 文件变更 2 提交数 3 评论 11 代码增减 +70 / -0

执行摘要

修复 KimiK2 推理 parser 流式输出中 end-token 缓冲导致文本分裂错误

用户报告(issue #41067)KimiK2ReasoningParser 在配置 stop sequences 后流式输出被静默损坏,因为 output_text_buffer_length >0 造成 token ID 先于文本到达,而 parser 未处理此情况导致 find() 返回 -1。此修复保护了两种关键 token: 和 <|tool_calls_section_begin|>,与之前在其他 parsers(#39044、#40352)采用的模式一致。

值得精读,尤其对推理 parser 开发者和维护者展示了处理 token/text 时机不一致的通用模式。也建议关注后续可能有的补充修复 PR。

讨论亮点

用户 marcostephan 确认了此 bug,并指出仅此 PR 的修复仍不足以完全消除泄漏(8/8 次泄漏),建议在返回 None 之前还需额外处理。提交者 JasonKeyiL 同意先合并当前 PR,由 marcostephan 创建另外的 PR 进行补充修复。这是社区协作的体现,也提醒了修复可能只是第一步。

实现拆解

  1. 添加文本存在性检查:在 vllm/reasoning/kimi_k2_reasoning_parser.pyextract_reasoning_streaming 方法中,对于 </think><|tool_calls_section_begin|> 两种 token,在调用 find() 之前先判断其字符串是否已出现在 delta_text 中。若不存在(即文本仍被输出缓冲区延迟),立即返回 None,指示调用者等待下一个 delta。
  2. 保持正常流程:当文本存在时,继续执行原有的分割逻辑,无其他变更。
  3. 增加回归测试:在 tests/reasoning/test_kimi_k2_reasoning_parser.py 中新增 mock_kimi_k2_tokenizer fixture(使用 MagicMock 预定义词汇表),以及 test_streaming_end_token_id_bufferedtest_streaming_tool_section_id_buffered 两个测试,模拟 token ID 已到达但文本仍在缓冲的场景,验证 parser 返回 None 而不是错误的分割结果。测试无需 GPU 或下载模型,可快速执行。
文件 模块 状态 重要度
vllm/reasoning/kimi_k2_reasoning_parser.py 推理解析器 modified 5.23
tests/reasoning/test_kimi_k2_reasoning_parser.py 推理测试 modified 6.08

关键符号

extract_reasoning_streaming mock_kimi_k2_tokenizer test_streaming_end_token_id_buffered test_streaming_tool_section_id_buffered

关键源码片段

vllm/reasoning/kimi_k2_reasoning_parser.py core-logic

核心源码修改,添加了文本存在性检查逻辑,仅 7 行变更但修复了关键正确性 bug

    def extract_reasoning_streaming(self,
        previous_text: str,
        current_text: str,
        delta_text: str,
        previous_token_ids: Sequence[int],
        current_token_ids: Sequence[int],
        delta_token_ids: Sequence[int],
    ) -> DeltaMessage | None:
        """
        从流式 delta 消息中提取推理内容。
        """
        if self._identity_parser is not None:
            return self._identity_parser.extract_reasoning_streaming(
                previous_text, current_text, delta_text,
                previous_token_ids, current_token_ids, delta_token_ids)
​
        # 如果推理在之前的 tokens 中已结束,则这是内容
        if self.is_reasoning_end(previous_token_ids):
            return DeltaMessage(content=delta_text)
​
        # 跳过单一特殊 token
        if len(delta_token_ids) == 1 and delta_token_ids[0] in [
            self._start_token_id, self._end_token_id]:
            return None
​
        if self._end_token_id in delta_token_ids:
            if self._end_token not in delta_text:
                # Token ID 已到达但文本仍未刷新(stop-sequence 缓冲)。
                # 等待下一个 delta 时文本可见。
                return None
            end_index = delta_text.find(self._end_token)
            reasoning = delta_text[:end_index]
            content = delta_text[end_index + len(self._end_token):]
            return DeltaMessage(reasoning=reasoning, content=content if content else None)
​
        if self._tool_section_start_token_id in delta_token_ids:
            if self._tool_section_start_token not in delta_text:
                # Token ID 已到达但文本仍未刷新。
                return None
            tool_index = delta_text.find(self._tool_section_start_token)
            reasoning = delta_text[:tool_index]
            content = delta_text[tool_index:]
            return DeltaMessage(reasoning=reasoning, content=content)
​
        # 仍在推理中(无结束 token)
        return DeltaMessage(reasoning=delta_text)

评论区精华

当前修复不完整,需额外逻辑消除泄漏 正确性

marcostephan 报告在应用此 PR 后仍观察到 8/8 泄漏,提议在返回 None 前还需其他处理(如清除缓冲区标签逻辑)。JasonKeyiL 表示同意,并邀请另开 PR 补充修复。

结论:当前 PR 仍合并作为第一步,额外修复将作为单独 PR 跟进。 · 已解决

风险与影响

风险低。修改仅添加了两个提前返回 None 的分支,不会影响正常路径。返回 None 仅延迟一帧分割,不会导致错误文本。但 marcostephan 指出当前修复不完全,可能仍存在泄漏,需要后续跟进。

对使用 KimiK2 推理 parser 且配置 stop sequences 的用户,流式输出不再损坏。未使用 stop sequences 的用户无行为变化。影响范围仅限于 vLLM 推理流中的 KimiK2 parser,属于局部正确性修复。

修复不完整(需后续跟进) 低风险

关联 Issue

#39044 fix(reasoning): prevent streaming end-token desync in base and other parsers
#40352 [Bugfix][Reasoning] Handle buffered Step3 end-token deltas
#41067 [Bug]: KimiK2ReasoningParser silently corrupts streaming output when stop sequences buffer text

完整报告

参与讨论