新增: aichat Plan-driven Phase1(LLM 出 Plan + 开关默认关 + 三重兜底 + emit AiPlanCreated)

This commit is contained in:
lxy
2026-08-01 16:01:11 +08:00
parent a69057a1ef
commit 8c0ff80cd4
2 changed files with 789 additions and 2 deletions
+139
View File
@@ -970,6 +970,90 @@ pub(crate) async fn run_agentic_loop(
.unwrap_or_default()
};
// ── Plan-driven Phase 1:LLM 规划端(AICHAT_PLAN_ENABLED 门控) ──
//
// 对齐 docs/02-架构设计/专项设计/aichat-plan-driven-设计-2026-08-01.md Phase 1:
// 开启时由 LLM 生成 Plan JSON(替代 coordinator.decompose 关键词匹配),成功后 emit
// AiPlanCreated 事件(步骤 + 状态 pending)供前端展示(前端卡片 Phase 3 后续,本步只 emit)。
//
// **职责边界(对齐设计文档)**:
// - Phase 1 只治「Plan 从哪来」(LLM 出 Plan)+ 事件可见性,**不改执行逻辑**;
// - 执行调度(Plan→to_layers→并行/串行 dispatch)是 Phase 2 范围,留 plan_executor 接通后做;
// - 故本块 emit 后**继续走 ReAct 主链**(不 early return),Plan 仅作规划可见性,执行仍单链 ReAct。
//
// **开关默认关 + 兜底回退**(memory ai-improvement-principles):
// - AICHAT_PLAN_ENABLED=false(默认)→ 整块跳过,零行为变更(现有 ReAct 不受影响);
// - LLM 调用失败/JSON 解析失败/validate 失败 → decompose_with_llm 返 None → 整块跳过,
// 继续走 ReAct(不阻断,三重兜底);
// - 与 plan_executor::PLAN_EXECUTION_ENABLED 正交:执行开关独立治 dispatch,本块不碰。
//
// LLM 调用复用主对话 provider + resolved_model(已解析,无需二次 build);available_tools
// 喂 LLM 限定 tool_hint 取值域防幻觉(取 tool_defs 名清单,即 LLM 可见工具子集)。
if df_ai::coordinator::aichat_plan_enabled() && conf >= INTENT_CONF_THRESHOLD {
let coord = Coordinator::new(PersonaRegistry::new());
// 可用工具名清单(供 LLM 填 tool_hint,防幻觉不存在的工具名)
let available_tools: Vec<String> = tool_defs
.iter()
.map(|d| d.function.name.clone())
.collect();
// LLM 调用包 30s 超时(对齐 compress_via_llm 60s / title 20s 量级):规划属独立 LLM
// 调用,超时返 None 回退 ReAct,不拖垮 agentic loop。
let plan_result = tokio::time::timeout(
std::time::Duration::from_secs(30),
coord.decompose_with_llm(
&*provider,
&resolved_model,
intent.as_str(),
&user_text,
&available_tools,
),
)
.await;
match plan_result {
Err(_) => {
tracing::warn!(
conv_id = %conv_id,
"[PLAN-LLM] LLM 规划调用超时(30s),回退纯 ReAct"
);
}
Ok(Some(decompose_result)) => {
// emit AiPlanCreated(步骤 + 状态 pending)供前端展示(Phase 3 前端卡片后续)
let layers_payload = build_plan_layers_payload(&decompose_result.plan, &coord);
let plan_id = format!("plan-{}", ulid_like_id());
tracing::info!(
conv_id = %conv_id,
plan_id = %plan_id,
layer_count = layers_payload.len(),
task_count = decompose_result.subtasks.len(),
"[PLAN-LLM] Plan 创建,emit AiPlanCreated"
);
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiPlanCreated {
plan_id: plan_id.clone(),
layers: layers_payload,
conversation_id: Some(conv_id.clone()),
});
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(
AiChatEvent::AiPlanCreated {
plan_id,
// layers 已 move,复用 decompose_result 重建等价载荷(reborrow 避免 move)
layers: build_plan_layers_payload(&decompose_result.plan, &coord),
conversation_id: Some(conv_id.clone()),
}
);
// Phase 1:emit 后继续走 ReAct 主链(执行调度是 Phase 2)。
// Plan 仅作规划可见性 + 前端展示;执行仍单链 ReAct,不改 dispatch 逻辑。
}
Ok(None) => {
// 三重兜底已触发(LLM 错/解析错/validate 错),decompose_with_llm 内已记 warn。
// 静默继续 ReAct(不阻断)。
tracing::info!(
conv_id = %conv_id,
"[PLAN-LLM] LLM 规划未产出有效 Plan,继续走 ReAct"
);
}
}
}
if df_ai::plan_executor::plan_execution_enabled() && conf >= INTENT_CONF_THRESHOLD {
let coord = Coordinator::new(PersonaRegistry::new());
let decompose_result = coord.decompose(intent.as_str(), &user_text);
@@ -2119,3 +2203,58 @@ fn update_pinned_goals(
}
}
}
// ── Plan-driven Phase 1:Plan→事件载荷 + plan_id 生成辅助 ──
//
// 对齐 docs/02-架构设计/专项设计/aichat-plan-driven-设计-2026-08-01.md Phase 1:
// LLM 出 Plan 后 emit AiPlanCreated(步骤 + 状态 pending)。Plan 是 DAG(kahn to_layers
// 分层),事件载荷按 layer 分组(同层并行,层间串行),前端据此画 DAG 卡片(Phase 3)。
/// 生成 plan_id(时间戳 + 计数器,无需 ulid 依赖)。
///
/// 形如 `plan-1757000000000-42`,单调递增 + 进程内唯一。与 df-ai-core 的 new_message_id
/// 同思路(AtomicU64 计数),但本处 plan_id 仅供事件载荷标识,无落库无溯源需求。
fn ulid_like_id() -> String {
use std::sync::atomic::{AtomicU64, Ordering as O};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
let n = COUNTER.fetch_add(1, O::SeqCst);
format!("{}-{}", ts, n)
}
/// 把 Plan(DAG)转为 AiPlanCreated 事件载荷(按 layer 分组)。
///
/// - 调 `plan.to_layers()`(kahn 拓扑分层)得层级顺序;
/// - 每层任务映射为 SubTaskInfo(id / persona_id / intent / status="pending");
/// - persona_id 由 `registry.recommend_for_intent` 推荐(对齐 dispatch 路径);
/// - to_layers 失败(环,正常不会发生——decompose_with_llm 已 validate 兜底)→
/// fallback 单层平铺(不阻断事件 emit)。
fn build_plan_layers_payload(
plan: &df_ai::planner::Plan,
coord: &Coordinator,
) -> Vec<super::PlanLayerInfo> {
let layers = plan.to_layers().unwrap_or_else(|_| {
// 环(unreachable:decompose_with_llm 已 validate 兜底)→ fallback 单层平铺
vec![plan.tasks.clone()]
});
layers
.into_iter()
.map(|layer| super::PlanLayerInfo {
items: layer
.into_iter()
.map(|t| {
let persona_id = coord.recommend_persona_id(&t.intent);
super::SubTaskInfo {
id: t.id,
persona_id,
intent: t.intent,
status: "pending".to_string(),
}
})
.collect(),
})
.collect()
}