发散式探讨确定方案A: 在现有建议生成流程中并行拉取4个智能上下文 (UserProfiler + 痛点 + 经验 + 技能路由),注入增强prompt。 新增1个只读Tauri命令(experience_find_relevant),消除2s人为延迟。
11 KiB
动态建议智能化设计
日期: 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):
// 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 冷读、
PainAggregator的RwLock竞争、以及ExperienceExtractor的 FTS5 查询性能。建议在实现后用performance.now()埋点实测。
7. 关键文件清单
新增
desktop/src/lib/suggestion-context.ts—fetchSuggestionContext()聚合函数 + 类型定义
修改
desktop/src-tauri/src/intelligence/experience.rs— 新增experience_find_relevantTauri 命令desktop/src-tauri/src/lib.rs— 注册新命令到 invoke_handlerdesktop/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. 验证方式
- Rust 编译:
cargo check --workspace --exclude zclaw-saas - Rust 测试:
cargo test -p zclaw-kernel -- experience - TypeScript 类型:
cd desktop && pnpm tsc --noEmit - 前端测试:
cd desktop && pnpm vitest run - 手动验证:
- 启动
pnpm start:dev - 进行 2-3 轮对话,观察建议内容是否个性化
- 检查开发者工具 console 无上下文拉取错误
- 对比改造前后建议相关性和出现速度
- 启动