Prhub

#45216 [Rust Frontend] Add standalone `granite4` tool parser

原始 PR 作者 tahsintunan 合并时间 2026-06-13 00:16 文件变更 6 提交数 4 评论 2 代码增减 +511 / -7

执行摘要

Rust 前端新增 granite4 工具解析器

Granite 4 模型的 arguments 可能是 JSON 对象或转义 JSON 字符串(参见 issue #36827),共享的 JSON 配置无法表达字符串参数的分步解析,因此需要一个独立的解析器。该 PR 是跟踪 issue #44280 和前置 PR #44713 的后续。

建议批准合并。实现清晰,代码质量好,复用共享组件,评审通过。对 Granite 4 用户属必要功能,风险低。

讨论亮点

BugenZhao 在审查中指出一处注释不准确:“<tool_call> 是 added token 但不是 special token,不应在注释中称为特殊 token”。作者已采纳并删除相关注释(后续 commit 中应用 suggestion)。

实现拆解

  1. 创建 rust/src/tool-parser/src/json/granite4.rs,定义 Granite4Mode 状态机和 Granite4Event 事件,实现 Granite4ToolParser 结构体。通过 parse_into 方法利用 winnow 解析器逐字节扫描缓冲区,将文本解析为事件序列,并由 apply_event 更新输出。关键设计是当 arguments 首字节为 { 时走对象路径(流式收集),为 " 时走字符串路径(解码后整体发射)。
  2. rust/src/tool-parser/src/json/mod.rs 中添加 mod granite4;pub use granite4::Granite4ToolParser;,使其成为子模块公开成员。
  3. rust/src/tool-parser/src/lib.rspub use json:: 列表中添加 Granite4ToolParser,供外部 crate 使用。
  4. rust/src/chat/src/parser/tool/mod.rsnames 模块添加 pub const GRANITE4: &str = "granite4";在 ToolParserFactory::new 中添加 register_parser::<Granite4ToolParser>(names::GRANITE4)register_pattern("granite-4", names::GRANITE4),支持名称和模型 ID 自动路由。
  5. 更新测试文件 rust/src/chat/src/parser/tool/tests.rs,添加断言 ibm-granite/granite-4.0-h-tiny 路由到 GRANITE4
  6. 更新 rust/src/chat/src/lib.rs 中的错误报告字符串,将 granite4 加入可用解析器列表。
文件 模块 状态 重要度
rust/src/tool-parser/src/json/granite4.rs 工具解析器 added 9.08
rust/src/chat/src/parser/tool/mod.rs 工具注册 modified 5.6
rust/src/chat/src/parser/tool/tests.rs 测试 modified 4.5
rust/src/tool-parser/src/lib.rs 导出 modified 4.5
rust/src/tool-parser/src/json/mod.rs 模块组织 modified 3.92

关键符号

Granite4ToolParser::new Granite4ToolParser::apply_event Granite4ToolParser::push_arguments Granite4ToolParser::reset Granite4ToolParser::create Granite4ToolParser::parse_into

关键源码片段

rust/src/tool-parser/src/json/granite4.rs core-logic

新增核心解析器实现,包含状态机和事件处理逻辑

//! rust/src/tool-parser/src/json/granite4.rs
//! Granite 4 工具解析器:支持 `<tool_call>` 标签,
//! `arguments` 可以是 JSON 对象或转义 JSON 字符串。use winnow::prelude::*;
// ... 其他导入/// 解析器状态:处于文本、头部、参数或关闭模式。
#[derive(Debug, Clone, PartialEq, Eq)]
enum Granite4Mode {
    Text,
    Header,
    /// 参数解析中:`None` 表示尚未决定是对象还是字符串,
    /// `Some` 表示正在流式解析对象值。
    Args { json_scan: Option<JsonObjectScanState> },
    Close,
}/// 解析过程中触发的事件。
#[derive(Debug, Clone, PartialEq, Eq)]
enum Granite4Event {
    Text { len: usize },
    ToolCallStart,
    ToolCallHeader { function_name: String },
    /// 对象参数的增量字节;`complete` 表示闭合大括号已到。
    ObjectArgsDelta { len: usize, complete: bool },
    /// 字符串参数的完整解码结果。
    StringArgs { decoded: String },
    ToolCallEnd,
}/// Granite 4 工具解析器主结构体。
pub struct Granite4ToolParser {
    buffer: String,
    mode: Granite4Mode,
    active_tool_index: Option<usize>,
    emitted_tool_count: usize,
}impl Granite4ToolParser {
    fn new(_tools: &[Tool]) -> Self {
        Self { buffer: String::new(), mode: Granite4Mode::Text, active_tool_index: None, emitted_tool_count: 0 }
    }    /// 将事件应用到输出,并根据事件转换状态。
    fn apply_event(&mut self, event: Granite4Event, output: &mut ToolParserOutput) -> Result<()> {
        match event {
            Granite4Event::Text { len } => output.normal_text.push_str(&self.buffer[..len]),
            Granite4Event::ToolCallStart => self.mode = Granite4Mode::Header,
            Granite4Event::ToolCallHeader { function_name } => {
                let tool_index = self.emitted_tool_count;
                self.emitted_tool_count += 1;
                self.active_tool_index = Some(tool_index);
                self.mode = Granite4Mode::Args { json_scan: None };
                output.calls.push(ToolCallDelta {
                    tool_index,
                    name: Some(function_name),
                    arguments: String::new(),
                });
            }
            Granite4Event::ObjectArgsDelta { len, complete } => {
                let arguments = self.buffer[..len].to_string();
                self.push_arguments(arguments, output)?;
                if complete { self.mode = Granite4Mode::Close; }
            }
            Granite4Event::StringArgs { decoded } => {
                self.push_arguments(decoded, output)?;
                self.mode = Granite4Mode::Close;
            }
            Granite4Event::ToolCallEnd => { self.active_tool_index = None; self.mode = Granite4Mode::Text; }
        }
        Ok(())
    }
    // 其他方法如 push_arguments, reset, create, parse_into 在完整文件中。
}
rust/src/chat/src/parser/tool/mod.rs dependency-wiring

注册 granite4 解析器及其模型路由模式

// rust/src/chat/src/parser/tool/mod.rs ( 部分 )/// 注册的解析器名称。
pub mod names {
    // 其他解析器名称 ...
    pub const GRANITE4: &str = "granite4"; // Granite 4 解析器名称
}impl ToolParserFactory {
    /// 创建默认注册表并注册内置解析器。
    pub fn new() -> Self {
        let mut factory = Self::default();        factory
            .register_parser::<Granite4ToolParser>(names::GRANITE4) // 注册解析器
            // 其他解析器 ...
            .register_pattern("granite-4", names::GRANITE4); // 模型 ID 模式路由        factory
    }
}

评论区精华

关于 `<tool_call>` 是否为特殊 token 的讨论 documentation

BugenZhao 指出 `<tool_call>` 是 added token 但不是 special token,不应在注释中称为特殊 token,建议修正。

结论:已采纳建议,删除相关注释(后续 commit 中应用 suggestion)。 · 已解决

风险与影响

  1. 参数格式兼容性:字符串参数解码路径需要与 Python 实现一致,编码错误可能导致输出异常,现有实现已验证与 Python 对齐。
  2. 模型路由依赖:新模式前缀 granite-4 确保仅匹配 Granite 4 系列,但若未来 Granite 5 也用类似格式可能需调整。当前负面路由(旧 granite-3.x)已通过模式约束自动避免。
  3. 缺少测试覆盖:仅包含路由测试,缺乏针对流式解析逻辑(特别是字符串参数解码)的独立单元测试,依赖集成验证。

仅影响 Rust 前端。新增 granite4 解析器,用户可通过 --tool-call-parser granite4 或模型 ID 自动启用。不影响现有任何解析器,向后兼容。Granite 4 模型用户获得正确的工具调用支持。

参数格式兼容性 模型路由依赖 缺少测试覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论