Prhub

#43662 [Rust Frontend] Align tool parser fallback behavior between streaming & non-streaming paths

原始 PR 作者 BugenZhao 合并时间 2026-05-27 18:13 文件变更 26 提交数 6 评论 4 代码增减 +1187 / -899

执行摘要

统一流式 / 非流式路径的工具解析器回退行为

参考 PR body:'Align Rust frontend tool parser fallback behavior between streaming and non-streaming paths. The core change is to let parser implementations append committed output into a caller-owned ToolParserOutput while keeping uncommitted buffered text recoverable on parse errors.' 目标是保证流式与非流式路径在遇到解析错误时行为一致,不丢失已成功解析的工具调用。

值得精读,尤其是 parse_intoreset 的设计,以及错误处理中保留部分输出的模式。建议关注 easy-ext 依赖的最终处理方式,以及是否所有解析器都正确实现了 reset

讨论亮点

审查中,gemini-code-assist[bot] 指出引入 easy-ext 依赖是不必要的,且可能引发编译问题(当 vllm-tool-parser 作为其他 crate 的依赖且启用 test-util feature 时)。建议手动实现扩展 trait 而非依赖 easy-ext。该建议未被明确回复,但 PR 最终被批准合并,可能已在后续提交中处理。

实现拆解

  1. 修改 Trait 定义:在 rust/src/tool-parser/src/lib.rs 中,将 ToolParser trait 的 push() 方法替换为 parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()>,并新增 reset(&mut self) -> String 方法,同时将 finish 的返回类型调整为 Result<ToolParserOutput>

  2. 更新所有解析器实现:对 qwen_coder.rsgemma4.rsdeepseek_v32.rsminimax_m2.rsllama.rskimi_k2.rsglm_xml 等文件逐一调整,修改 apply_event 签名以接受 &mut ToolParserOutput,将 reset 实现改为返回缓冲文本(std::mem::take),并重命名局部变量 resultoutput

  3. 调整调用适配器:在 rust/src/chat/src/output/default/tool.rsToolState 中,创建 ToolParserOutput 实例并调用 parse_into;在解析错误时,先处理已追加的部分输出(process_parser_output),再通过 parser.reset() 获取剩余缓冲文本并作为纯文本 delta 发出。

  4. 重命名与迁移:将 ToolParseResult 及所有引用重命名为 ToolParserOutput;将 parse_chunk/parse_complete 默认实现从 trait 移除,迁移到 ToolParserTestExt 扩展 trait(仅在 test-util feature 下可用),并更新测试代码(tests.rs、各 parser 的 mod tests)使用 use crate::ToolParserTestExt as _

  5. 测试配套:新增 DeepSeek V4 回归测试,验证一个完整工具调用后紧跟一个错误格式时,仍能保留第一个调用并只对错误部分回退。

文件 模块 状态 重要度
rust/src/chat/src/output/default/tool.rs 流式输出处理 modified 8.93
rust/src/tool-parser/src/qwen_coder.rs Qwen Coder 解析器 modified 8.78
rust/src/tool-parser/src/lib.rs 解析器接口 modified 8.06

关键符号

parse_into reset finish process_parser_output apply_event

关键源码片段

rust/src/chat/src/output/default/tool.rs core-logic

核心调用适配器,实现工具解析结果到 AssistantEvent 的转换,包含错误回退逻辑的主要变更。

fn process_text_delta(
    &mut self,
    kind: AssistantBlockKind,
    delta: String,
) -> Result<Vec<AssistantEvent>> {
    let mut events = Vec::new();    // 仅普通文本块且解析器未失败时才进行工具解析
    if kind != AssistantBlockKind::Text || self.parser_failed {
        self.open_call_index = None;
        events.push(AssistantEvent::TextDelta { kind, delta });
        return Ok(events);
    }    let mut output = ToolParserOutput::default();
    let parse_result = self.parser.parse_into(&delta, &mut output);    match parse_result {
        Ok(()) => self.process_parser_output(kind, output, &mut events)?,
        Err(error) => {
            warn!(
                error = %error.as_report(),
                "tool parser failed; falling back to plain text deltas"
            );
            self.parser_failed = true;
            // 即使解析失败,也先输出已经解析的部分
            self.process_parser_output(kind, output, &mut events)?;
            self.open_call_index = None;
            // 重置解析器并获取剩余缓冲文本,以纯文本 delta 发出
            push_text_delta(&mut events, kind, self.parser.reset());
        }
    }    Ok(events)
}

评论区精华

移除 easy-ext 依赖 设计

gemini-code-assist[bot] 在 review 中指出,引入 easy-ext 作为 dev-dependency 是不必要的,且当 vllm-tool-parser 作为其他 crate 的依赖且启用 test-util feature 编译时,dev-dependency 不会被包含,导致编译失败。建议手动实现扩展 trait。

结论:PR 作者未明确回应,PR 最终被合并,easy-ext 是否已移除不确定。 · closed

风险与影响

  1. 大规模机械变更(~1200 行)覆盖所有解析器,存在拼写或签名不匹配风险,但编译期可发现。
  2. reset() 返回类型从 () 变为 String,调用者需确保获取返回值(尤其在测试代码中)。
  3. 新增的 DeepSeek V4 回归测试覆盖了部分成功场景,但其他模型(如 Qwen Coder、Gemma4)可能缺少类似的错误恢复测试。
  4. 依赖 easy-ext 如果最终未移除,会在下游 crate 以非 dev 模式编译时引入传递依赖问题。

用户端:对齐流式与非流式响应中的工具调用行为,错误恢复更合理(不会丢弃部分成功的调用)。系统端:无破坏性变更,所有接口向后兼容(仅内部 trait 变化)。团队端:接口更清晰,未来新增解析器需遵循新方法;但机械变更审查负担较重。

机械性跨 parser 变更 依赖 easy-ext 编译风险 错误恢复测试未全覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论