修复+重构: 全库走查真bug+架构+P1/P2 后端 crate
- df-nodes: schema required 对齐 + docker POSIX 注入防御 + HumanNode timeout 1800 + parse_review_json(verdict规范/score clamp/正则兜底) - df-mcp: update 实体校验(防跨实体 B-260801-01) - df-storage: keyring 迁移失败达阈值清除明文 - df-ai: router estimated_context+tier tiebreak+DataReadOnly 兜底 + sanitize step4 显式不制造 orphan - df-ideas: adversarial tier:None 对齐
This commit is contained in:
@@ -219,6 +219,7 @@ pub(crate) fn parse_params(
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: true,
|
||||
estimated_context: 0,
|
||||
tier: None,
|
||||
};
|
||||
select_model_id(&node_req, &provider.model_pool).unwrap_or_default()
|
||||
};
|
||||
@@ -260,9 +261,20 @@ pub(crate) const REVIEW_SYSTEM_PROMPT: &str = "\
|
||||
|
||||
/// 解析 LLM 自审输出为结构化 review JSON。
|
||||
///
|
||||
/// 成功路径:serde_json::from_str 得到 Object 且含 verdict 字段 → 原样返回。
|
||||
/// 成功路径:serde_json::from_str 得到 Object 且含 verdict 字段 → 规范化后返回。
|
||||
/// 兜底路径:解析失败 / 非 Object / 缺 verdict → 返回 verdict=unknown + summary=原文,
|
||||
/// 防 LLM 不按要求输出导致下游崩溃。dimensions 留空对象(前端容缺展示)。
|
||||
///
|
||||
/// 三道加固(P2):
|
||||
/// 1. verdict 规范化:to_lowercase + trim,统一输出 pass/fail/unknown 三态。
|
||||
/// 防 LLM 输出 "Pass"/"PASS"/" Fail " 类大小写/空白变体致 gate_should_block 精确
|
||||
/// 匹配误判(原 == "fail" 对 "Fail" 放行,漏阻断)。
|
||||
/// 2. score clamp:dimensions.*.score 读时 clamp 到 [0,10]。防 LLM 输出越界值
|
||||
/// (99/-1/NaN)污染前端展示与闸门阈值判定(维度 score<6 视 fail 由 prompt 约定,
|
||||
/// 越界值会破坏该约定)。
|
||||
/// 3. 正则兜底:LLM 偶尔在 JSON 前置解释文字("好的,审查结果:\n{...}")致整段 serde
|
||||
/// 失败。参考 adversarial parse_llm_eval 的 extract_json,提取首个 { 到末 } 重试。
|
||||
/// 用纯字符串 find/rfind 实现等价语义(避免为单条提取引入 regex 依赖)。
|
||||
pub(crate) fn parse_review_json(raw: &str) -> serde_json::Value {
|
||||
// 先尝试整段解析;LLM 偶尔会包 markdown 代码块,剥离 ```json ... ``` 后重试一次。
|
||||
let trimmed = raw.trim();
|
||||
@@ -272,9 +284,19 @@ pub(crate) fn parse_review_json(raw: &str) -> serde_json::Value {
|
||||
.map(|s| s.trim_end_matches("```").trim())
|
||||
.unwrap_or(trimmed);
|
||||
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(cleaned) {
|
||||
if v.is_object() && v.get("verdict").and_then(|x| x.as_str()).is_some() {
|
||||
return v;
|
||||
// 候选解析文本:整段失败 → 正则兜底提取首个 { 到末 } 再试一次(前置文字容错)。
|
||||
// 等价于 adversarial extract_json 的 (?s)\{.*\} 但用 find/rfind 零依赖实现。
|
||||
let candidates = [cleaned, extract_first_json_object(cleaned).as_str()];
|
||||
|
||||
for cand in candidates {
|
||||
if let Ok(mut v) = serde_json::from_str::<serde_json::Value>(cand) {
|
||||
if v.is_object() && v.get("verdict").and_then(|x| x.as_str()).is_some() {
|
||||
// 加固 1:verdict 规范化为 pass/fail/unknown 三态(to_lowercase + trim)。
|
||||
normalize_verdict_in_place(&mut v);
|
||||
// 加固 2:dimensions.*.score clamp 到 [0,10]。
|
||||
clamp_dimension_scores_in_place(&mut v);
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 兜底:保留原文供人查阅,verdict=unknown 不阻断流程(自审辅助,人定)。
|
||||
@@ -286,6 +308,50 @@ pub(crate) fn parse_review_json(raw: &str) -> serde_json::Value {
|
||||
})
|
||||
}
|
||||
|
||||
/// 从文本中提取「首个 `{` 到最后一个 `}`」的片段(正则 `(?s)\{.*\}` 的零依赖等价)。
|
||||
///
|
||||
/// 用于 LLM 在 JSON 前后夹带解释文字("好的,审查如下:\n{...}\n以上。")时兜底提取。
|
||||
/// 提取失败(无 { 或无 })返回空串,调用方按整段重试→失败→兜底 unknown 走原路径。
|
||||
fn extract_first_json_object(s: &str) -> String {
|
||||
match (s.find('{'), s.rfind('}')) {
|
||||
(Some(start), Some(end)) if start < end => s[start..=end].to_string(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 原地规范化 verdict 字段:to_lowercase + trim,统一为 pass/fail/unknown 三态。
|
||||
///
|
||||
/// LLM 偶发输出 "Pass"/"PASS"/" Fail "/"FAIL." 类变体,原样透传会让 gate_should_block
|
||||
/// 精确匹配漏判("Fail" 不 == "fail" → 不阻断)。规范化后下游闸门/展示/落库值一致。
|
||||
/// 非 pass/fail 的值(如空串、拼写错)统一为 unknown(保守不阻断,保人定权)。
|
||||
fn normalize_verdict_in_place(v: &mut serde_json::Value) {
|
||||
let Some(obj) = v.as_object_mut() else { return };
|
||||
let Some(raw_verdict) = obj.get("verdict").and_then(|x| x.as_str()).map(str::to_string) else {
|
||||
return;
|
||||
};
|
||||
let normalized = match raw_verdict.trim().to_lowercase().as_str() {
|
||||
"pass" => "pass",
|
||||
"fail" => "fail",
|
||||
// 含拼写错/大小写变体未命中(如 "passed"/"failed"/"ok")→ 保守归 unknown。
|
||||
_ => "unknown",
|
||||
};
|
||||
obj.insert("verdict".into(), serde_json::Value::String(normalized.into()));
|
||||
}
|
||||
|
||||
/// 原地 clamp dimensions.*.score 到 [0,10]。只处理 number 类型,跳过非 number(留原值,
|
||||
/// serde 反序列化由调用方按 schema 容错)。
|
||||
fn clamp_dimension_scores_in_place(v: &mut serde_json::Value) {
|
||||
let Some(obj) = v.as_object_mut() else { return };
|
||||
let Some(dims) = obj.get_mut("dimensions").and_then(|d| d.as_object_mut()) else { return };
|
||||
for (_, dim) in dims.iter_mut() {
|
||||
let Some(dim_obj) = dim.as_object_mut() else { continue };
|
||||
if let Some(score) = dim_obj.get_mut("score").and_then(|s| s.as_f64()) {
|
||||
let clamped = score.clamp(0.0, 10.0);
|
||||
dim_obj.insert("score".into(), serde_json::json!(clamped));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// summary 截断(防原文过长撑爆 output_json / 审批卡)。
|
||||
pub(crate) fn truncate_for_summary(s: &str) -> String {
|
||||
const MAX: usize = 300;
|
||||
@@ -313,8 +379,11 @@ pub(crate) fn truncate_for_review_input(s: &str) -> String {
|
||||
|
||||
/// 自审闸门决策(纯函数,便于单测覆盖各 verdict/gate 组合)。
|
||||
///
|
||||
/// 仅当 `gate==true` 且 `verdict=="fail"` 时阻断。verdict="unknown"(LLM 输出不可靠)
|
||||
/// 与 "pass" 均不阻断 —— unknown 保持人定权(保守语义不变)。
|
||||
/// 仅当 `gate==true` 且规范化后 verdict=="fail" 时阻断。verdict="unknown"(LLM 输出
|
||||
/// 不可靠)与 "pass" 均不阻断 —— unknown 保持人定权(保守语义不变)。
|
||||
///
|
||||
/// 规范化(to_lowercase + trim):与 parse_review_json 的 verdict 规范化对齐,防御
|
||||
/// 非 parse_review_json 路径(如外部直接传 "Fail"/"FAIL")的精确匹配漏判。
|
||||
pub(crate) fn gate_should_block(gate: bool, verdict: &str) -> bool {
|
||||
gate && verdict == "fail"
|
||||
gate && verdict.trim().to_lowercase() == "fail"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user