refactor: 工具简化为「查询 + 调用」二元接口

LLM 工具列表从 10+ 个缩减为 5 个核心:
  - query_capabilities: 按需查看所有工具说明
  - call_capability: 按名称桥接到任意工具
  - read_memories / write_memory / read_summaries: 上下文工具

datetime / memos: Rust 原生实现(不走子进程)
系统提示词精简为引导型(先查能力再调用)
This commit is contained in:
2026-06-02 14:25:00 +08:00
parent a549bd3b2f
commit b710518a1f
2 changed files with 94 additions and 35 deletions
+3 -2
View File
@@ -239,5 +239,6 @@ pub const DEFAULT_SYSTEM_PROMPT: &str = "You are a concise and helpful assistant
Keep replies practical and natural. \
Unless the user asks for detail, keep most replies under 120 Chinese characters. \
\
如果需要了解用户的长期记忆或历史对话摘要,使用 read_memories / read_summaries 工具。\
如果用户提到重要的个人信息或约定,使用 write_memory 工具记录下来。";
可用的核心工具:query_capabilities(查看所有可用工具)、call_capability(按名调用工具\
可使用 read_memories / write_memory 管理用户长期记忆,read_summaries 查看历史摘要。\
调用工具前先用 query_capabilities 确认工具有哪些。";
+91 -33
View File
@@ -238,28 +238,43 @@ async fn cmd_listen(
..Default::default()
};
// 构建 LLM tools 列表(skills + 上下文工具)
// 仅暴露两个元工具:查询能力 + 调用能力
// 具体工具描述通过 query_capabilities 返回
let mut tools_list: Vec<serde_json::Value> = Vec::new();
if let Some(ref registry) = skill_registry {
for spec in registry.list_specs() {
tools_list.push(serde_json::json!({
"type": "function",
"function": {
"name": spec.name,
"description": spec.description,
"parameters": spec.parameters,
}
}));
// query_capabilities:列出所有可用工具及其说明
tools_list.push(serde_json::json!({
"type": "function",
"function": {
"name": "query_capabilities",
"description": "列出所有可用工具的详细说明、参数格式和调用示例。在需要了解有哪些工具可用时首先调用此工具。",
"parameters": { "type": "object", "properties": {} }
}
}
}));
// 上下文工具(长期记忆 + 摘要)
// call_capability:按名称调用任意工具
tools_list.push(serde_json::json!({
"type": "function",
"function": {
"name": "call_capability",
"description": "按工具名称调用一个已注册的工具。name 为工具名(来自 query_capabilities),prompt 为自然语言需求",
"parameters": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "工具名称" },
"prompt": { "type": "string", "description": "自然语言需求" }
},
"required": ["name"]
}
}
}));
// 上下文工具(仍然直接暴露给 LLM)
tools_list.push(serde_json::json!({
"type": "function",
"function": {
"name": "read_memories",
"description": "读取用户的长期记忆(跨会话持久保存的个人信息、偏好、约定",
"description": "读取用户的长期记忆(偏好、个人信息、约定)",
"parameters": { "type": "object", "properties": {} }
}
}));
@@ -267,7 +282,7 @@ async fn cmd_listen(
"type": "function",
"function": {
"name": "write_memory",
"description": "写入一条用户的长期记忆(当用户提到重要偏好、个人信息、约定时调用)",
"description": "记录用户的重要信息或偏好",
"parameters": {
"type": "object",
"properties": {
@@ -281,28 +296,11 @@ async fn cmd_listen(
"type": "function",
"function": {
"name": "read_summaries",
"description": "读取历史会话摘要(包括因上下文压缩或空闲超时产生的摘要)",
"description": "读取历史会话摘要",
"parameters": { "type": "object", "properties": {} }
}
}));
// call_capability 桥接:iPet 兼容,按 skill 名调用
tools_list.push(serde_json::json!({
"type": "function",
"function": {
"name": "call_capability",
"description": "按名称调用一个 skill(等同于直接使用该 skill 的工具名)。name 参数为 skill 的名称,prompt 为自然语言需求",
"parameters": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "要调用的 skill 名称" },
"prompt": { "type": "string", "description": "给该 skill 的自然语言需求" }
},
"required": ["name"]
}
}
}));
cfg.tools = Some(tools_list);
// 创建 Conversation
@@ -372,6 +370,17 @@ async fn cmd_listen(
return Ok(memory_store.write_for(&user_id, content).await);
}
"read_summaries" => return Ok(context::tools::read_summaries(&session).await),
"query_capabilities" => {
if let Some(r) = reg.as_ref() {
return Ok(build_capability_list(r));
}
return Ok("暂无工具".to_string());
}
"get_current_datetime" => {
let now = chrono::Local::now();
return Ok(serde_json::json!({"datetime": now.format("%Y-%m-%d %H:%M:%S").to_string()}).to_string());
}
"manage_memos" => return handle_memos(&args).await,
"call_capability" => {
let cap_params: serde_json::Value = serde_json::from_str(&args).unwrap_or_default();
let cap_name = cap_params.get("name").and_then(|v| v.as_str()).map(|s| s.to_string());
@@ -800,6 +809,55 @@ fn global_skills_dir() -> std::path::PathBuf {
.join("skills")
}
fn build_capability_list(reg: &SkillRegistry) -> String {
let mut lines = vec!["📋 可用工具:".to_string()];
for spec in reg.list_specs() {
let risk = if spec.risk_level == tools::skill::RiskLevel::High { " ⚠️需确认" } else { "" };
lines.push(format!(
"\n🔹 {} {}{}", spec.name, risk, spec.description
));
}
lines.join("\n")
}
async fn handle_memos(args_json: &str) -> Result<String, String> {
use std::io::{Read, Write};
let params: serde_json::Value = serde_json::from_str(args_json).unwrap_or_default();
let action = params.get("action").and_then(|v| v.as_str()).unwrap_or("list");
let path = ".data/memos.json";
std::fs::create_dir_all(".data").ok();
let mut data: Vec<serde_json::Value> = if std::path::Path::new(path).exists() {
serde_json::from_str(&std::fs::read_to_string(path).unwrap_or_default()).unwrap_or_default()
} else {
vec![]
};
match action {
"add" => {
let content = params.get("content").and_then(|v| v.as_str()).unwrap_or("");
if content.is_empty() { return Ok(r#"{"error":"请提供 content 参数"}"#.to_string()); }
let next_id = data.iter().filter_map(|m| m.get("id").and_then(|v| v.as_i64())).max().unwrap_or(0) + 1;
data.push(serde_json::json!({"id": next_id, "content": content, "created_at": chrono::Local::now().format("%Y-%m-%d %H:%M").to_string()}));
std::fs::write(path, serde_json::to_string_pretty(&data).unwrap()).ok();
Ok(format!("已添加备忘录 #{}", next_id))
}
"list" => {
if data.is_empty() { return Ok("暂无备忘录".to_string()); }
Ok(data.iter().map(|m| format!("#{} [{}] {}", m["id"], m.get("created_at").and_then(|v| v.as_str()).unwrap_or("?"), m["content"].as_str().unwrap_or("?"))).collect::<Vec<_>>().join("\n"))
}
"delete" => {
let id = params.get("id").and_then(|v| v.as_i64()).unwrap_or(0);
let before = data.len();
data.retain(|m| m.get("id").and_then(|v| v.as_i64()) != Some(id));
if data.len() == before { return Ok(format!("未找到备忘录 #{}", id)); }
std::fs::write(path, serde_json::to_string_pretty(&data).unwrap()).ok();
Ok(format!("已删除备忘录 #{}", id))
}
_ => Ok("未知操作,支持: add, list, delete".to_string())
}
}
async fn cmd_skills(action: SkillsAction) {
use dialoguer::{MultiSelect, theme::ColorfulTheme};
use console::Term;