feat: 添加技能匹配摘要功能,优化技能提示信息,明确技能与工具的区别

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
ooodc 2026-05-02 09:38:31 +08:00
parent 260266b90f
commit 495c8cdc7e
4 changed files with 109 additions and 5 deletions

View File

@ -529,6 +529,10 @@ pub trait EmittedMessageHandler: Send + Sync + 'static {
pub trait SkillProvider: Send + Sync + 'static { pub trait SkillProvider: Send + Sync + 'static {
fn system_index_prompt(&self) -> Option<String>; fn system_index_prompt(&self) -> Option<String>;
fn matching_skill_summary(&self, _name: &str) -> Option<String> {
None
}
} }
#[derive(Default)] #[derive(Default)]
@ -1024,9 +1028,17 @@ impl AgentLoop {
Some(t) => t, Some(t) => t,
None => { None => {
tracing::warn!(tool = %tool_call.name, "Tool not found"); tracing::warn!(tool = %tool_call.name, "Tool not found");
let skill_hint = self.skills.matching_skill_summary(&tool_call.name);
let error = match skill_hint {
Some(summary) => format!(
"Tool '{}' not found. A skill with the same name exists: {}. Skills are not tools. Call skill_activate with {{\"name\": \"{}\"}} first.",
tool_call.name, summary, tool_call.name
),
None => format!("Tool '{}' not found", tool_call.name),
};
return ToolExecutionOutcome::failure( return ToolExecutionOutcome::failure(
format!("Error: Tool '{}' not found", tool_call.name), format!("Error: {}", error),
Some(format!("Tool '{}' not found", tool_call.name)), Some(error),
); );
} }
}; };
@ -1073,6 +1085,7 @@ impl AgentLoop {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::config::LLMProviderConfig;
use crate::observability::{MultiObserver, Observer}; use crate::observability::{MultiObserver, Observer};
use tempfile::tempdir; use tempfile::tempdir;
@ -1098,6 +1111,63 @@ mod tests {
} }
} }
struct TestSkillProvider;
impl SkillProvider for TestSkillProvider {
fn system_index_prompt(&self) -> Option<String> {
None
}
fn matching_skill_summary(&self, name: &str) -> Option<String> {
(name == "baidu-search").then(|| "用于百度搜索和天气查询的技能".to_string())
}
}
fn test_runtime_config() -> LLMProviderConfig {
LLMProviderConfig {
provider_type: "openai".to_string(),
name: "test".to_string(),
base_url: "http://localhost".to_string(),
api_key: "test-key".to_string(),
extra_headers: std::collections::HashMap::new(),
llm_timeout_secs: 120,
model_id: "test-model".to_string(),
temperature: Some(0.0),
max_tokens: Some(32),
context_window_tokens: None,
model_extra: std::collections::HashMap::new(),
max_tool_iterations: 1,
tool_result_max_chars: 20_000,
context_tool_result_trim_chars: 20_000,
}
}
#[tokio::test]
async fn test_missing_tool_with_same_name_skill_returns_activation_hint() {
let loop_instance = AgentLoop::with_tools_and_skill_provider(
test_runtime_config(),
Arc::new(ToolRegistry::new()),
Arc::new(TestSkillProvider),
)
.unwrap();
let outcome = loop_instance
.execute_tool_internal(&ToolCall {
id: "call-1".to_string(),
name: "baidu-search".to_string(),
arguments: serde_json::json!({
"queries": "佛山今天几点下雨"
}),
})
.await;
assert_eq!(outcome.state, ToolExecutionState::Completed);
assert!(!outcome.success);
assert!(outcome.output.contains("技能"));
assert!(outcome.output.contains("skill_activate"));
assert!(outcome.output.contains("baidu-search"));
}
#[tokio::test] #[tokio::test]
async fn test_observer_receives_tool_events() { async fn test_observer_receives_tool_events() {
// Verify MultiObserver works // Verify MultiObserver works

View File

@ -47,3 +47,4 @@
- 回答应以帮助用户完成当前目标为中心。 - 回答应以帮助用户完成当前目标为中心。
- 在信息不足时先补关键前提,在信息充分时直接执行。 - 在信息不足时先补关键前提,在信息充分时直接执行。
- Skill 不是工具名。看到可用 Skill 时,不能直接调用 Skill 名称;必须先调用 skill_activate并传入对应的 name。

View File

@ -1 +1,30 @@
你是 PicoBot 的后台记忆整理器。你必须根据输入的候选记忆做语义整理,并严格返回 JSON不要输出 Markdown 代码块,不要输出额外解释。输出 JSON 字段必须包含user_facts, preferences, behavior_patterns, merges, conflicts, low_value_ids, managed_markdown。user_facts、preferences、behavior_patterns 是字符串数组。merges 是对象数组,每个对象必须包含 source_ids、namespace、memory_key、content。conflicts 是对象数组,每个对象必须包含 source_ids、note。low_value_ids 是需要删除的候选记忆 id 数组。只能引用输入里出现过的候选 id。managed_markdown 必须是 Markdown 文本,且只保留稳定模式,不写一次性事件。 你是 PicoBot 的后台记忆整理器。
你的任务是:
- 根据输入的候选记忆做语义整理。
- 严格返回 JSON。
- 不要输出 Markdown 代码块。
- 不要输出额外解释。
输出 JSON 必须包含以下字段:
- user_facts
- preferences
- behavior_patterns
- merges
- conflicts
- low_value_ids
- managed_markdown
字段要求如下:
- user_facts、preferences、behavior_patterns字符串数组。
- merges对象数组。每个对象必须包含 source_ids、namespace、memory_key、content。
- conflicts对象数组。每个对象必须包含 source_ids、note。
- low_value_ids需要删除的候选记忆 id 数组。
- managed_markdown必须是 Markdown 文本,且只保留稳定模式,不写一次性事件。
额外约束:
- 只能引用输入里出现过的候选 id。

View File

@ -226,6 +226,10 @@ impl crate::agent::SkillProvider for SkillRuntime {
fn system_index_prompt(&self) -> Option<String> { fn system_index_prompt(&self) -> Option<String> {
SkillRuntime::system_index_prompt(self) SkillRuntime::system_index_prompt(self)
} }
fn matching_skill_summary(&self, name: &str) -> Option<String> {
self.get_skill(name).map(|skill| skill.description)
}
} }
impl SkillSource { impl SkillSource {
@ -319,7 +323,7 @@ impl SkillCatalog {
} }
let mut prompt = String::from( let mut prompt = String::from(
"You have access to skills discovered from local skill directories. Use a skill only when the user's request clearly matches the skill description.\nIf a skill is needed, call tool skill_activate with {\"name\": \"<skill-name>\"}.\nAvailable skills:\n", "You have access to skills discovered from local skill directories. Use a skill only when the user's request clearly matches the skill description.\nSkills are not tools. Never call a skill name directly as a tool, even if the skill name looks tool-like.\nIf a skill is needed, you must call tool skill_activate with {\"name\": \"<skill-name>\"} before using the skill instructions.\nDo not call <skill-name> directly. Always call skill_activate first.\nAvailable skills:\n",
); );
for skill in self.skills.iter().take(self.max_listed_skills) { for skill in self.skills.iter().take(self.max_listed_skills) {