@@ -162,14 +162,9 @@ impl AdversarialEngine {
. 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
} )
// 数值 clamp 由 parse_llm_eval 单点收口( final_score∈[0,10]、confidence∈[0,1],
// 均在 parse 内对所有 Ok 路径完成),此处不再重复 clamp( CR-40-1 去冗余)。
parse_llm_eval ( & resp . text , & idea . id )
}
/// 启发式评估(基于评分与内容信号,稳定有区分度)
@@ -530,7 +525,7 @@ fn parse_llm_eval(text: &str, idea_id: &str) -> Result<AdversarialEval> {
positive ,
negative ,
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 ) ,
recommendation ,
// 由 evaluate() 按 LLM 调度路径覆盖为 EvaluatedBy::Llm
@@ -540,11 +535,16 @@ fn parse_llm_eval(text: &str, idea_id: &str) -> Result<AdversarialEval> {
/// 从 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 {
let trimmed = text . trim ( ) ;
// 去除 ```json ... ``` 或 ``` ... ``` 围栏
// 快路径:围栏在开头( ```json ... ``` 或 ``` ... ```)
if let Some ( rest ) = trimmed . strip_prefix ( " ``` " ) {
// 跳过语言标记( json/JSON 等)到首个换行
let after_lang = match rest . find ( '\n' ) {
@@ -557,7 +557,15 @@ fn extract_json(text: &str) -> 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 文档约定。
@@ -875,5 +883,22 @@ mod tests {
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 ) ;
}
}