From 9981a4674ee30ecb68c32bff36eb2af9b2a14ba5 Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 24 Mar 2026 15:39:18 +0800 Subject: [PATCH] fix(skills): inject skill list into system prompt for LLM awareness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Agent could not invoke appropriate skills when user asked about financial reports because LLM didn't know which skills were available. Root causes: 1. System prompt lacked available skill list 2. SkillManifest struct missing 'triggers' field 3. SKILL.md loader not parsing triggers list 4. "财报" keyword not matching "财务报告" trigger Changes: - Add triggers field to SkillManifest struct - Parse triggers list from SKILL.md frontmatter - Inject skill list into system prompt in kernel.rs - Add "财报", "财务数据", "盈利", "营收" triggers to finance-tracker - Add "财报分析" trigger to analytics-reporter - Document fix in troubleshooting.md Co-Authored-By: Claude Opus 4.6 --- crates/zclaw-kernel/src/kernel.rs | 59 +++++++-- crates/zclaw-skills/src/loader.rs | 31 +++++ crates/zclaw-skills/src/skill.rs | 3 + docs/knowledge-base/troubleshooting.md | 169 ++++++++++++++++++++++++- skills/analytics-reporter/SKILL.md | 2 + skills/finance-tracker/SKILL.md | 5 + 6 files changed, 256 insertions(+), 13 deletions(-) diff --git a/crates/zclaw-kernel/src/kernel.rs b/crates/zclaw-kernel/src/kernel.rs index 6fd4a92..608d26f 100644 --- a/crates/zclaw-kernel/src/kernel.rs +++ b/crates/zclaw-kernel/src/kernel.rs @@ -123,6 +123,47 @@ impl Kernel { tools } + /// Build a system prompt with skill information injected + fn build_system_prompt_with_skills(&self, base_prompt: Option<&String>) -> String { + // Get skill list synchronously (we're in sync context) + let skills = futures::executor::block_on(self.skills.list()); + + let mut prompt = base_prompt + .map(|p| p.clone()) + .unwrap_or_else(|| "You are a helpful AI assistant.".to_string()); + + // Inject skill information + if !skills.is_empty() { + prompt.push_str("\n\n## Available Skills\n\n"); + prompt.push_str("You have access to the following skills that can help with specific tasks. "); + prompt.push_str("Use the `execute_skill` tool with the skill_id to invoke them:\n\n"); + + for skill in skills { + prompt.push_str(&format!( + "- **{}**: {}", + skill.id.as_str(), + skill.description + )); + + // Add trigger words if available + if !skill.triggers.is_empty() { + prompt.push_str(&format!( + " (Triggers: {})", + skill.triggers.join(", ") + )); + } + prompt.push('\n'); + } + + prompt.push_str("\n### When to use skills:\n"); + prompt.push_str("- When the user's request matches a skill's trigger words\n"); + prompt.push_str("- When you need specialized expertise for a task\n"); + prompt.push_str("- When the task would benefit from a structured workflow\n"); + } + + prompt + } + /// Spawn a new agent pub async fn spawn_agent(&self, config: AgentConfig) -> Result { let id = config.id; @@ -197,12 +238,9 @@ impl Kernel { .with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens())) .with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature())); - // Add system prompt if configured - let loop_runner = if let Some(ref prompt) = agent_config.system_prompt { - loop_runner.with_system_prompt(prompt) - } else { - loop_runner - }; + // Build system prompt with skill information injected + let system_prompt = self.build_system_prompt_with_skills(agent_config.system_prompt.as_ref()); + let loop_runner = loop_runner.with_system_prompt(&system_prompt); // Run the loop let result = loop_runner.run(session_id, message).await?; @@ -243,12 +281,9 @@ impl Kernel { .with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens())) .with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature())); - // Add system prompt if configured - let loop_runner = if let Some(ref prompt) = agent_config.system_prompt { - loop_runner.with_system_prompt(prompt) - } else { - loop_runner - }; + // Build system prompt with skill information injected + let system_prompt = self.build_system_prompt_with_skills(agent_config.system_prompt.as_ref()); + let loop_runner = loop_runner.with_system_prompt(&system_prompt); // Run with streaming loop_runner.run_streaming(session_id, message).await diff --git a/crates/zclaw-skills/src/loader.rs b/crates/zclaw-skills/src/loader.rs index cecde8b..39b79e4 100644 --- a/crates/zclaw-skills/src/loader.rs +++ b/crates/zclaw-skills/src/loader.rs @@ -41,6 +41,8 @@ pub fn parse_skill_md(content: &str) -> Result { let mut mode = SkillMode::PromptOnly; let mut capabilities = Vec::new(); let mut tags = Vec::new(); + let mut triggers = Vec::new(); + let mut in_triggers_list = false; // Parse frontmatter if present if content.starts_with("---") { @@ -51,6 +53,15 @@ pub fn parse_skill_md(content: &str) -> Result { if line.is_empty() || line == "---" { continue; } + + // Handle triggers list items + if in_triggers_list && line.starts_with("- ") { + triggers.push(line[2..].trim().trim_matches('"').to_string()); + continue; + } else { + in_triggers_list = false; + } + if let Some((key, value)) = line.split_once(':') { let key = key.trim(); let value = value.trim().trim_matches('"'); @@ -69,6 +80,16 @@ pub fn parse_skill_md(content: &str) -> Result { .map(|s| s.trim().to_string()) .collect(); } + "triggers" => { + // Check if it's a list on next lines or inline + if value.is_empty() { + in_triggers_list = true; + } else { + triggers = value.split(',') + .map(|s| s.trim().trim_matches('"').to_string()) + .collect(); + } + } _ => {} } } @@ -137,6 +158,7 @@ pub fn parse_skill_md(content: &str) -> Result { input_schema: None, output_schema: None, tags, + triggers, enabled: true, }) } @@ -159,6 +181,7 @@ pub fn parse_skill_toml(content: &str) -> Result { let mut mode = "prompt_only".to_string(); let mut capabilities = Vec::new(); let mut tags = Vec::new(); + let mut triggers = Vec::new(); for line in content.lines() { let line = line.trim(); @@ -189,6 +212,13 @@ pub fn parse_skill_toml(content: &str) -> Result { .filter(|s| !s.is_empty()) .collect(); } + "triggers" => { + let value = value.trim_start_matches('[').trim_end_matches(']'); + triggers = value.split(',') + .map(|s| s.trim().trim_matches('"').to_string()) + .filter(|s| !s.is_empty()) + .collect(); + } _ => {} } } @@ -215,6 +245,7 @@ pub fn parse_skill_toml(content: &str) -> Result { input_schema: None, output_schema: None, tags, + triggers, enabled: true, }) } diff --git a/crates/zclaw-skills/src/skill.rs b/crates/zclaw-skills/src/skill.rs index 4884864..f56aa8a 100644 --- a/crates/zclaw-skills/src/skill.rs +++ b/crates/zclaw-skills/src/skill.rs @@ -32,6 +32,9 @@ pub struct SkillManifest { /// Tags for categorization #[serde(default)] pub tags: Vec, + /// Trigger words for skill activation + #[serde(default)] + pub triggers: Vec, /// Whether the skill is enabled #[serde(default = "default_enabled")] pub enabled: bool, diff --git a/docs/knowledge-base/troubleshooting.md b/docs/knowledge-base/troubleshooting.md index 4c17b6e..a46e9a4 100644 --- a/docs/knowledge-base/troubleshooting.md +++ b/docs/knowledge-base/troubleshooting.md @@ -1000,7 +1000,172 @@ ZCLAW 的设计是让用户在"模型与 API"页面设置全局模型,而不 --- -## 10. 相关文档 +## 9.4 自我进化系统启动错误 + +### 问题:DateTime 类型不匹配导致编译失败 + +**症状**: +``` +error[E0277]: cannot subtract `chrono::DateTime` from `chrono::DateTime` + --> desktop\src-tauri\src\intelligence\heartbeat.rs:542:27 + | +542 | let idle_hours = (now - last_time).num_hours(); + | ^ no implementation for `chrono::DateTime - chrono::DateTime` +``` + +**根本原因**: `chrono::DateTime::parse_from_rfc3339()` 返回 `DateTime`,但 `chrono::Utc::now()` 返回 `DateTime`,两种类型不能直接相减。 + +**解决方案**: +将 `DateTime` 转换为 `DateTime` 后再计算: + +```rust +// 错误写法 +let last_time = chrono::DateTime::parse_from_rfc3339(&last_interaction).ok()?; +let now = chrono::Utc::now(); +let idle_hours = (now - last_time).num_hours(); // 编译错误! + +// 正确写法 +let last_time = chrono::DateTime::parse_from_rfc3339(&last_interaction) + .ok()? + .with_timezone(&chrono::Utc); // 转换为 UTC +let now = chrono::Utc::now(); +let idle_hours = (now - last_time).num_hours(); // OK +``` + +**相关文件**: +- `desktop/src-tauri/src/intelligence/heartbeat.rs` + +### 问题:未使用的导入警告 + +**症状**: +``` +warning: unused import: `Manager` +warning: unused import: `futures::StreamExt` +``` + +**解决方案**: +1. 手动移除未使用的导入 +2. 或使用 `cargo fix --lib -p --allow-dirty` 自动修复 + +**自动修复命令**: +```bash +cargo fix --lib -p desktop --allow-dirty +cargo fix --lib -p zclaw-hands --allow-dirty +cargo fix --lib -p zclaw-runtime --allow-dirty +cargo fix --lib -p zclaw-kernel --allow-dirty +cargo fix --lib -p zclaw-protocols --allow-dirty +``` + +**注意**: `dead_code` 警告(未使用的字段、方法)不影响编译,可以保留供将来使用。 + +--- + +## 10. 技能系统问题 + +### 10.1 Agent 无法调用合适的技能 + +**症状**: 用户发送消息(如"查询某公司财报"),Agent 没有调用相关技能,只是直接回复文本 + +**根本原因**: + +1. **系统提示词缺少技能列表**: LLM 不知道有哪些技能可用 +2. **SkillManifest 缺少 triggers 字段**: 触发词无法传递给 LLM +3. **技能触发词覆盖不足**: "财报" 无法匹配 "财务报告" + +**问题分析**: + +Agent 调用技能的完整链路: +``` +用户消息 → LLM → 选择 execute_skill 工具 → 传入 skill_id → 执行技能 +``` + +如果 LLM 不知道有哪些 skill_id 可用,就无法主动调用。 + +**修复方案**: + +1. **在系统提示词中注入技能列表** (`kernel.rs`): + +```rust +/// Build a system prompt with skill information injected +fn build_system_prompt_with_skills(&self, base_prompt: Option<&String>) -> String { + let skills = futures::executor::block_on(self.skills.list()); + + let mut prompt = base_prompt + .map(|p| p.clone()) + .unwrap_or_else(|| "You are a helpful AI assistant.".to_string()); + + if !skills.is_empty() { + prompt.push_str("\n\n## Available Skills\n\n"); + prompt.push_str("Use the `execute_skill` tool with the skill_id to invoke them:\n\n"); + + for skill in skills { + prompt.push_str(&format!( + "- **{}**: {}", + skill.id.as_str(), + skill.description + )); + + if !skill.triggers.is_empty() { + prompt.push_str(&format!( + " (Triggers: {})", + skill.triggers.join(", ") + )); + } + prompt.push('\n'); + } + } + + prompt +} +``` + +2. **添加 triggers 字段到 SkillManifest** (`skill.rs`): + +```rust +pub struct SkillManifest { + // ... existing fields + /// Trigger words for skill activation + #[serde(default)] + pub triggers: Vec, +} +``` + +3. **解析 SKILL.md 中的 triggers** (`loader.rs`): + +```rust +// Parse triggers list in frontmatter +if in_triggers_list && line.starts_with("- ") { + triggers.push(line[2..].trim().trim_matches('"').to_string()); + continue; +} +``` + +4. **添加常见触发词** (`skills/finance-tracker/SKILL.md`): + +```yaml +triggers: + - "财务分析" + - "财报" # 新增 + - "财务数据" # 新增 + - "盈利" + - "营收" + - "利润" +``` + +**影响范围**: +- `crates/zclaw-kernel/src/kernel.rs` - 系统提示词构建 +- `crates/zclaw-skills/src/skill.rs` - SkillManifest 结构 +- `crates/zclaw-skills/src/loader.rs` - SKILL.md 解析 +- `skills/*/SKILL.md` - 技能定义文件 + +**验证修复**: +1. 重启应用 +2. 发送"查询腾讯财报" +3. Agent 应该调用 `execute_skill` 工具,传入 `skill_id: "finance-tracker"` + +--- + +## 11. 相关文档 - [OpenFang 配置指南](./openfang-configuration.md) - 配置文件位置、格式和最佳实践 - [Agent 和 LLM 提供商配置](./agent-provider-config.md) - Agent 管理和 Provider 配置 @@ -1012,6 +1177,8 @@ ZCLAW 的设计是让用户在"模型与 API"页面设置全局模型,而不 | 日期 | 变更 | |------|------| +| 2026-03-24 | 添加 10.1 节:Agent 无法调用合适的技能 - 系统提示词注入技能列表 + triggers 字段 | +| 2026-03-24 | 添加 9.4 节:自我进化系统启动错误 - DateTime 类型不匹配和未使用导入警告 | | 2026-03-23 | 添加 9.3 节:更换模型配置后仍使用旧模型 - Agent 配置优先于 Kernel 配置导致的问题 | | 2026-03-22 | 添加内核 LLM 响应问题:loop_runner.rs 硬编码模型和响应导致 Coding Plan API 不工作 | | 2026-03-20 | 添加端口配置问题:runtime-manifest.json 声明 4200 但实际运行 50051 | diff --git a/skills/analytics-reporter/SKILL.md b/skills/analytics-reporter/SKILL.md index 9b1d371..ab27e52 100644 --- a/skills/analytics-reporter/SKILL.md +++ b/skills/analytics-reporter/SKILL.md @@ -9,6 +9,8 @@ triggers: - "业务洞察" - "KPI追踪" - "预测分析" + - "财报分析" + - "数据报表" tools: - bash - read diff --git a/skills/finance-tracker/SKILL.md b/skills/finance-tracker/SKILL.md index baf33a4..4330333 100644 --- a/skills/finance-tracker/SKILL.md +++ b/skills/finance-tracker/SKILL.md @@ -6,9 +6,14 @@ triggers: - "预算管理" - "现金流" - "财务报告" + - "财报" - "投资分析" - "成本优化" - "财务规划" + - "财务数据" + - "盈利" + - "营收" + - "利润" tools: - bash - read