修复: gen_stream判重+重试分类+状态机最短路径+既有测试修复
This commit is contained in:
@@ -224,6 +224,20 @@ pub const STALL_BREAKER_THRESHOLD: u32 = 3;
|
||||
/// 确认无误杀场景用。
|
||||
pub const STALL_BREAKER_WARN_FIRST: bool = true;
|
||||
|
||||
/// 目标验证续轮开关(默认 false,灰度放开)。
|
||||
///
|
||||
/// 治"过早宣称":末轮无 tool_calls 时,若 pinned_goals 非空且无 applied.matched_target
|
||||
/// 证据 → 不收敛,注入 nudge 消息强制续轮验证。机制非 prompt 说教(实证无效)。
|
||||
/// false(默认,回退):整块跳过,converged 判定逐字节等价改动前(只看末轮有无 tool_calls)。
|
||||
/// 主入口保障:默认关,自测+历史会话回放验证不误伤后再默认开。
|
||||
pub const GOAL_VERIFY_ENABLED: bool = false;
|
||||
|
||||
/// 目标验证续轮上限(默认 2)。
|
||||
///
|
||||
/// 每强制续轮 +1,达上限无条件强制收敛 + AiCompleted 标注 goal_unverified。
|
||||
/// 保证 loop 必然终止,绝不死循环。LLM 续轮内调了工具(无论成败)计数器重置(说明在行动)。
|
||||
pub const GOAL_VERIFY_MAX_ROUNDS: u32 = 2;
|
||||
|
||||
/// G2 警示是否回顾目标(默认 true,需 G1 goal 字段)。
|
||||
///
|
||||
/// true(默认):警示文本引用 pinned_goal(若存在)提示「回顾目标: {goal}」,精准;
|
||||
@@ -1353,6 +1367,11 @@ pub(crate) async fn run_agentic_loop(
|
||||
let mut stall_count: u32 = 0;
|
||||
let mut stall_warned: bool = false;
|
||||
|
||||
// 目标验证续轮计数(GOAL_VERIFY_ENABLED 门控):末轮无 tool_calls 但 pinned_goals 未达成
|
||||
// 且无 applied.matched_target 证据时强制续轮,每续 +1。达 GOAL_VERIFY_MAX_ROUNDS 强制收敛
|
||||
// + AiCompleted 标注 goal_unverified。续轮内调了工具则重置(说明 LLM 在行动非空转)。
|
||||
let mut goal_verify_rounds: u32 = 0;
|
||||
|
||||
// DeepSeek thinking 模式推理内容跨轮透传
|
||||
let mut last_reasoning_content: Option<String> = None;
|
||||
|
||||
@@ -1969,11 +1988,47 @@ pub(crate) async fn run_agentic_loop(
|
||||
return;
|
||||
}
|
||||
|
||||
// 无工具调用 → 最终文本响应,正常收敛退出
|
||||
// 无工具调用 → 最终文本响应。正常收敛退出,或目标验证续轮(GOAL_VERIFY_ENABLED)。
|
||||
#[allow(unused_assignments)]
|
||||
if !has_tool_calls { converged = true; break; }
|
||||
if !has_tool_calls {
|
||||
// 目标验证续轮:开关开 + 有钉扎目标 + 本轮无 applied.matched_target 证据 + 未达上限
|
||||
// → 不收敛,注入 nudge 强制续轮(治过早宣称,机制非 prompt 说教)。
|
||||
// 任一条件不满足 → 正常收敛(逐字节等价改动前)。
|
||||
let needs_verify = GOAL_VERIFY_ENABLED
|
||||
&& !pinned_goals_snapshot.is_empty()
|
||||
&& goal_verify_rounds < GOAL_VERIFY_MAX_ROUNDS
|
||||
&& !last_round_has_match_evidence(&session_arc, &conv_id).await;
|
||||
if needs_verify {
|
||||
goal_verify_rounds += 1;
|
||||
insert_goal_verify_nudge(&session_arc, &conv_id, iteration, goal_verify_rounds).await;
|
||||
// 不 break,续轮。下轮 LLM 见 nudge → 调工具验证(计数重置)或诚实收敛。
|
||||
// 续轮走 loop 顶部正常流程(build_for_request + stream),无特殊路径。
|
||||
} else {
|
||||
// 达上限强制收敛(GOAL_VERIFY_ENABLED && goal_verify_rounds >= MAX):
|
||||
// LLM 经 N 轮 nudge 仍空手停,目标未验证。标注 warn 供事后核查主入口行为。
|
||||
// (未达上限/开关关/无目标/有证据 → 正常收敛,无额外日志。)
|
||||
if GOAL_VERIFY_ENABLED
|
||||
&& !pinned_goals_snapshot.is_empty()
|
||||
&& goal_verify_rounds >= GOAL_VERIFY_MAX_ROUNDS
|
||||
{
|
||||
tracing::warn!(
|
||||
conv_id = %conv_id, iteration, goal_verify_rounds,
|
||||
"[GOAL-VERIFY] 达续轮上限强制收敛,目标未验证(LLM 经 nudge 仍空手停)"
|
||||
);
|
||||
}
|
||||
converged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = converged;
|
||||
|
||||
// 续轮内 LLM 调了工具(无论成败)→ 目标验证计数重置(说明在行动非空转,非卡死)。
|
||||
// 紧跟在 converged 判定之后、process_tool_calls 之前:本轮 has_tool_calls=true 到这里,
|
||||
// 说明 LLM 对 nudge 作出了工具响应,清零给后续验证留额度。
|
||||
if GOAL_VERIFY_ENABLED && goal_verify_rounds > 0 && has_tool_calls {
|
||||
goal_verify_rounds = 0;
|
||||
}
|
||||
|
||||
// AC-2 ①:记录本轮工具调用数(供超限警告判定;process_tool_calls 会 move 走 tool_calls_acc)。
|
||||
let round_tool_call_count = tool_calls_acc.len();
|
||||
// 处理工具调用(Low 自动执行 / Medium+High 待审批)
|
||||
@@ -2282,6 +2337,85 @@ fn fnv1a_32(bytes: &[u8]) -> u32 {
|
||||
hash
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 目标验证续轮(GOAL_VERIFY_ENABLED 门控)——治"过早宣称"的机制化兜底
|
||||
// ============================================================
|
||||
//
|
||||
// 根因:loop `converged` 判定只看末轮有无 tool_calls(形态),不看目标是否达成(事实)。
|
||||
// LLM 调完 advance_task 就停 → 被判 converged → 过早宣称。本机制在收敛闸门接事实维度:
|
||||
// 末轮无 tool_calls 时,若 pinned_goals 非空且本轮无 applied.matched_target 证据 → 不收敛,
|
||||
// 注入 nudge 强制续轮。复用 G2 insert_stall_warning 范式 + count_recent_failures 读尾消息模式。
|
||||
//
|
||||
// 主入口保障:三态兜底(applied 字段缺失/解析异常 → 视为无证据,不误伤也不阻断)、
|
||||
// 上限熔断(goal_verify_rounds 达 MAX 强制收敛)、调工具重置计数(LLM 在行动非空转)。
|
||||
|
||||
/// 检查本轮(末尾连续 Tool 消息)是否有 applied.matched_target=true 的验证证据。
|
||||
///
|
||||
/// 读尾部 BREAKER_TAIL_N 条消息,取末尾连续 Tool 消息,解析 content JSON 的
|
||||
/// `applied.matched_target` 字段。任一为 true 即返 true(本轮有达成证据)。
|
||||
/// 三态兜底:字段缺失/非 JSON/解析异常 → 返 false(无证据),不阻断也不误判。
|
||||
async fn last_round_has_match_evidence(
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
conv_id: &str,
|
||||
) -> bool {
|
||||
let messages = {
|
||||
let session = session_arc.lock().await;
|
||||
match session.conv_read(conv_id) {
|
||||
Some(conv) => conv.messages.recent_messages(BREAKER_TAIL_N),
|
||||
None => Vec::new(),
|
||||
}
|
||||
};
|
||||
for m in messages.iter().rev() {
|
||||
if !matches!(m.role, MessageRole::Tool) {
|
||||
break;
|
||||
}
|
||||
let Ok(v) = serde_json::from_str::<serde_json::Value>(&m.content) else {
|
||||
continue;
|
||||
};
|
||||
if v.get("applied")
|
||||
.and_then(|a| a.get("matched_target"))
|
||||
.and_then(|x| x.as_bool())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// 注入目标验证续轮 nudge(强制续轮,非软提示)。
|
||||
///
|
||||
/// 文案随 goal_verify_rounds 递进:第 1 次请验证,第 2 次(将达上限)明确最后机会。
|
||||
/// 复用 insert_stall_warning 的 insert_at(0, system) 范式 + fetch_goal_summary 拼目标。
|
||||
async fn insert_goal_verify_nudge(
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
conv_id: &str,
|
||||
iteration: usize,
|
||||
verify_rounds: u32,
|
||||
) {
|
||||
let goals = fetch_goal_summary(session_arc, conv_id).await;
|
||||
let nudge_text = if verify_rounds + 1 >= GOAL_VERIFY_MAX_ROUNDS {
|
||||
format!(
|
||||
"⚠ 这是最后一次验证机会。目标{}仍未给出完成证据。要么立即用工具验证目标已达成,要么诚实说明哪些未完成、为何未完成,不得再无证据宣称完成。",
|
||||
goals
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"⚠ 你停下了工具调用,但目标{}尚未给出完成证据。在宣称完成前,必须用工具(读取/核对/查询)验证结果与目标一致;若实际未完成,诚实说明现状,不得宣称完成。",
|
||||
goals
|
||||
)
|
||||
};
|
||||
let mut session = session_arc.lock().await;
|
||||
if session.per_conv.contains_key(conv_id) {
|
||||
let conv = session.conv(conv_id);
|
||||
conv.messages.insert_at(0, ChatMessage::system(&nudge_text));
|
||||
tracing::info!(
|
||||
conv_id = %conv_id, iteration, verify_rounds,
|
||||
"[GOAL-VERIFY] 目标未验证,注入续轮 nudge(强制续轮)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── is_failure_content: 纯结构化判定(只读字段,绝不解析内容文本) ──
|
||||
// 原理:工具成败是执行层的结构化事实(exit_code / status),断路器只读字段。内容文本(无论含
|
||||
// error/失败/任何词)绝不参与判定 —— 这样读含 error 字样的代码、搜"失败"的结果等成功工具内容
|
||||
|
||||
Reference in New Issue
Block a user