22 KiB
F-01 模型能力系统 Phase 1 — 模型配置与智能路由设计
状态:📐 设计定稿(2026-06-16,含多角度佐证 + 厂商适配 + 用户流程) 类型:架构设计文档(不碰任何 code) 关联:F-260614-05 多模态 Phase 2 / F-260614-04 多 Provider 负载均衡池 前置:F-07 df-ai-core trait 下沉 ✅ 已完成(batch61·2069f79) 决策记录:见 功能决策记录-2026-06-14.md §「模型能力系统」行
0. 摘要
将当前「一个 Provider 一个 default_model 跑全场」升级为「多模型池 + 能力感知 + 智能路由」。 用户只需填 Provider 基本信息 → 自动拉取模型列表 → 自动探测每个模型的模态/能力/价格/智力 → 路由器按场景自动选最优模型。
核心价值:含图消息自动选 Vision 模型、标题生成自动选便宜模型、复杂推理自动选强模型、限流自动 fallback。
1. 现状盘点
1.1 数据模型(AiProviderRecord)
crates/df-storage/src/models.rs:127-140
pub struct AiProviderRecord {
pub provider_type: String, // "openai_compat" | "anthropic_compat"
pub base_url: String,
pub default_model: String, // 唯一实际使用的模型名
pub models: Option<String>, // JSON array of model names(展示用,未消费)
pub config: Option<String>, // JSON extra config(几乎没用)
pub is_default: bool,
...
}
问题:
models存了模型列表但没有能力标记,不知道哪个支持 vision / tool use- 没有启用/禁用开关,配了的模型无法控制哪些参与路由
- 没有权重/优先级,无法表达"优先用 A,A 不可用时 fallback 到 B"
default_model是唯一实际使用的模型名,models列表形同虚设
1.2 模型选择机制(当前)
用户在 Settings 配置 Provider(含 base_url + default_model)
↓
AiSession.active_provider_id ← 全局唯一活跃 Provider
↓
get_active_provider() → 取 Provider 配置
↓
CompletionRequest { model: provider.default_model } ← 直接用固定 model
- 一个时刻只有一个活跃 Provider
- 所有场景(对话/标题/知识提炼/项目扫描)共用同一个 model
- 无 ModelCapability / ModelRouter / 按场景路由机制
2. 模型配置数据模型
2.1 ModelConfig(每个模型的完整描述)
/// 单个模型的完整配置(4 维度 + 路由控制)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelConfig {
// ── 基础 ──
/// 模型名(如 "glm-4v-flash")
pub model_id: String,
/// 启用/禁用(false = 配了但不参与路由,相当于"备档")
#[serde(default = "default_true")]
pub enabled: bool,
/// 用户自定义别名(可选)
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
// ── 维度 1:模态(能接收什么输入)──
pub modalities: Vec<Modality>,
// ── 维度 2:能力(能做什么)──
pub capabilities: Vec<Capability>,
// ── 维度 3:价格(成本分级)──
pub cost_tier: CostTier,
// ── 维度 4:聪明程度(智力分级)──
pub intelligence: IntelligenceTier,
// ── 路由控制 ──
/// 权重(0-100,同能力候选中优先选权重高的)
#[serde(default = "default_weight")]
pub weight: u32,
/// 上下文窗口大小(tokens)
#[serde(default = "default_context_window")]
pub context_window: usize,
// ── 探测元数据(只读,由 ModelProbe 填充)──
#[serde(skip_serializing_if = "Option::is_none")]
pub probe_source: Option<ProbeSource>,
}
fn default_true() -> bool { true }
fn default_weight() -> u32 { 50 }
fn default_context_window() -> usize { 8192 }
2.2 枚举定义
/// 模态(输入类型)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Modality {
Text,
Vision,
// 未来扩展:Audio, Video
}
/// 能力
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Capability {
ToolUse, // function calling / tool use
Embedding, // 向量嵌入
CodeGen, // 代码生成强项
}
/// 价格分级
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CostTier {
Free, // 免费
Low, // flash / mini 系列
Medium, // 标准定价
High, // 旗舰定价
}
/// 聪明程度(智力分级)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum IntelligenceTier {
Lite, // flash/mini/lite/nano — 快但简单任务
Standard, // air/standard — 日常够用
Plus, // plus/pro/max — 复杂推理
Ultra, // 最强模型 — 兜底用
}
2.3 存储方式:扩展 models JSON 字段(零迁移)
AiProviderRecord.models 从 ["glm-4-flash", "glm-4v"] 升级为:
[
{
"model_id": "glm-4-flash",
"enabled": true,
"modalities": ["text"],
"capabilities": ["tool_use"],
"cost_tier": "low",
"intelligence": "lite",
"weight": 90,
"context_window": 128000
},
{
"model_id": "glm-4v",
"enabled": true,
"modalities": ["text", "vision"],
"capabilities": ["tool_use"],
"cost_tier": "medium",
"intelligence": "plus",
"weight": 70,
"context_window": 128000
}
]
向后兼容:自定义反序列化器,老格式 ["model-a"] → 自动转成默认配置列表。
fn deserialize_models<'de, D>(d: D) -> Result<Vec<ModelConfig>, D::Error>
where D: serde::Deserializer<'de> {
let v = serde_json::Value::deserialize(d)?;
match v {
// 老格式:字符串数组 → 每个转默认 ModelConfig
serde_json::Value::Array(arr) if arr.iter().all(|v| v.is_string()) => {
arr.into_iter()
.filter_map(|v| v.as_str().map(|s| ModelConfig::with_defaults(s)))
.collect::<Vec<_>>()
.pipe(Ok)
}
// 新格式:ModelConfig 数组
serde_json::Value::Array(_) => {
serde_json::from_value::<Vec<ModelConfig>>(v).map_err(D::Error::custom)
}
_ => Ok(vec![]),
}
}
否决方案:新建 ai_models 独立表 — 增量复杂度高(迁移 + 新 Repo),models JSON 字段已够用。
3. 多角度佐证
3.1 行业对标
| 产品 | 模型配置方式 | 对照 |
|---|---|---|
| OpenRouter | 每个模型标 modality/pricing/context_length | 4 维度完全覆盖,且加了 intelligence 分级 |
| Cursor | 内置模型能力感知(自动判断图片/工具) | 预设表 + 启发式探测本质相同 |
| LangChain ModelRouter | 按 max_tokens/supports_tool_use 路由 | capabilities + modalities 路由一致 |
| One-API / New-API | 纯渠道管理,无能力感知 | 能力标记是显著增量 |
结论:4 维度配置是行业标配的超集,非过度设计。
3.2 路由器实际需求倒推
| 路由决策 | 必须知道 | 对应维度 | 缺了会怎样 |
|---|---|---|---|
| 含图消息选哪个 | 是否支持 Vision | 模态 | 发给纯文本模型 → 400 |
| Agentic loop 选哪个 | 是否支持 tool_use | 能力 | 发 tool 定义给不支持的 → 忽略/报错 |
| 标题生成选哪个 | 够便宜够快 | 价格+智力 | 用旗舰模型 → 烧钱 |
| 复杂推理选哪个 | 够聪明 | 智力 | 用 flash → 质量差 |
| 上下文超长选哪个 | 窗口够大 | context_window | 超窗口 → 截断 |
结论:每个维度直接对应路由器必须做的判断,缺一不可。
3.3 用户认知负担
| 信息 | 用户负担 | 说明 |
|---|---|---|
| 模型名 | 零 | 从文档复制 |
| 模态 | 低 | 文档一眼可见,或名字带 v |
| 能力 | 低 | 文档明确标注 |
| 价格 | 中 | 查文档,但分级别直觉可判 |
| 智力 | 低 | flash < air < plus 是行业共识 |
结论:4 个维度都是用户本来就知道的,加上自动填充,门槛极低。
3.4 可扩展性
| 未来需求 | 支持 | 扩展方式 |
|---|---|---|
| 音频输入 | ✅ | Modality 加 Audio |
| 视频输入 | ✅ | Modality 加 Video |
| 精确价格控制 | ✅ | cost_tier 升级为精确数值 |
| 流式支持差异 | ✅ | Capability 加 Streaming |
| 自定义能力标签 | ✅ | Capability 加 Custom(String) |
结论:enum + Vec 开放式,未来加维度只需加变体。
3.5 数据兼容性
老 models: ["model-a"] → 自定义反序列化自动补默认值。default_model 字段保留向后兼容。零迁移脚本。
3.6 竞品缺陷反证
| 问题 | 竞品现状 | 我们的方案 |
|---|---|---|
| 含图发给纯文本模型 | One-API 不感知能力 | 路由器自动选 Vision |
| 标题生成用旗舰模型 | 固定模型手动切 | 按 intelligence+cost 自动选 Lite |
| 新模型不知道能不能用 | 用户自己试 | 预设表 + 启发式自动推断 |
| 限流后无 fallback | 手动切换 | 按 weight 自动 fallback |
3.7 实施风险
| 风险 | 评估 | 缓解 |
|---|---|---|
| 预设表维护 | 低(主流 20-30 个,半年更新) | JSON 配置文件可热更新 |
| 启发式误判 | 低(命名是行业惯例) | 标注 Medium 可信度,用户可修正 |
| 路由器选错 | 低 | weight fallback + 降级兜底 |
7 个角度一致指向:方案可行且合理。
4. 模型探测器(ModelProbe)
4.1 多源探测
每个模型的信息从多个来源获取,交叉验证:
pub struct ModelProbe {
preset_table: ModelPresetTable,
}
impl ModelProbe {
pub async fn probe(&self, provider: &AiProviderRecord, model_id: &str) -> ModelProfile {
// 1. 启发式:从模型名推断
let heuristic = self.heuristic_infer(model_id);
// 2. 查预设表(精确匹配 > 模糊匹配)
let preset = self.preset_table.lookup(model_id);
// 3. 多源合并(预设表 > 启发式)
let merged = self.merge_sources(heuristic, preset);
// 4. 最终结果 + 标注每个维度的置信度
self.finalize(merged)
}
}
4.2 探测优先级链
用户手动设定 (UserSet) ← 最高优先,永不被覆盖
↓
内置预设表精确匹配 (PresetTable) ← 高可信
↓
模型名启发式 (Heuristic) ← 中可信,标注"建议确认"
↓
默认值 (Default) ← 低可信
4.3 探测结果带置信度
pub struct ModelProfile {
pub model_id: String,
pub modalities: SourcedField<Vec<Modality>>,
pub capabilities: SourcedField<Vec<Capability>>,
pub cost_tier: SourcedField<CostTier>,
pub intelligence: SourcedField<IntelligenceTier>,
pub context_window: SourcedField<usize>,
}
pub struct SourcedField<T> {
pub value: T,
pub source: Source,
pub confidence: Confidence,
}
pub enum Source { PresetTable, Heuristic, UserSet }
pub enum Confidence { High, Medium, Low }
4.4 内置预设表
pub struct ModelPresetTable {
exact: HashMap<String, ModelPreset>, // 精确匹配
patterns: Vec<PatternRule>, // 模糊匹配
}
// 精确匹配示例
"glm-4-flash" => { modalities:[Text], capabilities:[ToolUse], cost:Low, intelligence:Lite, context:128K }
"glm-4-air" => { modalities:[Text], capabilities:[ToolUse], cost:Low, intelligence:Standard }
"glm-4-plus" => { modalities:[Text], capabilities:[ToolUse], cost:Medium, intelligence:Plus }
"glm-4v" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Medium, intelligence:Plus }
"glm-4v-flash" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Low, intelligence:Lite }
"deepseek-chat" => { modalities:[Text], capabilities:[ToolUse], cost:Low, intelligence:Standard }
"deepseek-coder"=> { modalities:[Text], capabilities:[ToolUse,CodeGen], cost:Low, intelligence:Plus }
"claude-3-5-sonnet-20241022" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:High, intelligence:Ultra }
"claude-3-haiku-20240307" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Low, intelligence:Standard }
"gpt-4o" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:High, intelligence:Ultra }
"gpt-4o-mini" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Low, intelligence:Standard }
"embedding-3" => { modalities:[], capabilities:[Embedding], cost:Low, intelligence:Lite }
// 模糊匹配规则
PatternRule { keyword: "flash|mini|lite|nano", → Intelligence(Lite) }
PatternRule { keyword: "plus|pro|max|ultra", → Intelligence(Plus) }
PatternRule { keyword: "v$|vl|vision|-v\\d", → Modality(Vision) }
PatternRule { keyword: "embed", → Capability(Embedding) }
PatternRule { keyword: "code|coder", → Capability(CodeGen) }
预设表以 JSON 配置文件形式存储(crates/df-ai/presets/models.json),可热更新不随版本绑。
5. 厂商模型列表适配
5.1 差异盘点
| 厂商/协议 | 端点 | 鉴权 | 返回格式 |
|---|---|---|---|
| OpenAI 兼容(GLM/DeepSeek/Kimi/通义) | GET /v1/models |
Bearer | {data:[{id, owned_by}]} |
| Anthropic | GET /v1/models |
x-api-key + version | {data:[{id, display_name}]} |
| 中转站 | GET /v1/models |
Bearer | 同 OpenAI,但可能混入噪音或返回 404 |
| Ollama | GET /api/tags |
无 | {models:[{name, size}]} — 完全不同 |
5.2 统一拉取接口
/// 模型拉取结果(统一格式)
pub struct FetchedModel {
pub model_id: String,
pub display_name: Option<String>,
}
/// 按 provider_type 分派
pub async fn fetch_models(
provider_type: &str,
base_url: &str,
api_key: &str,
) -> Result<Vec<FetchedModel>, FetchError> {
match provider_type {
"openai_compat" => fetch_openai_compat(base_url, api_key).await,
"anthropic_compat" => fetch_anthropic_compat(base_url, api_key).await,
_ => Err(FetchError::Unsupported),
}
}
5.3 URL 智能拼接
fn build_models_url(base_url: &str) -> String {
let url = base_url.trim_end_matches('/');
if url.ends_with("/chat/completions") {
url.replace("/chat/completions", "/models")
} else if url.ends_with("/v1") || url.ends_with("/v4") {
format!("{}/models", url)
} else if url.ends_with("/models") {
url.to_string()
} else {
format!("{}/v1/models", url)
}
}
5.4 噪音过滤
fn is_non_chat_model(id: &str) -> bool {
let id = id.to_lowercase();
id.contains("dall-e") || id.contains("midjourney") // 图片生成
|| id.contains("tts") || id.contains("whisper") // 语音
|| id.contains("moderation") // 审查
// embedding 保留(知识库需要)
}
5.5 失败降级
| 错误 | 处理 |
|---|---|
| 404 / 不支持 | 静默降级到手动输入(批量文本) |
| 401 / 403 | 提示密钥问题 |
| 网络/超时 | 提示检查连接 |
| 解析失败 | 提示格式不兼容,降级手动 |
核心原则:自动拉取是锦上添花,手动输入永远保底。
6. 模型路由器(ModelRouter)
6.1 路由逻辑
pub struct TaskRequirements {
pub modalities: Vec<Modality>, // 任务需要什么模态
pub needs_tool_use: bool, // 是否需要工具调用
pub min_intelligence: IntelligenceTier, // 最低智力要求
pub max_cost: Option<CostTier>, // 成本上限(可选)
pub estimated_context: usize, // 预估上下文大小
}
impl ModelRouter {
pub fn select(&self, req: &TaskRequirements, pool: &[ModelConfig]) -> Option<&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, -(m.cost_tier as i32))) // 7. 权重优先,同权重选便宜的
}
}
6.2 场景路由表
| 场景 | 路由偏好 | 典型选择 |
|---|---|---|
| 标题生成 | Lite + Low cost | glm-4-flash |
| 日常对话 | Standard | glm-4-air |
| 写代码 | Standard/Plus + CodeGen | deepseek-coder |
| 含图片 | Vision + Standard | glm-4v |
| 复杂推理 | Plus/Ultra | glm-4-plus |
| 知识嵌入 | Embedding | embedding-3 |
6.3 7 个调用点接入
| 调用点 | 当前代码位置 | TaskRequirements |
|---|---|---|
| 主对话 stream_llm | agentic.rs:50 | 动态(按消息内容:含图→Vision) |
| 标题生成 | title.rs:63 | Lite + Low(省钱) |
| 知识提炼 | knowledge_inject.rs:33,323 | Standard |
| 知识嵌入 | knowledge_inject.rs embedding | Embedding |
| 项目扫描描述 | project.rs:378 | Standard(含图→Vision) |
| 灵感评估 | df-ideas adversarial.rs | Standard |
| AiNode 工作流 | df-nodes ai_node.rs:118 | 按节点配置 |
7. 用户配置与使用流程
7.1 配置流程(做一次)
Step 1: 填 Provider 基本信息(名称/类型/地址/密钥)
↓
Step 2: 点「测试连接并拉取模型」
↓ 自动调 /v1/models(按协议分派)
↓ 自动过滤非 chat 模型
↓ 自动探测每个模型特征(预设表 + 启发式)
↓
Step 3: 显示模型列表(已标注特征 + 可信度)
↓ 用户确认或微调(大多数不需要进这步)
↓
Step 4: 完成
拉取失败时降级:手动输入模型名(批量文本,每行一个)→ 同样走探测。
7.2 日常使用(零配置)
用户发消息 → 路由器自动选模型 → 用户无感 → 出结果
手动覆盖:AiChat 顶部下拉选「自动(推荐)」或指定具体模型。
自动 fallback:模型限流/失败 → 按 weight 选次高 → 自动重试。
7.3 简洁性指标
| 传统做法 | 我们的做法 | 省了什么 |
|---|---|---|
| 手动填每个模型能力 | 预设表自动填充 | 省 90% 配置 |
| 自己记哪个支持图片 | 路由器自动选 | 省记忆 |
| 每次对话手动切模型 | 自动路由 | 省操作 |
| 限流后手动换 | 自动 fallback | 省故障处理 |
用户最小操作路径:填 3 个字段 → 点 1 个按钮 → 完成。
8. 实施分阶段
阶段 1:数据模型(df-ai-core + df-storage)
| 文件 | 改动 |
|---|---|
crates/df-ai-core/src/provider.rs |
新增 ModelConfig / Modality / Capability / CostTier / IntelligenceTier |
crates/df-storage/src/models.rs |
AiProviderRecord.models 反序列化升级(兼容老格式) |
crates/df-ai/src/model_config.rs(新建) |
deserialize_models 兼容函数 + ModelConfig::with_defaults |
阶段 2:预设表 + 探测器(df-ai)
| 文件 | 改动 |
|---|---|
crates/df-ai/presets/models.json(新建) |
主流模型预设数据 |
crates/df-ai/src/model_probe.rs(新建) |
ModelProbe + 启发式 + 多源合并 |
crates/df-ai/src/preset_table.rs(新建) |
预设表加载 + 精确/模糊匹配 |
阶段 3:厂商模型列表拉取(df-ai)
| 文件 | 改动 |
|---|---|
crates/df-ai/src/model_fetch.rs(新建) |
fetch_models + openai_compat/anthropic_compat 分派 + URL 拼接 + 噪音过滤 |
阶段 4:路由器(df-ai)
| 文件 | 改动 |
|---|---|
crates/df-ai/src/router.rs(新建) |
ModelRouter + TaskRequirements + select 逻辑 |
阶段 5:7 调用点接入(src-tauri)
| 文件 | 改动 |
|---|---|
src-tauri/src/commands/ai/agentic.rs |
主对话路由(按 has_image 动态选) |
src-tauri/src/commands/ai/title.rs |
标题生成路由(Lite + Low) |
src-tauri/src/commands/ai/knowledge_inject.rs |
知识提炼/嵌入路由 |
src-tauri/src/commands/project.rs |
项目扫描路由 |
src-tauri/src/commands/ai/commands.rs |
新增 IPC:ai_fetch_models / ai_probe_model |
阶段 6:前端(src)
| 文件 | 改动 |
|---|---|
src/api/types.ts |
ModelConfig 类型对齐 |
src/components/settings/ |
Provider 配置页加模型列表 + 探测结果展示 + 启用/禁用/权重 |
src/components/AiChat.vue |
顶部模型下拉(自动/指定) |
9. 与 F-05(多模态)的关系
| 子能力 | 依赖 F-01? | 说明 |
|---|---|---|
| ContentPart 数据模型 | ❌ | 纯类型升级,与 ModelConfig 解耦 |
| provider content 数组化 | ❌ | 协议层改造,与路由器无关 |
| 前端粘贴/拖拽/渲染 | ❌ | 纯前端 |
| vision 模型自动路由 | ✅ | ModelRouter 按 modalities 含 Vision 筛选 |
F-05 可独立先做 80%(数据模型 + provider + 前端),vision 路由留到 F-01 落地后接入。 过渡期靠 ModelConfig.modalities 手动标注 + 路由器筛选。
10. 不做的事(显式排除)
- 不做模型自动下载/安装(仅 API 模型,不含本地模型管理)
- 不做模型 benchmark 自动跑分(智力分级靠预设表 + 用户确认)
- 不做多 Provider 负载均衡(属 F-04,本任务只做单 Provider 内多模型路由)
- 不做模型用量统计/成本告警(属运维增强,后续独立做)
- 不新建 ai_models 独立表(扩展 models JSON 字段够用)
- 本文档不实施任何 code(纯设计)