Files
DevFlow/crates/df-ideas/src/adversarial.rs
绝尘 356c8f27f8 重构: 拆adversarial对抗评估(strategy核心库)
- 新建 df-ideas/adversarial_helpers.rs(366行): 6类型(EvaluatedBy/AdversarialEval/Argument/AnalystAnalysis/AssessmentLevel/Recommendation) + SYSTEM_PROMPT + 8纯fn(build_adversarial_prompt/parse_llm_eval/extract_json/parse_assessment_level/parse_recommendation/priority_label/assessment_desc/action_hint) + LlmEvalRaw中间结构
- adversarial.rs 930→596: AdversarialEngine impl保留 + pub use helpers(外部idea.rs路径零变更)
- lib.rs: mod adversarial_helpers(私有)
- ARC-260618-01-e(evaluate_with_llm一致性)逻辑保留(待决策, doc标注)
主代兜底(独立 -p df-ideas 避 df-ai): cargo 0 + test 20
strategy: 核心库, 纯函数/类型抽离, evaluate主入口+ARC-e保留
git add指定(df-ideas/*)
2026-06-19 11:45:26 +08:00

597 lines
28 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 对抗式评估系统 — 正反方辩论 + AI 分析师
//!
//! 双轨实现:
//! - **启发式**(默认/降级):基于评分与内容信号生成正反方论点,稳定有区分度。
//! - **LLM**(注入 provider 后):调一次 `complete()` 让论点由 LLM 生成,失败自动降级启发式。
//!
//! 评估来源由 [`EvaluatedBy`] 三态标记:`Llm`LLM 深度评估)/ `Heuristic`(主动选启发式,
//! 无 provider/ `HeuristicFallback`LLM 调用失败降级)。前端可据此显示评估深度标签。
//!
//! LLM prompt 构造与 JSON 解析在 F-260614-03 接入:[`AdversarialEngine::evaluate_with_llm`]
//! 构造三角色辩论 prompt正方/反方/分析师),调一次 `complete()` 要求返回对齐结构的 JSON
//! 解析失败/字段缺失/枚举非法 → `bail` 触发降级([`AdversarialEngine::evaluate`] 已兜底)。
//!
//! 重构strategy·自底向上类型定义 / 常量 / prompt 构造 / JSON 解析等无副作用逻辑抽至
//! [`adversarial_helpers`],本文件只保留 [`AdversarialEngine`](有状态引擎 + evaluate 主入口,
//! 含 ARC-260618-01-e evaluate_with_llm 一致性待决策逻辑,原样保留)。外部已用路径(
//! `df_ideas::adversarial::{AdversarialEval, Recommendation, …}`)经 `pub use` 不变。
use std::sync::Arc;
use anyhow::Result;
use df_ai_core::model::ModelConfig;
use df_ai_core::provider::LlmProvider;
use df_types::types::Priority;
use crate::capture::Idea;
use crate::scoring::IdeaScores;
// 类型 / 常量 / 纯函数自 adversarial_helpers 引回,并 re-export外部调用方经
// `df_ideas::adversarial::{...}` 路径访问的类型零变更pub use 同时将符号带入当前
// 命名空间供 impl/tests 使用,无需额外 use。AdversarialEngine 本身定义在本文件。
pub use crate::adversarial_helpers::{
AdversarialEval, AnalystAnalysis, Argument, AssessmentLevel, EvaluatedBy, Recommendation,
};
use crate::adversarial_helpers::{
SYSTEM_PROMPT, action_hint, assessment_desc, build_adversarial_prompt, parse_llm_eval,
priority_label,
};
/// 对抗评估引擎
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), 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, model_pool: Vec::new() }
}
/// 执行完整的对抗评估(内部按 provider 有无调度 LLM / 启发式,失败降级)
pub async fn evaluate(&self, idea: &Idea) -> Result<AdversarialEval> {
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正方/反方/分析师),调一次 `complete()`,要求 LLM
/// 返回对齐 [`AdversarialEval`] 结构的 JSON。任一环节失败HTTP / 非 JSON / 字段
/// 缺失 / 枚举非法 / 数值越界无法修正)→ `bail` 由 [`Self::evaluate`] 捕获降级启发式。
///
/// temperature 取 0.4:低于 0.3 偏机械重复启发式信号,高于 0.5 易发散到无关风险,
/// 0.4 在「稳定可复现」与「论点多样性」间取得平衡。
///
/// ARC-260618-01-e: evaluate_with_llm 返回值一致性未校验final_score 与
/// analyst.final_assessment 自洽性等),待产品决策,当前逻辑原样保留不调整。
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,
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 {
// 路由命中 → 用 model_id;否则留空让 provider impl 回填自身 default_model。
// (OpenAICompatProvider::convert_request 在 req.model.is_empty() 时回退 default_model)
model,
messages: vec![
df_ai_core::provider::ChatMessage::system(SYSTEM_PROMPT),
df_ai_core::provider::ChatMessage::user(prompt),
],
temperature: Some(0.4),
max_tokens: Some(2048),
stream: false,
tools: None,
tool_choice: None,
reasoning_content: None,
};
let resp = provider
.complete(request)
.await
.map_err(|e| anyhow::anyhow!("LLM complete 调用失败: {e}"))?;
// 数值 clamp 由 parse_llm_eval 单点收口final_score∈[0,10]、confidence∈[0,1]
// 均在 parse 内对所有 Ok 路径完成),此处不再重复 clampCR-40-1 去冗余)。
parse_llm_eval(&resp.text, &idea.id)
}
/// 启发式评估(基于评分与内容信号,稳定有区分度)
fn evaluate_heuristic(&self, 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,
// 由 evaluate() 调用方按调度路径覆盖Heuristic / HeuristicFallback
evaluated_by: EvaluatedBy::Heuristic,
})
}
/// 生成正方观点(支持执行)— confidence 由可行性 + 影响力驱动
/// 注:返回 Result 为后续 LLM 注入失败预留,启发式阶段恒 Ok
fn generate_positive_argument(&self, 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(&self, 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(&self, 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(&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,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::capture::Idea;
use crate::scoring::ScoringEngine;
use df_types::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 应含标题");
}
// ────────────────────────────────────────────────────────────
// LLM 路径测试F-260614-03
// ────────────────────────────────────────────────────────────
/// mock LlmProvider按构造时给定的响应文本回放仅供 adversarial 单测。
///
/// `complete` 返回预设 `text`(或预设错误),`stream` 不被 adversarial 路径调用,
/// 返回 Err 即可。不引入 futures 依赖。
struct MockProvider {
text: String,
}
#[async_trait::async_trait]
impl LlmProvider for MockProvider {
async fn complete(
&self,
_request: df_ai_core::provider::CompletionRequest,
) -> anyhow::Result<df_ai_core::provider::CompletionResponse> {
Ok(df_ai_core::provider::CompletionResponse {
text: self.text.clone(),
model: "mock-model".to_string(),
usage: df_ai_core::provider::TokenUsage::default(),
tool_calls: None,
// 对抗评估路径不消费推理内容,留 None 即可(CompletionResponse 新增字段,
// 老构造点漏填 → 测试编译失败,回归补齐)。
reasoning_content: None,
})
}
async fn stream(
&self,
_request: df_ai_core::provider::CompletionRequest,
) -> anyhow::Result<df_ai_core::provider::StreamResult> {
anyhow::bail!("MockProvider 不支持 streamadversarial 路径不调用)")
}
fn name(&self) -> &str {
"mock-model"
}
}
/// LLM mock 返回结构良好的 JSON → 解析正确evaluated_by=Llm
#[tokio::test]
async fn a8_llm_mock_parse_success() {
let idea = make_idea(
"AI 增长引擎",
"面向用户的核心功能,带来显著增长。集成成熟方案。",
Priority::Critical,
vec!["增长"],
);
// 一份对齐 schema 的合法 LLM 返回(含 ```json 围栏,验证容错)
let llm_text = r#"```json
{
"positive": {
"thesis": "该想法价值高,路径清晰,应立即推进",
"evidence": ["用户增长是当前关键路径", "已有成熟方案可复用"],
"reasoning": ["可行性高", "ROI 明显"],
"confidence": 0.9
},
"negative": {
"thesis": "资源投入与替代方案需权衡",
"evidence": ["机会成本存在"],
"reasoning": ["需验证更优解"],
"confidence": 0.35
},
"analyst": {
"summary": "综合双方,价值明确且路径清晰,建议立即推进",
"strengths": ["业务价值明确", "技术路径清晰"],
"weaknesses": ["资源需求待评估"],
"risks": ["需求范围蔓延"],
"opportunities": ["可形成可复用能力"],
"final_assessment": "StrongGo"
},
"final_score": 8.5,
"recommendation": "ImmediateAction"
}
```"#;
let provider = Arc::new(MockProvider { text: llm_text.to_string() });
let engine = AdversarialEngine::new(provider);
let eval = engine.evaluate(&idea).await.unwrap();
println!("\n[a8] LLM mock 成功路径");
println!(" evaluated_by = {:?}", eval.evaluated_by);
println!(" final_score = {:.2} recommendation = {:?}", eval.final_score, eval.recommendation);
println!(" positive.confidence = {:.2} negative.confidence = {:.2}",
eval.positive.confidence, eval.negative.confidence);
println!(" analyst.final_assessment = {:?}", eval.analyst.final_assessment);
assert_eq!(eval.evaluated_by, EvaluatedBy::Llm, "成功路径应为 Llm");
assert!((eval.final_score - 8.5).abs() < 1e-9);
assert_eq!(eval.recommendation, Recommendation::ImmediateAction);
assert_eq!(eval.analyst.final_assessment, AssessmentLevel::StrongGo);
assert!((eval.positive.confidence - 0.9).abs() < 1e-9);
assert!((eval.negative.confidence - 0.35).abs() < 1e-9);
assert_eq!(eval.positive.evidence.len(), 2);
assert!(eval.positive.thesis.contains("立即推进"));
}
/// LLM mock 返回非法 JSON → bail → 降级 HeuristicFallbackevaluated_by 标记)
#[tokio::test]
async fn a9_llm_mock_bad_json_fallback() {
let idea = make_idea("坏想法", "测试降级", Priority::Medium, vec![]);
let provider = Arc::new(MockProvider { text: "这不是 JSON".to_string() });
let engine = AdversarialEngine::new(provider);
let eval = engine.evaluate(&idea).await.unwrap();
println!("\n[a9] LLM mock 非 JSON → 降级");
println!(" evaluated_by = {:?} (期望 HeuristicFallback)", eval.evaluated_by);
assert_eq!(eval.evaluated_by, EvaluatedBy::HeuristicFallback, "非 JSON 应降级");
}
/// LLM mock 返回枚举非法值 → bail → 降级
#[tokio::test]
async fn a10_llm_mock_bad_enum_fallback() {
let idea = make_idea("枚举非法", "测试枚举映射", Priority::High, vec![]);
let llm_text = r#"{
"positive": {"thesis":"x","evidence":[],"reasoning":[],"confidence":0.5},
"negative": {"thesis":"y","evidence":[],"reasoning":[],"confidence":0.5},
"analyst": {"summary":"s","strengths":[],"weaknesses":[],"risks":[],"opportunities":[],"final_assessment":"GoNow"},
"final_score": 5.0,
"recommendation": "Soon"
}"#;
let provider = Arc::new(MockProvider { text: llm_text.to_string() });
let engine = AdversarialEngine::new(provider);
let eval = engine.evaluate(&idea).await.unwrap();
println!("\n[a10] LLM mock 枚举非法 → 降级");
println!(" evaluated_by = {:?} (期望 HeuristicFallback)", eval.evaluated_by);
assert_eq!(eval.evaluated_by, EvaluatedBy::HeuristicFallback);
}
/// 数值越界 → clampconfidence>1 / final_score>10应被截断不触发降级
#[tokio::test]
async fn a11_llm_mock_value_clamp() {
let idea = make_idea("越界测试", "数值超出区间", Priority::Medium, vec![]);
let llm_text = r#"{
"positive": {"thesis":"p","evidence":[],"reasoning":[],"confidence":1.5},
"negative": {"thesis":"n","evidence":[],"reasoning":[],"confidence":-0.3},
"analyst": {"summary":"s","strengths":[],"weaknesses":[],"risks":[],"opportunities":[],"final_assessment":"Recommended"},
"final_score": 99.0,
"recommendation": "Soon"
}"#;
let provider = Arc::new(MockProvider { text: llm_text.to_string() });
let engine = AdversarialEngine::new(provider);
let eval = engine.evaluate(&idea).await.unwrap();
println!("\n[a11] LLM mock 数值越界 clamp");
println!(" positive.confidence={:.2} (原 1.5 → 期望 1.0)", eval.positive.confidence);
println!(" negative.confidence={:.2} (原 -0.3 → 期望 0.0)", eval.negative.confidence);
println!(" final_score={:.2} (原 99 → 期望 10.0)", eval.final_score);
assert_eq!(eval.evaluated_by, EvaluatedBy::Llm, "仅越界不降级");
assert!((eval.positive.confidence - 1.0).abs() < 1e-9, "confidence>1 应 clamp 到 1.0");
assert!((eval.negative.confidence - 0.0).abs() < 1e-9, "confidence<0 应 clamp 到 0.0");
assert!((eval.final_score - 10.0).abs() < 1e-9, "final_score>10 应 clamp 到 10.0");
}
/// LLM 返回「前缀文字 + ```json + {...} + ```」(围栏不在开头)→ extract_json
/// 正则兜底提取首个 JSON 对象 → 解析成功不降级CR-40-2 兜底路径)。
#[tokio::test]
async fn a12_llm_mock_fenced_not_at_start() {
let idea = make_idea("围栏不在开头", "测试正则兜底", Priority::High, vec![]);
let llm_text = "好的,以下是我的评估:\n```json\n{\n \"positive\": {\"thesis\":\"p\",\"evidence\":[],\"reasoning\":[],\"confidence\":0.7},\n \"negative\": {\"thesis\":\"n\",\"evidence\":[],\"reasoning\":[],\"confidence\":0.4},\n \"analyst\": {\"summary\":\"s\",\"strengths\":[],\"weaknesses\":[],\"risks\":[],\"opportunities\":[],\"final_assessment\":\"Recommended\"},\n \"final_score\": 7.0,\n \"recommendation\": \"Soon\"\n}\n```\n希望对你有帮助。";
let provider = Arc::new(MockProvider { text: llm_text.to_string() });
let engine = AdversarialEngine::new(provider);
let eval = engine.evaluate(&idea).await.unwrap();
println!("\n[a12] 围栏不在开头 → 正则兜底提取");
println!(" evaluated_by = {:?} (期望 Llm不应降级)", eval.evaluated_by);
assert_eq!(eval.evaluated_by, EvaluatedBy::Llm, "围栏不在开头应正则兜底解析成功");
assert!((eval.final_score - 7.0).abs() < 1e-9);
assert_eq!(eval.recommendation, Recommendation::Soon);
}
}