修复: 压缩边界对齐与断路器学习语义

- 压缩边界按对话单元回退,不再劈开工具对
- 停止与接管退出补跳过结果,畸形不再新增
- 失败指纹纳入参数,改参重试不再累计熔断
- 成功断连失败段,红绿循环不再误熔断
- 脚本非零退出豁免,同命令死磕由签名熔断兜底
- 熔断前加自纠警示轮,临时路径写删豁免
This commit is contained in:
lxy
2026-10-01 19:35:22 +08:00
parent b8416dea63
commit 4417b3ddd3
4 changed files with 416 additions and 47 deletions
+92 -1
View File
@@ -771,6 +771,10 @@ pub(crate) async fn run_agentic_loop(
// 治"全 loop 累加不衰减":早期偶发失败不会与后期叠加误熔断。
let mut fail_window: std::collections::VecDeque<String> = std::collections::VecDeque::new();
// L1 断路器两段式警示位(对齐 G2 WARN_FIRST):同 key 失败达 THRESHOLD-1 时注入一次
// 系统警示给 LLM 自纠机会,达 THRESHOLD 才熔断。置位后本 loop 不重复警示。
let mut breaker_warned: bool = false;
// G2 探索熔断:重复探索计数器(loop 生命周期累计,与 fail_window 同生命周期)。
// 每轮 process_tool_calls 后 check_stall_breaker 取最近 N 个 assistant tool_calls 签名,
// 喂 is_repetitive_exploration 判重复,重复 stall_count+=1,非重复重置 0。
@@ -1086,7 +1090,7 @@ pub(crate) async fn run_agentic_loop(
&mut guard, &mut tokens, &mut saved_token_snapshot,
&resolved_model,
tool_calls_acc,
&mut fail_window,
&mut fail_window, &mut breaker_warned,
&mut stall_count, &mut stall_warned, &mut loop_count, &mut loop_warned,
).await {
return;
@@ -1612,6 +1616,24 @@ async fn finalize_round_output(
// 停止信号 或 已被新 loop 接管(stale):已生成文本入库后退出,不再执行后续工具调用。
// F1:stale 时旧 loop 在此退出,不执行工具(防重复工具执行);teardown_round 内部跳过 emit。
if stop_flag.load(Ordering::SeqCst) || loop_epoch_arc.load(Ordering::SeqCst) != my_epoch {
// 本轮 tool_calls 尚未执行,而 assistant 头(含 tool_calls)已 push 且 P0-2 已落库,
// 直接退出会给历史留下无 result 的悬空 tool_calls(sanitize 每请求告警的确定性供给)。
// 退出前逐个补跳过型 tool_result 闭环三元组,随后走既有 teardown save 落库。
// 短锁段纯写,锁边界与 epoch 语义不变:stale 时 teardown 因 epoch 不匹配跳过 save,
// 补的 result 留在内存,由 owner loop 的下一次 save 收敛落库。
if !tool_calls_acc.is_empty() {
let mut drafts: Vec<(u32, &super::ToolCallDraft)> =
tool_calls_acc.iter().map(|(k, d)| (*k, d)).collect();
drafts.sort_by_key(|(k, _)| *k);
let mut session = session_arc.lock().await;
if session.per_conv.contains_key(conv_id) {
let conv = session.conv(conv_id);
for (_, d) in drafts {
conv.messages.push(ChatMessage::tool_result(
&d.id, "用户停止或被接管,工具未执行"));
}
}
}
// save_usage 增量(自上次 save 后新增);emit_usage 累计快照(前端展示本 loop 总量)
let save_usage = usage_delta_since(tokens, saved_token_snapshot);
let emit_usage = tokens_snapshot(tokens);
@@ -1697,6 +1719,7 @@ async fn process_round_tools(
resolved_model: &str,
tool_calls_acc: std::collections::HashMap<u32, super::ToolCallDraft>,
fail_window: &mut std::collections::VecDeque<String>,
breaker_warned: &mut bool,
stall_count: &mut u32,
stall_warned: &mut bool,
loop_count: &mut u32,
@@ -1732,6 +1755,13 @@ async fn process_round_tools(
if CIRCUIT_BREAKER_ENABLED {
let (max_count, sample_key) = count_recent_failures(session_arc, conv_id, fail_window).await;
// 锁已随作用域 drop,可安全 await/emit(避免持锁 await 死锁)。
// 两段式(对齐 G2 WARN_FIRST 骨架):达 THRESHOLD-1 先经 insert_system_notice 注入
// 警示(尾部追加不破前缀),带失败样本 key 与建议动作,给 LLM 查环境/换工具/调参数的
// 自纠机会;达 THRESHOLD 才熔断。
if max_count == CIRCUIT_BREAKER_THRESHOLD.saturating_sub(1) && !*breaker_warned {
*breaker_warned = true;
insert_circuit_breaker_warning(session_arc, conv_id, iteration, &sample_key).await;
}
if max_count >= CIRCUIT_BREAKER_THRESHOLD {
tracing::warn!(
conv_id = %conv_id,
@@ -2114,6 +2144,30 @@ async fn insert_stall_warning(
.await;
}
// L1 断路器两段式警示:同 key 失败达 THRESHOLD-1 时注入系统软提示(尾部追加,不破前缀)。
// 文本带失败样本 key(工具与参数指纹、错误指纹)与建议动作,给 LLM 自纠机会,熔断前先警示。
async fn insert_circuit_breaker_warning(
session_arc: &Arc<Mutex<AiSession>>,
conv_id: &str,
iteration: usize,
sample_key: &str,
) {
let warn_text = format!(
"⚠ 工具连续失败(样本: {})。同一做法再试大概率仍失败,建议先查环境(路径/权限/依赖是否就绪),再换用其他工具或调整参数与方案;若确认无法继续,直接向用户说明阻塞点。",
sample_key
);
helpers::insert_system_notice(
session_arc,
conv_id,
&warn_text,
&format!(
"L1 断路器:同类失败警示已 insert(软提示,给 LLM 自纠机会) iteration={} sample_key={}",
iteration, sample_key
),
)
.await;
}
// ── fetch_goal_summary: 读 pinned_goals 拼接为 "(当前目标: a; b)" 或空串 ──
async fn fetch_goal_summary(session_arc: &Arc<Mutex<AiSession>>, conv_id: &str) -> String {
let goals = session_arc
@@ -2250,6 +2304,43 @@ async fn emit_ai_completed_once(
"AI 回复已就绪,点击托盘图标查看",
);
}
// 编排会话完成通知桥:工作流 AiAgentNode 挂起的等待方在此投递最终报告。
// 本函数是全部正常出口(5 类 teardown_round 出口 + 正常完成直调)的统一单点,
// 到达者必为 owner loop(epoch 守卫在各调用方:teardown_round 入口 / 正常完成双检),
// force_send 接管后的旧 loop 不触发。无等待方(普通用户会话)has 判空短路,零会话锁开销。
{
let app_state = app_handle.state::<AppState>();
if app_state.has_agent_completion_waiter(conv_id) {
let report = last_active_assistant_text(&app_state.ai_session, conv_id).await;
let delivered = app_state.notify_agent_completion(conv_id, report);
tracing::info!(
conv_id = %conv_id,
delivered,
"[ai] 编排会话收敛,完成通知桥投递最终报告"
);
}
}
}
/// 取会话末条 active assistant 消息文本(编排会话最终报告投递用)。
///
/// conv 不存在 / 无 assistant 消息返空串,原样投递,空文本由节点侧判「无输出」失败
/// (用户 stop 在任何输出前的场景)。读内存 per_conv 非常规出口:正常完成路径 save 落库
/// 在后台 spawn 里,时序上可能晚于本函数,内存消息是唯一已确定就绪的真相源。
async fn last_active_assistant_text(
session_arc: &Arc<Mutex<AiSession>>,
conv_id: &str,
) -> String {
let session = session_arc.lock().await;
session
.conv_read(conv_id)
.map(|c| c.messages.all_messages_clone())
.unwrap_or_default()
.into_iter()
.rev()
.find(|m| matches!(m.role, MessageRole::Assistant) && m.is_active())
.map(|m| m.content)
.unwrap_or_default()
}
/// 构造 AiCompleted 事件(emit_ai_completed_once 内部去重用,字段口径见其文档)。