重构:删 5 零引用 crate(df-evolve/plugin/stages/task/traceability)+ 清死模块、ai.rs 拆 11 子 module、ai.ts 拆 6 composable、i18n 拆目录 功能:知识库全栈(df-project/scan + CRUD + 时间线 + 前端)、Settings 拆分、appSettings KV 迁移、模型池、LLM 并发 Semaphore 修复:审批持久化根治、ConditionEngine 默认拒绝、NodeRegistry unimplemented 清除、promote 补偿删除、工具结果截断 50KB、路径校验防 symlink 逃逸 文档:B-03 人工审批设计、决策记录三分档、规格契约自检、经验记录、todo 看板、PROGRESS 更新 详见 PROGRESS.md。src-tauri/儿童每日打卡应用/ 与本项目无关,已排除。
381 lines
16 KiB
Rust
381 lines
16 KiB
Rust
//! 对抗式评估系统 — 正反方辩论 + AI 分析师
|
||
//!
|
||
//! 当前为基于评分与内容信号的启发式实现(稳定、有区分度)。
|
||
//! TODO: 接入 df-ai LlmProvider 让正反方论点由 LLM 生成,启发式降级为 fallback。
|
||
|
||
use anyhow::Result;
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
use df_core::types::{IdeaId, Priority};
|
||
use crate::capture::Idea;
|
||
use crate::scoring::IdeaScores;
|
||
|
||
/// 对抗评估结果
|
||
#[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,
|
||
}
|
||
|
||
/// 论点(正方/反方共用同一结构)
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct Argument {
|
||
pub thesis: String, // 核心观点
|
||
pub evidence: Vec<String>, // 证据支持
|
||
pub reasoning: Vec<String>, // 推理过程
|
||
pub confidence: f64, // 置信度 0-1
|
||
}
|
||
|
||
/// AI 分析师综合分析
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct AnalystAnalysis {
|
||
pub summary: String, // 综合总结
|
||
pub strengths: Vec<String>, // 主要优势
|
||
pub weaknesses: Vec<String>, // 主要劣势
|
||
pub risks: Vec<String>, // 潜在风险
|
||
pub opportunities: Vec<String>, // 机会点
|
||
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;
|
||
|
||
impl AdversarialEngine {
|
||
/// 执行完整的对抗评估
|
||
#[allow(clippy::unused_async)] // 签名保留 async,待接 LLM 注入异步调用
|
||
pub async fn evaluate(idea: &Idea) -> Result<AdversarialEval> {
|
||
// 先做多维评分,作为正反方论点与置信度的依据
|
||
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,
|
||
})
|
||
}
|
||
|
||
/// 生成正方观点(支持执行)— confidence 由可行性 + 影响力驱动
|
||
/// 注:返回 Result 为后续 LLM 注入失败预留,启发式阶段恒 Ok
|
||
fn generate_positive_argument(idea: &Idea, scores: &IdeaScores) -> Result<Argument> {
|
||
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(idea: &Idea, scores: &IdeaScores) -> Result<Argument> {
|
||
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(idea: &Idea, scores: &IdeaScores) -> Result<AnalystAnalysis> {
|
||
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(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::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::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::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::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::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::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::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 应含标题");
|
||
}
|
||
}
|
||
|