Prhub

#47283 [Rust Frontend] Use enum-backed domain types for engine outputs and structured outputs

原始 PR 作者 BugenZhao 合并时间 2026-07-02 17:41 文件变更 16 提交数 3 评论 4 代码增减 +760 / -603

执行摘要

Rust 前端引擎输出和结构化输出改用枚举域类型

PR body 明确指出:'Encoding invariants in public Rust types makes invalid states harder to construct and keeps validation closer to the serde boundary.' 原始 Python 协议以单一产品形结构体传输多种语义输出,Rust 前端通过重构为枚举类型显式暴露语义变体,从而减少运行时检查和无效状态的可能性。

推荐阅读此 PR 以了解 'enum-backed domain type' 设计模式——一种在 Rust 中实现类型安全且向后兼容的多协议负载处理的典型方法。关键设计决策包括:私有 Wire 类型与公共域类型分离、自定义 ser/de 委托、EnumAsInner 简化模式匹配。此模式在构建复用外部协议输入的 Rust 接口时很有参考价值。

讨论亮点

无实质讨论。Claude bot 自动触发 code review 建议,njhill 直接批准合并。无未解决疑虑。

实现拆解

  1. 重构 EngineCoreOutputs——从结构体到枚举
    将原公开结构体 EngineCoreOutputs 重命名为私有的 WireEngineCoreOutputs,仅用于 serde 边界的 msgpack 编解码。新建公开枚举 EngineCoreOutputs(通过 EnumAsInner 派生),包含三个语义变体:RequestBatch(RequestBatchOutputs)Utility(UtilityCallOutput)DpControl(DpControlOutput)。新增 DpControlOutput 结构体封装 DP 控制消息。为各变体实现 From trait,并自定义 Serialize/Deserialize 将枚举委托给私有 Wire 类型进行 msgpack 序列化。原 classify() 方法被消除,调用方直接匹配枚举变体(见 imp.rs 中的 run_output_dispatcher_loop)。
    关键文件:rust/src/engine-core-client/src/protocol/output.rs

  2. 重构 StructuredOutputsParams——从可选字段到必选约束枚举
    将原产品形结构体(所有字段 Option)拆分为 StructuredOutputConstraint 枚举(Json/Regex/Choice/Grammar/JsonObject/StructuralTag)和 StructuredOutputOptions 结构体。新 StructuredOutputsParams 保证 constraint 字段必选,并保持 backend 字段。提供便捷构造方法如 json()regex() 等,底层调用 from_constraint()。原始产品形结构体被保留为私有 WireStructuredOutputsParams,用于从 python 来的 msgpack 反序列化时兜底。
    关键文件:rust/src/engine-core-client/src/protocol/structured_outputs.rs

  3. 适配全链路下游模块
    更新所有测试文件(client.rsroutes/tests.rsgrpc/tests.rshttp_client_tests.rs)以使用新类型构造输出,例如用 RequestBatchOutputs { ... }.into() 替代 EngineCoreOutputs { engine_index: ..., ... }。更新 mock-engine、chat 输出处理器和路由层中对应的匹配逻辑。删除对 classify 的调用,直接 match outputs { EngineCoreOutputs::RequestBatch(batch) => ... }
    关键文件:rust/src/engine-core-client/src/tests/client.rsrust/src/server/src/routes/tests.rsrust/src/mock-engine/src/engine.rs 等。

文件 模块 状态 重要度
rust/src/engine-core-client/src/protocol/output.rs 引擎核心客户端 modified 9.21
rust/src/engine-core-client/src/protocol/structured_outputs.rs 引擎核心客户端 modified 9.21
rust/src/engine-core-client/src/tests/client.rs 集成测试 modified 7.13

关键符号

EngineCoreOutputs::from EngineCoreOutputs::serialize EngineCoreOutputs::deserialize StructuredOutputsParams::json StructuredOutputsParams::regex StructuredOutputsParams::choice StructuredOutputsParams::grammar StructuredOutputsParams::json_object StructuredOutputsParams::structural_tag StructuredOutputsParams::from_constraint

关键源码片段

rust/src/engine-core-client/src/protocol/output.rs core-logic

核心变更:将 `EngineCoreOutputs` 从结构体重构为语义枚举,新增 `DpControlOutput` 变体,私有 Wire 类型用于 serde 边界。

/// Data-parallel control output, replacing the raw `wave_complete` / `start_wave` fields.
// 中文注释:这是 DP 控制消息的强类型封装,替代原本在原始 `WireEngineCoreOutputs` 中的可选字段。
#[derive(Debug, Clone, PartialEq)]
pub struct DpControlOutput {
    pub engine_index: u32,
    pub timestamp: f64,
    pub control: DpControlMessage,
}/// Semantic engine-core output families.
// 中文注释:枚举每个语义变体,不能再构造出无效组合(如同时设置 `wave_complete` 和 `utility_output`)。
#[derive(Debug, Clone, PartialEq, EnumAsInner)]
pub enum EngineCoreOutputs {
    RequestBatch(RequestBatchOutputs),
    Utility(UtilityCallOutput),
    DpControl(DpControlOutput),
}// 中文注释:从各变体类型的 `From` 实现,方便用 `.into()` 构造。
impl From<RequestBatchOutputs> for EngineCoreOutputs {
    fn from(outputs: RequestBatchOutputs) -> Self { Self::RequestBatch(outputs) }
}
impl From<UtilityCallOutput> for EngineCoreOutputs {
    fn from(output: UtilityCallOutput) -> Self { Self::Utility(output) }
}
impl From<DpControlOutput> for EngineCoreOutputs {
    fn from(output: DpControlOutput) -> Self { Self::DpControl(output) }
}// 中文注释:自定义 Serialize / Deserialize,转给私有 WireEngineCoreOutputs 保持 msgpack 格式不变。
impl Serialize for EngineCoreOutputs {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        WireEngineCoreOutputs::from(self.clone()).serialize(serializer)
    }
}
impl<'de> Deserialize<'de> for EngineCoreOutputs {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let wire = WireEngineCoreOutputs::deserialize(deserializer)?;
        Self::try_from(wire).map_err(serde::de::Error::custom)
    }
}
rust/src/engine-core-client/src/protocol/structured_outputs.rs core-logic

核心变更:将 `StructuredOutputsParams` 从所有可选字段重构为必选约束枚举,同时保持 msgpack 格式兼容。

/// The single structured-output constraint selected for a request.
// 中文注释:这是一个枚举,每个变体对应一种约束模式;移除了原产品形结构体中的“所有字段可选”设计,确保每次只有一个约束被设置。
#[derive(Debug, Clone, PartialEq, EnumAsInner)]
pub enum StructuredOutputConstraint {
    Json(Value),
    Regex(String),
    Choice(Vec<String>),
    Grammar(String),
    JsonObject,
    StructuralTag(String),
}/// Additional structured-output options that do not select the constraint mode.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StructuredOutputOptions {
    pub disable_any_whitespace: bool,
    pub disable_additional_properties: bool,
    pub whitespace_pattern: Option<String>,
}/// Parameters for configuring structured outputs (guided decoding).
// 中文注释:必选字段 `constraint` 加上 `options` 和 `backend`;对外使用时无法构造同时包含 `json` 和 `regex` 的无效状态。
#[derive(Debug, Clone, PartialEq)]
pub struct StructuredOutputsParams {
    pub constraint: StructuredOutputConstraint,
    pub options: StructuredOutputOptions,
    pub backend: StructuredOutputBackend,
}impl StructuredOutputsParams {
    pub fn json(json: Value) -> Self {
        Self::from_constraint(StructuredOutputConstraint::Json(json))
    }
    pub fn regex(regex: impl Into<String>) -> Self {
        Self::from_constraint(StructuredOutputConstraint::Regex(regex.into()))
    }
    pub fn choice(choice: Vec<String>) -> Self {
        Self::from_constraint(StructuredOutputConstraint::Choice(choice))
    }
    pub fn grammar(grammar: impl Into<String>) -> Self {
        Self::from_constraint(StructuredOutputConstraint::Grammar(grammar.into()))
    }
    pub fn json_object() -> Self {
        Self::from_constraint(StructuredOutputConstraint::JsonObject)
    }
    pub fn structural_tag(structural_tag: impl Into<String>) -> Self {
        Self::from_constraint(StructuredOutputConstraint::StructuralTag(structural_tag.into()))
    }    fn from_constraint(constraint: StructuredOutputConstraint) -> Self {
        Self {
            constraint,
            options: StructuredOutputOptions::default(),
            backend: StructuredOutputBackend::default(),
        }
    }
}// 中文注释:私有的 Wire 类型,用于与 Python msgpack 交换数据;始终是产品形(所有字段可选)。
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
struct WireStructuredOutputsParams {
    json: Option<Value>,
    regex: Option<String>,
    choice: Option<Vec<String>>,
    grammar: Option<String>,
    json_object: Option<bool>,
    disable_any_whitespace: bool,
    disable_additional_properties: bool,
    whitespace_pattern: Option<String>,
    structural_tag: Option<String>,
}

评论区精华

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

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

风险与影响

主要风险在于自定义 Serialize/Deserialize 是否正确维持 msgpack 二进制格式。若序列化/反序列化逻辑有偏差,可导致前端与引擎核心通信失败。但测试结果全部通过,且 PR 属于堆叠提交的一部分(#47265),集成验证已在 CI 完成。另一个风险是:新枚举移除了原先的 Other 回退变体,若遇到新型未识别的输出模式,编译会报错而非静默 fallback,虽然更安全但可能暴露先前被隐藏的边缘情况。

影响范围限定在 Rust 前端内部各 crate:引擎核心客户端、服务器路由、mock 引擎、chat 后端。对最终用户无感知,对 Rust 前端开发者的构造和匹配方式有明确变更:必须使用显式枚举变体来生成输出或处理输出。测试覆盖更新,验证了主要路径。未引入新外部依赖。

自定义 serde 兼容性风险 移除 Other fallback 导致编译错误暴露边缘情况

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论