Files
zclaw_openfang/docs/superpowers/specs/2026-04-23-dynamic-suggestion-intelligence-design.md
iven 00ebf18f23
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
docs(spec): 动态建议智能化设计 — 接通智能层的 Prompt 增强方案
发散式探讨确定方案A: 在现有建议生成流程中并行拉取4个智能上下文
(UserProfiler + 痛点 + 经验 + 技能路由),注入增强prompt。
新增1个只读Tauri命令(experience_find_relevant),消除2s人为延迟。
2026-04-23 17:16:25 +08:00

11 KiB
Raw Blame History

动态建议智能化设计

日期: 2026-04-23 | 状态: Draft | 方案: Prompt 增强法

1. 问题与目标

现状

ZCLAW 的 SuggestionChips 系统能工作,但建议内容是"有引擎没燃料"的状态:

  • 建议由 LLM 基于最近 6 条对话文本生成,纯通用续问
  • Hermes 管线ExperienceStore、PainAggregator、UserProfiler已实现但未接入
  • ButlerRouter 的行业检测 + SemanticSkillRouter 的技能匹配未用于建议
  • SaaS 模式有 2s 人为延迟(setTimeout(2000) 避免与记忆提取并发)
  • 冷启动的行业检测与动态建议完全断开

目标

接通 UserProfiler + 痛点/经验 + 行业/技能路由,让建议从"通用续问"变"个性化混合建议"2 条续问 + 1 条管家关怀),不改 UI 形态。

约束

  • 稳定化功能冻结:不新增 SaaS 端点、不新增 SKILL.md、不新增 admin 页面
  • 允许小幅扩展:可新增 1-2 个只读 Tauri 命令
  • 复用 @reserved 命令5 个 Butler 命令已注册未接通,优先复用

2. 方案选择

评估了 3 种方案:

方案 描述 改动量 风险
A. Prompt 增强(选定) 拉取智能上下文注入建议 prompt
B. 双轨建议引擎 LLM 续问 + 规则引擎管家关怀分离
C. 中间件注入 在中间件链中生成建议上下文

选择 A 的理由:改动最小、增量安全(上下文是可选增强)、复用现有 @reserved 命令、可并行化消除人为延迟。

3. 架构设计

3.1 改造后流程

[Stream 完成]
  ↓
createCompleteHandler()
  ↓ Promise.all (并行)
  ├── extractFromConversation()     ← 记忆提取(已有)
  ├── reflection.record()           ← 反思记录(已有)
  └── fetchSuggestionContext()      ← 🆕 智能上下文拉取
       ├── 检查 __TAURI_INTERNALS__ 是否存在SaaS 模式下不存在则跳过全部)
       ├── identity_get_file("userprofile")
       ├── butler_list_pain_points()
       ├── experience_find_relevant()    ← 🆕 新 Tauri 命令
       └── route_intent()               ← 技能/流水线匹配
  ↓
generateLLMSuggestions(对话文本 + 智能上下文)  ← 增强 prompt
  ↓
SuggestionChips 渲染UI 不变)

SaaS 模式处理: fetchSuggestionContext() 首先检查 window.__TAURI_INTERNALS__ 是否存在。SaaS 模式下浏览器环境无 Tauri 运行时,此检查失败后直接返回空上下文——建议生成回退到纯对话续问,与改造前行为一致。无需新增 SaaS API 端点。

3.2 上下文源详细设计

源 1: 用户画像

  • 命令: identity_get_file(agent_id, "userprofile") (已有 @connected
  • 注意: 参数用 "userprofile"identity.rs 641 行的规范键名,"user_profile" 也兼容)
  • 返回: String — 用户画像文本(行业、角色、专长、沟通风格)
  • 前端处理: 截取前 200 字符,格式化为 用户是{行业}{角色}{偏好}。最近关注{话题}。
  • 降级: 为空时跳过该段落

源 2: 痛点列表

  • 命令: butler_list_pain_points(agent_id) (已在 invoke_handler 注册,@reserved 仅表示无前端 UI前端可直接 invoke('butler_list_pain_points', { agentId }) 调用)
  • 返回: Vec<PainPoint> — 含 summary, category, confidence, status, occurrence_count
  • 前端处理:
    • 过滤: confidence >= 0.5 && status ∉ {Solved, Dismissed}
    • 排序: 按 confidence 降序
    • 取前 3 条,格式化为 1. [{category}] {summary}(出现{n}次)
  • 降级: 为空时跳过管家关怀指令,全部 3 条生成对话续问

源 3: 相关经验

  • 命令: experience_find_relevant(agent_id, query) 新增 1 个只读命令
  • Rust 实现: 封装 ExperienceExtractor::find_relevant_experiences()
  • 返回: Vec<ExperienceBrief>{ pain_pattern: String, solution_summary: String, reuse_count: u32 }
  • 前端处理: 取前 2 条,格式化为 上次解决"{pain}"的方法:{solution}(已复用{n}次)
  • 超时: 500ms超时后跳过

源 4: 技能/流水线匹配

  • 命令: route_intent({ userInput }) (已有 @connectedTauri 自动注入 PipelineState + KernelState
  • 返回: RouteResultResponse::NoMatch { suggestions: Vec<PipelineCandidateInfo> }
  • 前端处理: 取 confidence 最高的 1 条,格式化为 你可能需要:{display_name} — {description}
  • 降级: 无匹配时跳过

3.3 新增 Tauri 命令

只需 1 个新的只读命令。遵循 butler_list_pain_points 的无状态单例模式(不使用 tauri::State

// desktop/src-tauri/src/intelligence/experience.rs

static EXPERIENCE_EXTRACTOR: OnceLock<Arc<ExperienceExtractor>> = OnceLock::new();

fn get_extractor() -> Option<Arc<ExperienceExtractor>> {
    EXPERIENCE_EXTRACTOR.get().cloned()
}

/// Initialize the global ExperienceExtractor with a VikingAdapter-backed store.
/// Called once during app startup (alongside init_pain_storage).
pub async fn init_experience_extractor(pool: sqlx::SqlitePool) -> Result<()> {
    let sqlite_storage = crate::viking_commands::get_storage().await
        .map_err(|e| anyhow::anyhow!("viking storage: {}", e))?;
    let viking = Arc::new(zclaw_growth::VikingAdapter::from_sqlite_storage(sqlite_storage));
    let store = Arc::new(ExperienceStore::new(viking));
    let extractor = Arc::new(ExperienceExtractor::new(store));
    EXPERIENCE_EXTRACTOR.set(extractor)
        .map_err(|_| anyhow::anyhow!("ExperienceExtractor already initialized"))?;
    Ok(())
}

#[tauri::command]
pub async fn experience_find_relevant(
    agent_id: String,
    query: String,
) -> Result<Vec<ExperienceBrief>, String> {
    let extractor = get_extractor()
        .ok_or("ExperienceExtractor not initialized".to_string())?;
    let experiences = extractor.find_relevant_experiences(&agent_id, &query).await;
    // Map full Experience → brief (in command, not in extractor)
    Ok(experiences.into_iter().take(3).map(|e| ExperienceBrief {
        pain_pattern: e.pain_pattern,
        solution_summary: e.solution_steps.join("")
            .chars().take(100).collect(),
        reuse_count: e.reuse_count,
    }).collect())
}

ExperienceBrief 结构(定义在同一文件):

#[derive(Serialize, Deserialize)]
pub struct ExperienceBrief {
    pub pain_pattern: String,
    pub solution_summary: String,
    pub reuse_count: u32,
}

关键设计决策

  • 使用 OnceLock<Arc<ExperienceExtractor>> 单例,与 PAIN_AGGREGATOR 模式一致
  • 通过 viking_commands::get_storage()VikingAdapter::from_sqlite_storage()ExperienceStore 获取持久化后端
  • Experience → ExperienceBrief 映射在命令内完成,ExperienceExtractor 保持原样不变
  • 启动时在 init_pain_storage() 旁调用 init_experience_extractor()

4. 增强 Prompt 模板

4.1 双层 Prompt 结构

System prompt静态OTA 可缓存):保持 HARDCODED_PROMPTS.suggestions 作为基础 system prompt只修改生成规则部分

根据对话上下文和用户画像,生成恰好 3 个个性化建议。

## 生成规则
1. 2 条对话续问(深入当前话题,帮助用户继续探索)
2. 1 条管家关怀(基于用户消息中提供的痛点、经验或技能信息)
   - 如果有未解决痛点 → 回访建议
   - 如果有相关经验 → 引导复用
   - 如果有匹配技能 → 推荐使用
   - 无特殊信号时 → 也生成对话续问
3. 每个不超过 30 个中文字符
4. 返回 JSON 数组 ["建议1", "建议2", "建议3"]
5. 使用与用户相同的语言
6. 不要重复已经讨论过的内容

User message动态每次请求拼装:由 fetchSuggestionContext() 生成的上下文段落拼入 user message与对话历史一起发送

以下是用户的背景信息,请在生成建议时参考:

{user_profile_section}
{pain_points_section}
{experiences_section}
{skill_match_section}

最近对话:
{conversation_text}

OTA 兼容System prompt 仍走 SaaS OTA 缓存(getSystemPrompt('suggestions')),动态上下文只在 user message 中注入,不影响缓存机制。

4.2 全部为空时的回退

当所有上下文段落均为空时user message 不注入背景信息,直接使用对话文本——行为与改造前完全一致。

5. 降级策略

故障场景 降级行为 用户感知
用户画像为空 跳过该段落
痛点列表为空 跳过管家关怀指令 无——3 条都是对话续问
经验查询超时500ms 跳过该段落
技能无匹配 跳过该段落
所有上下文全部失败 使用原始 prompt纯对话续问 无——与改造前完全一致
LLM 建议生成失败 触发现有关键词 fallback 无变化

核心原则: 上下文是可选增强,任何失败都静默降级,不破坏现有体验。

错误日志: 所有降级通过 createLogger('StreamStore')warn 级别记录,与现有记忆提取失败的处理方式一致。不在用户界面显示错误。

6. 延迟优化

对比项 改造前 改造后
上下文拉取 Promise.all 并行 ~100-300ms
人为延迟 setTimeout(2000) 消除
LLM 调用时机 +2000ms 后 +max(记忆, 上下文) 后
建议出现时间 ~2s + LLM ~0.3s + LLM
净提升 ~1.7s 更快(估算值,需实测验证)

注意: 表中"~100-300ms"为估算值。实际延迟取决于 SQLite 冷读、PainAggregatorRwLock 竞争、以及 ExperienceExtractor 的 FTS5 查询性能。建议在实现后用 performance.now() 埋点实测。

7. 关键文件清单

新增

  • desktop/src/lib/suggestion-context.tsfetchSuggestionContext() 聚合函数 + 类型定义

修改

  • desktop/src-tauri/src/intelligence/experience.rs — 新增 experience_find_relevant Tauri 命令
  • desktop/src-tauri/src/lib.rs — 注册新命令到 invoke_handler
  • desktop/src/store/chat/streamStore.ts — 改造 createCompleteHandler()generateLLMSuggestions()
  • desktop/src/lib/llm-service.ts — 更新 suggestion prompt 模板

复用(已有,不修改)

  • desktop/src-tauri/src/intelligence/pain_aggregator.rsbutler_list_pain_points 命令
  • desktop/src-tauri/src/intelligence/identity.rsidentity_get_file 命令
  • desktop/src-tauri/src/pipeline_commands/intent_router.rsroute_intent 命令

8. 验证方式

  1. Rust 编译: cargo check --workspace --exclude zclaw-saas
  2. Rust 测试: cargo test -p zclaw-kernel -- experience
  3. TypeScript 类型: cd desktop && pnpm tsc --noEmit
  4. 前端测试: cd desktop && pnpm vitest run
  5. 手动验证:
    • 启动 pnpm start:dev
    • 进行 2-3 轮对话,观察建议内容是否个性化
    • 检查开发者工具 console 无上下文拉取错误
    • 对比改造前后建议相关性和出现速度