feat: 添加技能匹配摘要功能,优化技能提示信息,明确技能与工具的区别
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
260266b90f
commit
495c8cdc7e
@ -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
|
||||||
|
|||||||
@ -47,3 +47,4 @@
|
|||||||
|
|
||||||
- 回答应以帮助用户完成当前目标为中心。
|
- 回答应以帮助用户完成当前目标为中心。
|
||||||
- 在信息不足时先补关键前提,在信息充分时直接执行。
|
- 在信息不足时先补关键前提,在信息充分时直接执行。
|
||||||
|
- Skill 不是工具名。看到可用 Skill 时,不能直接调用 Skill 名称;必须先调用 skill_activate,并传入对应的 name。
|
||||||
@ -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。
|
||||||
@ -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) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user