执行摘要
本 PR 对齐了 Rust 前端流式与非流式路径的工具解析器回退行为。核心是将解析器入口从 push() 改为 parse_into(),让解析器将已提交输出追加到调用者拥有的 ToolParserOutput 中,同时在解析错误时保留已成功解析的部分并通过 reset() 获取剩余缓冲文本作为纯文本回退。该变更涉及所有 8 个解析器实现和调用适配器,并新增 DeepSeek V4 回归测试。该设计确保了工具调用错误恢复的合理性,不会丢弃部分成功的调用。
功能与动机
参考 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.' 之前流式路径在解析错误时会丢失所有已解析的工具调用,而非流式路径可以正确处理。此 PR 统一了行为,使两种路径在遇到解析错误时都能保留已成功解析的工具调用。
实现拆解
-
Trait 定义变更(rust/src/tool-parser/src/lib.rs):将 ToolParser trait 的 push() 替换为 parse_into(),新增 reset() 方法返回已有缓冲文本,调整 finish() 返回类型为 ToolParserOutput。
-
所有解析器实现更新(qwen_coder.rs、gemma4.rs、deepseek_v32.rs、minimax_m2.rs、llama.rs、kimi_k2.rs、glm_xml 等):重命名 ToolParseResult 为 ToolParserOutput,修改 apply_event 方法签名以接受 &mut ToolParserOutput,将 reset() 改为返回 String(使用 std::mem::take 获取缓冲区内容),并重命名局部变量 result 为 output。
-
调用适配器调整(rust/src/chat/src/output/default/tool.rs):ToolState::process_text_delta 中,创建 ToolParserOutput 实例后调用 parse_into;在解析错误时,先调用 process_parser_output 追加已解析部分,再通过 parser.reset() 获取剩余缓冲文本并作为普通文本 delta 发出,同时永久标记解析器为失败状态。
-
测试扩展迁移:将 parse_chunk 和 parse_complete 默认实现从 trait 中移除,放入与 test-util feature 绑定的 ToolParserTestExt 扩展 trait 中。所有测试代码改为通过 use crate::ToolParserTestExt as _ 导入使用。
-
回归测试:在 deepseek_v32.rs 的测试中添加 DeepSeek V4 场景,验证在解析器缓冲区内先成功解析一个完整工具调用、然后出现错误格式时,仍能保留第一个工具调用并只对错误部分回退。
rust/src/chat/src/output/default/tool.rs
核心调用适配器,实现工具解析结果到 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)
}
评论区精华
gemini-code-assist[bot] 指出引入 easy-ext 依赖是不必要的,并可能导致编译失败(当 vllm-tool-parser 作为依赖且启用 test-util 编译时)。建议手动实现扩展 trait 而非依赖 easy-ext。该建议未被直接回复,但 PR 最终合并,不确定是否已采纳。这是一个值得关注的架构决策。
风险与影响
风险:1)大规模机械变更,但编译期可捕获大多数错误;2)reset 返回类型变化,外部调用者需更新;3)新增 DeepSeek V4 测试覆盖了错误恢复,但其他模型可能缺少类似测试;4)easy-ext 依赖若保留可能引入下游编译问题。
影响:对齐了流式与非流式路径的行为,提高了工具调用错误恢复的合理性;无破坏性变更;为未来解析器扩展设定了更清晰的接口模式。
关联脉络
- 关联 PR #43582(Rust 前端往返测试)为此 PR 的测试方法论奠定了基础。
- 本 PR 完成后,
ToolParser 接口更加稳定,未来新增解析器(如 MiniCPM5 XML 解析器 #43175)将遵循新的 parse_into 模式。
- 整体而言,Rust 前端的工具调用处理正趋向成熟,错误恢复和测试覆盖逐步完善。
参与讨论