feat: 更新记忆工具使用说明,增加高价值场景记录要求;优化技能索引提示格式,支持 XML 标记

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
ooodc 2026-05-06 17:22:01 +08:00
parent 69364e484b
commit 42eb9f85d5
3 changed files with 51 additions and 6 deletions

View File

@ -43,7 +43,7 @@
- 优先调用 memory_manage(action='put');同一 namespace/key 可直接覆盖更新。 - 优先调用 memory_manage(action='put');同一 namespace/key 可直接覆盖更新。
### 以下场景视为高价值加分 ### 以下场景视为高价值加分,必须记录记忆
- 用户多次跟你交互去优化输出 - 用户多次跟你交互去优化输出
- 用户对你的纠正 - 用户对你的纠正
- 确定的事实,路径/地址/网址等 - 确定的事实,路径/地址/网址等

View File

@ -160,6 +160,12 @@ impl WechatChannel {
.map(ToOwned::to_owned); .map(ToOwned::to_owned);
Ok(vec![media_item]) Ok(vec![media_item])
} }
async fn send_typing_indicator(bot: Arc<WeChatBot>, chat_id: &str) {
if let Err(error) = bot.send_typing(chat_id).await {
tracing::debug!(chat_id = %chat_id, error = %error, "Failed to send WeChat typing indicator");
}
}
} }
#[async_trait] #[async_trait]
@ -202,6 +208,8 @@ impl Channel for WechatChannel {
let bot = bot_for_handler.clone(); let bot = bot_for_handler.clone();
let channel_name_for_publish = channel_name.clone(); let channel_name_for_publish = channel_name.clone();
tokio::spawn(async move { tokio::spawn(async move {
Self::send_typing_indicator(bot.clone(), &sender_id).await;
let media = match Self::download_inbound_media(bot, msg.clone()).await { let media = match Self::download_inbound_media(bot, msg.clone()).await {
Ok(media) => media, Ok(media) => media,
Err(error) => { Err(error) => {

View File

@ -429,18 +429,25 @@ 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.\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", "技能为特定任务提供专用说明和工作流。\n当任务匹配其描述时,使用 skill_activate 工具加载技能。\n技能不是工具名,即使技能名看起来像工具,也不能直接调用技能名。\n如果需要某个技能,必须先调用 tool skill_activate并传入 {\"name\": \"<skill-name>\"},再根据返回的技能说明执行。\n\n<available_skills>\n",
); );
for skill in self.skills.iter().take(self.max_listed_skills) { for skill in self.skills.iter().take(self.max_listed_skills) {
let line = format!("- {}: {}\n", skill.name, skill.description); let entry = format!(
if prompt.len() + line.len() > self.max_index_chars { " <skill>\n <name>{}</name>\n <description>{}</description>\n <location>{}</location>\n </skill>\n",
prompt.push_str("- ... (truncated)\n"); xml_escape(&skill.name),
xml_escape(&skill.description),
xml_escape(&format!("file://{}", skill.path.display())),
);
if prompt.len() + entry.len() + "</available_skills>\n".len() > self.max_index_chars {
prompt.push_str(" <truncated>true</truncated>\n");
break; break;
} }
prompt.push_str(&line); prompt.push_str(&entry);
} }
prompt.push_str("</available_skills>\n");
Some(prompt) Some(prompt)
} }
@ -827,6 +834,13 @@ fn split_frontmatter(content: &str) -> Option<(&str, &str)> {
Some((frontmatter, body)) Some((frontmatter, body))
} }
fn xml_escape(value: &str) -> String {
value
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -925,6 +939,29 @@ mod tests {
assert!(payload.contains("Step A")); assert!(payload.contains("Step A"));
} }
#[test]
fn test_system_index_prompt_uses_available_skills_markup() {
let catalog = SkillCatalog {
skills: vec![Skill {
name: "demo-skill".to_string(),
description: "demo <skill> & usage".to_string(),
body: String::new(),
source: SkillSource::Project,
path: PathBuf::from("/tmp/demo-skill/SKILL.md"),
}],
max_index_chars: 4000,
max_listed_skills: 32,
};
let prompt = catalog.system_index_prompt().unwrap();
assert!(prompt.contains("<available_skills>"));
assert!(prompt.contains("技能为特定任务提供专用说明和工作流。"));
assert!(prompt.contains("<name>demo-skill</name>"));
assert!(prompt.contains("<description>demo &lt;skill&gt; &amp; usage</description>"));
assert!(prompt.contains("<location>file:///tmp/demo-skill/SKILL.md</location>"));
assert!(prompt.contains("</available_skills>"));
}
#[test] #[test]
fn test_runtime_create_update_delete_reload() { fn test_runtime_create_update_delete_reload() {
let _lock = acquire_test_lock(); let _lock = acquire_test_lock();