# 动态建议智能化设计 > 日期: 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` — 含 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` — `{ pain_pattern: String, solution_summary: String, reuse_count: u32 }` - **前端处理**: 取前 2 条,格式化为 `上次解决"{pain}"的方法:{solution}(已复用{n}次)` - **超时**: 500ms,超时后跳过 #### 源 4: 技能/流水线匹配 - **命令**: `route_intent({ userInput })` (已有 @connected,Tauri 自动注入 `PipelineState` + `KernelState`) - **返回**: `RouteResultResponse::NoMatch { suggestions: Vec }` - **前端处理**: 取 confidence 最高的 1 条,格式化为 `你可能需要:{display_name} — {description}` - **降级**: 无匹配时跳过 ### 3.3 新增 Tauri 命令 只需 1 个新的只读命令。遵循 `butler_list_pain_points` 的无状态单例模式(不使用 `tauri::State`): ```rust // desktop/src-tauri/src/intelligence/experience.rs static EXPERIENCE_EXTRACTOR: OnceLock> = OnceLock::new(); fn get_extractor() -> Option> { 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, 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` 结构(定义在同一文件): ```rust #[derive(Serialize, Deserialize)] pub struct ExperienceBrief { pub pain_pattern: String, pub solution_summary: String, pub reuse_count: u32, } ``` **关键设计决策**: - 使用 `OnceLock>` 单例,与 `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 冷读、`PainAggregator` 的 `RwLock` 竞争、以及 `ExperienceExtractor` 的 FTS5 查询性能。建议在实现后用 `performance.now()` 埋点实测。 ## 7. 关键文件清单 ### 新增 - `desktop/src/lib/suggestion-context.ts` — `fetchSuggestionContext()` 聚合函数 + 类型定义 ### 修改 - `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.rs` — `butler_list_pain_points` 命令 - `desktop/src-tauri/src/intelligence/identity.rs` — `identity_get_file` 命令 - `desktop/src-tauri/src/pipeline_commands/intent_router.rs` — `route_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 无上下文拉取错误 - 对比改造前后建议相关性和出现速度