Prhub

#42128 [Bugfix] Fix Gemma4ToolParser streaming float corruption

原始 PR 作者 abinggo 合并时间 2026-05-14 09:03 文件变更 2 提交数 2 评论 7 代码增减 +41 / -0

执行摘要

修复 Gemma4 流式浮点数损坏

关联 Issue #42047 报告 Gemma4ToolParser 流式输出浮点数错误(如 108.2 变 108.02)。根因是浮点值末尾带 '.'(如 "108.")被过早解析为 108.0,造成流式 diff 前缀不一致。

值得精读,展示了流式 diff 场景下防御性保留的典型处理模式。设计上只改动了最必要的部分,避免了过度工程。

讨论亮点

gemini-code-assist[bot] 提出两点核心讨论:

  • 性能优化:建议将 strip() 调用移到 if partial: 内部,避免非流式路径的额外开销(已被作者采纳并在第二 commit 修复)。
  • 语义争议:检查仅在值已被分隔符终止时触发(i < n),理论上值已完整,但作者 abinggo 解释为“防御性做法”,因为 Gemma4 并不会将末尾带点的浮点作为完整值输出;核心的不完整保护仍是 i >= n 的 break。

bbrowning 批准时评价“改动很小,回归风险低,最坏情况只是流式保- 持时间稍长,但远比输出错误浮点数好。”

实现拆解

  1. _parse_gemma4_args() 中增加 trailing-dot 保留检查(第 214-221 行):在 bare value 分支中,当 partial=True 且原始值以 '.' 结尾时,直接 break 保留该值,避免输出不完整的浮点 JSON。
  2. _parse_gemma4_array() 中增加对称检查(第 305-308 行):数组元素同样处理,防止数组中的浮点数损坏。
  3. 性能优化(第二次提交):将 raw_val = args_str[val_start:i].strip() 和条件检查移到 if partial: 块内,避免非流式路径产生额外开销。
  4. 回归测试:在 tests/tool_parsers/test_gemma4_tool_parser.py 中新增 test_trailing_dot_float_partial_withheld 测试方法(dict 和 array 各一个),验证拖尾点号在 partial 模式被保留、非 partial 模式正常解析,以及多个参数中受影响值不会影响已稳定的前面参数。
文件 模块 状态 重要度
vllm/tool_parsers/gemma4_tool_parser.py 工具解析器 modified 5.97
tests/tool_parsers/test_gemma4_tool_parser.py 测试 modified 5.41

关键符号

_parse_gemma4_args _parse_gemma4_array

关键源码片段

vllm/tool_parsers/gemma4_tool_parser.py core-logic

核心源码改动:在 _parse_gemma4_args 和 _parse_gemma4_array 中加入 trailing-dot 保留检查,防止流式解析输出错误浮点数。

# vllm/tool_parsers/gemma4_tool_parser.py (partial excerpt)def _parse_gemma4_args(args_str: str, *, partial: bool = False) -> dict:
    # ... 其他解析逻辑 ...
    while i < n:
        # ... 键解析 ...
        # Bare value (number, boolean, etc.)
        else:
            val_start = i
            while i < n and args_str[i] not in (",", "}", "]"):
                i += 1
            if partial and i >= n:
                # 值可能不完整(如部分布尔值)—— 保留避免类型不稳定
                break
            if i == val_start:
                logger.warning(
                    "Gemma4 args parser made no progress at position %d; "
                    "aborting on malformed input.",
                    i,
                )
                break
            # 新增:partial 模式下,如果值以 '.' 结尾则保留
            # 避免 float("108.") → 108.0 产生的 JSON rep "108.0" 造成流式 diff 损坏
            if partial:
                raw_val = args_str[val_start:i].strip()
                if raw_val.endswith("."):
                    break
            result[key] = _parse_gemma4_value(args_str[val_start:i])
    return resultdef _parse_gemma4_array(arr_str: str, *, partial: bool = False) -> list:
    # ... 类似逻辑 ...
    # Bare value
        else:
            val_start = i
            while i < n and arr_str[i] not in (",", "]"):
                i += 1
            if partial and i >= n:
                break
            if i == val_start:
                # ... 警告 ...
                break
            if partial:
                raw_val = arr_str[val_start:i].strip()
                if raw_val.endswith("."):
                    break
            items.append(_parse_gemma4_value(arr_str[val_start:i]))
    return items
tests/tool_parsers/test_gemma4_tool_parser.py test-coverage

新增回归测试,验证 trailing dot 保留行为正确,覆盖 dict 和 array 两种情况。

# tests/tool_parsers/test_gemma4_tool_parser.pydef test_trailing_dot_float_partial_withheld(self):
    """Bare float ending with '.' is withheld in partial mode.    Regression test for #42047: float("108.") → 108.0 causes
    streaming diff corruption (108.0 → 108.2 becomes 108.02).
    """
    # Single key with trailing dot — withheld entirely
    result = _parse_gemma4_args("left:108.,right:22.8", partial=True)
    assert result == {}
​
    # Stable key before trailing-dot key — stable key is kept
    result = _parse_gemma4_args(
        'name:<|"|">test<|"|>,score:3.,count:1', partial=True
    )
    assert result == {"name": "test"}
​
    # Non-partial mode parses trailing dot normally
    result = _parse_gemma4_args("left:108.,right:22.8", partial=False)
    assert result == {"left": 108.0, "right": 22.8}

评论区精华

性能:strip() 应在 partial 内部调用 性能

gemini-code-assist[bot] 建议将 strip() 移到 if partial: 块内,避免非流式路径开销。

结论:作者 abinggo 采纳建议并在第二次提交中修复。 · 已解决

语义:值已经被分隔符终止,是否还需要保留? 正确性

gemini-code-assist[bot] 指出当 i < n 时值已被分隔符终止,理论上完整。作者回应这是防御性做法,Gemma4 不会输出末尾带点的完整浮点值。

结论:保持防御性保留,不影响功能。 · 已解决

风险与影响

低风险。核心逻辑改动仅 12 行,在 partial 模式下增加了一个字符串前缀检查;非 partial 路径不受影响。最坏情况是流式保- 持可能略慢,但对用户体验影响极小。测试覆盖了 dict 和 array 两种场景。

  • 用户影响:Gemma4 模型流式工具调用场景浮点数错误修复,提升准确性。
  • 系统影响:无额外性能开销(除非 partial 模式,但 strip 仅在 partial 模式下执行)。
  • 团队影响:小范围增量修复,便于后续更全面的 parser 改进。
边界条件防御性修改

关联 Issue

#42047 [Bug] Gemma4ToolParser streams incorrect float values (e.g., 108.2 → 108.02)

完整报告

参与讨论