重构: 拆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/*)
This commit is contained in:
@@ -10,86 +10,32 @@
|
||||
//! 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 serde::{Deserialize, Serialize};
|
||||
|
||||
use df_ai_core::model::ModelConfig;
|
||||
use df_ai_core::provider::LlmProvider;
|
||||
use df_types::types::{IdeaId, Priority};
|
||||
use df_types::types::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<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, // 持续监控
|
||||
}
|
||||
// 类型 / 常量 / 纯函数自 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 {
|
||||
@@ -146,10 +92,13 @@ impl AdversarialEngine {
|
||||
///
|
||||
/// 构造三角色对抗式辩论 prompt(正方/反方/分析师),调一次 `complete()`,要求 LLM
|
||||
/// 返回对齐 [`AdversarialEval`] 结构的 JSON。任一环节失败(HTTP / 非 JSON / 字段
|
||||
/// 缺失 / 枚举非法 / 数值越界无法修正)→ `bail` 由 [`evaluate`] 捕获降级启发式。
|
||||
/// 缺失 / 枚举非法 / 数值越界无法修正)→ `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,无工具)。
|
||||
@@ -368,288 +317,6 @@ 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,
|
||||
// final_score clamp 单点收口于此(evaluate_with_llm 不再重复 clamp,CR-40-1)
|
||||
final_score: raw.final_score.clamp(0.0, 10.0),
|
||||
recommendation,
|
||||
// 由 evaluate() 按 LLM 调度路径覆盖为 EvaluatedBy::Llm
|
||||
evaluated_by: EvaluatedBy::Llm,
|
||||
})
|
||||
}
|
||||
|
||||
/// 从 LLM 返回文本中提取 JSON 主体。
|
||||
///
|
||||
/// 提取优先级:
|
||||
/// 1. 代码围栏在开头(```json … ``` 或 ``` … ```)→ 快路径剥离围栏返回主体。
|
||||
/// 2. 围栏不在开头(LLM 输出「前缀文字 + ```json + {...} + ```」)→ 正则
|
||||
/// `(?s)\{.*\}` 兜底提取首个 `{` 到最后一个 `}` 的片段(增强命中率)。
|
||||
///
|
||||
/// 注意:兜底返回的是「原始文本中首个 JSON 对象字面量」,**不再** trim 后整段交给
|
||||
/// serde——由 [`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();
|
||||
}
|
||||
// 围栏闭合缺失(如 JSON 与 ``` 同行 / 闭合后仍有文字 / 多行 JSON 末尾混噪声):
|
||||
// 快路径无法干净剥离 → 落到正则兜底提取首个 JSON 对象,提升命中率。
|
||||
// (原实现在此处 return body.trim() 会把语言标记/尾随文字一起喂 serde,
|
||||
// 必然失败降级启发式,即便正则本可救回。)
|
||||
}
|
||||
// 兜底:围栏不在开头或混杂前后文字 → 正则提取首个 JSON 对象(CR-40-2)。
|
||||
// (?s) 让 . 匹配换行,贪婪 {*} 取首 { 到末 },覆盖嵌套对象。
|
||||
// 提取失败(无 { })则返回 trimmed 走原文 serde 报错降级,语义不变。
|
||||
static JSON_RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
|
||||
let re = JSON_RE.get_or_init(|| regex::Regex::new(r"(?s)\{.*\}").expect("合法静态正则"));
|
||||
match re.captures(trimmed) {
|
||||
Some(c) => c.get(0).map(|m| m.as_str().to_string()).unwrap_or_else(|| trimmed.to_string()),
|
||||
None => 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 => "紧急",
|
||||
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::*;
|
||||
@@ -927,4 +594,3 @@ mod tests {
|
||||
assert_eq!(eval.recommendation, Recommendation::Soon);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
366
crates/df-ideas/src/adversarial_helpers.rs
Normal file
366
crates/df-ideas/src/adversarial_helpers.rs
Normal file
@@ -0,0 +1,366 @@
|
||||
//! 对抗评估 — 纯函数 / 类型 / 常量(自 adversarial.rs 抽离,纯搬迁,零行为变更)。
|
||||
//!
|
||||
//! 本文件承载 [`AdversarialEval`] 及其字段类型、LLM prompt 构造与 JSON 解析等无副作用
|
||||
//! 逻辑;有状态的引擎 [`AdversarialEngine`](含 evaluate / evaluate_with_llm 主入口与
|
||||
//! ARC-260618-01-e 待决策逻辑)仍留在 `adversarial.rs`,二者经 [`use`] 互相引用。
|
||||
//!
|
||||
//! 抽离边界:类型定义(非 impl)+ 常量 + 顶层 free function。impl 主体不动,外部
|
||||
//! 已用路径(`df_ideas::adversarial::{AdversarialEngine, AdversarialEval, Recommendation}` 等)
|
||||
//! 由 adversarial.rs 通过 `pub use` 重新导出,调用方零变更。
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use df_types::types::{IdeaId, Priority};
|
||||
use crate::capture::Idea;
|
||||
|
||||
/// 评估来源标记
|
||||
///
|
||||
/// `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<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, // 持续监控
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LLM 对抗评估 — prompt 构造 / JSON 解析(F-260614-03)
|
||||
// ============================================================
|
||||
|
||||
/// LLM 角色 / 输出契约的系统级约束。
|
||||
///
|
||||
/// 放为 const 便于在 prompt 头部注入,与 [`build_adversarial_prompt`] 的「想法上下文」
|
||||
/// 分离:角色设定稳定不变,想法上下文按评估对象动态拼。
|
||||
pub(crate) 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 单轮产出可解析的结构化评估。
|
||||
pub(crate) 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 字段省略——由 [`AdversarialEngine::evaluate`] 调用方覆盖)。
|
||||
///
|
||||
/// 采用独立中间结构而非直接 serde 到 [`AdversarialEval`]:便于在枚举非法时给出
|
||||
/// 字段级错误信息(`final_assessment: "GoNow" 不在合法集合`),而非整条记录丢弃;
|
||||
/// 同时留出数值 clamp 的统一收口。
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct LlmEvalRaw {
|
||||
pub(crate) positive: ArgumentRaw,
|
||||
pub(crate) negative: ArgumentRaw,
|
||||
pub(crate) analyst: AnalystRaw,
|
||||
pub(crate) final_score: f64,
|
||||
pub(crate) recommendation: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct ArgumentRaw {
|
||||
pub(crate) thesis: String,
|
||||
#[serde(default)]
|
||||
pub(crate) evidence: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) reasoning: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) confidence: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct AnalystRaw {
|
||||
#[serde(default)]
|
||||
pub(crate) summary: String,
|
||||
#[serde(default)]
|
||||
pub(crate) strengths: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) weaknesses: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) risks: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) opportunities: Vec<String>,
|
||||
pub(crate) final_assessment: String,
|
||||
}
|
||||
|
||||
/// 解析 LLM 返回文本为 [`AdversarialEval`]。
|
||||
///
|
||||
/// 容错策略(任一失败 `bail` → 由 [`AdversarialEngine::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]。
|
||||
pub(crate) 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,
|
||||
// final_score clamp 单点收口于此(evaluate_with_llm 不再重复 clamp,CR-40-1)
|
||||
final_score: raw.final_score.clamp(0.0, 10.0),
|
||||
recommendation,
|
||||
// 由 evaluate() 按 LLM 调度路径覆盖为 EvaluatedBy::Llm
|
||||
evaluated_by: EvaluatedBy::Llm,
|
||||
})
|
||||
}
|
||||
|
||||
/// 从 LLM 返回文本中提取 JSON 主体。
|
||||
///
|
||||
/// 提取优先级:
|
||||
/// 1. 代码围栏在开头(```json … ``` 或 ``` … ```)→ 快路径剥离围栏返回主体。
|
||||
/// 2. 围栏不在开头(LLM 输出「前缀文字 + ```json + {...} + ```」)→ 正则
|
||||
/// `(?s)\{.*\}` 兜底提取首个 `{` 到最后一个 `}` 的片段(增强命中率)。
|
||||
///
|
||||
/// 注意:兜底返回的是「原始文本中首个 JSON 对象字面量」,**不再** trim 后整段交给
|
||||
/// serde——由 [`parse_llm_eval`] 的 serde 步骤校验合法性,失败即 bail 降级。
|
||||
pub(crate) 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();
|
||||
}
|
||||
// 围栏闭合缺失(如 JSON 与 ``` 同行 / 闭合后仍有文字 / 多行 JSON 末尾混噪声):
|
||||
// 快路径无法干净剥离 → 落到正则兜底提取首个 JSON 对象,提升命中率。
|
||||
// (原实现在此处 return body.trim() 会把语言标记/尾随文字一起喂 serde,
|
||||
// 必然失败降级启发式,即便正则本可救回。)
|
||||
}
|
||||
// 兜底:围栏不在开头或混杂前后文字 → 正则提取首个 JSON 对象(CR-40-2)。
|
||||
// (?s) 让 . 匹配换行,贪婪 {*} 取首 { 到末 },覆盖嵌套对象。
|
||||
// 提取失败(无 { })则返回 trimmed 走原文 serde 报错降级,语义不变。
|
||||
static JSON_RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
|
||||
let re = JSON_RE.get_or_init(|| regex::Regex::new(r"(?s)\{.*\}").expect("合法静态正则"));
|
||||
match re.captures(trimmed) {
|
||||
Some(c) => c.get(0).map(|m| m.as_str().to_string()).unwrap_or_else(|| trimmed.to_string()),
|
||||
None => trimmed.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 枚举字面量 → [`AssessmentLevel`]。区分大小写匹配 schema 文档约定。
|
||||
pub(crate) 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 文档约定。
|
||||
pub(crate) 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
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn priority_label(p: &Priority) -> &'static str {
|
||||
match p {
|
||||
Priority::Critical => "紧急",
|
||||
Priority::High => "高",
|
||||
Priority::Medium => "中",
|
||||
Priority::Low => "低",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn assessment_desc(level: &AssessmentLevel) -> &'static str {
|
||||
match level {
|
||||
AssessmentLevel::StrongGo => "价值高且可行性强",
|
||||
AssessmentLevel::Recommended => "整体值得推进",
|
||||
AssessmentLevel::Conditional => "有条件地推进",
|
||||
AssessmentLevel::Revised => "需调整后再评估",
|
||||
AssessmentLevel::Defer => "建议暂缓",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn action_hint(level: &AssessmentLevel) -> &'static str {
|
||||
match level {
|
||||
AssessmentLevel::StrongGo => "立即立项启动",
|
||||
AssessmentLevel::Recommended => "尽快排期",
|
||||
AssessmentLevel::Conditional => "配置资源后启动",
|
||||
AssessmentLevel::Revised => "补充信息后重新评估",
|
||||
AssessmentLevel::Defer => "持续观察时机",
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! df-ideas: 想法池 — 捕获、评估、评分、晋升
|
||||
|
||||
pub mod adversarial;
|
||||
mod adversarial_helpers;
|
||||
pub mod capture;
|
||||
pub mod promotion;
|
||||
pub mod scoring;
|
||||
|
||||
Reference in New Issue
Block a user