优化: AI Chat全栈多批审查修复与架构清理(risk_level清理/路由解耦/工具渲染/测试补测/死代码)

This commit is contained in:
2026-06-18 22:57:19 +08:00
parent 0ca5d9805f
commit a2871a66e0
87 changed files with 5720 additions and 3012 deletions

View File

@@ -8,61 +8,58 @@
//!
//! ModelRouter 为单元结构,select 是无状态关联函数(对齐任务规格,非设计文档的 `&self` 方法)。
use std::cmp::Reverse;
// 阶段5: 调用点经 `df_ai::router::{Modality, Capability, CostTier, IntelligenceTier}`
// 直接 import 维度枚举构造 TaskRequirements(对齐任务规格 import 风格),re-export 避免调用点
// 各自从 df_ai_core::model 取(跨 crate 路径冗长)。select/select_model_id 仅借用枚举,无重定义。
// 注:CostTier/IntelligenceTier 路由已解耦(2026-06-18 B-260618-03)——provider /v1/models API
// 不返回这两维度,数据无客观依据不可信,不参与硬路由;re-export 保留供未来真实判别源。
pub use df_ai_core::model::{Capability, CostTier, IntelligenceTier, Modality, ModelConfig};
/// 任务对模型的需求(5 维度,对齐设计 §6.1)。
/// 任务对模型的需求(3 维度)。
///
/// 由调用点构造(阶段5),描述本次调用需要什么模态/能力/智力/成本/上下文,
/// 由调用点构造(阶段5),描述本次调用需要什么模态/能力/上下文,
/// 交 ModelRouter::select 在候选池中选最优模型。
///
/// 路由已解耦(2026-06-18 B-260618-03):原 `min_intelligence`/`max_cost` 两字段删除。
/// provider /v1/models API 不返回 cost_tier/intelligence,这两维度 100% 靠预设表写死 +
/// 模型名启发式猜,数据无客观依据不可信,不应参与硬路由。枚举(CostTier/IntelligenceTier)
/// 保留供未来出现真实判别源时再接回。
#[derive(Debug, Clone)]
pub struct TaskRequirements {
/// 任务所需的模态集合(全子集匹配:任务所需模态都必须在模型模态里)
pub modalities: Vec<Modality>,
/// 是否需要工具调用能力(needs_tool_use=true 时候选必须含 Capability::ToolUse)
pub needs_tool_use: bool,
/// 最低智力要求(模型 intelligence 必须 >= min_intelligence)
pub min_intelligence: IntelligenceTier,
/// 成本上限(可选:None 表示不设上限)
pub max_cost: Option<CostTier>,
/// 预估上下文大小(tokens,模型 context_window 必须 >= 此值)
pub estimated_context: usize,
}
/// 模型路由器(单元结构,无状态)。
///
/// select 为关联函数:给定需求 + 候选池,执行 7 步过滤链选最优模型。
/// select 为关联函数:给定需求 + 候选池,执行过滤链选最优模型。
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 小)
/// 4. 窗口够大context_window >= estimated_context
/// 5. max_by_key 选最优:纯 weight 主导(权重高者胜)
///
/// 第 7 步取反说明:CostTier derive Ord(Free<Low<Medium<High),
/// "选便宜"=选小 Ord 值,但 max_by_key 默认选大
/// 用 `std::cmp::Reverse(m.cost_tier)` 包装让小值变"大":
/// Free(Reverse 值大)胜过 High(Reverse 值小)。比 `-(as i32)` 取反更 idiomatic
/// 路由已解耦(2026-06-18 B-260618-03):原「智力达标」/「成本可控」两步删除,
/// 原第 7 步排序的 `Reverse(cost_tier)` 同权重选便宜也已删除——排序纯 weight 主导
/// cost_tier/intelligence 数据无客观依据(provider /v1/models 不返回,靠预设表+模型名
/// 启发式猜),不参与硬路由。枚举保留供未来真实判别源再接回
pub fn select<'a>(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. 权重优先,同权重选便宜
.filter(|m| m.context_window >= req.estimated_context) // 4. 窗口够大
.max_by_key(|m| m.weight) // 5. 纯 weight 主导
}
}
@@ -105,8 +102,6 @@ mod tests {
TaskRequirements {
modalities: vec![Modality::Text],
needs_tool_use: false,
min_intelligence: IntelligenceTier::Lite,
max_cost: None,
estimated_context: 0,
}
}
@@ -163,6 +158,63 @@ mod tests {
assert!(ModelRouter::select(&r, &pool).is_none());
}
#[test]
fn modality_subset_multimodal_required_filters_text_only() {
// 多模态子集匹配:任务需 [Text, Vision](两个模态都得支持),
// 模型仅 [Text](缺 Vision)→ 步骤 2 `.all(|r| m.modalities.contains(r))` 不成立,滤掉。
// 区别于 modality_mismatch_filtered(单模态 Vision):本测覆盖「任务需多模态全子集」路径。
let pool = vec![ModelConfig {
modalities: vec![Modality::Text],
..model("text-only")
}];
let r = TaskRequirements {
modalities: vec![Modality::Text, Modality::Vision],
..req()
};
assert!(ModelRouter::select(&r, &pool).is_none());
// 对照:模型补全 Vision 后通过(Text+Vision ⊇ 任务需求 Text+Vision)
let pool_ok = vec![ModelConfig {
modalities: vec![Modality::Text, Modality::Vision],
..model("vision-capable")
}];
assert_eq!(
ModelRouter::select(&r, &pool_ok).unwrap().model_id,
"vision-capable"
);
}
#[test]
fn enabled_false_excluded_from_routing() {
// enabled=false 不参与路由:混池里禁用候选 weight 更高(100),
// 但应被步骤 1 `.filter(|m| m.enabled)` 滤掉,只选 enabled=true 的低 weight 候选。
// 区别于 all_disabled_returns_none(全禁用返 None):本测覆盖「部分禁用」混池场景。
let pool = vec![
ModelConfig {
enabled: false,
weight: 100, // 若未被 enabled 过滤,weight 100 会胜
..model("disabled-heavy")
},
ModelConfig {
enabled: true,
weight: 30,
..model("enabled-light")
},
];
assert_eq!(
ModelRouter::select(&req(), &pool).unwrap().model_id,
"enabled-light"
);
}
#[test]
fn empty_candidate_pool_returns_none() {
// 空候选池 → None(与 select_model_id_empty_pool_returns_none 对应,
// 本测覆盖 select() 关联函数本身而非 helper;语义等价但断言点不同)。
let pool: Vec<ModelConfig> = vec![];
assert!(ModelRouter::select(&req(), &pool).is_none());
}
// ── 步骤 3:能力匹配(ToolUse) ──
#[test]
@@ -196,53 +248,7 @@ mod tests {
);
}
// ── 步骤 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:窗口够大 ──
// ── 步骤 4(原智力/成本过滤已解耦 B-260618-03):窗口够大 ──
#[test]
fn context_window_insufficient() {
@@ -255,7 +261,7 @@ mod tests {
assert!(ModelRouter::select(&r, &pool).is_none());
}
// ── 步骤 7:max_by_key (weight, Reverse(cost)) ──
// ── 步骤 5:max_by_key (weight) ──
#[test]
fn single_match_returns_it() {
@@ -286,8 +292,9 @@ mod tests {
}
#[test]
fn same_weight_picks_cheaper_cost_tier() {
// 同 weight 70,cost Free 胜 cost High(Reverse 让 Free 的 Ord 值变大)
fn same_weight_picks_first_match() {
// 同 weight 70,纯 weight 主导(无 cost_tier tie-break):max_by_key 遇并列 key
// 返回最后一个(rust Iterator::max_by_key 语义)。验证同 weight 不再按 cost 取舍。
let pool = vec![
ModelConfig {
weight: 70,
@@ -296,28 +303,29 @@ mod tests {
},
ModelConfig {
weight: 70,
cost_tier: CostTier::Free,
..model("free")
cost_tier: CostTier::Low,
..model("low-cost")
},
];
assert_eq!(
ModelRouter::select(&req(), &pool).unwrap().model_id,
"free"
"low-cost"
);
}
#[test]
fn all_dimensions_match_picks_best() {
// 3+ 候选各维度参差,验证 7 步全过 + max_by_key 选最优组合
// 3+ 候选各维度参差,验证过滤链全过 + max_by_key 纯 weight 选最优。
// (B-260618-03:智力/成本过滤已解耦,原步骤 4/5 删除,候选 d 不再因 intelligence 滤掉)
//
// 候选:
// 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 滤掉
// a: weight 60 → 通过全部过滤,key=60
// b: weight 80 → 通过,key=80 — weight 最高档(与 c 并列)
// c: weight 80 → 通过,key=80 — 同 weight 80,max_by_key 并列返回最后
// d: weight 90 → 通过(B-260618-03 后 intelligence 不参与过滤),key=90 — weight 最高,胜
// e: enabled=false → 步骤 1 滤掉
//
// 预期:c 胜(weight 80 最高档中,Free 最便宜)
// 预期:d 胜(weight 90 最高,不再被 intelligence 滤掉)
let pool = vec![
ModelConfig {
weight: 60,
@@ -333,14 +341,14 @@ mod tests {
},
ModelConfig {
weight: 80,
cost_tier: CostTier::Free,
cost_tier: CostTier::Low,
intelligence: IntelligenceTier::Plus,
..model("c")
},
ModelConfig {
weight: 90,
cost_tier: CostTier::Low,
intelligence: IntelligenceTier::Lite, // 低于 min,滤掉
intelligence: IntelligenceTier::Lite,
..model("d")
},
ModelConfig {
@@ -352,10 +360,8 @@ mod tests {
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");
assert_eq!(ModelRouter::select(&r, &pool).unwrap().model_id, "d");
}
}