新增: F-01阶段5模型路由调用点接入
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -820,6 +820,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"df-ai",
|
||||
"df-ai-core",
|
||||
"df-core",
|
||||
"regex",
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,13 @@ use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider};
|
||||
// CR-30-1: 复用 retry::backoff_delay(jitter 1s→2s→4s) + is_status_retryable(Fatal 分类)
|
||||
// 实现流前失败重试退避对齐(决策 F-260616-07 a1),避免重写退避逻辑。
|
||||
use df_ai::retry;
|
||||
// F-01 阶段5: 智能路由 helper + TaskRequirements + 维度枚举。
|
||||
// 调用点构造 TaskRequirements(主对话:needs_tool_use=true,min_intelligence=Standard,
|
||||
// 当前仅 Text 模态;后续多模态接入时检测消息内 Part/Image 追加 Vision),
|
||||
// 经 select_model_id 在 provider.model_configs 池中选最优;池空/无匹配兜底 default_model。
|
||||
use df_ai::router::{
|
||||
select_model_id, IntelligenceTier, Modality, TaskRequirements,
|
||||
};
|
||||
|
||||
use df_storage::db::Database;
|
||||
use df_storage::models::AiProviderRecord;
|
||||
@@ -154,6 +161,19 @@ pub(crate) async fn run_agentic_loop(
|
||||
key_len = key_len,
|
||||
"[ai] 发起 LLM 请求"
|
||||
);
|
||||
|
||||
// F-01 阶段5: 主对话路由 — TaskRequirements(needs_tool_use=true,min_intelligence=Standard)。
|
||||
// 模态当前仅 Text(图像消息类型未实现,后续多模态接入时检测 Part/Image 追加 Vision)。
|
||||
// select_model_id None(池空/无匹配)→ 兜底 default_model,行为与接入前一致。
|
||||
let agentic_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: true,
|
||||
min_intelligence: IntelligenceTier::Standard,
|
||||
max_cost: None,
|
||||
estimated_context: 0,
|
||||
};
|
||||
let resolved_model = select_model_id(&agentic_req, &provider_config.model_configs)
|
||||
.unwrap_or_else(|| provider_config.default_model.clone());
|
||||
let tool_defs = tools_arc.tool_definitions();
|
||||
// 停止信号副本:stream_llm 与每轮迭代共享读取,避免重复加锁
|
||||
// notify 同取一份 Arc 引用(B-260615-14):stream_llm select! 监听 notified() 即时唤醒
|
||||
@@ -265,7 +285,7 @@ pub(crate) async fn run_agentic_loop(
|
||||
// 一致,重建与复用等价。收益:省去每次重试的 lock() + build_for_request(含
|
||||
// all_messages_clone 全量 clone)。
|
||||
let retry_request = CompletionRequest {
|
||||
model: provider_config.default_model.clone(),
|
||||
model: resolved_model.clone(),
|
||||
messages: messages.clone(),
|
||||
temperature: Some(0.7),
|
||||
max_tokens: Some(8192),
|
||||
@@ -387,16 +407,16 @@ pub(crate) async fn run_agentic_loop(
|
||||
}
|
||||
if !full_text.is_empty() {
|
||||
let mut msg = ChatMessage::assistant(&full_text);
|
||||
msg.model = Some(provider_config.default_model.clone());
|
||||
msg.model = Some(resolved_model.clone());
|
||||
session.messages.push(msg);
|
||||
// 追加系统提示消息:响应因网络中断不完整(对齐决策 a1 系统提示机制)
|
||||
let mut notice = ChatMessage::system("⚠ 响应因网络中断不完整,以上为已接收的部分内容。可重新发送以获取完整回复。");
|
||||
notice.model = Some(provider_config.default_model.clone());
|
||||
notice.model = Some(resolved_model.clone());
|
||||
session.messages.push(notice);
|
||||
}
|
||||
}
|
||||
|
||||
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&provider_config.default_model)).await;
|
||||
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model)).await;
|
||||
// 标题生成后台化(失败有 extract_title 兜底)
|
||||
spawn_ensure_title(&provider_config, &db, &conv_id, &app_handle, &session_arc, &llm_concurrency);
|
||||
guard.reset().await;
|
||||
@@ -442,11 +462,11 @@ pub(crate) async fn run_agentic_loop(
|
||||
})
|
||||
.collect();
|
||||
let mut msg = ChatMessage::assistant_with_tools(&full_text, ai_tool_calls);
|
||||
msg.model = Some(provider_config.default_model.clone());
|
||||
msg.model = Some(resolved_model.clone());
|
||||
session.messages.push(msg);
|
||||
} else if !full_text.is_empty() {
|
||||
let mut msg = ChatMessage::assistant(&full_text);
|
||||
msg.model = Some(provider_config.default_model.clone());
|
||||
msg.model = Some(resolved_model.clone());
|
||||
session.messages.push(msg);
|
||||
}
|
||||
}
|
||||
@@ -458,7 +478,7 @@ pub(crate) async fn run_agentic_loop(
|
||||
completion_tokens: tokens.completion(),
|
||||
total_tokens: tokens.total(),
|
||||
};
|
||||
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&provider_config.default_model)).await;
|
||||
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model)).await;
|
||||
// 标题生成后台化:不阻塞 Completed emit(失败有 extract_title 兜底)
|
||||
spawn_ensure_title(&provider_config, &db, &conv_id, &app_handle, &session_arc, &llm_concurrency);
|
||||
guard.reset().await;
|
||||
@@ -483,7 +503,7 @@ pub(crate) async fn run_agentic_loop(
|
||||
completion_tokens: tokens.completion(),
|
||||
total_tokens: tokens.total(),
|
||||
};
|
||||
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&provider_config.default_model)).await;
|
||||
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model)).await;
|
||||
// B-260615-26: 审批等待 return 前 disarm guard——保持 generating=true 留 try_continue 续生成,
|
||||
// 同时 Drop 因 done=true 跳过复位 spawn(避免误复位审批态 generating 致 ai_approve→try_continue 不续)
|
||||
guard.disarm();
|
||||
@@ -512,7 +532,7 @@ pub(crate) async fn run_agentic_loop(
|
||||
completion_tokens: tokens.completion(),
|
||||
total_tokens: tokens.total(),
|
||||
};
|
||||
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&provider_config.default_model)).await;
|
||||
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model)).await;
|
||||
// 暂停态保持 generating=true(防其他 send 抢占,仿审批),disarm guard 跳过 Drop 兜底复位
|
||||
guard.disarm();
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiMaxRoundsReached {
|
||||
@@ -540,8 +560,9 @@ pub(crate) async fn run_agentic_loop(
|
||||
let knowledge_config = knowledge_config.clone();
|
||||
let app_handle = app_handle.clone();
|
||||
let llm_concurrency = llm_concurrency.clone();
|
||||
let resolved_model = resolved_model.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&provider_config.default_model)).await;
|
||||
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model)).await;
|
||||
// 知识提炼:需读已落库的对话消息,故在 save 之后
|
||||
if let Err(e) = maybe_spawn_extraction(&session_arc, &db, &conv_id, &provider_config, &knowledge_config, llm_concurrency.clone()).await {
|
||||
tracing::warn!("知识提炼触发失败(非阻断): {}", e);
|
||||
|
||||
@@ -5,6 +5,13 @@ use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider, MessageRole};
|
||||
// F-01 阶段5: 知识提炼 / 嵌入两路路由。
|
||||
// - 提炼: TaskRequirements(Standard,无工具)— 在对话 provider 的 model_configs 池中选。
|
||||
// - 嵌入: TaskRequirements(Capability::Embedding,无工具)— 在 embedding provider 的池中选,
|
||||
// 池空兜底 config.embedding_model(行为不变,现有 KnowledgeConfig 配置即生效)。
|
||||
use df_ai::router::{
|
||||
select_model_id, IntelligenceTier, Modality, TaskRequirements,
|
||||
};
|
||||
use df_storage::crud::{AiConversationRepo, KnowledgeRepo};
|
||||
use df_storage::db::Database;
|
||||
use df_storage::models::{AiProviderRecord, KnowledgeRecord};
|
||||
@@ -18,12 +25,19 @@ use crate::commands::err_str;
|
||||
use super::{AiSession};
|
||||
|
||||
/// 按配置构建 embedding provider + model。None = 配置缺失/provider 不存在。
|
||||
///
|
||||
/// F-01 阶段5: model 选择路由优先 — 在 embedding provider 的 model_configs 池中按
|
||||
/// TaskRequirements(needs_tool_use=false)选最优;池空(未拉取)兜底 config.embedding_model。
|
||||
///
|
||||
/// 注:TaskRequirements 无 capabilities 字段(router 仅按 needs_tool_use 过滤 ToolUse),
|
||||
/// 嵌入模型由 embedding provider 的池构成(用户在 KnowledgeSettings 配 embedding provider,
|
||||
/// 该 provider 的 model_configs 通常即嵌入模型),故 needs_tool_use=false 宽松过滤即可命中。
|
||||
async fn resolve_embed_provider(
|
||||
state: &AppState,
|
||||
config: &crate::state::KnowledgeConfig,
|
||||
) -> Option<(Box<dyn LlmProvider>, String)> {
|
||||
let id = config.embedding_provider_id.as_ref()?;
|
||||
let model = config.embedding_model.clone().unwrap_or_else(|| "embedding-3".to_string());
|
||||
let fallback_model = config.embedding_model.clone().unwrap_or_else(|| "embedding-3".to_string());
|
||||
let rec = match state.ai_providers.get_by_id(id).await {
|
||||
Ok(Some(r)) => r,
|
||||
_ => {
|
||||
@@ -39,6 +53,16 @@ async fn resolve_embed_provider(
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// 嵌入路由:needs_tool_use=false(排除 ToolUse 专要求,允许嵌入模型入选)。
|
||||
// select_model_id None(池空/无匹配)→ 兜底 config.embedding_model(行为不变)。
|
||||
let embed_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: false,
|
||||
min_intelligence: IntelligenceTier::Lite,
|
||||
max_cost: None,
|
||||
estimated_context: 0,
|
||||
};
|
||||
let model = select_model_id(&embed_req, &rec.model_configs).unwrap_or(fallback_model);
|
||||
Some((provider, model))
|
||||
}
|
||||
|
||||
@@ -314,8 +338,19 @@ async fn extract_knowledge_from_conversation(
|
||||
ChatMessage::system(EXTRACTION_SYSTEM_PROMPT),
|
||||
ChatMessage::user(&format!("请从以下对话中提炼可复用知识:\n\n{}", conv_text)),
|
||||
];
|
||||
// F-01 阶段5: 知识提炼路由 — TaskRequirements(Standard,无工具)。
|
||||
// select_model_id None(池空/无匹配)→ 兜底 default_model(行为不变)。
|
||||
let extract_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: false,
|
||||
min_intelligence: IntelligenceTier::Standard,
|
||||
max_cost: None,
|
||||
estimated_context: 0,
|
||||
};
|
||||
let extract_model = select_model_id(&extract_req, &provider_config.model_configs)
|
||||
.unwrap_or_else(|| provider_config.default_model.clone());
|
||||
let request = CompletionRequest {
|
||||
model: provider_config.default_model.clone(),
|
||||
model: extract_model,
|
||||
messages: extract_messages,
|
||||
temperature: Some(0.3),
|
||||
max_tokens: Some(2048),
|
||||
|
||||
@@ -6,6 +6,11 @@ use tauri::{AppHandle, Emitter};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider, MessageRole};
|
||||
// F-01 阶段5: 标题生成路由 — TaskRequirements(Lite + Low 成本,无需工具)。
|
||||
// 标题是短文本摘要任务,选便宜 Lite 模型;池空/无匹配兜底 default_model(行为不变)。
|
||||
use df_ai::router::{
|
||||
select_model_id, CostTier, IntelligenceTier, Modality, TaskRequirements,
|
||||
};
|
||||
use df_storage::crud::AiConversationRepo;
|
||||
use df_storage::db::Database;
|
||||
use df_storage::models::AiProviderRecord;
|
||||
@@ -69,7 +74,18 @@ pub(crate) async fn ensure_conversation_title(
|
||||
return;
|
||||
}
|
||||
};
|
||||
let title = match generate_title_via_llm(&*provider, &provider_config.default_model, summary_msgs, &llm_concurrency).await {
|
||||
// F-01 阶段5: 标题生成路由 — 选便宜 Lite 模型(Low 成本上限,无工具)。
|
||||
// select_model_id None(池空/无匹配)→ 兜底 default_model。
|
||||
let title_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: false,
|
||||
min_intelligence: IntelligenceTier::Lite,
|
||||
max_cost: Some(CostTier::Low),
|
||||
estimated_context: 0,
|
||||
};
|
||||
let title_model = select_model_id(&title_req, &provider_config.model_configs)
|
||||
.unwrap_or_else(|| provider_config.default_model.clone());
|
||||
let title = match generate_title_via_llm(&*provider, &title_model, summary_msgs, &llm_concurrency).await {
|
||||
Some(t) => t,
|
||||
None => extract_title(&all_msgs).unwrap_or_else(|| "新对话".to_string()),
|
||||
};
|
||||
|
||||
@@ -187,9 +187,12 @@ pub async fn evaluate_idea(
|
||||
let scores = df_ideas::scoring::ScoringEngine::compute_default(&idea);
|
||||
|
||||
// 对抗式评估(构造注入:从 DB 读默认 provider 装配 LLM,无 provider/构造失败 → 启发式兜底)
|
||||
// F-01 阶段5: 透传 model_configs 池,evaluate_with_llm 经路由选模型(池空兜底 default_model)。
|
||||
let provider = build_default_provider(&state).await;
|
||||
let engine = match provider {
|
||||
Some(p) => df_ideas::adversarial::AdversarialEngine::new(Arc::from(p)),
|
||||
Some((p, pool)) => {
|
||||
df_ideas::adversarial::AdversarialEngine::with_pool(Arc::from(p), pool)
|
||||
}
|
||||
None => df_ideas::adversarial::AdversarialEngine::heuristic(),
|
||||
};
|
||||
let eval = engine.evaluate(&idea).await.map_err(err_str)?;
|
||||
@@ -262,7 +265,12 @@ pub async fn evaluate_idea(
|
||||
///
|
||||
/// 复用 `commands::ai::secret::build_provider_for`(resolve→ensure→build 三步),
|
||||
/// 与 AI Chat / 项目扫描的 provider 构造路径统一(FR-S1 密钥解析一致)。
|
||||
async fn build_default_provider(state: &State<'_, AppState>) -> Option<Box<dyn LlmProvider>> {
|
||||
///
|
||||
/// 返回 (provider, model_pool):model_pool = 选中 provider 的 model_configs(F-01 阶段5,
|
||||
/// 供对抗评估路由)。池空(用户未拉取)→ 调用方兜底 default_model。
|
||||
async fn build_default_provider(
|
||||
state: &State<'_, AppState>,
|
||||
) -> Option<(Box<dyn LlmProvider>, Vec<df_ai::df_ai_core::model::ModelConfig>)> {
|
||||
let providers = state.ai_providers.list_all().await.ok()?;
|
||||
let pc = providers
|
||||
.iter()
|
||||
@@ -270,7 +278,7 @@ async fn build_default_provider(state: &State<'_, AppState>) -> Option<Box<dyn L
|
||||
.cloned()
|
||||
.or_else(|| providers.into_iter().next())?;
|
||||
match crate::commands::ai::secret::build_provider_for(&pc) {
|
||||
Ok(p) => Some(p),
|
||||
Ok(p) => Some((p, pc.model_configs.clone())),
|
||||
Err(e) => {
|
||||
// 密钥不可用:启发式兜底,不阻断评估(与 evaluate_idea LLM 失败降级语义一致)
|
||||
tracing::warn!("默认 provider 密钥不可用,对抗评估走启发式: {e}");
|
||||
|
||||
@@ -6,6 +6,11 @@ use serde::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
|
||||
use df_ai::provider::{ChatMessage, CompletionRequest};
|
||||
// F-01 阶段5: 项目扫描描述路由 — TaskRequirements(Standard;采样含图时追加 Vision,
|
||||
// 当前采样(ProjectSample)未含图像,故仅 Text)。池空/无匹配兜底 default_model。
|
||||
use df_ai::router::{
|
||||
select_model_id, IntelligenceTier, Modality, TaskRequirements,
|
||||
};
|
||||
use df_core::types::new_id;
|
||||
use df_project::scan::{
|
||||
collect_sample, detect_stack, discover_projects, extract_description, is_monorepo,
|
||||
@@ -522,8 +527,19 @@ async fn extract_description_via_llm(
|
||||
.map_err(err_str)?
|
||||
.map_err(err_str)?;
|
||||
|
||||
// F-01 阶段5: 项目扫描描述路由 — Standard,无工具;采样未含图故仅 Text。
|
||||
// select_model_id None(池空/无匹配)→ 兜底 default_model(行为不变)。
|
||||
let scan_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: false,
|
||||
min_intelligence: IntelligenceTier::Standard,
|
||||
max_cost: None,
|
||||
estimated_context: 0,
|
||||
};
|
||||
let scan_model = select_model_id(&scan_req, &pc.model_configs)
|
||||
.unwrap_or_else(|| pc.default_model.clone());
|
||||
let request = CompletionRequest {
|
||||
model: pc.default_model.clone(),
|
||||
model: scan_model,
|
||||
messages: build_scan_prompt(&sample, &rule_stack),
|
||||
temperature: Some(0.2),
|
||||
max_tokens: Some(400),
|
||||
@@ -606,8 +622,19 @@ pub async fn scan_project_with_ai(
|
||||
});
|
||||
}
|
||||
};
|
||||
// F-01 阶段5: 项目扫描描述路由 — Standard,无工具;采样未含图故仅 Text。
|
||||
// select_model_id None(池空/无匹配)→ 兜底 default_model(行为不变)。
|
||||
let scan_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: false,
|
||||
min_intelligence: IntelligenceTier::Standard,
|
||||
max_cost: None,
|
||||
estimated_context: 0,
|
||||
};
|
||||
let scan_model = select_model_id(&scan_req, &pc.model_configs)
|
||||
.unwrap_or_else(|| pc.default_model.clone());
|
||||
let request = CompletionRequest {
|
||||
model: pc.default_model.clone(),
|
||||
model: scan_model,
|
||||
messages: build_scan_prompt(&sample, &rule_stack),
|
||||
temperature: Some(0.2),
|
||||
max_tokens: Some(400),
|
||||
|
||||
Reference in New Issue
Block a user