fix(skills): inject skill list into system prompt for LLM awareness
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 <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,47 @@ impl Kernel {
|
|||||||
tools
|
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
|
/// Spawn a new agent
|
||||||
pub async fn spawn_agent(&self, config: AgentConfig) -> Result<AgentId> {
|
pub async fn spawn_agent(&self, config: AgentConfig) -> Result<AgentId> {
|
||||||
let id = config.id;
|
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_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens()))
|
||||||
.with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature()));
|
.with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature()));
|
||||||
|
|
||||||
// Add system prompt if configured
|
// Build system prompt with skill information injected
|
||||||
let loop_runner = if let Some(ref prompt) = agent_config.system_prompt {
|
let system_prompt = self.build_system_prompt_with_skills(agent_config.system_prompt.as_ref());
|
||||||
loop_runner.with_system_prompt(prompt)
|
let loop_runner = loop_runner.with_system_prompt(&system_prompt);
|
||||||
} else {
|
|
||||||
loop_runner
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run the loop
|
// Run the loop
|
||||||
let result = loop_runner.run(session_id, message).await?;
|
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_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens()))
|
||||||
.with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature()));
|
.with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature()));
|
||||||
|
|
||||||
// Add system prompt if configured
|
// Build system prompt with skill information injected
|
||||||
let loop_runner = if let Some(ref prompt) = agent_config.system_prompt {
|
let system_prompt = self.build_system_prompt_with_skills(agent_config.system_prompt.as_ref());
|
||||||
loop_runner.with_system_prompt(prompt)
|
let loop_runner = loop_runner.with_system_prompt(&system_prompt);
|
||||||
} else {
|
|
||||||
loop_runner
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run with streaming
|
// Run with streaming
|
||||||
loop_runner.run_streaming(session_id, message).await
|
loop_runner.run_streaming(session_id, message).await
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ pub fn parse_skill_md(content: &str) -> Result<SkillManifest> {
|
|||||||
let mut mode = SkillMode::PromptOnly;
|
let mut mode = SkillMode::PromptOnly;
|
||||||
let mut capabilities = Vec::new();
|
let mut capabilities = Vec::new();
|
||||||
let mut tags = Vec::new();
|
let mut tags = Vec::new();
|
||||||
|
let mut triggers = Vec::new();
|
||||||
|
let mut in_triggers_list = false;
|
||||||
|
|
||||||
// Parse frontmatter if present
|
// Parse frontmatter if present
|
||||||
if content.starts_with("---") {
|
if content.starts_with("---") {
|
||||||
@@ -51,6 +53,15 @@ pub fn parse_skill_md(content: &str) -> Result<SkillManifest> {
|
|||||||
if line.is_empty() || line == "---" {
|
if line.is_empty() || line == "---" {
|
||||||
continue;
|
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(':') {
|
if let Some((key, value)) = line.split_once(':') {
|
||||||
let key = key.trim();
|
let key = key.trim();
|
||||||
let value = value.trim().trim_matches('"');
|
let value = value.trim().trim_matches('"');
|
||||||
@@ -69,6 +80,16 @@ pub fn parse_skill_md(content: &str) -> Result<SkillManifest> {
|
|||||||
.map(|s| s.trim().to_string())
|
.map(|s| s.trim().to_string())
|
||||||
.collect();
|
.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<SkillManifest> {
|
|||||||
input_schema: None,
|
input_schema: None,
|
||||||
output_schema: None,
|
output_schema: None,
|
||||||
tags,
|
tags,
|
||||||
|
triggers,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -159,6 +181,7 @@ pub fn parse_skill_toml(content: &str) -> Result<SkillManifest> {
|
|||||||
let mut mode = "prompt_only".to_string();
|
let mut mode = "prompt_only".to_string();
|
||||||
let mut capabilities = Vec::new();
|
let mut capabilities = Vec::new();
|
||||||
let mut tags = Vec::new();
|
let mut tags = Vec::new();
|
||||||
|
let mut triggers = Vec::new();
|
||||||
|
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
@@ -189,6 +212,13 @@ pub fn parse_skill_toml(content: &str) -> Result<SkillManifest> {
|
|||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.collect();
|
.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<SkillManifest> {
|
|||||||
input_schema: None,
|
input_schema: None,
|
||||||
output_schema: None,
|
output_schema: None,
|
||||||
tags,
|
tags,
|
||||||
|
triggers,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ pub struct SkillManifest {
|
|||||||
/// Tags for categorization
|
/// Tags for categorization
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
|
/// Trigger words for skill activation
|
||||||
|
#[serde(default)]
|
||||||
|
pub triggers: Vec<String>,
|
||||||
/// Whether the skill is enabled
|
/// Whether the skill is enabled
|
||||||
#[serde(default = "default_enabled")]
|
#[serde(default = "default_enabled")]
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
|||||||
@@ -1000,7 +1000,172 @@ ZCLAW 的设计是让用户在"模型与 API"页面设置全局模型,而不
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. 相关文档
|
## 9.4 自我进化系统启动错误
|
||||||
|
|
||||||
|
### 问题:DateTime 类型不匹配导致编译失败
|
||||||
|
|
||||||
|
**症状**:
|
||||||
|
```
|
||||||
|
error[E0277]: cannot subtract `chrono::DateTime<FixedOffset>` from `chrono::DateTime<Utc>`
|
||||||
|
--> desktop\src-tauri\src\intelligence\heartbeat.rs:542:27
|
||||||
|
|
|
||||||
|
542 | let idle_hours = (now - last_time).num_hours();
|
||||||
|
| ^ no implementation for `chrono::DateTime<Utc> - chrono::DateTime<FixedOffset>`
|
||||||
|
```
|
||||||
|
|
||||||
|
**根本原因**: `chrono::DateTime::parse_from_rfc3339()` 返回 `DateTime<FixedOffset>`,但 `chrono::Utc::now()` 返回 `DateTime<Utc>`,两种类型不能直接相减。
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
将 `DateTime<FixedOffset>` 转换为 `DateTime<Utc>` 后再计算:
|
||||||
|
|
||||||
|
```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 <package> --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<String>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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) - 配置文件位置、格式和最佳实践
|
- [OpenFang 配置指南](./openfang-configuration.md) - 配置文件位置、格式和最佳实践
|
||||||
- [Agent 和 LLM 提供商配置](./agent-provider-config.md) - Agent 管理和 Provider 配置
|
- [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-23 | 添加 9.3 节:更换模型配置后仍使用旧模型 - Agent 配置优先于 Kernel 配置导致的问题 |
|
||||||
| 2026-03-22 | 添加内核 LLM 响应问题:loop_runner.rs 硬编码模型和响应导致 Coding Plan API 不工作 |
|
| 2026-03-22 | 添加内核 LLM 响应问题:loop_runner.rs 硬编码模型和响应导致 Coding Plan API 不工作 |
|
||||||
| 2026-03-20 | 添加端口配置问题:runtime-manifest.json 声明 4200 但实际运行 50051 |
|
| 2026-03-20 | 添加端口配置问题:runtime-manifest.json 声明 4200 但实际运行 50051 |
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ triggers:
|
|||||||
- "业务洞察"
|
- "业务洞察"
|
||||||
- "KPI追踪"
|
- "KPI追踪"
|
||||||
- "预测分析"
|
- "预测分析"
|
||||||
|
- "财报分析"
|
||||||
|
- "数据报表"
|
||||||
tools:
|
tools:
|
||||||
- bash
|
- bash
|
||||||
- read
|
- read
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ triggers:
|
|||||||
- "预算管理"
|
- "预算管理"
|
||||||
- "现金流"
|
- "现金流"
|
||||||
- "财务报告"
|
- "财务报告"
|
||||||
|
- "财报"
|
||||||
- "投资分析"
|
- "投资分析"
|
||||||
- "成本优化"
|
- "成本优化"
|
||||||
- "财务规划"
|
- "财务规划"
|
||||||
|
- "财务数据"
|
||||||
|
- "盈利"
|
||||||
|
- "营收"
|
||||||
|
- "利润"
|
||||||
tools:
|
tools:
|
||||||
- bash
|
- bash
|
||||||
- read
|
- read
|
||||||
|
|||||||
Reference in New Issue
Block a user