Prhub

#43850 [Rust Frontend] Reduce Gemma4 tool parser args scan complexity

原始 PR 作者 BugenZhao 合并时间 2026-05-28 22:52 文件变更 2 提交数 1 评论 2 代码增减 +254 / -53

执行摘要

增量扫描 Gemma4 工具参数,性能提升约 600 倍

PR 作者指出原实现每次收到新 chunk 时都在缓冲区上重新运行完整参数解析器(gemma4_args),导致 O(n²) 性能开销。此 PR 通过仅在 ToolCall 模式下做轻量增量扫描,只在遇到结束标记时才触发一次完整解析,大幅降低长流式工具调用下的解析成本。

值得精读。该 PR 演示了如何通过引入轻量状态机避免重复全解析的经典优化技巧,设计决策清晰(保持框架扫描与完整语法分离),且性能数据令人印象深刻。对于需要使用流式工具解析的开发者有较高参考价值。

讨论亮点

无实质性的 Review 讨论。PR 作者在 body 中说明本 PR 是 #43513 的简化版本:选择更小的合约——只将 <tool_call|> 作为框架定界符,除了出现在 <|\"|> 字符串内部时;完整的参数解析仍由原解析器处理,避免在流式扫描器中重复部分语法。作者认为对模型格式而言,极少数 marker 形状文本被误分割为结束符是可接受的权衡。

实现拆解

  1. 引入 Gemma4Mode 状态机(TextHeaderToolCall),替换原来的二值化事件枚举 Gemma4Event
  2. 修改 parse_next_gemma4_event 函数,使其根据当前模式分发:Text 模式调用 parse_text_event 产出 Text 事件;Header 模式解析工具调用头(call:func_name{)并产出 ToolCallHeader 事件,同时模式转为 ToolCallToolCall 模式调用新增的 gemma4_raw_args_until_tool_call_end 函数进行增量扫描。
  3. 实现 gemma4_raw_args_until_tool_call_end:逐字节推进扫描位置,跟踪 <|\"|> 字符串分隔符的奇偶性,忽略字符串内部出现的 <tool_call|> 标记,直到在字符串外找到结束标记;返回本次扫描消耗的字节数。
  4. gemma4_raw_args_until_tool_call_end 成功找到结束标记时,将累计的原始参数字符串交由原 gemma4_args 解析器做一次完整解析,产生 ToolCall 事件并重置模式为 Text
  5. 重构 reset 方法,根据当前模式保留正确的前缀以便在中断后恢复时重建缓冲区。
  6. 新增基准测试用例 long_tool_argument_fixture,模拟带大量内容的流式工具调用参数,并注册到 criterion 基准组。
文件 模块 状态 重要度
rust/src/tool-parser/src/gemma4.rs 工具解析 modified 8.84
rust/src/tool-parser/benches/gemma4.rs 工具解析 modified 6.1

关键符号

Gemma4Mode Gemma4ArgsScanState parse_next_gemma4_event gemma4_raw_args_until_tool_call_end parse_text_event tool_call_header_event tool_call_args_event reset

关键源码片段

rust/src/tool-parser/src/gemma4.rs core-logic

核心逻辑变更:引入 Gemma4Mode 状态机、增量扫描函数 gemma4_raw_args_until_tool_call_end,以及对应的事件分发和复位逻辑。

/// 增量扫描 Gemma4 工具调用原始参数,直到遇到字符串外部的 `<tool_call|>` 结束标记。
/// 返回本次扫描消耗的字节数(不含结束标记本身)。
/// 通过跟踪 `<|\"|>` 字符串分隔符的奇偶性,避免将字符串内部的标记误判。
fn gemma4_raw_args_until_tool_call_end<'i>(
    input: &mut Gemma4Input<'i>,
    state: &mut Gemma4ArgsScanState,
) -> ModalResult<usize> {
    // 从上次扫描结束的位置继续
    let start = state.scanned_len;
    let mut offset = 0;
    let data = &input.as_ref()[start..];    // 逐个字节扫描,注意偏移量与实际字符串位置的转换
    // 标准写法使用 winnow 的 `take_until` 和 `literal` 组合,
    // 但为了跟踪 in_string 状态,这里手动推进
    while offset < data.len() {
        // 检查是否遇到字符串分隔符
        if data[offset..].starts_with(STRING_DELIM) {
            state.in_string = !state.in_string;
            offset += STRING_DELIM.len();
            continue;
        }
        // 仅在字符串外检查结束标记
        if !state.in_string && data[offset..].starts_with(TOOL_CALL_END) {
            // 消耗掉所有原始参数(结束标记之前的部分),更新扫描长度
            state.scanned_len += offset;
            return Ok(offset);
        }
        offset += 1;
    }
    // 未找到结束标记,消耗所有内容并报告不完全
    state.scanned_len += offset;
    Err(ErrMode::Incomplete(Needed::Unknown))
}/// 主分发函数:根据当前模式调用不同的事件解析逻辑
fn parse_next_gemma4_event<'i>(
    input: &mut Gemma4Input<'i>,
    parser: &mut Gemma4ToolParser,
) -> ModalResult<Gemma4Event> {
    match &parser.mode {
        Gemma4Mode::Text => {
            // Text 模式:解析普通文本,直到遇到 `<|tool_call>` 或 EOI
            let event = parse_text_event(input)?;
            Ok(event)
        }
        Gemma4Mode::Header => {
            // Header 模式:解析函数调用头,如 `call:func_name{`
            let event = tool_call_header_event(input)?;
            Ok(event)
        }
        Gemma4Mode::ToolCall { name: _, args_scan: state } => {
            // ToolCall 模式:增量扫描原始参数,直到结束标记
            // 如果找到标记,则取出完整参数并用 gemma4_args 解析
            let consumed = gemma4_raw_args_until_tool_call_end(input, state)?;
            // 如果成功(返回 Ok),表示找到了结束标记
            // 消耗掉输入中对应的内容(包括结束标记,但结束标记由上层处理)
             // 后续 : 使用已消耗的原始参数字符串调用 gemma4_args 解析器
        }
    }
}

注:上述代码为简化示意,实际实现使用了 winnow 的组合子优化性能。

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 标记误识别风险:如果工具参数中存在未引用的 <tool_call|> 文本(例如作为裸值),扫描器会将其误判为结束标记,导致提前触发解析。PR 作者认为这在模型格式中罕见且可接受。
  2. 状态复位风险:如果流式请求中途 reset,reset 方法需根据模式重建缓冲区前缀。若模式状态与缓冲区不一致,可能产生无效输入。当前实现显式处理了三种模式,但缺少针对异常中断的防御。
  3. 性能退化风险:短内容场景下,状态机切换和增量扫描的额外开销可能导致微小退化。基准测试显示混合场景仍在个位数微秒级,影响可忽略。
  4. 测试覆盖局限:当前只添加了基准测试,未增加单元测试覆盖边界状态转换(例如 reset 在 Header 模式下的行为、字符串分隔符嵌套等)。

影响范围:局限于 Gemma4 模型工具调用解析模块(Rust 前端),不影响其他模型或服务端逻辑。
影响程度:对使用 Gemma4 工具调用且流式参数较长的场景,解析延迟降低约 600 倍,大幅改善首 Token 感知延迟。短内容场景影响微乎其微。无 API 或行为变更,输出格式不变。

标记误识别 状态复位边界 短内容微退化

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论