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
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:
@@ -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 })` (已有 @connected,Tauri 自动注入 `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 无上下文拉取错误
|
||||
- 对比改造前后建议相关性和出现速度
|
||||
Reference in New Issue
Block a user