新增: F-01阶段5模型路由调用点接入
This commit is contained in:
@@ -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