优化: 对抗评估clamp去冗余+JSON提取正则兜底+update_task注释对齐status收口
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -805,6 +805,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"df-ai-core",
|
"df-ai-core",
|
||||||
"df-core",
|
"df-core",
|
||||||
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ chrono = { version = "0.4", features = ["serde"] }
|
|||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
regex = "1"
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ async-trait = { workspace = true }
|
|||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
regex = { workspace = true }
|
||||||
|
|||||||
@@ -162,14 +162,9 @@ impl AdversarialEngine {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("LLM complete 调用失败: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("LLM complete 调用失败: {e}"))?;
|
||||||
|
|
||||||
parse_llm_eval(&resp.text, &idea.id).map(|mut eval| {
|
// 数值 clamp 由 parse_llm_eval 单点收口(final_score∈[0,10]、confidence∈[0,1],
|
||||||
// LLM 路径 final_score 由分析师裁决给出,统一 clamp 到启发式同等量纲 [0,10]
|
// 均在 parse 内对所有 Ok 路径完成),此处不再重复 clamp(CR-40-1 去冗余)。
|
||||||
eval.final_score = eval.final_score.clamp(0.0, 10.0);
|
parse_llm_eval(&resp.text, &idea.id)
|
||||||
// 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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 启发式评估(基于评分与内容信号,稳定有区分度)
|
/// 启发式评估(基于评分与内容信号,稳定有区分度)
|
||||||
@@ -530,7 +525,7 @@ fn parse_llm_eval(text: &str, idea_id: &str) -> Result<AdversarialEval> {
|
|||||||
positive,
|
positive,
|
||||||
negative,
|
negative,
|
||||||
analyst,
|
analyst,
|
||||||
// clamp 由调用方 evaluate_with_llm 再做一次结构兜底
|
// final_score clamp 单点收口于此(evaluate_with_llm 不再重复 clamp,CR-40-1)
|
||||||
final_score: raw.final_score.clamp(0.0, 10.0),
|
final_score: raw.final_score.clamp(0.0, 10.0),
|
||||||
recommendation,
|
recommendation,
|
||||||
// 由 evaluate() 按 LLM 调度路径覆盖为 EvaluatedBy::Llm
|
// 由 evaluate() 按 LLM 调度路径覆盖为 EvaluatedBy::Llm
|
||||||
@@ -540,11 +535,16 @@ fn parse_llm_eval(text: &str, idea_id: &str) -> Result<AdversarialEval> {
|
|||||||
|
|
||||||
/// 从 LLM 返回文本中提取 JSON 主体。
|
/// 从 LLM 返回文本中提取 JSON 主体。
|
||||||
///
|
///
|
||||||
/// 优先按代码围栏提取;无围栏时整段去首尾空白后原样返回。整段若非合法 JSON
|
/// 提取优先级:
|
||||||
/// 由 [`parse_llm_eval`] 的 serde 步骤报错 bail。
|
/// 1. 代码围栏在开头(```json … ``` 或 ``` … ```)→ 快路径剥离围栏返回主体。
|
||||||
|
/// 2. 围栏不在开头(LLM 输出「前缀文字 + ```json + {...} + ```」)→ 正则
|
||||||
|
/// `(?s)\{.*\}` 兜底提取首个 `{` 到最后一个 `}` 的片段(增强命中率)。
|
||||||
|
///
|
||||||
|
/// 注意:兜底返回的是「原始文本中首个 JSON 对象字面量」,**不再** trim 后整段交给
|
||||||
|
/// serde——由 [`parse_llm_eval`] 的 serde 步骤校验合法性,失败即 bail 降级。
|
||||||
fn extract_json(text: &str) -> String {
|
fn extract_json(text: &str) -> String {
|
||||||
let trimmed = text.trim();
|
let trimmed = text.trim();
|
||||||
// 去除 ```json ... ``` 或 ``` ... ``` 围栏
|
// 快路径:围栏在开头(```json ... ``` 或 ``` ... ```)
|
||||||
if let Some(rest) = trimmed.strip_prefix("```") {
|
if let Some(rest) = trimmed.strip_prefix("```") {
|
||||||
// 跳过语言标记(json/JSON 等)到首个换行
|
// 跳过语言标记(json/JSON 等)到首个换行
|
||||||
let after_lang = match rest.find('\n') {
|
let after_lang = match rest.find('\n') {
|
||||||
@@ -557,7 +557,15 @@ fn extract_json(text: &str) -> String {
|
|||||||
}
|
}
|
||||||
return body.trim().to_string();
|
return body.trim().to_string();
|
||||||
}
|
}
|
||||||
trimmed.to_string()
|
// 兜底:围栏不在开头或混杂前后文字 → 正则提取首个 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 文档约定。
|
/// 枚举字面量 → [`AssessmentLevel`]。区分大小写匹配 schema 文档约定。
|
||||||
@@ -875,5 +883,22 @@ mod tests {
|
|||||||
assert!((eval.negative.confidence - 0.0).abs() < 1e-9, "confidence<0 应 clamp 到 0.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");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,8 +97,13 @@ pub async fn update_task(
|
|||||||
field: String,
|
field: String,
|
||||||
value: String,
|
value: String,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
// 字段名注入由 df-storage 白名单兜底;这里补 status / priority 值校验,
|
// status 值校验保留:仅对「非法值」(拼写错 in-progess / "in progress" / 大小写错 / 越界)
|
||||||
// 拦截拼写错误(in-progess / "in progress" / 大小写错)与越界数值(999 / "abc")静默落库。
|
// 给出友好早错误(先于 crud.rs 白名单那串冷冰冰的「字段不在白名单」拒)。合法 status 值
|
||||||
|
// 不可经本 IPC 写入——crud.rs tasks 白名单已收口移除 status(F-03 batch64 b94e74a /
|
||||||
|
// D-260616-04),所有 status 改动须走 advance_task_atomic 状态机(CAS + can_transition +
|
||||||
|
// review_rounds 累加,唯一 status 写入路径)。即此 is_valid 校验的「通过」分支永不会触达
|
||||||
|
// update_field 的 status 写入(白名单会先拒);它只为非法值兜底 UX,不承担合法值写入职责。
|
||||||
|
// priority 值校验同理补在下方:拦截 "abc" / 999 等脏数据静默落库。
|
||||||
if field == "status" && !TaskStatus::is_valid(&value) {
|
if field == "status" && !TaskStatus::is_valid(&value) {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"非法 status 值 {:?},合法值: {:?}",
|
"非法 status 值 {:?},合法值: {:?}",
|
||||||
|
|||||||
Reference in New Issue
Block a user