重构: aichat agent 能力系统化(L1元能力+L2/L3后端+list去重)
L1 agent 元能力层(治痛①死循环零交付): - env_profile 环境姿势注入 + shell 默认 PowerShell(防引号地狱) - 断路器:同类工具失败≥3熔断 + guard.reset - detect_environment 主动探测工具(python/node/shell) - 求助协议 AiHelpRequired 事件 + 前端求助卡 L2 统一状态机后端(治痛②,前端批2): - ConvState enum 5态 + 合法转换守卫(conv_state.rs) - GeneratingGuard 接入视图层(guard.rs) L3 事件总线后端骨架(治痛③④⑤,接入批2): - EventBus pub-sub + AiBusEvent 8变体(event_bus.rs) list 工具调用重复治理第一步: - build_system_prompt_with_excluded 去重被@实体 + 清单注明语
This commit is contained in:
@@ -21,7 +21,7 @@ use df_ai::context_helpers::{
|
||||
// 收敛的扁平子集之上叠加 plan_hint 编排(并行组同批聚拢/顺序依赖源在前),供 LLM 看到
|
||||
// 一份按编排意图排序的工具列表。feature flag PLANNING_ENABLED(false 默认关)门控接入。
|
||||
use df_ai::intent::{filter_tool_defs, filter_tool_defs_planned, IntentRecognizer};
|
||||
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider};
|
||||
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider, MessageRole};
|
||||
// CR-30-1: 复用 retry::backoff_delay(jitter 1s→2s→4s) + is_status_retryable(Fatal 分类)
|
||||
// 实现流前失败重试退避对齐(决策 F-260616-07 a1),避免重写退避逻辑。
|
||||
use df_ai::retry;
|
||||
@@ -106,6 +106,30 @@ pub const TOOL_RESULT_COMPRESS_ENABLED: bool = true;
|
||||
/// 保守:双高置信才标(任一 topic None 不标),不强制 LLM(软提示非硬约束)。
|
||||
pub const TOPIC_MARKER_ENABLED: bool = true;
|
||||
|
||||
/// L1 断路器:连续同类工具失败熔断阈值(治 kms 会话 53 轮 0 产出死循环)。
|
||||
///
|
||||
/// 背景:agent 无止损,某工具反复同类失败(权限拒绝/路径错误等)仍每轮重试,
|
||||
/// 耗尽 max_iterations 前 0 产出。机制(非 prompt 教 AI):每轮 process_tool_calls
|
||||
/// 后取末尾连续 Tool 消息,失败内容前 40 字符归一为 key 计数,同一 key 累计达此阈值 →
|
||||
/// guard.reset + emit AiError + return 强制熔断,逼用户换思路或人工介入。
|
||||
///
|
||||
/// 阈值 3:同类失败 3 次足以判死循环(去重后仍累加,不同错误各自计数互不干扰)。
|
||||
pub const CIRCUIT_BREAKER_THRESHOLD: u32 = 3;
|
||||
|
||||
/// L1 断路器总开关(默认 true)。false → 跳过断路器检查,降级为纯 max_iterations
|
||||
/// 旧行为(排障/对比/临时关闭用)。机制优先 prompt 说教,每改配开关 + 兜底(关降级旧行为)。
|
||||
pub const CIRCUIT_BREAKER_ENABLED: bool = true;
|
||||
|
||||
/// L1 断路器熔断时是否发结构化求助(aichat 体验与 agent 能力系统化重构 §2.3,2026-06-21)。
|
||||
///
|
||||
/// true(默认):熔断 emit AiHelpRequired(结构化求助卡:reason + context + options),
|
||||
/// 引导用户换思路/授权路径/人工接管(机制优先 prompt 说教,非教 AI 自己止损)。
|
||||
/// false(兜底回退):熔断仍 emit AiError(旧行为,前端错误气泡),用于求助卡未就绪/
|
||||
/// 排障/对比。两路保留 guard.reset + return 强制熔断语义不变,仅换前端呈现形态。
|
||||
/// 配合 CIRCUIT_BREAKER_ENABLED:CIRCUIT_BREAKER_ENABLED=false 时断路器整段跳过,
|
||||
/// 本开关无意义;CIRCUIT_BREAKER_ENABLED=true 时本开关决定呈现形态。
|
||||
pub const CIRCUIT_BREAKER_HELP_EVENT: bool = true;
|
||||
|
||||
// 阶段2(path_auth 审批链重构):占位配对完整性开关(解 400 orphan)。
|
||||
//
|
||||
// 单一真相源:`df_ai::context_helpers::PLACEHOLDER_INTEGRITY_ENABLED`(本模块顶部已 use)。
|
||||
@@ -131,6 +155,22 @@ pub const TOPIC_MARKER_ENABLED: bool = true;
|
||||
mod guard;
|
||||
use guard::GeneratingGuard;
|
||||
|
||||
// ============================================================
|
||||
// L2 统一状态机(渐进第一步,2026-06-21):ConvState enum + 转换守卫。
|
||||
// 设计:generating状态机加固-2026-06-15.md §3 + aichat体验与agent能力系统化重构-2026-06-21.md §3。
|
||||
//
|
||||
// 本批范围(渐进):
|
||||
// - 新建 conv_state.rs(纯逻辑 enum + 守卫 + 单测)。
|
||||
// - run_agentic_loop 入口桥接:guard 置 generating=true 时同步迁移 ConvState
|
||||
// (Idle→Generating / Error→Generating),作视图层。CONV_STATE_ENABLED 开关门控。
|
||||
// - guard.reset 同步 ConvState→Idle(正常退出路径)。
|
||||
// - **不替换** PerConvState.generating bool(渐进不一次全换,批2+ 逐步迁读侧)。
|
||||
//
|
||||
// 开关 CONV_STATE_ENABLED(默认 on):off 降级纯旧 bool 行为(可回退)。
|
||||
// 兜底:状态机层迁移失败(非法转换)记 warn 不 panic,核心 generating 复位仍走旧 bool。
|
||||
// ============================================================
|
||||
pub mod conv_state;
|
||||
|
||||
// ============================================================
|
||||
// F-260614-04 / F-260614-04b: 单 Provider 流式结果 + fallback 辅助
|
||||
// ============================================================
|
||||
@@ -374,6 +414,11 @@ pub(crate) async fn run_agentic_loop(
|
||||
//
|
||||
// 单 loop 安全性:批2 阶段是单 active_conversation_id(决策 e 真并发批3+ 落地),无并发 loop 抢
|
||||
// per_conv 覆盖。批3+ 多 loop 并发时,每 conv 各自 per_conv 条目,互不干扰(本桥接无需改)。
|
||||
//
|
||||
// L2 状态机视图层:ConvState 的写收敛已收敛到 `guard`(GeneratingGuard::new 已在上方 L399 创建,
|
||||
// new 内部 Idle→Generating 迁移 + reset/drop Generating→Idle 迁移,CONV_STATE_ENABLED 门控)。
|
||||
// 此处不再重复迁移 enum,仅写核心 generating bool(唯一真相源,enum 是其视图)。批2+ 把 enum
|
||||
// 提升到 PerConvState.conv_state 字段后,此 bool 写入与 enum 迁移由 guard 统一收敛。
|
||||
{
|
||||
let mut session = session_arc.lock().await;
|
||||
let conv = session.conv(&conv_id);
|
||||
@@ -601,6 +646,10 @@ pub(crate) async fn run_agentic_loop(
|
||||
// 区分"正常收敛退出"与"达 MAX 被截断退出"——后者末轮 tool_calls 仍非空(tool_result 不再回传 LLM),属异常
|
||||
let mut converged = false;
|
||||
|
||||
// L1 断路器:连续同类工具失败计数器(key=失败内容前 40 字符,value=累计次数)。
|
||||
// loop 生命周期内累加,每轮 process_tool_calls 后检查。达 CIRCUIT_BREAKER_THRESHOLD → 熔断退出。
|
||||
let mut fail_counts: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
|
||||
|
||||
// BUG-260617-12: DeepSeek thinking 模式推理内容跨轮透传
|
||||
let mut last_reasoning_content: Option<String> = None;
|
||||
|
||||
@@ -1240,6 +1289,95 @@ pub(crate) async fn run_agentic_loop(
|
||||
"[AI-DIRAUTH-DIAG] agentic loop 收到 pending"
|
||||
);
|
||||
|
||||
// L1 断路器:连续同类工具失败熔断(治 agent 无止损死循环,机制非 prompt 说教)。
|
||||
// CIRCUIT_BREAKER_ENABLED=false → 整段跳过降级 max_iterations 旧行为(开关 + 兜底)。
|
||||
// 仅检查自动执行(Low)的工具结果——pending_count>0(待审批)交给下方审批分支,
|
||||
// 此处只看已回填的 Tool 消息。取末尾连续 Tool 消息(倒序 take_while role==Tool),
|
||||
// 失败内容前 40 字符归一 key 计数,同 key 累计达阈值 → guard.reset + AiError + return。
|
||||
if CIRCUIT_BREAKER_ENABLED {
|
||||
let (max_count, sample_key) = {
|
||||
let session = session_arc.lock().await;
|
||||
// conv 可能已被删除(stop/新对话),get 不到 → 无消息可判,跳过本轮断路器检查。
|
||||
let messages = match session.conv_read(&conv_id) {
|
||||
Some(conv) => conv.messages.all_messages_clone(),
|
||||
None => Vec::new(),
|
||||
};
|
||||
// 倒序取末尾连续 role==Tool 消息(本轮工具回填结果;非 Tool 即停)。
|
||||
// MessageRole 未派生 PartialEq,用 matches! 宏判变体(不改共享类型 df-ai-core)。
|
||||
let recent_tool_results: Vec<&ChatMessage> = messages
|
||||
.iter()
|
||||
.rev()
|
||||
.take_while(|m| matches!(m.role, MessageRole::Tool))
|
||||
.collect();
|
||||
for m in recent_tool_results {
|
||||
let content = m.content.as_str();
|
||||
// 失败判定:禁止/跳过重试/失败/Error 关键词(覆盖权限拒绝/路径错误/异常等)。
|
||||
let is_failure = content.starts_with("禁止")
|
||||
|| content.starts_with("已跳过重试")
|
||||
|| content.contains("失败")
|
||||
|| content.contains("Error")
|
||||
|| content.contains("error");
|
||||
if is_failure {
|
||||
// 前 40 字符归一 key:同类失败(同前缀)累加,不同错误各自计数互不干扰。
|
||||
let key: String = content.chars().take(40).collect();
|
||||
*fail_counts.entry(key).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
// 取当前最大计数及其 key(无失败 → max_count=0,不触发)。
|
||||
fail_counts
|
||||
.iter()
|
||||
.max_by_key(|(_, &v)| v)
|
||||
.map(|(k, &v)| (v, k.clone()))
|
||||
.unwrap_or((0u32, String::new()))
|
||||
};
|
||||
// 锁已随作用域 drop,可安全 await/emit(避免持锁 await 死锁)。
|
||||
if max_count >= CIRCUIT_BREAKER_THRESHOLD {
|
||||
tracing::warn!(
|
||||
conv_id = %conv_id,
|
||||
max_count,
|
||||
sample_key = %sample_key,
|
||||
"[ai] L1 断路器熔断:连续同类失败 {} 次,疑似死循环停止", max_count
|
||||
);
|
||||
guard.reset().await;
|
||||
// L1 求助协议(§2.3,2026-06-21):熔断改发结构化 AiHelpRequired(机制优先 prompt 说教)。
|
||||
// 开关 CIRCUIT_BREAKER_HELP_EVENT=true(默认)→ AiHelpRequired(求助卡,显 reason/options
|
||||
// 供用户选);false(兜底回退)→ AiError(旧错误气泡)。两路均 guard.reset + return 强制熔断,
|
||||
// 仅前端呈现形态不同,语义不变(均终止 loop,逼用户介入)。
|
||||
if CIRCUIT_BREAKER_HELP_EVENT {
|
||||
let _ = app_handle.emit(
|
||||
"ai-chat-event",
|
||||
AiChatEvent::AiHelpRequired {
|
||||
reason: format!(
|
||||
"连续同类失败 {} 次,疑似死循环已停止",
|
||||
max_count
|
||||
),
|
||||
context: format!("最近错误: {}", sample_key),
|
||||
options: vec![
|
||||
"换思路".into(),
|
||||
"授权路径".into(),
|
||||
"人工接管".into(),
|
||||
],
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// 兜底回退:求助卡未就绪/排障/对比时,沿用旧 AiError 错误气泡呈现。
|
||||
let _ = app_handle.emit(
|
||||
"ai-chat-event",
|
||||
AiChatEvent::AiError {
|
||||
error: format!(
|
||||
"连续同类失败 {} 次,疑似死循环已停止。请换思路或人工介入。最近错误: {}",
|
||||
max_count, sample_key
|
||||
),
|
||||
error_type: Some(ErrorType::Unknown),
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
},
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 有待审批 → 暂停循环,等待用户审批后通过 ai_approve → try_continue_agent_loop 恢复
|
||||
if pending_count > 0 {
|
||||
let usage = df_ai::provider::TokenUsage {
|
||||
|
||||
Reference in New Issue
Block a user