From b357916d9771175e22de5ad2482cd8ae6da9265b Mon Sep 17 00:00:00 2001 From: iven Date: Sun, 12 Apr 2026 18:31:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(intelligence):=20Phase=205=20=E4=B8=BB?= =?UTF-8?q?=E5=8A=A8=E8=A1=8C=E4=B8=BA=E6=BF=80=E6=B4=BB=20=E2=80=94=20?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=E6=A0=BC=E5=BC=8F=20+=20=E8=B7=A8=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E8=BF=9E=E7=BB=AD=E6=80=A7=20+=20=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 5.1+5.4: ButlerRouter/experience 注入格式升级为 XML fencing - butler_router: [路由上下文] → ... - experience: [过往经验] → ... - 统一 system-note 提示,引导 LLM 自然运用上下文 Task 5.2: 跨会话连续性 — pre_conversation_hook 注入活跃痛点 + 相关经验 - 从 VikingStorage 检索相关记忆(相似度>=0.3) - 从 pain_aggregator 获取 High severity 痛点(top 3) Task 5.3: 触发信号持久化 — post_conversation_hook 将触发信号存入 VikingStorage - store_trigger_experience(): 模板提取,零 LLM 成本 - 为未来 LLM 深度反思积累数据基础 --- .../src/middleware/butler_router.rs | 15 +- .../src-tauri/src/intelligence/experience.rs | 17 ++- desktop/src-tauri/src/intelligence_hooks.rs | 135 +++++++++++++++++- 3 files changed, 152 insertions(+), 15 deletions(-) diff --git a/crates/zclaw-runtime/src/middleware/butler_router.rs b/crates/zclaw-runtime/src/middleware/butler_router.rs index 53ef8cf..6e8fb82 100644 --- a/crates/zclaw-runtime/src/middleware/butler_router.rs +++ b/crates/zclaw-runtime/src/middleware/butler_router.rs @@ -201,12 +201,15 @@ impl ButlerRouterMiddleware { } /// Domain context to inject into system prompt based on routing hint. + /// + /// Uses structured `` XML fencing (Hermes-inspired) for + /// reliable prompt cache preservation across turns. fn build_context_injection(hint: &RoutingHint) -> String { // Semantic skill routing if hint.category == "semantic_skill" { if let Some(ref skill_id) = hint.skill_id { return format!( - "\n\n[语义路由] 匹配技能: {} (置信度: {:.0}%)\n系统检测到用户的意图与已注册技能高度相关,请在回答中充分利用该技能的能力。", + "\n\n\n匹配技能: {} (置信度: {:.0}%)\n系统检测到用户的意图与已注册技能高度相关,请在回答中充分利用该技能的能力。\n", skill_id, hint.confidence * 100.0 ); @@ -230,11 +233,11 @@ impl ButlerRouterMiddleware { } let skill_info = hint.skill_id.as_ref().map_or(String::new(), |id| { - format!("\n关联技能: {}", id) + format!("\n{}", id) }); format!( - "\n\n[路由上下文] (置信度: {:.0}%)\n{}{}", + "\n\n\n{}{}以上是管家系统对您当前意图的分析。在对话中自然运用这些信息,主动提供有帮助的建议。\n", hint.confidence * 100.0, domain_context, skill_info @@ -357,7 +360,7 @@ mod tests { domain_prompt: None, }; let injection = ButlerRouterMiddleware::build_context_injection(&hint); - assert!(injection.contains("路由上下文")); + assert!(injection.contains("butler-context")); assert!(injection.contains("医院")); assert!(injection.contains("80%")); } @@ -435,7 +438,7 @@ mod tests { let decision = mw.before_completion(&mut ctx).await.unwrap(); assert!(matches!(decision, MiddlewareDecision::Continue)); - assert!(ctx.system_prompt.contains("路由上下文")); + assert!(ctx.system_prompt.contains("butler-context")); assert!(ctx.system_prompt.contains("医院")); } @@ -464,7 +467,7 @@ mod tests { let decision = mw.before_completion(&mut ctx).await.unwrap(); assert!(matches!(decision, MiddlewareDecision::Continue)); - assert!(ctx.system_prompt.contains("路由上下文")); + assert!(ctx.system_prompt.contains("butler-context")); assert!(ctx.system_prompt.contains("电商运营管家")); } diff --git a/desktop/src-tauri/src/intelligence/experience.rs b/desktop/src-tauri/src/intelligence/experience.rs index aa0c20f..b0b3db7 100644 --- a/desktop/src-tauri/src/intelligence/experience.rs +++ b/desktop/src-tauri/src/intelligence/experience.rs @@ -204,6 +204,7 @@ impl ExperienceExtractor { /// Format experiences for system prompt injection. /// Returns a concise block capped at ~200 Chinese characters. + /// Uses `` XML fencing for structured injection. /// Includes industry context when available. pub fn format_for_injection( experiences: &[zclaw_growth::experience_store::Experience], @@ -224,14 +225,14 @@ impl ExperienceExtractor { .map(|s| truncate(s, 40)) .unwrap_or_default(); let industry_tag = exp.industry_context.as_ref() - .map(|i| format!(", 行业:{}", i)) + .map(|i| format!(" 行业:{}", i)) .unwrap_or_default(); let line = format!( - "[过往经验{}] 类似「{}」做过:{},结果是{}", - industry_tag, + "- 类似「{}」做过:{},结果是{} ({})", truncate(&exp.pain_pattern, 30), step_summary, - exp.outcome + exp.outcome, + industry_tag.trim_start() ); total_chars += line.chars().count(); parts.push(line); @@ -241,7 +242,10 @@ impl ExperienceExtractor { return String::new(); } - format!("\n\n--- 过往经验参考 ---\n{}", parts.join("\n")) + format!( + "\n\n\n\n{}\n\n", + parts.join("\n") + ) } } @@ -345,7 +349,8 @@ mod tests { "成功解决", ); let formatted = ExperienceExtractor::format_for_injection(&[exp]); - assert!(formatted.contains("过往经验")); + assert!(formatted.contains("butler-context")); + assert!(formatted.contains("experience")); assert!(formatted.contains("出口包装问题")); } diff --git a/desktop/src-tauri/src/intelligence_hooks.rs b/desktop/src-tauri/src/intelligence_hooks.rs index a4be413..7ac4440 100644 --- a/desktop/src-tauri/src/intelligence_hooks.rs +++ b/desktop/src-tauri/src/intelligence_hooks.rs @@ -16,7 +16,8 @@ use zclaw_runtime::driver::LlmDriver; /// Run pre-conversation intelligence hooks /// -/// Builds identity-enhanced system prompt (SOUL.md + instructions). +/// Builds identity-enhanced system prompt (SOUL.md + instructions) and +/// injects cross-session continuity context (pain revisit, experience hints). /// /// NOTE: Memory context injection is NOT done here — it is handled by /// `MemoryMiddleware.before_completion()` in the Kernel's middleware chain. @@ -40,7 +41,15 @@ pub async fn pre_conversation_hook( } }; - Ok(enhanced_prompt) + // Cross-session continuity: check for unresolved pain points and recent experiences + let continuity_context = build_continuity_context(agent_id, _user_message).await; + + let mut result = enhanced_prompt; + if !continuity_context.is_empty() { + result.push_str(&continuity_context); + } + + Ok(result) } /// Run post-conversation intelligence hooks @@ -129,7 +138,15 @@ pub async fn post_conversation_hook( "[intelligence_hooks] Learning triggers activated: {:?}", signal_names ); - // Future: Pass signals to LLM experience extraction (Phase 5) + // Store lightweight experiences from trigger signals (template-based, no LLM cost) + for signal in &signals { + if let Err(e) = store_trigger_experience(agent_id, signal, _user_message).await { + warn!( + "[intelligence_hooks] Failed to store trigger experience: {}", + e + ); + } + } } } @@ -305,3 +322,115 @@ async fn query_memories_for_reflection( Ok(memories) } + +/// Build cross-session continuity context for the current conversation. +/// +/// Injects relevant context from previous sessions: +/// - Active pain points (severity >= High, recent) +/// - Relevant past experiences matching the user's input +/// +/// Uses `` XML fencing for structured injection. +async fn build_continuity_context(agent_id: &str, user_message: &str) -> String { + let mut parts = Vec::new(); + + // 1. Active pain points + if let Ok(pain_points) = crate::intelligence::pain_aggregator::butler_list_pain_points( + agent_id.to_string(), + ).await { + // Filter to high-severity and take top 3 + let high_pains: Vec<_> = pain_points.iter() + .filter(|p| matches!(p.severity, crate::intelligence::pain_aggregator::PainSeverity::High)) + .take(3) + .collect(); + if !high_pains.is_empty() { + let pain_lines: Vec = high_pains.iter() + .filter_map(|p| { + let summary = &p.summary; + let count = p.occurrence_count; + let conf = (p.confidence * 100.0) as u8; + Some(format!( + "- {} (出现{}次, 置信度 {}%)", + summary, count, conf + )) + }) + .collect(); + if !pain_lines.is_empty() { + parts.push(format!("\n{}\n", pain_lines.join("\n"))); + } + } + } + + // 2. Relevant experiences (if user message is non-trivial) + if user_message.chars().count() >= 4 { + if let Ok(storage) = crate::viking_commands::get_storage().await { + let options = zclaw_growth::FindOptions { + scope: Some(format!("agent://{}", agent_id)), + limit: Some(3), + min_similarity: Some(0.3), + }; + if let Ok(entries) = zclaw_growth::VikingStorage::find( + storage.as_ref(), + user_message, + options, + ).await { + if !entries.is_empty() { + let exp_lines: Vec = entries.iter() + .map(|e| { + let overview = e.overview.as_deref().unwrap_or(&e.content); + let truncated: String = overview.chars().take(60).collect(); + let score_pct = (e.access_count as f64).min(10.0) / 10.0 * 100.0; + format!("- {} (相关度: {:.0}%)", truncated, score_pct) + }) + .collect(); + parts.push(format!("\n{}\n", exp_lines.join("\n"))); + } + } + } + } + + if parts.is_empty() { + return String::new(); + } + + format!( + "\n\n\n{}\n以上是管家系统从过往对话中提取的信息。在对话中自然运用这些信息,主动提供有帮助的建议。不要逐条复述以上内容。\n", + parts.join("\n") + ) +} + +/// Store a lightweight experience entry from a trigger signal. +/// +/// Uses VikingStorage directly — template-based, no LLM cost. +/// Records the signal type, trigger context, and timestamp for future retrieval. +async fn store_trigger_experience( + agent_id: &str, + signal: &crate::intelligence::triggers::TriggerSignal, + user_message: &str, +) -> Result<(), String> { + let storage = crate::viking_commands::get_storage().await?; + + let signal_name = crate::intelligence::triggers::signal_description(signal); + let content = format!( + "[触发信号: {}]\n用户消息: {}\n时间: {}", + signal_name, + user_message.chars().take(200).collect::(), + chrono::Utc::now().to_rfc3339(), + ); + + let entry = zclaw_growth::MemoryEntry::new( + agent_id, + zclaw_growth::MemoryType::Experience, + &format!("trigger/{:?}", signal), + content, + ); + + zclaw_growth::VikingStorage::store(storage.as_ref(), &entry) + .await + .map_err(|e| format!("Failed to store trigger experience: {}", e))?; + + debug!( + "[intelligence_hooks] Stored trigger experience: {} for agent {}", + signal_name, agent_id + ); + Ok(()) +}