docs(spec): 动态建议智能化设计 — 接通智能层的 Prompt 增强方案
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

发散式探讨确定方案A: 在现有建议生成流程中并行拉取4个智能上下文
(UserProfiler + 痛点 + 经验 + 技能路由),注入增强prompt。
新增1个只读Tauri命令(experience_find_relevant),消除2s人为延迟。
This commit is contained in:
iven
2026-04-23 17:16:25 +08:00
parent aa84172ca4
commit 00ebf18f23

View File

@@ -0,0 +1,255 @@
# 动态建议智能化设计
> 日期: 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`
```rust
// 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` 结构(定义在同一文件):
```rust
#[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 冷读、`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 无上下文拉取错误
- 对比改造前后建议相关性和出现速度