修复: gen_stream判重+重试分类+状态机最短路径+既有测试修复
This commit is contained in:
@@ -163,7 +163,7 @@ pub(super) async fn insert_pending_approval(
|
||||
// (High only),本 guard 按 tc_id 匹配(覆盖 Med + High 残留场景)。
|
||||
// 同 tc_id 已有审计落定记录 → retry_count≥1,跳过审批 + emit Completed,断死循环。
|
||||
// 兜底:flag 关或无审计记录 → retry_count=0,等价原行为。
|
||||
let retry_count = detect_retry_count(audit_repo, &draft.id).await;
|
||||
let retry_count = detect_retry_count(audit_repo, conv_id, &draft.name, &draft.args).await;
|
||||
if retry_count >= 1 {
|
||||
let skip_msg = format!(
|
||||
"已跳过重试(同 tool_call_id={} 此前已审批执行过,防 LLM 死循环重试同卡死工具)",
|
||||
@@ -219,28 +219,41 @@ pub(super) async fn insert_pending_approval(
|
||||
audit_tool_call(audit_repo, conv_id, &draft.id, &draft.name, &draft.args, "pending", risk_level, None, None, current_message_id).await;
|
||||
}
|
||||
|
||||
/// (容错/恢复,开关 `df-ai-approval-retry`):查审计表推算同 tc_id 重试计数。
|
||||
/// (容错/恢复,开关 `df-ai-approval-retry`):查审计表推算同 (会话, 工具, 参数) 重试计数。
|
||||
///
|
||||
/// 判重语义:同会话内同工具同参数的重试。区别于旧实现按裸 tool_call_id 判重——
|
||||
/// 弱模型 provider(如 sensenova)的 tool_call_id 每轮从 0 重排(gen_stream_0/1/2...),
|
||||
/// 跨轮不同会话的合法调用会复用同一 id,裸 id 判重会把合法新操作误判为「同 id 死循环重试」
|
||||
/// 静默跳过(实证 2026-08-11:推进任务被跳过 4 次,AI 却宣称成功)。
|
||||
///
|
||||
/// 返回语义:
|
||||
/// - 0:审计表无该 tc_id 落定记录(或仅 pending),属首次审批执行,正常挂起。
|
||||
/// - ≥1:审计表已有该 tc_id 的落定记录(executed/failed/rejected/skipped_retry),即该
|
||||
/// tc_id 此前已被审批执行过一次,LLM 又用同 id 重试 → 调用方据 ≥1 跳过执行 + emit Completed,
|
||||
/// 断「超时/权限错→LLM 死循环重试同 id→重新挂起→用户被迫二次授权」循环。
|
||||
/// - 0:审计表无该 (conv, tool, args) 落定记录(或仅 pending),属首次审批执行,正常挂起。
|
||||
/// - ≥1:审计表已有该 (conv, tool, args) 的落定记录(executed/failed/rejected/skipped_retry),
|
||||
/// 即该调用此前已被审批执行过一次,LLM 又重试同参数 → 调用方据 ≥1 跳过执行 + emit Completed,
|
||||
/// 断「超时/权限错→LLM 死循环重试→重新挂起→用户被迫二次授权」循环。
|
||||
///
|
||||
/// 实现:查 `find_by_tool_call_id`,status 为 pending 视为"尚未落定"(返 0,首次挂起审批的
|
||||
/// 实现:查 `find_by_conv_tool_args`,status 为 pending 视为"尚未落定"(返 0,首次挂起审批的
|
||||
/// 正常态);其余落定状态返 1。retry_count 当前仅取 0/1(断路器语义:第二次即跳过),
|
||||
/// 字段类型 u32 留给未来"允许多次重试"扩展(配置上限阈值)。
|
||||
///
|
||||
/// 兜底/回退:flag 关(文档标记)或审计查询失败 → 返 0,等价原行为(单次审批执行,无重试防护)。
|
||||
pub(super) async fn detect_retry_count(audit_repo: &AiToolExecutionRepo, tc_id: &str) -> u32 {
|
||||
pub(super) async fn detect_retry_count(
|
||||
audit_repo: &AiToolExecutionRepo,
|
||||
conv_id: &str,
|
||||
tool_name: &str,
|
||||
args: &str,
|
||||
) -> u32 {
|
||||
// 审计查询失败不阻断主流程(DB 故障等降级为无重试防护,返回 0 走原审批流程)
|
||||
let rec = match audit_repo.find_by_tool_call_id(tc_id).await {
|
||||
let rec = match audit_repo.find_by_conv_tool_args(conv_id, tool_name, args).await {
|
||||
Ok(opt) => match opt {
|
||||
Some(r) => r,
|
||||
None => return 0, // 无记录 = 首次
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::warn!("[approval-retry] 查审计表 tc_id={} 失败(降级无重试防护): {}", tc_id, e);
|
||||
tracing::warn!(
|
||||
"[approval-retry] 查审计表 (conv={}, tool={}) 失败(降级无重试防护): {}",
|
||||
conv_id, tool_name, e
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -376,7 +376,7 @@ pub(crate) async fn process_tool_calls(
|
||||
.unwrap_or_default();
|
||||
let path_str = req.raw_paths.first().cloned().unwrap_or_default();
|
||||
// 锁外:(容错/恢复,开关 df-ai-approval-retry):同 tc_id 重试检测。
|
||||
let retry_count = detect_retry_count(&audit_repo, &draft.id).await;
|
||||
let retry_count = detect_retry_count(&audit_repo, conv_id, &draft.name, &draft.args).await;
|
||||
if retry_count >= 1 {
|
||||
let skip_msg = format!(
|
||||
"已跳过重试(同 tool_call_id={} 此前已审批执行过,防 LLM 死循环重试同卡死工具)",
|
||||
|
||||
Reference in New Issue
Block a user