Prhub

#44760 [Rust Frontend] Support `parallel_tool_calls = false`

原始 PR 作者 FAUST-BENCHOU 合并时间 2026-06-15 15:50 文件变更 8 提交数 2 评论 11 代码增减 +135 / -23

执行摘要

Rust 前端支持 parallel_tool_calls=false

作为 Rust 前端功能对等性 Roadmap (Issue #44280) 的一部分,支持 parallel_tool_calls 参数是补齐与 Python 前端功能差异的关键项。在高并发场景下,部分用户需要禁用并行工具调用以保持响应顺序可控。

值得精读,特别是 structured.rs 中抑制逻辑的实现方式——在事件流层通过状态机做过滤是一个非常清晰的设计决策。review 过程显示了将功能正确下放到合适抽象层的重要性。此外,PR 展示了一个良好的小功能实现与 review 沟通模式。

讨论亮点
  • BugenZhao 建议将过滤逻辑下沉到 structured_chat_event_stream 内部,而不是在路由层处理。他认为 Rust 已经有结构化工具调用事件流,在流层应用最大工具调用策略能使流式和非流式共享相同语义,并避免未来在其他端点重复实现。该建议被采纳,最终实现完全在 structured.rs 中处理抑制。
  • tahsintunan 指出路由层新增的 should_emit_tool_call 和 filter_parallel_tool_calls 函数实际上是冗余的,因为上游逻辑已经保证了 tool_index==0,建议移除并改用 debug_assert。作者同意并修复了该问题。
  • 另外有关于 None 与 unwrap_or(true) 的边缘讨论,tahsintunan 认为当前行为(null 视为 true)与文档一致,合理。

实现拆解

  1. 数据模型扩展:在 ChatRequest 结构体(rust/src/chat/src/request.rs)中新增 parallel_tool_calls 字段(bool 类型),指示是否允许并行工具调用。
  2. 请求映射解析:在 convert.rs 的 prepare_chat_request 函数中通过 request.parallel_tool_calls.unwrap_or(true) 将 OpenAI 请求中的 parallel_tool_calls 映射到 ChatRequest;默认值为 true 以保持向后兼容。
  3. 移除非拒绝校验:在 validate.rs 的 validate_request_compat 中移除对 parallel_tool_calls 字段的拒绝逻辑,允许其通过验证。
  4. 核心抑制逻辑:在 structured.rs 的 StructuredEventState 中增加 parallel_tool_calls 和 suppressing_tool_call 标志。在 start_tool_call 中,当 parallel_tool_calls=false 且工具调用索引 >= 1 时,将 suppressing_tool_call 置为 true 并跳过事件生成;push_tool_call_arguments 和 close_open_tool_call 在抑制状态下同样静默跳过。最终 AssistantMessage 中仅包含第一个工具调用。
  5. 输出处理器传递参数:在 DefaultChatOutputProcessor(default/mod.rs)和 HarmonyChatOutputProcessor(harmony/mod.rs)的 process 方法中,将 parallel_tool_calls 参数传递给 structured_chat_event_stream,确保不同输出风格都支持该特性。
  6. 单元测试:在 convert.rs 中添加 prepare_chat_request_maps_parallel_tool_calls 和 prepare_chat_request_defaults_parallel_tool_calls_to_true 两个测试用例,验证映射逻辑;同时更新 tool.rs 中的现有测试,显示传递 true 保持原有行为。
文件 模块 状态 重要度
rust/src/chat/src/output/structured.rs 聊天输出 modified 7.86
rust/src/server/src/routes/openai/chat_completions/convert.rs 服务路由 modified 7.04
rust/src/chat/src/output/default/tool.rs 聊天输出 modified 5.82
rust/src/server/src/routes/openai/chat_completions/validate.rs 服务路由 modified 5.67
rust/src/chat/src/output/harmony/mod.rs 聊天输出 modified 5.45
rust/src/chat/src/output/default/mod.rs 聊天输出 modified 5.22
rust/src/chat/src/request.rs 聊天输出 modified 5.22
rust/src/server/src/routes/tokenize/types.rs 服务路由 modified 4.58

关键符号

structured_chat_event_stream StructuredEventState::new StructuredEventState::start_tool_call StructuredEventState::push_tool_call_arguments StructuredEventState::close_open_tool_call prepare_chat_request validate_request_compat

关键源码片段

rust/src/chat/src/output/structured.rs core-logic

实现了核心抑制逻辑,新增 parallel_tool_calls 和 suppressing_tool_call 状态,控制流式响应中工具调用的过滤。

/// 结构化事件流的组装状态机。
struct StructuredEventState {
    message: AssistantMessage,
    open_text_block: Option<OpenTextBlock>,
    open_tool_call: Option<OpenToolCall>,
    next_tool_call_index: usize,
    /// 是否允许多个工具调用并行(即是否暴露索引 > 0 的工具调用)。
    parallel_tool_calls: bool,
    /// 是否正在抑制当前工具调用(当 parallel_tool_calls = false 且索引 >= 1 时为 true)。
    suppressing_tool_call: bool,
}
​
impl StructuredEventState {
    /// 创建新的状态实例;parallel_tool_calls 决定是否抑制后续工具调用。
    fn new(parallel_tool_calls: bool) -> Self {
        Self {
            message: AssistantMessage::default(),
            open_text_block: None,
            open_tool_call: None,
            next_tool_call_index: 0,
            parallel_tool_calls,
            suppressing_tool_call: false,
        }
    }
​
    /// 开始一个新的工具调用。如果 parallel_tool_calls = false 且索引 >= 1,
    /// 则设置 suppressing_tool_call 为 true,并返回空事件列表(不生成 ToolCallStart 事件)。
    fn start_tool_call(&mut self, id: String, name: String) -> Result<Vec<ChatEvent>> {
        let mut events = Vec::new();
        self.close_open_text_block(&mut events);
        self.close_open_tool_call(&mut events);
​
        let index = self.next_tool_call_index;
        self.next_tool_call_index += 1;
        if !self.parallel_tool_calls && index >= 1 {
            self.suppressing_tool_call = true;
            return Ok(events);
        }
​
        self.suppressing_tool_call = false;
        self.open_tool_call = Some(OpenToolCall {
            index,
            id: id.clone(),
            name: name.clone(),
            arguments: String::new(),
        });
        events.push(ChatEvent::ToolCallStart { index, id, name });
        Ok(events)
    }
​
    /// 添加工具调用参数增量。如果正在抑制,则直接返回空事件列表。
    fn push_tool_call_arguments(&mut self, delta: String) -> Result<Vec<ChatEvent>> {
        if self.suppressing_tool_call {
            return Ok(Vec::new());
        }
​
        let mut events = Vec::new();
        let Some(open_tool_call) = self.open_tool_call.as_mut() else {
            return Err(Error::ToolCallStreamInvariant {
                message: "received tool-call arguments delta without an open tool call".to_string(),
            });
        };
        open_tool_call.arguments.push_str(&delta);
        events.push(ChatEvent::ToolCallArgumentsDelta {
            index: open_tool_call.index,
            delta,
        });
        Ok(events)
    }
​
    /// 关闭当前工具调用。如果正在抑制,则清除抑制标志但不生成事件。
    fn close_open_tool_call(&mut self, events: &mut Vec<ChatEvent>) {
        if self.suppressing_tool_call {
            self.suppressing_tool_call = false;
            return;
        }
        let Some(open_tool_call) = self.open_tool_call.take() else {
            return;
        };
        // 原有逻辑:生成 ToolCallEnd 事件并附加到 message 的 tool_calls 中
    }
}
rust/src/server/src/routes/openai/chat_completions/convert.rs entrypoint

负责将 OpenAI 请求中的 parallel_tool_calls 映射到 ChatRequest,并添加了对应的单元测试。

// 在 prepare_chat_request 函数中映射 parallel_tool_calls,默认值为 true
parallel_tool_calls: request.parallel_tool_calls.unwrap_or(true),
​
// 测试 parallel_tool_calls = false 映射
#[test]
fn prepare_chat_request_maps_parallel_tool_calls() {
    let mut request = base_request();
    request.parallel_tool_calls = Some(false);
​
    let prepared = prepare_chat_request(
        request,
        &served(&["Qwen/Qwen1.5-0.5B-Chat"]),
        ResolvedRequestContext::default(),
    )
    .expect("request is valid");
​
    assert!(!prepared.chat_request.parallel_tool_calls);
}
​
// 测试默认值 true
#[test]
fn prepare_chat_request_defaults_parallel_tool_calls_to_true() {
    let prepared = prepare_chat_request(
        base_request(),
        &served(&["Qwen/Qwen1.5-0.5B-Chat"]),
        ResolvedRequestContext::default(),
    )
    .expect("request is valid");
​
    assert!(prepared.chat_request.parallel_tool_calls);
}

评论区精华

过滤位置选择 设计

BugenZhao 建议将过滤逻辑下沉到 structured_chat_event_stream 内部,而不是在路由层处理。他认为 Rust 已经有结构化工具调用事件流,在流层应用最大工具调用策略能使流式和非流式共享相同语义,并避免未来在其他端点重复实现。

结论:作者采纳建议,最终实现完全在 structured.rs 中处理抑制,路由层只负责传递参数。 · 已解决

路由层冗余代码 设计

tahsintunan 指出路由层新增的 should_emit_tool_call 和 filter_parallel_tool_calls 函数是冗余的,因为上游已保证 index==0,建议移除。

结论:作者同意并移除了这些冗余函数。 · 已解决

风险与影响

该变更加入了一个影响流式输出的状态机标志。主要风险是 suppressing_tool_call 与现有事件处理路径的交互(例如在抑制状态下意外关闭 text block 导致事件丢失)。当前单元测试覆盖了流式场景的主要路径,但未覆盖所有边界(如在工具调用中间出现推理块或 logprobs 时抑制行为是否依然正确)。此外,默认值 true 保证了向后兼容,但用户若依赖旧版本的“拒绝”错误,升级后需注意行为变化。

对 Rust 前端用户而言,现在可以传递 parallel_tool_calls=false 来控制工具调用的数量。默认行为保持不变。该功能对系统性能无显著影响(抑制操作只是跳过事件发射)。团队内需要了解新的数据模型字段和流式抑制逻辑。

默认向前兼容 抑制状态机边界 单元测试覆盖基本路径

关联 Issue

#44280 [Roadmap] Rust Frontend Feature Parity

完整报告

参与讨论