From 8dad15ecc173875ebdfdc841bbb14207df80f35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Tue, 16 Jun 2026 23:59:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20F-01=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E8=83=BD=E5=8A=9B=E9=98=B6=E6=AE=B54=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E5=99=A8(ModelRouter+select=207=E6=AD=A5=E8=BF=87=E6=BB=A4?= =?UTF-8?q?=E9=93=BE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/df-ai/src/lib.rs | 1 + crates/df-ai/src/router.rs | 328 +++++++++++++++++++++++++++++++++++++ 2 files changed, 329 insertions(+) create mode 100644 crates/df-ai/src/router.rs diff --git a/crates/df-ai/src/lib.rs b/crates/df-ai/src/lib.rs index b593351..fa1dfe7 100644 --- a/crates/df-ai/src/lib.rs +++ b/crates/df-ai/src/lib.rs @@ -8,6 +8,7 @@ pub mod model_fetch; pub mod model_probe; pub mod openai_compat; pub mod provider; +pub mod router; // CR-30-1: 流前重试退避对外复用。complete() 的 retry_with_backoff 仍 crate 内用, // stream_recv/agentic 流前重试需复用 backoff_delay(jitter)+is_status_retryable(Fatal 分类) // 避免重写退避/分类逻辑(对齐决策 F-260616-07 a1)。改 pub mod 后对外仅暴露纯函数 + 常量。 diff --git a/crates/df-ai/src/router.rs b/crates/df-ai/src/router.rs new file mode 100644 index 0000000..4957e15 --- /dev/null +++ b/crates/df-ai/src/router.rs @@ -0,0 +1,328 @@ +//! 模型路由器 — F-01 阶段4 +//! +//! 纯函数核心,零 IO / 零状态。给定 TaskRequirements + 候选池,返回最优 ModelConfig。 +//! 不接调用点(那是阶段5:agentic.rs / title.rs / knowledge_inject.rs / project.rs / +//! df-ideas / df-nodes ai_node.rs)。 +//! +//! 设计来源:docs/02-架构设计/F-01-模型能力系统与智能路由设计-2026-06-16.md §6.1。 +//! +//! ModelRouter 为单元结构,select 是无状态关联函数(对齐任务规格,非设计文档的 `&self` 方法)。 + +use std::cmp::Reverse; + +use df_ai_core::model::{Capability, CostTier, IntelligenceTier, Modality, ModelConfig}; + +/// 任务对模型的需求(5 维度,对齐设计 §6.1)。 +/// +/// 由调用点构造(阶段5),描述本次调用需要什么模态/能力/智力/成本/上下文, +/// 交 ModelRouter::select 在候选池中选最优模型。 +#[derive(Debug, Clone)] +pub struct TaskRequirements { + /// 任务所需的模态集合(全子集匹配:任务所需模态都必须在模型模态里) + pub modalities: Vec, + /// 是否需要工具调用能力(needs_tool_use=true 时候选必须含 Capability::ToolUse) + pub needs_tool_use: bool, + /// 最低智力要求(模型 intelligence 必须 >= min_intelligence) + pub min_intelligence: IntelligenceTier, + /// 成本上限(可选:None 表示不设上限) + pub max_cost: Option, + /// 预估上下文大小(tokens,模型 context_window 必须 >= 此值) + pub estimated_context: usize, +} + +/// 模型路由器(单元结构,无状态)。 +/// +/// select 为关联函数:给定需求 + 候选池,执行 7 步过滤链选最优模型。 +pub struct ModelRouter; + +impl ModelRouter { + /// 在候选池中选出最优模型(7 步过滤链,严格对齐设计 §6.1 L469-478)。 + /// + /// 步骤: + /// 1. enabled — 只选启用的 + /// 2. 模态匹配 — 任务所需模态全在模型模态里 + /// 3. 能力匹配 — needs_tool_use 时候选必须含 ToolUse + /// 4. 智力达标 — intelligence >= min_intelligence + /// 5. 成本可控 — max_cost 设定时 cost_tier <= max_cost + /// 6. 窗口够大 — context_window >= estimated_context + /// 7. max_by_key 选最优:权重优先,同权重选便宜(cost_tier 小) + /// + /// 第 7 步取反说明:CostTier derive Ord(Free(req: &TaskRequirements, pool: &'a [ModelConfig]) -> Option<&'a ModelConfig> { + pool.iter() + .filter(|m| m.enabled) // 1. 只选启用的 + .filter(|m| req.modalities.iter().all(|r| m.modalities.contains(r))) // 2. 模态匹配 + .filter(|m| !req.needs_tool_use || m.capabilities.contains(&Capability::ToolUse)) // 3. 能力匹配 + .filter(|m| m.intelligence >= req.min_intelligence) // 4. 智力达标 + .filter(|m| req.max_cost.map_or(true, |max| m.cost_tier <= max)) // 5. 成本可控 + .filter(|m| m.context_window >= req.estimated_context) // 6. 窗口够大 + .max_by_key(|m| (m.weight, Reverse(m.cost_tier))) // 7. 权重优先,同权重选便宜 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// 构造一个全维度可定制的 ModelConfig(默认全过过滤,调用方按需覆盖字段)。 + fn model(model_id: &str) -> ModelConfig { + ModelConfig { + model_id: model_id.into(), + enabled: true, + label: None, + modalities: vec![Modality::Text], + capabilities: vec![Capability::ToolUse], + cost_tier: CostTier::Medium, + intelligence: IntelligenceTier::Standard, + weight: 50, + context_window: 8192, + probe_source: None, + } + } + + /// 构造一个宽松需求(默认全过过滤,调用方按需覆盖字段)。 + fn req() -> TaskRequirements { + TaskRequirements { + modalities: vec![Modality::Text], + needs_tool_use: false, + min_intelligence: IntelligenceTier::Lite, + max_cost: None, + estimated_context: 0, + } + } + + // ── 步骤 1:enabled 过滤 ── + + #[test] + fn empty_pool_returns_none() { + let pool: Vec = vec![]; + assert!(ModelRouter::select(&req(), &pool).is_none()); + } + + #[test] + fn all_disabled_returns_none() { + let pool = vec![ + ModelConfig { + enabled: false, + ..model("a") + }, + ModelConfig { + enabled: false, + ..model("b") + }, + ]; + assert!(ModelRouter::select(&req(), &pool).is_none()); + } + + // ── 步骤 2:模态匹配 ── + + #[test] + fn modality_mismatch_filtered() { + // 任务需要 Vision,但池里模型只有 Text + let pool = vec![model("text-only")]; + let r = TaskRequirements { + modalities: vec![Modality::Vision], + ..req() + }; + assert!(ModelRouter::select(&r, &pool).is_none()); + } + + // ── 步骤 3:能力匹配(ToolUse) ── + + #[test] + fn needs_tool_use_filters_non_tool() { + // 任务需要 ToolUse,池里模型 capabilities 无 ToolUse + let pool = vec![ModelConfig { + capabilities: vec![Capability::Embedding], + ..model("embed-only") + }]; + let r = TaskRequirements { + needs_tool_use: true, + ..req() + }; + assert!(ModelRouter::select(&r, &pool).is_none()); + } + + #[test] + fn needs_tool_use_false_allows_non_tool() { + // 任务不需 ToolUse,池里模型无 ToolUse 仍入选 + let pool = vec![ModelConfig { + capabilities: vec![Capability::Embedding], + ..model("embed-only") + }]; + let r = TaskRequirements { + needs_tool_use: false, + ..req() + }; + assert_eq!( + ModelRouter::select(&r, &pool).unwrap().model_id, + "embed-only" + ); + } + + // ── 步骤 4:智力达标 ── + + #[test] + fn intelligence_below_min_filtered() { + // 任务最低 Plus,池里模型 Standard + let pool = vec![model("standard")]; + let r = TaskRequirements { + min_intelligence: IntelligenceTier::Plus, + ..req() + }; + assert!(ModelRouter::select(&r, &pool).is_none()); + } + + // ── 步骤 5:成本可控 ── + + #[test] + fn max_cost_filters_expensive() { + // 任务上限 Low,池里模型 High + let pool = vec![ModelConfig { + cost_tier: CostTier::High, + ..model("expensive") + }]; + let r = TaskRequirements { + max_cost: Some(CostTier::Low), + ..req() + }; + assert!(ModelRouter::select(&r, &pool).is_none()); + } + + #[test] + fn max_cost_none_allows_any() { + // 任务不设上限,池里模型 High 仍入选 + let pool = vec![ModelConfig { + cost_tier: CostTier::High, + ..model("expensive") + }]; + let r = TaskRequirements { + max_cost: None, + ..req() + }; + assert_eq!( + ModelRouter::select(&r, &pool).unwrap().model_id, + "expensive" + ); + } + + // ── 步骤 6:窗口够大 ── + + #[test] + fn context_window_insufficient() { + // 任务预估 100000,池里模型 8192 + let pool = vec![model("small-window")]; + let r = TaskRequirements { + estimated_context: 100_000, + ..req() + }; + assert!(ModelRouter::select(&r, &pool).is_none()); + } + + // ── 步骤 7:max_by_key (weight, Reverse(cost)) ── + + #[test] + fn single_match_returns_it() { + let pool = vec![model("only-one")]; + assert_eq!( + ModelRouter::select(&req(), &pool).unwrap().model_id, + "only-one" + ); + } + + #[test] + fn weight_priority_higher_wins() { + // 两候选都满足,weight 70 胜 50 + let pool = vec![ + ModelConfig { + weight: 50, + ..model("low-weight") + }, + ModelConfig { + weight: 70, + ..model("high-weight") + }, + ]; + assert_eq!( + ModelRouter::select(&req(), &pool).unwrap().model_id, + "high-weight" + ); + } + + #[test] + fn same_weight_picks_cheaper_cost_tier() { + // 同 weight 70,cost Free 胜 cost High(Reverse 让 Free 的 Ord 值变大) + let pool = vec![ + ModelConfig { + weight: 70, + cost_tier: CostTier::High, + ..model("expensive") + }, + ModelConfig { + weight: 70, + cost_tier: CostTier::Free, + ..model("free") + }, + ]; + assert_eq!( + ModelRouter::select(&req(), &pool).unwrap().model_id, + "free" + ); + } + + #[test] + fn all_dimensions_match_picks_best() { + // 3+ 候选各维度参差,验证 7 步全过 + max_by_key 选最优组合。 + // + // 候选: + // a: weight 60, cost Medium → 通过全部过滤,key=(60, Reverse(Medium)) + // b: weight 80, cost High → 通过,key=(80, Reverse(High)) — weight 最高,胜 + // c: weight 80, cost Free → 通过,key=(80, Reverse(Free)) — 同 weight 80,Free 比 High 便宜,胜过 b + // d: weight 90, cost Low, 但 intelligence Lite(< min Standard) → 步骤 4 滤掉 + // e: enabled=false → 步骤 1 滤掉 + // + // 预期:c 胜(weight 80 最高档中,Free 最便宜) + let pool = vec![ + ModelConfig { + weight: 60, + cost_tier: CostTier::Medium, + intelligence: IntelligenceTier::Standard, + ..model("a") + }, + ModelConfig { + weight: 80, + cost_tier: CostTier::High, + intelligence: IntelligenceTier::Plus, + ..model("b") + }, + ModelConfig { + weight: 80, + cost_tier: CostTier::Free, + intelligence: IntelligenceTier::Plus, + ..model("c") + }, + ModelConfig { + weight: 90, + cost_tier: CostTier::Low, + intelligence: IntelligenceTier::Lite, // 低于 min,滤掉 + ..model("d") + }, + ModelConfig { + enabled: false, + weight: 100, + ..model("e") + }, + ]; + let r = TaskRequirements { + modalities: vec![Modality::Text], + needs_tool_use: true, + min_intelligence: IntelligenceTier::Standard, + max_cost: None, + estimated_context: 0, + }; + assert_eq!(ModelRouter::select(&r, &pool).unwrap().model_id, "c"); + } +}