新增: aichat Plan-driven Phase1(LLM 出 Plan + 开关默认关 + 三重兜底 + emit AiPlanCreated)
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user