新增: F-01阶段5模型路由调用点接入
This commit is contained in:
@@ -10,7 +10,10 @@
|
||||
|
||||
use std::cmp::Reverse;
|
||||
|
||||
use df_ai_core::model::{Capability, CostTier, IntelligenceTier, Modality, ModelConfig};
|
||||
// 阶段5: 调用点经 `df_ai::router::{Modality, Capability, CostTier, IntelligenceTier}`
|
||||
// 直接 import 维度枚举构造 TaskRequirements(对齐任务规格 import 风格),re-export 避免调用点
|
||||
// 各自从 df_ai_core::model 取(跨 crate 路径冗长)。select/select_model_id 仅借用枚举,无重定义。
|
||||
pub use df_ai_core::model::{Capability, CostTier, IntelligenceTier, Modality, ModelConfig};
|
||||
|
||||
/// 任务对模型的需求(5 维度,对齐设计 §6.1)。
|
||||
///
|
||||
@@ -63,6 +66,20 @@ impl ModelRouter {
|
||||
}
|
||||
}
|
||||
|
||||
/// 阶段5 调用点 helper — 路由选模型并直接返回 model_id(纯函数)。
|
||||
///
|
||||
/// 给定 TaskRequirements + 候选池,返回最优模型的 `model_id`。
|
||||
/// 调用点用法:`provider.model_configs`(Vec<ModelConfig>)→ `select_model_id(&req, &pool)`
|
||||
/// → `Option<String>`;None 时兜底 `provider.default_model`(行为不变,平滑过渡)。
|
||||
///
|
||||
/// 行为不变保证:
|
||||
/// - 池空(用户未通过 Settings 拉取模型)→ 返回 None → 调用点兜底 default_model
|
||||
/// - 池非空但无候选满足需求 → 返回 None → 兜底 default_model
|
||||
/// - 池非空命中 → 返回 model_id(F-01 路由目标,拉取即启用路由)
|
||||
pub fn select_model_id(req: &TaskRequirements, pool: &[ModelConfig]) -> Option<String> {
|
||||
ModelRouter::select(req, pool).map(|m| m.model_id.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -94,6 +111,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 阶段5 select_model_id helper ──
|
||||
|
||||
#[test]
|
||||
fn select_model_id_empty_pool_returns_none() {
|
||||
// 池空(用户未拉取模型)→ None,调用点兜底 default_model
|
||||
let pool: Vec<ModelConfig> = vec![];
|
||||
assert!(select_model_id(&req(), &pool).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_model_id_hit_returns_model_id() {
|
||||
// 池非空命中 → 返回 model_id 字符串(非引用)
|
||||
let pool = vec![model("glm-4-flash")];
|
||||
assert_eq!(select_model_id(&req(), &pool).as_deref(), Some("glm-4-flash"));
|
||||
}
|
||||
|
||||
// ── 步骤 1:enabled 过滤 ──
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6,6 +6,10 @@ edition = "2021"
|
||||
[dependencies]
|
||||
df-core = { path = "../df-core" }
|
||||
df-ai-core = { path = "../df-ai-core" }
|
||||
# F-01 阶段5: df-ideas 对抗评估接入 df_ai::router::select_model_id(纯函数路由)。
|
||||
# 注:df-ai 引入 reqwest/futures/eventsource-stream 重依赖,但 router 模块仅依赖
|
||||
# df-ai-core::model,实际编译期 df-ideas 仅引用 router 符号(零 HTTP 代码路径)。
|
||||
df-ai = { path = "../df-ai" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
@@ -16,6 +16,7 @@ use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use df_ai_core::model::ModelConfig;
|
||||
use df_ai_core::provider::LlmProvider;
|
||||
use df_core::types::{IdeaId, Priority};
|
||||
use crate::capture::Idea;
|
||||
@@ -95,17 +96,26 @@ pub struct AdversarialEngine {
|
||||
/// 可选 LLM provider。Some → 优先 LLM 评估(失败降级启发式);None → 纯启发式。
|
||||
/// 构造注入(与 IdeaPromoter::new(policy) 同一模式),批量评估复用同一 provider。
|
||||
provider: Option<Arc<dyn LlmProvider>>,
|
||||
/// F-01 阶段5: 候选模型池。非空时 evaluate_with_llm 经 select_model_id 路由选模型;
|
||||
/// 空(None provider 或未注入池)→ model 留空由 provider impl 回填自身 default_model
|
||||
/// (与接入前行为一致,平稳过渡)。
|
||||
model_pool: Vec<ModelConfig>,
|
||||
}
|
||||
|
||||
impl AdversarialEngine {
|
||||
/// 注入 LLM provider 构造(provider Some 时走 LLM,调用失败自动降级启发式)
|
||||
pub fn new(provider: Arc<dyn LlmProvider>) -> Self {
|
||||
Self { provider: Some(provider) }
|
||||
Self { provider: Some(provider), model_pool: Vec::new() }
|
||||
}
|
||||
|
||||
/// F-01 阶段5: 注入 provider + 候选模型池构造。池非空时 evaluate_with_llm 走路由。
|
||||
pub fn with_pool(provider: Arc<dyn LlmProvider>, model_pool: Vec<ModelConfig>) -> Self {
|
||||
Self { provider: Some(provider), model_pool }
|
||||
}
|
||||
|
||||
/// 纯启发式构造(无 LLM 配置时的默认模式)
|
||||
pub fn heuristic() -> Self {
|
||||
Self { provider: None }
|
||||
Self { provider: None, model_pool: Vec::new() }
|
||||
}
|
||||
|
||||
/// 执行完整的对抗评估(内部按 provider 有无调度 LLM / 启发式,失败降级)
|
||||
@@ -142,10 +152,21 @@ impl AdversarialEngine {
|
||||
/// 0.4 在「稳定可复现」与「论点多样性」间取得平衡。
|
||||
async fn evaluate_with_llm(&self, idea: &Idea, provider: &Arc<dyn LlmProvider>) -> Result<AdversarialEval> {
|
||||
let prompt = build_adversarial_prompt(idea);
|
||||
// F-01 阶段5: 智能路由 — 对抗评估 TaskRequirements(Standard,无工具)。
|
||||
// 池非空 → select_model_id 选最优 model_id;池空/无匹配 → 留空由 provider impl
|
||||
// 回填自身 default_model(与接入前行为一致,平稳过渡)。
|
||||
let eval_req = df_ai::router::TaskRequirements {
|
||||
modalities: vec![df_ai_core::model::Modality::Text],
|
||||
needs_tool_use: false,
|
||||
min_intelligence: df_ai_core::model::IntelligenceTier::Standard,
|
||||
max_cost: None,
|
||||
estimated_context: 0,
|
||||
};
|
||||
let model = df_ai::router::select_model_id(&eval_req, &self.model_pool).unwrap_or_default();
|
||||
let request = df_ai_core::provider::CompletionRequest {
|
||||
// provider 自带 default_model;model 留空让 provider impl 回填自身默认模型。
|
||||
// 路由命中 → 用 model_id;否则留空让 provider impl 回填自身 default_model。
|
||||
// (OpenAICompatProvider::convert_request 在 req.model.is_empty() 时回退 default_model)
|
||||
model: String::new(),
|
||||
model,
|
||||
messages: vec![
|
||||
df_ai_core::provider::ChatMessage::system(SYSTEM_PROMPT),
|
||||
df_ai_core::provider::ChatMessage::user(prompt),
|
||||
|
||||
@@ -10,7 +10,11 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use df_ai::df_ai_core::model::{IntelligenceTier, Modality, ModelConfig};
|
||||
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider};
|
||||
// F-01 阶段5: AiNode 路由 — 节点 config.model_id 优先;否则按 TaskRequirements 路由
|
||||
// (默认 Standard + needs_tool_use=true)。池空/无匹配兜底 record.default_model。
|
||||
use df_ai::router::{select_model_id, TaskRequirements};
|
||||
use df_storage::crud::{AiProviderRepo, TaskRepo};
|
||||
use df_storage::db::Database;
|
||||
use df_storage::models::AiProviderRecord;
|
||||
@@ -45,6 +49,9 @@ struct ResolvedProvider {
|
||||
api_key: String,
|
||||
/// model 为空时的占位(record.default_model 或 "gpt-4o-mini"),避免 provider 构造 panic
|
||||
default_model: String,
|
||||
/// F-01 阶段5: 候选模型池(来自 record.model_configs)。parse_params 路由用:
|
||||
/// config.model 留空时经 select_model_id 选最优;池空兜底 default_model。
|
||||
model_pool: Vec<ModelConfig>,
|
||||
}
|
||||
|
||||
/// 经 ai_providers 表 + df_storage::secret 解析 provider 构造要素(FR-S1 注入链核心)。
|
||||
@@ -117,6 +124,8 @@ async fn resolve_provider(
|
||||
base_url,
|
||||
api_key,
|
||||
default_model,
|
||||
// 老明文路径无 record,候选池空(无路由能力,兜底 default_model)。
|
||||
model_pool: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -154,6 +163,7 @@ fn resolve_from_record(
|
||||
base_url: record.base_url.clone(),
|
||||
api_key,
|
||||
default_model,
|
||||
model_pool: record.model_configs.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -179,11 +189,30 @@ fn parse_params(
|
||||
.ok_or_else(|| anyhow::anyhow!("AiNode 缺少必填参数: prompt(config 或上游输入均无)"))?;
|
||||
|
||||
// ── 可选参数 ──
|
||||
let model = config
|
||||
// model 解析优先级(F-01 阶段5):config.model 显式指定 > 路由选优(provider.model_pool 非空时)
|
||||
// > 空(CompletionRequest.model 留空由 provider impl 回填 default_model,行为不变)。
|
||||
// 注:provider.model_pool 在 provider move 进 AiNodeParams 前先借引用路由,选中的 model_id
|
||||
// 填入 CompletionRequest.model;provider.default_model 仍是 build_provider 兜底用。
|
||||
let config_model = config
|
||||
.get("model")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let model = if !config_model.is_empty() {
|
||||
config_model
|
||||
} else {
|
||||
// F-01 阶段5: AiNode 默认路由 — Standard + needs_tool_use=true(工作流无人值守 AI 步骤
|
||||
// 常含工具调用,如检索/生成;无需工具的节点应在 config 显式指定 model)。
|
||||
// select_model_id None(池空/无匹配)→ 空串(由 provider impl 回填 default_model)。
|
||||
let node_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: true,
|
||||
min_intelligence: IntelligenceTier::Standard,
|
||||
max_cost: None,
|
||||
estimated_context: 0,
|
||||
};
|
||||
select_model_id(&node_req, &provider.model_pool).unwrap_or_default()
|
||||
};
|
||||
let temperature = config
|
||||
.get("temperature")
|
||||
.and_then(|v| v.as_f64())
|
||||
@@ -600,6 +629,7 @@ mod tests {
|
||||
base_url: "https://api.example.com".to_string(),
|
||||
api_key: "sk-test".to_string(),
|
||||
default_model: "gpt-4o-mini".to_string(),
|
||||
model_pool: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user