//! 对抗式评估系统 — 正反方辩论 + AI 分析师 //! //! 双轨实现: //! - **启发式**(默认/降级):基于评分与内容信号生成正反方论点,稳定有区分度。 //! - **LLM**(注入 provider 后):调一次 `complete()` 让论点由 LLM 生成,失败自动降级启发式。 //! //! 评估来源由 [`EvaluatedBy`] 三态标记:`Llm`(LLM 深度评估)/ `Heuristic`(主动选启发式, //! 无 provider)/ `HeuristicFallback`(LLM 调用失败降级)。前端可据此显示评估深度标签。 //! //! LLM prompt 构造与 JSON 解析在 F-260614-03(已由本任务解锁)接入,当前 `evaluate_with_llm` //! 返回 Err 触发降级路径——机制完整,仅缺 prompt/解析实现。 use std::sync::Arc; use anyhow::Result; use serde::{Deserialize, Serialize}; use df_ai_core::provider::LlmProvider; use df_core::types::{IdeaId, Priority}; use crate::capture::Idea; use crate::scoring::IdeaScores; /// 评估来源标记 /// /// `Default = Heuristic`:老数据(F-07 之前)序列化时无 evaluated_by 字段, /// 反序列化回落启发式(与 F-07 之前行为一致)。 #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub enum EvaluatedBy { /// LLM 深度评估 Llm, /// 启发式评估(无 LLM 配置时的默认模式,也是老数据反序列化默认值) #[default] Heuristic, /// 启发式降级(LLM 调用失败后 fallback) HeuristicFallback, } /// 对抗评估结果 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AdversarialEval { pub idea_id: IdeaId, pub positive: Argument, pub negative: Argument, pub analyst: AnalystAnalysis, pub final_score: f64, pub recommendation: Recommendation, /// 评估来源(Llm / Heuristic / HeuristicFallback),前端据此显示评估深度标签 #[serde(default)] pub evaluated_by: EvaluatedBy, } /// 论点(正方/反方共用同一结构) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Argument { pub thesis: String, // 核心观点 pub evidence: Vec, // 证据支持 pub reasoning: Vec, // 推理过程 pub confidence: f64, // 置信度 0-1 } /// AI 分析师综合分析 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AnalystAnalysis { pub summary: String, // 综合总结 pub strengths: Vec, // 主要优势 pub weaknesses: Vec, // 主要劣势 pub risks: Vec, // 潜在风险 pub opportunities: Vec, // 机会点 pub final_assessment: AssessmentLevel, // 最终评估 } /// 评估等级 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum AssessmentLevel { StrongGo, // 强烈推荐执行 Recommended, // 推荐执行 Conditional, // 有条件执行 Revised, // 需要修改后执行 Defer, // 推迟执行 } /// 最终建议 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum Recommendation { ImmediateAction, // 立即行动 Soon, // 尽快行动 WithResources, // 配置资源后行动 ResearchMore, // 需要更多研究 Monitor, // 持续监控 } /// 对抗评估引擎 pub struct AdversarialEngine { /// 可选 LLM provider。Some → 优先 LLM 评估(失败降级启发式);None → 纯启发式。 /// 构造注入(与 IdeaPromoter::new(policy) 同一模式),批量评估复用同一 provider。 provider: Option>, } impl AdversarialEngine { /// 注入 LLM provider 构造(provider Some 时走 LLM,调用失败自动降级启发式) pub fn new(provider: Arc) -> Self { Self { provider: Some(provider) } } /// 纯启发式构造(无 LLM 配置时的默认模式) pub fn heuristic() -> Self { Self { provider: None } } /// 执行完整的对抗评估(内部按 provider 有无调度 LLM / 启发式,失败降级) pub async fn evaluate(&self, idea: &Idea) -> Result { match &self.provider { Some(p) => match self.evaluate_with_llm(idea, p).await { Ok(mut eval) => { eval.evaluated_by = EvaluatedBy::Llm; Ok(eval) } Err(e) => { // LLM 调用失败/超时/格式异常 → 自动降级启发式,保证前端结构完整返回 tracing::warn!("LLM 对抗评估失败, 降级到启发式: {e}"); let mut eval = self.evaluate_heuristic(idea)?; eval.evaluated_by = EvaluatedBy::HeuristicFallback; Ok(eval) } }, None => { let mut eval = self.evaluate_heuristic(idea)?; eval.evaluated_by = EvaluatedBy::Heuristic; Ok(eval) } } } /// LLM 对抗评估(注入 provider 后走此路)。 /// /// prompt 构造 + JSON 解析在 F-260614-03(已由本任务解锁)接入。当前返回 Err /// 触发降级路径——降级机制与启发式评估路径完整,仅缺 LLM 调用实现。 async fn evaluate_with_llm(&self, _idea: &Idea, _provider: &Arc) -> Result { anyhow::bail!("LLM 对抗评估尚未实现(F-260614-03)") } /// 启发式评估(基于评分与内容信号,稳定有区分度) fn evaluate_heuristic(&self, idea: &Idea) -> Result { // 先做多维评分,作为正反方论点与置信度的依据 let scores = crate::scoring::ScoringEngine::compute_default(idea); let positive = self.generate_positive_argument(idea, &scores)?; let negative = self.generate_negative_argument(idea, &scores)?; let analyst = self.analyst_analysis(idea, &scores)?; let recommendation = self.recommendation_for(&analyst.final_assessment); Ok(AdversarialEval { idea_id: idea.id.clone(), positive, negative, analyst, final_score: scores.overall, recommendation, // 由 evaluate() 调用方按调度路径覆盖(Heuristic / HeuristicFallback) evaluated_by: EvaluatedBy::Heuristic, }) } /// 生成正方观点(支持执行)— confidence 由可行性 + 影响力驱动 /// 注:返回 Result 为后续 LLM 注入失败预留,启发式阶段恒 Ok fn generate_positive_argument(&self, idea: &Idea, scores: &IdeaScores) -> Result { let desc = idea.description.trim(); let mut evidence = Vec::new(); evidence.push(format!("优先级:{}", priority_label(&idea.priority))); if desc.is_empty() { evidence.push("需求待补充(建议补全描述)".to_string()); } else { let head: String = desc.chars().take(60).collect(); evidence.push(format!("明确需求:{}", head)); } if idea.tags.is_empty() { evidence.push("关联领域待界定".to_string()); } else { evidence.push(format!("关联领域:{}", idea.tags.join("、"))); } if scores.impact >= 7.0 { evidence.push("业务价值显著,影响面较广".to_string()); } // 正方置信度:可行性+影响力等权折算到 [0.1, 0.95],满分≈0.95 留质疑余地 let confidence = ((scores.feasibility * 0.5 + scores.impact * 0.5) / 10.0).clamp(0.1, 0.95); let reasoning = vec![ format!("可行性评分 {:.1}/10,路径相对清晰", scores.feasibility), format!("影响力评分 {:.1}/10,预期回报可观", scores.impact), "整体风险可控,适合推进".to_string(), ]; Ok(Argument { thesis: format!("「{}」具备明确价值与可行性,建议优先推进", idea.title), evidence, reasoning, confidence, }) } /// 生成反方观点(反对或谨慎)— 论点基于想法实际缺陷,confidence 随风险上升 fn generate_negative_argument(&self, idea: &Idea, scores: &IdeaScores) -> Result { let desc = idea.description.trim(); let mut evidence = Vec::new(); if desc.is_empty() { evidence.push("描述过于简略,需求边界不清".to_string()); } else if desc.chars().count() < 50 { evidence.push("描述偏短,实现细节尚未论证".to_string()); } if idea.tags.is_empty() { evidence.push("缺少标签,影响范围未界定".to_string()); } if scores.feasibility < 6.0 { evidence.push(format!("可行性 {:.1}/10 偏低,实现路径存疑", scores.feasibility)); } if matches!(idea.priority, Priority::Low) { evidence.push("优先级偏低,可能非当前关键路径".to_string()); } if evidence.is_empty() { evidence.push("机会成本需权衡,可能存在更优替代方案".to_string()); } // 反方强度:feasibility 每降 1 分 +0.04,impact 每降 1 分 +0.03,基线 0.25(满分也保留最低质疑),clamp [0.1, 0.9] let confidence = ((10.0 - scores.feasibility) * 0.04 + (10.0 - scores.impact) * 0.03 + 0.25) .clamp(0.1, 0.9); let reasoning = vec![ format!("资源投入与当前综合评分 {:.1} 需匹配", scores.overall), "ROI 需进一步验证".to_string(), "需评估是否存在更优解".to_string(), ]; Ok(Argument { thesis: format!("「{}」需谨慎评估,存在风险与机会成本", idea.title), evidence, reasoning, confidence, }) } /// AI 分析师综合分析 — 评估等级由综合评分决定,优势/劣势按维度动态生成 fn analyst_analysis(&self, idea: &Idea, scores: &IdeaScores) -> Result { let final_assessment = match scores.overall { x if x >= 7.5 => AssessmentLevel::StrongGo, x if x >= 6.0 => AssessmentLevel::Recommended, x if x >= 4.5 => AssessmentLevel::Conditional, x if x >= 3.0 => AssessmentLevel::Revised, _ => AssessmentLevel::Defer, }; let mut strengths = Vec::new(); if scores.impact >= 6.0 { strengths.push("业务价值明确".to_string()); } if scores.feasibility >= 6.0 { strengths.push("技术路径清晰".to_string()); } if scores.urgency >= 7.0 { strengths.push("时间窗口合适".to_string()); } if strengths.is_empty() { strengths.push("方向值得探索".to_string()); } let mut weaknesses = Vec::new(); if scores.feasibility < 6.0 { weaknesses.push("可行性论证不足".to_string()); } if idea.description.trim().is_empty() { weaknesses.push("需求描述缺失".to_string()); } if scores.urgency < 4.0 { weaknesses.push("紧急度偏低,易被搁置".to_string()); } if weaknesses.is_empty() { weaknesses.push("资源需求待评估".to_string()); } // 启发式占位:固定风险模板,与具体想法无关,接 LLM 后改动态生成 let risks = vec![ "技术实现难度可能超出预期".to_string(), "优先级与资源争夺".to_string(), "需求范围蔓延".to_string(), ]; let opportunities = vec![ "可能形成可复用能力".to_string(), "积累技术资产".to_string(), ]; let summary = format!( "「{}」综合评分 {:.1}/10,{}。建议{}。", idea.title, scores.overall, assessment_desc(&final_assessment), action_hint(&final_assessment) ); Ok(AnalystAnalysis { summary, strengths, weaknesses, risks, opportunities, final_assessment, }) } /// 评估等级 → 最终建议 fn recommendation_for(&self, level: &AssessmentLevel) -> Recommendation { match level { AssessmentLevel::StrongGo => Recommendation::ImmediateAction, AssessmentLevel::Recommended => Recommendation::Soon, AssessmentLevel::Conditional => Recommendation::WithResources, AssessmentLevel::Revised => Recommendation::ResearchMore, AssessmentLevel::Defer => Recommendation::Monitor, } } } fn priority_label(p: &Priority) -> &'static str { match p { Priority::Critical => "紧急", Priority::High => "高", Priority::Medium => "中", Priority::Low => "低", } } fn assessment_desc(level: &AssessmentLevel) -> &'static str { match level { AssessmentLevel::StrongGo => "价值高且可行性强", AssessmentLevel::Recommended => "整体值得推进", AssessmentLevel::Conditional => "有条件地推进", AssessmentLevel::Revised => "需调整后再评估", AssessmentLevel::Defer => "建议暂缓", } } fn action_hint(level: &AssessmentLevel) -> &'static str { match level { AssessmentLevel::StrongGo => "立即立项启动", AssessmentLevel::Recommended => "尽快排期", AssessmentLevel::Conditional => "配置资源后启动", AssessmentLevel::Revised => "补充信息后重新评估", AssessmentLevel::Defer => "持续观察时机", } } #[cfg(test)] mod tests { use super::*; use crate::capture::Idea; use crate::scoring::ScoringEngine; use df_core::types::{IdeaStatus, Priority}; fn make_idea(title: &str, desc: &str, priority: Priority, tags: Vec<&str>) -> Idea { Idea { id: "test-id".to_string(), title: title.to_string(), description: desc.to_string(), status: IdeaStatus::Draft, priority, scores: None, tags: tags.into_iter().map(String::from).collect(), source: None, related_ids: Vec::new(), created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), } } #[tokio::test] async fn a1_high_score_immediate_action() { let desc = "面向用户的核心功能,带来显著增长,大幅提升效率。集成成熟方案,复用已有组件。".repeat(3); let idea = make_idea("AI增长引擎", &desc, Priority::Critical, vec!["增长", "核心"]); let scores = ScoringEngine::compute_default(&idea); let eval = AdversarialEngine::heuristic().evaluate(&idea).await.unwrap(); println!("\n[a1] 高分想法 → 期望 ImmediateAction"); println!(" scores: feas={:.2} impact={:.2} urg={:.2} overall={:.2}", scores.feasibility, scores.impact, scores.urgency, scores.overall); println!(" eval: final_score={:.2} recommendation={:?}", eval.final_score, eval.recommendation); println!(" 正方 confidence={:.2} 反方 confidence={:.2}", eval.positive.confidence, eval.negative.confidence); assert!(eval.final_score >= 7.5, "final_score 应≥7.5, 实际 {:.2}", eval.final_score); assert_eq!(eval.recommendation, Recommendation::ImmediateAction); } #[tokio::test] async fn a2_mid_score_soon() { let desc = "面向用户的功能,集成已有方案,提升体验".to_string(); let idea = make_idea("体验优化", &desc, Priority::Medium, vec!["体验"]); let scores = ScoringEngine::compute_default(&idea); let eval = AdversarialEngine::heuristic().evaluate(&idea).await.unwrap(); println!("\n[a2] 中分想法 → 期望 Soon"); println!(" scores overall={:.2} eval final_score={:.2} recommendation={:?}", scores.overall, eval.final_score, eval.recommendation); assert_eq!(eval.recommendation, Recommendation::Soon); } #[tokio::test] async fn a3_low_score_monitor() { let desc = "重构迁移大规模分布式重写从零全新架构高并发底层".to_string(); let idea = make_idea("过度工程", &desc, Priority::Low, vec![]); let scores = ScoringEngine::compute_default(&idea); let eval = AdversarialEngine::heuristic().evaluate(&idea).await.unwrap(); println!("\n[a3] 低分想法 → 期望 Monitor"); println!(" scores overall={:.2} eval final_score={:.2} recommendation={:?}", scores.overall, eval.final_score, eval.recommendation); assert!(eval.final_score < 3.0, "final_score 应<3.0, 实际 {:.2}", eval.final_score); assert_eq!(eval.recommendation, Recommendation::Monitor); } #[tokio::test] async fn a4_confidence_ranges() { let idea = make_idea("普通想法", "一般描述", Priority::Medium, vec!["标签"]); let eval = AdversarialEngine::heuristic().evaluate(&idea).await.unwrap(); println!("\n[a4] confidence 区间校验"); println!(" 正方={:.2} (应∈[0.1, 0.95]) 反方={:.2} (应∈[0.1, 0.9])", eval.positive.confidence, eval.negative.confidence); assert!(eval.positive.confidence >= 0.1 && eval.positive.confidence <= 0.95); assert!(eval.negative.confidence >= 0.1 && eval.negative.confidence <= 0.9); } #[tokio::test] async fn a5_positive_thesis_contains_title() { let idea = make_idea("独家创意", "描述内容", Priority::High, vec![]); let eval = AdversarialEngine::heuristic().evaluate(&idea).await.unwrap(); println!("\n[a5] 正方论点含标题"); println!(" thesis: {}", eval.positive.thesis); assert!(eval.positive.thesis.contains("独家创意"), "正方 thesis 应含标题"); } #[tokio::test] async fn a6_negative_evidence_nonempty() { let idea = make_idea("待质疑想法", "短", Priority::Low, vec![]); let eval = AdversarialEngine::heuristic().evaluate(&idea).await.unwrap(); println!("\n[a6] 反方证据非空 ({} 条)", eval.negative.evidence.len()); for (i, e) in eval.negative.evidence.iter().enumerate() { println!(" 证据{}: {}", i + 1, e); } assert!(!eval.negative.evidence.is_empty(), "反方 evidence 不应为空"); } #[tokio::test] async fn a7_final_score_consistency() { let desc = "面向用户的核心功能".to_string(); let idea = make_idea("一致性测试", &desc, Priority::High, vec!["核心"]); let scores = ScoringEngine::compute_default(&idea); let eval = AdversarialEngine::heuristic().evaluate(&idea).await.unwrap(); println!("\n[a7] final_score == scores.overall 一致性"); println!(" scores.overall={:.2} eval.final_score={:.2}", scores.overall, eval.final_score); println!(" analyst.summary: {}", eval.analyst.summary); assert!((eval.final_score - scores.overall).abs() < 0.001, "final_score 应等于 overall"); assert!(eval.analyst.summary.contains("一致性测试"), "summary 应含标题"); } }