新增: AiNode自审闸门阶段3(gate_should_block+verdict=fail阻断下游)
This commit is contained in:
@@ -419,6 +419,14 @@ fn truncate_for_summary(s: &str) -> String {
|
||||
format!("{truncated}…")
|
||||
}
|
||||
|
||||
/// 阶段3 自审闸门决策(纯函数,便于单测覆盖各 verdict/gate 组合)。
|
||||
///
|
||||
/// 仅当 `gate==true` 且 `verdict=="fail"` 时阻断。verdict="unknown"(LLM 输出不可靠)
|
||||
/// 与 "pass" 均不阻断 —— unknown 保持人定权(阶段2 保守语义不变)。
|
||||
fn gate_should_block(gate: bool, verdict: &str) -> bool {
|
||||
gate && verdict == "fail"
|
||||
}
|
||||
|
||||
/// AI 自审节点(决策 a 步骤③)
|
||||
pub struct AiSelfReviewNode {
|
||||
db: Arc<Database>,
|
||||
@@ -576,13 +584,52 @@ impl Node for AiSelfReviewNode {
|
||||
// ── NodeOutput.data 塞 review 摘要,供下游 human_review 经 inputs["ai_self_review"] 读 ──
|
||||
// human_review(HumanNode)从 config/inputs 拼 description;此处把 verdict/summary/suggestions
|
||||
// 平铺进 data,HumanNode 零改动即可透传(前端/模板层可据此拼审批卡描述)。
|
||||
Ok(NodeOutput::from_value(serde_json::json!({
|
||||
let output = NodeOutput::from_value(serde_json::json!({
|
||||
"verdict": verdict,
|
||||
"summary": summary,
|
||||
"suggestions": suggestions,
|
||||
"review": merged.get("review").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"model": response.model,
|
||||
})))
|
||||
}));
|
||||
|
||||
// ── 阶段3: 自审闸门(F-260616-07 决策 a 步骤③) ──
|
||||
// config["gate"]==true 时,AiSelfReviewNode 从「自审辅助」升级为「DAG 节点闸门」:
|
||||
// verdict="fail" → 返回 Err → executor first_err 中止后续层(下游 human_review 不跑)
|
||||
// → 工作流 failed → ②-4 回调退回 in_review(对齐工作流失败语义)
|
||||
// verdict="unknown" → 保持 Ok(LLM 输出不可靠时不阻断,人定权)
|
||||
// verdict="pass" → Ok 放行下游
|
||||
//
|
||||
// 落库在前(已 update_field output_json),故闸门 fail 时 task.output_json.review
|
||||
// 已含结论,前端/二次工作流可读自审详情(不丢失审查痕迹)。
|
||||
//
|
||||
// 设计选型(三方案对比,见 commit/设计文档):
|
||||
// A) AiNode 内部门控(本方案):1 处 return Err,复用 executor first_err + ②-4 回调,
|
||||
// 零 executor 核心改动,零依赖暂缓的条件引擎(T-260614-11)。最简。
|
||||
// B) DAG edges 条件 + ConditionEngine 求值:依赖 T-260614-11(暂缓),executor 当前
|
||||
// 完全不评估 edge.condition(topological_layers 无条件收录所有边),须先做条件
|
||||
// 引擎 Phase1+2 才能用 → 拆波。
|
||||
// C) executor 闸门检查钩子(节点 execute 后 executor 读 output.verdict):改 DagExecutor
|
||||
// 核心循环,牵动所有节点,风险/范围不符「最简不破坏」。
|
||||
// 选 A:语义最贴近「自审结果作为闸门」(自审节点自行决定放行/阻断),且 gate 可按节点
|
||||
// config 开关(默认 false = 阶段2 行为不变,模板/前端零强制改动,向后兼容)。
|
||||
let gate_enabled = ctx
|
||||
.config
|
||||
.get("gate")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
if gate_should_block(gate_enabled, &verdict) {
|
||||
tracing::info!(
|
||||
node_id = %ctx.node_id,
|
||||
task_id,
|
||||
"AiSelfReviewNode 闸门触发: verdict=fail, 阻断下游(工作流将 failed)"
|
||||
);
|
||||
return Err(anyhow::anyhow!(
|
||||
"AI 自审闸门未通过(verdict=fail): {}",
|
||||
if summary.is_empty() { "自审结论 fail" } else { &summary }
|
||||
));
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
@@ -593,7 +640,8 @@ impl Node for AiSelfReviewNode {
|
||||
"task_id": { "type": "string", "description": "自审目标任务 ID(必填)" },
|
||||
"provider_id": { "type": "string", "description": "AI Provider ID(FR-S1:密钥经 secret 解析不进 config;留空走默认 provider)" },
|
||||
"model": { "type": "string", "description": "模型名(可选,留空用 record.default_model)" },
|
||||
"max_tokens": { "type": "integer" }
|
||||
"max_tokens": { "type": "integer" },
|
||||
"gate": { "type": "boolean", "description": "阶段3 闸门开关:false(默认)=自审辅助,verdict 仅透传展示;true=自审结果作 DAG 闸门,verdict=fail 返回 Err 阻断下游(工作流 failed → ②-4 退回),verdict=unknown/pass 放行" }
|
||||
},
|
||||
"required": ["task_id", "provider_id"]
|
||||
}),
|
||||
@@ -1014,4 +1062,43 @@ mod tests {
|
||||
assert!(p.contains("边界处理"));
|
||||
assert!(p.contains("verdict"), "prompt 应含输出格式约束");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 阶段3 自审闸门(gate_should_block)单测
|
||||
// ============================================================
|
||||
//
|
||||
// gate 决策矩阵:
|
||||
// gate=false(默认,阶段2 行为) → 任何 verdict 都放行(辅助模式)
|
||||
// gate=true + verdict=pass → 放行
|
||||
// gate=true + verdict=unknown → 放行(LLM 不可靠时不阻断,人定权)
|
||||
// gate=true + verdict=fail → 阻断(返回 Err,工作流 failed)
|
||||
|
||||
#[test]
|
||||
fn gate_disabled_never_blocks_any_verdict() {
|
||||
// gate 默认 false(阶段2 兼容):无论 verdict 如何都不阻断,自审仅辅助展示
|
||||
assert!(!gate_should_block(false, "fail"), "gate 关闭时 fail 也不阻断");
|
||||
assert!(!gate_should_block(false, "pass"), "gate 关闭时 pass 放行");
|
||||
assert!(!gate_should_block(false, "unknown"), "gate 关闭时 unknown 放行");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gate_enabled_blocks_only_on_fail() {
|
||||
// gate 开启:仅 fail 阻断
|
||||
assert!(gate_should_block(true, "fail"), "gate 开 + fail 应阻断");
|
||||
assert!(!gate_should_block(true, "pass"), "gate 开 + pass 放行");
|
||||
assert!(
|
||||
!gate_should_block(true, "unknown"),
|
||||
"gate 开 + unknown 放行(LLM 不可靠时保人定权)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gate_unknown_verdict_does_not_block() {
|
||||
// 防御:verdict 非标准值(如解析异常)不应触发阻断(对齐 unknown 保守不阻断语义)
|
||||
assert!(
|
||||
!gate_should_block(true, "weird"),
|
||||
"非标准 verdict 不应阻断(对齐 unknown 保守)"
|
||||
);
|
||||
assert!(!gate_should_block(true, ""), "空 verdict 不应阻断");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,14 +51,22 @@ fn in_progress_template() -> DagDef {
|
||||
|
||||
/// in_review → testing:AiNode 自审 → HumanNode 核对。
|
||||
///
|
||||
/// 拓扑:ai → human 串行。HumanNode reject 走 Err(②-5 已做)→ 工作流 failed →
|
||||
/// ②-4 失败回调退回 in_review(review_rounds+=1)。
|
||||
/// 拓扑:ai → human 串行。阶段3(本批)起 ai_self_review 启用 gate:true:
|
||||
/// - verdict=fail → AiSelfReviewNode 返回 Err → 工作流 failed(不经 human_review)
|
||||
/// → ②-4 失败回调退回 in_review(review_rounds+=1)。
|
||||
/// - verdict=unknown/pass → 放行 human_review,人定最终是否推进。
|
||||
/// HumanNode reject 走 Err(②-5 已做)→ 工作流 failed → ②-4 失败回调退回 in_review。
|
||||
/// 通过则完成回调(②-3)推进 status 到 testing。
|
||||
fn testing_template() -> DagDef {
|
||||
let mut dag = DagDef::new();
|
||||
// 决策 a 步骤③:ai_self_review 节点类型对齐 state.rs 注册的独立自审节点
|
||||
// (四维度 prompt + JSON 解析兜底 + 写回 output_json 加 review 子字段)。
|
||||
dag.add_node("ai_self_review", "ai_self_review", serde_json::json!({}));
|
||||
// 阶段3:gate=true 启用自审闸门(verdict=fail 阻断下游,工作流 failed)。
|
||||
dag.add_node(
|
||||
"ai_self_review",
|
||||
"ai_self_review",
|
||||
serde_json::json!({ "gate": true }),
|
||||
);
|
||||
dag.add_node(
|
||||
"human_review",
|
||||
"human",
|
||||
@@ -127,6 +135,13 @@ mod tests {
|
||||
// 节点类型(决策 a 步骤③:ai_self_review 独立节点类型)
|
||||
let ai = dag.nodes.get("ai_self_review").expect("ai_self_review 存在");
|
||||
assert_eq!(ai.node_type, "ai_self_review");
|
||||
// 阶段3:gate=true 启用自审闸门(verdict=fail 阻断下游)
|
||||
let gate = ai
|
||||
.config
|
||||
.get("gate")
|
||||
.and_then(|v| v.as_bool())
|
||||
.expect("ai_self_review config 应含 gate");
|
||||
assert!(gate, "testing 模板 ai_self_review 应启用 gate:true(阶段3 闸门)");
|
||||
let human = dag.nodes.get("human_review").expect("human_review 存在");
|
||||
assert_eq!(human.node_type, "human");
|
||||
// 边方向:ai → human
|
||||
|
||||
Reference in New Issue
Block a user