新增: F-03对抗评估接LLM+F-02纯技能调用标题+i18n@转义
This commit is contained in:
@@ -7,8 +7,9 @@
|
||||
//! 评估来源由 [`EvaluatedBy`] 三态标记:`Llm`(LLM 深度评估)/ `Heuristic`(主动选启发式,
|
||||
//! 无 provider)/ `HeuristicFallback`(LLM 调用失败降级)。前端可据此显示评估深度标签。
|
||||
//!
|
||||
//! LLM prompt 构造与 JSON 解析在 F-260614-03(已由本任务解锁)接入,当前 `evaluate_with_llm`
|
||||
//! 返回 Err 触发降级路径——机制完整,仅缺 prompt/解析实现。
|
||||
//! LLM prompt 构造与 JSON 解析在 F-260614-03 接入:[`AdversarialEngine::evaluate_with_llm`]
|
||||
//! 构造三角色辩论 prompt(正方/反方/分析师),调一次 `complete()` 要求返回对齐结构的 JSON,
|
||||
//! 解析失败/字段缺失/枚举非法 → `bail` 触发降级([`AdversarialEngine::evaluate`] 已兜底)。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -133,10 +134,42 @@ impl AdversarialEngine {
|
||||
|
||||
/// LLM 对抗评估(注入 provider 后走此路)。
|
||||
///
|
||||
/// prompt 构造 + JSON 解析在 F-260614-03(已由本任务解锁)接入。当前返回 Err
|
||||
/// 触发降级路径——降级机制与启发式评估路径完整,仅缺 LLM 调用实现。
|
||||
async fn evaluate_with_llm(&self, _idea: &Idea, _provider: &Arc<dyn LlmProvider>) -> Result<AdversarialEval> {
|
||||
anyhow::bail!("LLM 对抗评估尚未实现(F-260614-03)")
|
||||
/// 构造三角色对抗式辩论 prompt(正方/反方/分析师),调一次 `complete()`,要求 LLM
|
||||
/// 返回对齐 [`AdversarialEval`] 结构的 JSON。任一环节失败(HTTP / 非 JSON / 字段
|
||||
/// 缺失 / 枚举非法 / 数值越界无法修正)→ `bail` 由 [`evaluate`] 捕获降级启发式。
|
||||
///
|
||||
/// temperature 取 0.4:低于 0.3 偏机械重复启发式信号,高于 0.5 易发散到无关风险,
|
||||
/// 0.4 在「稳定可复现」与「论点多样性」间取得平衡。
|
||||
async fn evaluate_with_llm(&self, idea: &Idea, provider: &Arc<dyn LlmProvider>) -> Result<AdversarialEval> {
|
||||
let prompt = build_adversarial_prompt(idea);
|
||||
let request = df_ai_core::provider::CompletionRequest {
|
||||
// provider 自带 default_model;model 留空让 provider impl 回填自身默认模型。
|
||||
// (OpenAICompatProvider::convert_request 在 req.model.is_empty() 时回退 default_model)
|
||||
model: String::new(),
|
||||
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,
|
||||
};
|
||||
|
||||
let resp = provider
|
||||
.complete(request)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("LLM complete 调用失败: {e}"))?;
|
||||
|
||||
parse_llm_eval(&resp.text, &idea.id).map(|mut eval| {
|
||||
// LLM 路径 final_score 由分析师裁决给出,统一 clamp 到启发式同等量纲 [0,10]
|
||||
eval.final_score = eval.final_score.clamp(0.0, 10.0);
|
||||
// confidence clamp [0,1](parse_llm_eval 已对各 Argument 做过,此处为结构兜底)
|
||||
eval.positive.confidence = eval.positive.confidence.clamp(0.0, 1.0);
|
||||
eval.negative.confidence = eval.negative.confidence.clamp(0.0, 1.0);
|
||||
eval
|
||||
})
|
||||
}
|
||||
|
||||
/// 启发式评估(基于评分与内容信号,稳定有区分度)
|
||||
@@ -320,6 +353,243 @@ impl AdversarialEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LLM 对抗评估 — prompt 构造 / JSON 解析(F-260614-03)
|
||||
// ============================================================
|
||||
|
||||
/// LLM 角色 / 输出契约的系统级约束。
|
||||
///
|
||||
/// 放为 const 便于在 prompt 头部注入,与 [`build_adversarial_prompt`] 的「想法上下文」
|
||||
/// 分离:角色设定稳定不变,想法上下文按评估对象动态拼。
|
||||
const SYSTEM_PROMPT: &str = "\
|
||||
你是一位资深的技术产品决策顾问。你将主持一场三角色对抗式辩论,对一个软件想法做结构化评估:
|
||||
- 正方(Advocate):论证为什么应该立即推进该想法,挖掘价值与可行性证据。
|
||||
- 反方(Skeptic):质疑该想法,挖掘风险、机会成本与替代方案,不放过任何隐患。
|
||||
- 分析师(Analyst):综合正反方观点,给出客观、平衡的最终裁决。
|
||||
|
||||
规则:
|
||||
1. 你必须只输出一个 JSON 对象,不要输出任何解释性文字、Markdown 代码块标记或前后缀。
|
||||
2. 所有枚举字段只能取下方 schema 列出的字符串字面量(区分大小写)。
|
||||
3. 数值字段必须落在指定区间内。
|
||||
4. 论点(thesis / evidence / reasoning / summary 等)用中文表达,简洁有信息密度。";
|
||||
|
||||
/// 构造对抗评估 prompt(三角色辩论 + 严格 JSON schema)。
|
||||
///
|
||||
/// 输出 prompt 包含:想法上下文(title/description/priority/tags)+ 角色 + JSON 输出
|
||||
/// 契约(含字段说明与枚举字面量),让 LLM 单轮产出可解析的结构化评估。
|
||||
fn build_adversarial_prompt(idea: &Idea) -> String {
|
||||
// 描述截断防 prompt 过长:> 800 字按 800 截并附省略号提示。
|
||||
let desc_raw = idea.description.trim();
|
||||
let description = if desc_raw.chars().count() > 800 {
|
||||
let head: String = desc_raw.chars().take(800).collect();
|
||||
format!("{head}……(描述已截断,原长超过 800 字)")
|
||||
} else if desc_raw.is_empty() {
|
||||
"(描述为空,需求边界待补充)".to_string()
|
||||
} else {
|
||||
desc_raw.to_string()
|
||||
};
|
||||
|
||||
let tags = if idea.tags.is_empty() {
|
||||
"(无标签)".to_string()
|
||||
} else {
|
||||
idea.tags.join("、")
|
||||
};
|
||||
|
||||
format!(
|
||||
"\
|
||||
请对以下软件想法进行三角色对抗式评估。
|
||||
|
||||
【想法上下文】
|
||||
标题:{title}
|
||||
描述:{description}
|
||||
优先级:{priority}
|
||||
标签:{tags}
|
||||
|
||||
【输出 JSON schema】(只输出该 JSON 对象,字段名与枚举字面量严格一致)
|
||||
{{
|
||||
\"positive\": {{
|
||||
\"thesis\": \"正方核心观点(一句话)\",
|
||||
\"evidence\": [\"证据 1\", \"证据 2\"],
|
||||
\"reasoning\": [\"推理 1\", \"推理 2\"],
|
||||
\"confidence\": 0.0到1.0之间的浮点数,正方置信度
|
||||
}},
|
||||
\"negative\": {{
|
||||
\"thesis\": \"反方核心观点(一句话)\",
|
||||
\"evidence\": [\"证据 1\"],
|
||||
\"reasoning\": [\"推理 1\"],
|
||||
\"confidence\": 0.0到1.0之间的浮点数,反方置信度
|
||||
}},
|
||||
\"analyst\": {{
|
||||
\"summary\": \"分析师综合总结(一段话)\",
|
||||
\"strengths\": [\"主要优势 1\"],
|
||||
\"weaknesses\": [\"主要劣势 1\"],
|
||||
\"risks\": [\"潜在风险 1\"],
|
||||
\"opportunities\": [\"机会点 1\"],
|
||||
\"final_assessment\": \"StrongGo | Recommended | Conditional | Revised | Defer\"
|
||||
}},
|
||||
\"final_score\": 0到10之间的浮点数,综合评分,
|
||||
\"recommendation\": \"ImmediateAction | Soon | WithResources | ResearchMore | Monitor\"
|
||||
}}
|
||||
|
||||
枚举字段说明:
|
||||
- final_assessment: StrongGo=强烈推荐执行 / Recommended=推荐执行 / Conditional=有条件执行 / Revised=需修改后执行 / Defer=推迟执行
|
||||
- recommendation: ImmediateAction=立即行动 / Soon=尽快行动 / WithResources=配置资源后行动 / ResearchMore=需更多研究 / Monitor=持续监控
|
||||
|
||||
要求:
|
||||
- positive 与 negative 的论点必须真实基于上方想法上下文,不要泛泛而谈。
|
||||
- analyst.summary 必须总结双方并给出明确倾向,不要模棱两可。
|
||||
- final_score 与 recommendation 应与 analyst.final_assessment 自洽(如 Defer 对应低分与 Monitor)。
|
||||
- 只输出 JSON,不要任何额外文字。",
|
||||
title = idea.title,
|
||||
description = description,
|
||||
priority = priority_label(&idea.priority),
|
||||
tags = tags,
|
||||
)
|
||||
}
|
||||
|
||||
/// LLM 返回的非类型化 JSON 表示(与 [`AdversarialEval`] 结构对齐,但枚举为字符串、
|
||||
/// evaluated_by 字段省略——由 [`evaluate`] 调用方覆盖)。
|
||||
///
|
||||
/// 采用独立中间结构而非直接 serde 到 [`AdversarialEval`]:便于在枚举非法时给出
|
||||
/// 字段级错误信息(`final_assessment: "GoNow" 不在合法集合`),而非整条记录丢弃;
|
||||
/// 同时留出数值 clamp 的统一收口。
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LlmEvalRaw {
|
||||
positive: ArgumentRaw,
|
||||
negative: ArgumentRaw,
|
||||
analyst: AnalystRaw,
|
||||
final_score: f64,
|
||||
recommendation: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ArgumentRaw {
|
||||
thesis: String,
|
||||
#[serde(default)]
|
||||
evidence: Vec<String>,
|
||||
#[serde(default)]
|
||||
reasoning: Vec<String>,
|
||||
#[serde(default)]
|
||||
confidence: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AnalystRaw {
|
||||
#[serde(default)]
|
||||
summary: String,
|
||||
#[serde(default)]
|
||||
strengths: Vec<String>,
|
||||
#[serde(default)]
|
||||
weaknesses: Vec<String>,
|
||||
#[serde(default)]
|
||||
risks: Vec<String>,
|
||||
#[serde(default)]
|
||||
opportunities: Vec<String>,
|
||||
final_assessment: String,
|
||||
}
|
||||
|
||||
/// 解析 LLM 返回文本为 [`AdversarialEval`]。
|
||||
///
|
||||
/// 容错策略(任一失败 `bail` → 由 [`evaluate`] 降级启发式):
|
||||
/// 1. 剥离 ```json … ``` 代码块围栏与首尾空白(部分 LLM 会无视「只输出 JSON」要求)。
|
||||
/// 2. `serde_json` 反序列化为 [`LlmEvalRaw`];字段类型错 / 缺失必填 → bail。
|
||||
/// 3. 枚举字符串映射([`parse_assessment_level`] / [`parse_recommendation`]);非法值 → bail。
|
||||
/// 4. 数值 clamp:confidence ∈ [0,1],final_score ∈ [0,10]。
|
||||
fn parse_llm_eval(text: &str, idea_id: &str) -> Result<AdversarialEval> {
|
||||
let json_str = extract_json(text);
|
||||
let raw: LlmEvalRaw = serde_json::from_str(&json_str).map_err(|e| {
|
||||
anyhow::anyhow!("LLM 返回非合法 JSON 或字段缺失: {e}")
|
||||
})?;
|
||||
|
||||
let final_assessment = parse_assessment_level(&raw.analyst.final_assessment)?;
|
||||
let recommendation = parse_recommendation(&raw.recommendation)?;
|
||||
|
||||
let positive = Argument {
|
||||
thesis: raw.positive.thesis,
|
||||
evidence: raw.positive.evidence,
|
||||
reasoning: raw.positive.reasoning,
|
||||
confidence: raw.positive.confidence.clamp(0.0, 1.0),
|
||||
};
|
||||
let negative = Argument {
|
||||
thesis: raw.negative.thesis,
|
||||
evidence: raw.negative.evidence,
|
||||
reasoning: raw.negative.reasoning,
|
||||
confidence: raw.negative.confidence.clamp(0.0, 1.0),
|
||||
};
|
||||
let analyst = AnalystAnalysis {
|
||||
summary: raw.analyst.summary,
|
||||
strengths: raw.analyst.strengths,
|
||||
weaknesses: raw.analyst.weaknesses,
|
||||
risks: raw.analyst.risks,
|
||||
opportunities: raw.analyst.opportunities,
|
||||
final_assessment,
|
||||
};
|
||||
|
||||
Ok(AdversarialEval {
|
||||
idea_id: idea_id.to_string(),
|
||||
positive,
|
||||
negative,
|
||||
analyst,
|
||||
// clamp 由调用方 evaluate_with_llm 再做一次结构兜底
|
||||
final_score: raw.final_score.clamp(0.0, 10.0),
|
||||
recommendation,
|
||||
// 由 evaluate() 按 LLM 调度路径覆盖为 EvaluatedBy::Llm
|
||||
evaluated_by: EvaluatedBy::Llm,
|
||||
})
|
||||
}
|
||||
|
||||
/// 从 LLM 返回文本中提取 JSON 主体。
|
||||
///
|
||||
/// 优先按代码围栏提取;无围栏时整段去首尾空白后原样返回。整段若非合法 JSON
|
||||
/// 由 [`parse_llm_eval`] 的 serde 步骤报错 bail。
|
||||
fn extract_json(text: &str) -> String {
|
||||
let trimmed = text.trim();
|
||||
// 去除 ```json ... ``` 或 ``` ... ``` 围栏
|
||||
if let Some(rest) = trimmed.strip_prefix("```") {
|
||||
// 跳过语言标记(json/JSON 等)到首个换行
|
||||
let after_lang = match rest.find('\n') {
|
||||
Some(idx) => &rest[idx + 1..],
|
||||
None => rest,
|
||||
};
|
||||
let body = after_lang.trim_end();
|
||||
if let Some(body_inner) = body.strip_suffix("```") {
|
||||
return body_inner.trim().to_string();
|
||||
}
|
||||
return body.trim().to_string();
|
||||
}
|
||||
trimmed.to_string()
|
||||
}
|
||||
|
||||
/// 枚举字面量 → [`AssessmentLevel`]。区分大小写匹配 schema 文档约定。
|
||||
fn parse_assessment_level(s: &str) -> Result<AssessmentLevel> {
|
||||
match s.trim() {
|
||||
"StrongGo" => Ok(AssessmentLevel::StrongGo),
|
||||
"Recommended" => Ok(AssessmentLevel::Recommended),
|
||||
"Conditional" => Ok(AssessmentLevel::Conditional),
|
||||
"Revised" => Ok(AssessmentLevel::Revised),
|
||||
"Defer" => Ok(AssessmentLevel::Defer),
|
||||
other => anyhow::bail!(
|
||||
"final_assessment 枚举值非法: {:?}(合法: StrongGo|Recommended|Conditional|Revised|Defer)",
|
||||
other
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// 枚举字面量 → [`Recommendation`]。区分大小写匹配 schema 文档约定。
|
||||
fn parse_recommendation(s: &str) -> Result<Recommendation> {
|
||||
match s.trim() {
|
||||
"ImmediateAction" => Ok(Recommendation::ImmediateAction),
|
||||
"Soon" => Ok(Recommendation::Soon),
|
||||
"WithResources" => Ok(Recommendation::WithResources),
|
||||
"ResearchMore" => Ok(Recommendation::ResearchMore),
|
||||
"Monitor" => Ok(Recommendation::Monitor),
|
||||
other => anyhow::bail!(
|
||||
"recommendation 枚举值非法: {:?}(合法: ImmediateAction|Soon|WithResources|ResearchMore|Monitor)",
|
||||
other
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn priority_label(p: &Priority) -> &'static str {
|
||||
match p {
|
||||
Priority::Critical => "紧急",
|
||||
@@ -451,5 +721,159 @@ mod tests {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
async fn stream(
|
||||
&self,
|
||||
_request: df_ai_core::provider::CompletionRequest,
|
||||
) -> anyhow::Result<df_ai_core::provider::StreamResult> {
|
||||
anyhow::bail!("MockProvider 不支持 stream(adversarial 路径不调用)")
|
||||
}
|
||||
|
||||
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 → 降级 HeuristicFallback(evaluated_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);
|
||||
}
|
||||
|
||||
/// 数值越界 → clamp(confidence>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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user