From 1288ba268fd8d04009a074d5ec1358a17fbbd9a9 Mon Sep 17 00:00:00 2001 From: ooodc <549496103@qq.com> Date: Sat, 30 May 2026 13:06:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=99=90=E5=AE=9A=E8=AE=B0=E5=BF=86?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E4=B8=BA7=E7=A7=8D?= =?UTF-8?q?=E5=88=86=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ALLOWED_MEMORY_NAMESPACES 常量定义允许的命名空间 - 添加 namespace 验证函数 is_valid_namespace() - memory_manage 工具 schema 使用 enum 限制 namespace - memory_search 工具 schema 使用 enum 提示可用 namespace - 更新系统提示词添加命名空间分类说明 - 更新记忆维护提示词添加命名空间分类说明 - 修复测试中使用旧 namespace 的问题 命名空间分类: - user: 用户记忆 - semantic: 语义记忆 - episodic: 情景记忆 - skill: 技能记忆 - environment: 环境记忆 - reflection: 反思记忆 - other: 其他记忆 --- src/gateway/default_agent_prompt.md | 12 ++++++- .../memory_maintenance_step1_system_prompt.md | 12 ++++++- src/gateway/session.rs | 34 +++++++++---------- src/storage/mod.rs | 25 +++++++------- src/storage/records.rs | 32 +++++++++++++++++ src/tools/memory_manage.rs | 24 +++++++++---- src/tools/memory_search.rs | 16 +++++---- 7 files changed, 110 insertions(+), 45 deletions(-) diff --git a/src/gateway/default_agent_prompt.md b/src/gateway/default_agent_prompt.md index d995b66..4b92a47 100644 --- a/src/gateway/default_agent_prompt.md +++ b/src/gateway/default_agent_prompt.md @@ -38,10 +38,20 @@ ### 记忆写入 +#### 命名空间分类 +记忆必须使用以下命名空间之一: +- `user` - 用户记忆:用户长期偏好、身份背景和历史协作信息 +- `semantic` - 语义记忆:结构化或非结构化知识内容 +- `episodic` - 情景记忆:历史对话、任务执行过程及关键事件 +- `skill` - 技能记忆:技能定义、工作流、工具调用策略及最佳实践 +- `environment` - 环境记忆:外部系统状态、运行环境配置和实时资源信息 +- `reflection` - 反思记忆:成功经验、失败原因和优化建议 +- `other` - 其他记忆:不属于以上分类的其他内容 + #### 写入规则 - 写入或修改记忆时使用 memory_manage。 - 遇到未来仍有用的信息时写入记忆:用户长期偏好、稳定事实、用户对你的纠正、持续任务或项目上下文、明确决策等。 -- 写入时优先使用规范 namespace:preferences、profile、tasks、decisions。 +- 写入时必须使用允许的命名空间:user、semantic、episodic、skill、environment、reflection、other。 - 优先调用 memory_manage(action='put');同一 namespace/key 可直接覆盖更新。 #### 【重要注意!】以下场景视为高价值加分,必须记录记忆 diff --git a/src/gateway/memory_maintenance_step1_system_prompt.md b/src/gateway/memory_maintenance_step1_system_prompt.md index d066c81..3f11948 100644 --- a/src/gateway/memory_maintenance_step1_system_prompt.md +++ b/src/gateway/memory_maintenance_step1_system_prompt.md @@ -19,7 +19,7 @@ - merges:对象数组。每个对象必须包含 source_ids、namespace、memory_key、content。 - source_ids: 字符串数组,要合并的源记忆ID列表 - - namespace: 目标命名空间 + - namespace: 目标命名空间(必须是以下之一:user、semantic、episodic、skill、environment、reflection、other) - memory_key: 目标记忆键(可以自由决定) - content: 合并后的内容 - conflicts:对象数组。每个对象必须包含 source_ids、note。 @@ -27,6 +27,16 @@ - note: 冲突说明 - low_value_ids:需要删除的低价值候选记忆 ID 数组 +命名空间分类说明: + +- `user` - 用户记忆:用户长期偏好、身份背景和历史协作信息 +- `semantic` - 语义记忆:结构化或非结构化知识内容 +- `episodic` - 情景记忆:历史对话、任务执行过程及关键事件 +- `skill` - 技能记忆:技能定义、工作流、工具调用策略及最佳实践 +- `environment` - 环境记忆:外部系统状态、运行环境配置和实时资源信息 +- `reflection` - 反思记忆:成功经验、失败原因和优化建议 +- `other` - 其他记忆:不属于以上分类的其他内容 + 组织原则: - 根据记忆的语义内容自然分组 diff --git a/src/gateway/session.rs b/src/gateway/session.rs index bc9d4c3..3505d79 100644 --- a/src/gateway/session.rs +++ b/src/gateway/session.rs @@ -1192,7 +1192,7 @@ mod tests { .put_memory(&crate::storage::MemoryUpsert { scope_kind: "user".to_string(), scope_key: "feishu:user-1".to_string(), - namespace: "profile".to_string(), + namespace: "user".to_string(), memory_key: "work".to_string(), content: "用户在做AI产品".to_string(), source_type: "message".to_string(), @@ -1282,7 +1282,7 @@ mod tests { .put_memory(&crate::storage::MemoryUpsert { scope_kind: "user".to_string(), scope_key: "feishu:user-1".to_string(), - namespace: "profile".to_string(), + namespace: "user".to_string(), memory_key: "work".to_string(), content: "用户在做AI产品".to_string(), source_type: "message".to_string(), @@ -1371,7 +1371,7 @@ mod tests { .put_memory(&crate::storage::MemoryUpsert { scope_kind: "user".to_string(), scope_key: "feishu:user-1".to_string(), - namespace: "profile".to_string(), + namespace: "user".to_string(), memory_key: "work".to_string(), content: "用户在做AI产品".to_string(), source_type: "message".to_string(), @@ -1442,7 +1442,7 @@ mod tests { .put_memory(&crate::storage::MemoryUpsert { scope_kind: "user".to_string(), scope_key: "feishu:user-1".to_string(), - namespace: "profile".to_string(), + namespace: "user".to_string(), memory_key: "work".to_string(), content: "用户在做AI产品".to_string(), source_type: "message".to_string(), @@ -1522,7 +1522,7 @@ mod tests { .put_memory(&crate::storage::MemoryUpsert { scope_kind: "user".to_string(), scope_key: "feishu:user-1".to_string(), - namespace: "profile".to_string(), + namespace: "user".to_string(), memory_key: "work".to_string(), content: "用户在做AI产品".to_string(), source_type: "message".to_string(), @@ -1590,7 +1590,7 @@ mod tests { .put_memory(&crate::storage::MemoryUpsert { scope_kind: "user".to_string(), scope_key: scope_key.to_string(), - namespace: "profile".to_string(), + namespace: "user".to_string(), memory_key: "work".to_string(), content: format!("{} 在做AI产品", scope_key), source_type: "message".to_string(), @@ -1623,7 +1623,7 @@ mod tests { .put_memory(&crate::storage::MemoryUpsert { scope_kind: "user".to_string(), scope_key: scope_key.to_string(), - namespace: "profile".to_string(), + namespace: "user".to_string(), memory_key: "work_short".to_string(), content: "用户在做AI产品".to_string(), source_type: "message".to_string(), @@ -1638,7 +1638,7 @@ mod tests { .put_memory(&crate::storage::MemoryUpsert { scope_kind: "user".to_string(), scope_key: scope_key.to_string(), - namespace: "profile".to_string(), + namespace: "user".to_string(), memory_key: "work_detail".to_string(), content: "用户主要在做AI产品设计和实现".to_string(), source_type: "message".to_string(), @@ -1653,7 +1653,7 @@ mod tests { .put_memory(&crate::storage::MemoryUpsert { scope_kind: "user".to_string(), scope_key: scope_key.to_string(), - namespace: "notes".to_string(), + namespace: "other".to_string(), memory_key: "temporary".to_string(), content: "今天临时提到过一个无后续的小细节".to_string(), source_type: "message".to_string(), @@ -1671,7 +1671,7 @@ mod tests { .put_memory(&crate::storage::MemoryUpsert { scope_kind: "user".to_string(), scope_key: scope_key.to_string(), - namespace: "profile".to_string(), + namespace: "user".to_string(), memory_key: format!("extra_{}", i), content: format!("额外记忆 {}", i), source_type: "message".to_string(), @@ -1692,7 +1692,7 @@ mod tests { let output = MemoryOrganizationOutput { merges: vec![MemoryMaintenanceMerge { source_ids: vec![work.id.clone(), role.id.clone()], - namespace: "profile".to_string(), + namespace: "user".to_string(), memory_key: "work".to_string(), content: "用户主要在做AI产品设计与实现".to_string(), }], @@ -1715,10 +1715,10 @@ mod tests { let all_memories = store.list_memories_for_scope("user", scope_key).unwrap(); // 过滤掉 _meta 记录 let user_memories: Vec<_> = all_memories.iter().filter(|m| m.namespace != "_meta").collect(); - // 合并 2 条为 1 条,删除 1 条,7 - 2 + 1 = 6 条(加上 _meta 记录) - assert_eq!(user_memories.len(), 6); + // 合并 2 条为 1 条,删除 1 条,7 - 2 + 1 = 5 条 + assert_eq!(user_memories.len(), 5); // 验证合并后的记忆存在 - assert!(user_memories.iter().any(|m| m.namespace == "profile" && m.memory_key == "work")); + assert!(user_memories.iter().any(|m| m.namespace == "user" && m.memory_key == "work")); } #[test] @@ -1948,7 +1948,7 @@ mod tests { id: "1".to_string(), scope_kind: "user".to_string(), scope_key: "feishu:user-1".to_string(), - namespace: "profile".to_string(), + namespace: "user".to_string(), memory_key: "work".to_string(), content: "用户在做AI产品".to_string(), source_type: "message".to_string(), @@ -1964,7 +1964,7 @@ mod tests { id: "2".to_string(), scope_kind: "user".to_string(), scope_key: "feishu:user-1".to_string(), - namespace: "profile".to_string(), + namespace: "user".to_string(), memory_key: "work".to_string(), content: "用户在做AI产品".to_string(), source_type: "message".to_string(), @@ -1980,7 +1980,7 @@ mod tests { id: "3".to_string(), scope_kind: "user".to_string(), scope_key: "feishu:user-1".to_string(), - namespace: "preferences".to_string(), + namespace: "user".to_string(), memory_key: "style".to_string(), content: "偏好简洁表达".to_string(), source_type: "message".to_string(), diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 32e85bb..0b7af0f 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -16,8 +16,9 @@ pub use ports::{ SkillEventRepository, }; pub use records::{ - MemoryRecord, MemoryUpsert, SchedulerJobRecord, SchedulerJobState, SchedulerJobStatus, - SchedulerJobUpsert, SessionRecord, SkillEventRecord, TopicRecord, + allowed_namespace_names, get_namespace_description, is_valid_namespace, + ALLOWED_MEMORY_NAMESPACES, MemoryRecord, MemoryUpsert, SchedulerJobRecord, SchedulerJobState, + SchedulerJobStatus, SchedulerJobUpsert, SessionRecord, SkillEventRecord, TopicRecord, }; #[derive(Clone)] @@ -2284,7 +2285,7 @@ mod tests { .put_memory(&MemoryUpsert { scope_kind: "user".to_string(), scope_key: format!("{}:user-1", TEST_CHANNEL), - namespace: "profile".to_string(), + namespace: "user".to_string(), memory_key: "language".to_string(), content: "Rust".to_string(), source_type: "message".to_string(), @@ -2303,7 +2304,7 @@ mod tests { assert_eq!(saved.source_message_seq, Some(7)); let fetched = store - .get_memory("user", "test-channel:user-1", "profile", "language") + .get_memory("user", "test-channel:user-1", "user", "language") .unwrap() .unwrap(); assert_eq!(fetched.id, saved.id); @@ -2318,7 +2319,7 @@ mod tests { .put_memory(&MemoryUpsert { scope_kind: "user".to_string(), scope_key: format!("{}:user-1", TEST_CHANNEL), - namespace: "preferences".to_string(), + namespace: "user".to_string(), memory_key: "editor".to_string(), content: "Prefers rust-analyzer and cargo test output".to_string(), source_type: "message".to_string(), @@ -2340,7 +2341,7 @@ mod tests { .put_memory(&MemoryUpsert { scope_kind: "user".to_string(), scope_key: format!("{}:user-1", TEST_CHANNEL), - namespace: "preferences".to_string(), + namespace: "user".to_string(), memory_key: "editor".to_string(), content: "Prefers clippy diagnostics".to_string(), source_type: "message".to_string(), @@ -2363,7 +2364,7 @@ mod tests { assert_eq!(new_hits.len(), 1); let deleted = store - .delete_memory("user", "test-channel:user-1", "preferences", "editor") + .delete_memory("user", "test-channel:user-1", "user", "editor") .unwrap(); assert!(deleted); @@ -2381,7 +2382,7 @@ mod tests { .put_memory(&MemoryUpsert { scope_kind: "user".to_string(), scope_key: format!("{}:user-1", TEST_CHANNEL), - namespace: "preferences".to_string(), + namespace: "user".to_string(), memory_key: "email_folder_preference".to_string(), content: "用户提到邮件时默认查看代收邮箱。".to_string(), source_type: "message".to_string(), @@ -2409,7 +2410,7 @@ mod tests { .put_memory(&MemoryUpsert { scope_kind: "user".to_string(), scope_key: format!("{}:user-1", TEST_CHANNEL), - namespace: "preferences".to_string(), + namespace: "user".to_string(), memory_key: "editor".to_string(), content: "Prefers rust-analyzer and cargo test output".to_string(), source_type: "message".to_string(), @@ -2425,7 +2426,7 @@ mod tests { .put_memory(&MemoryUpsert { scope_kind: "user".to_string(), scope_key: format!("{}:user-1", TEST_CHANNEL), - namespace: "tasks".to_string(), + namespace: "episodic".to_string(), memory_key: "quality".to_string(), content: "Tracks clippy warnings before release".to_string(), source_type: "message".to_string(), @@ -2460,7 +2461,7 @@ mod tests { .put_memory(&MemoryUpsert { scope_kind: "user".to_string(), scope_key: format!("{}:user-2", TEST_CHANNEL), - namespace: "preferences".to_string(), + namespace: "user".to_string(), memory_key: "style".to_string(), content: "偏好简洁表达".to_string(), source_type: "message".to_string(), @@ -2475,7 +2476,7 @@ mod tests { .put_memory(&MemoryUpsert { scope_kind: "user".to_string(), scope_key: format!("{}:user-1", TEST_CHANNEL), - namespace: "profile".to_string(), + namespace: "user".to_string(), memory_key: "work".to_string(), content: "用户在做AI产品".to_string(), source_type: "message".to_string(), diff --git a/src/storage/records.rs b/src/storage/records.rs index dce7f4c..115c991 100644 --- a/src/storage/records.rs +++ b/src/storage/records.rs @@ -1,5 +1,37 @@ use serde::{Deserialize, Serialize}; +/// 允许的记忆命名空间列表 +/// +/// 每个命名空间代表一类记忆内容,用于分类管理和检索。 +/// 禁止使用未在此列表中的 namespace 创建记忆。 +pub const ALLOWED_MEMORY_NAMESPACES: &[(&str, &str)] = &[ + ("user", "用户记忆:存储用户长期偏好、身份背景和历史协作信息,实现跨会话的个性化服务与持续协作"), + ("semantic", "语义记忆:存储结构化或非结构化知识内容,支持知识检索、问答增强和长期知识积累"), + ("episodic", "情景记忆:记录历史对话、任务执行过程及关键事件,支持经验回溯、案例复用和行为追踪"), + ("skill", "技能记忆:存储技能定义、工作流、工具调用策略及最佳实践,支持能力复用与自动化执行"), + ("environment", "环境记忆:存储外部系统状态、运行环境配置和实时资源信息,为智能决策提供环境感知能力"), + ("reflection", "反思记忆:沉淀任务执行过程中的成功经验、失败原因和优化建议,支持智能体持续学习与自我改进"), + ("other", "其他记忆:不属于以上分类的其他记忆内容"), +]; + +/// 验证 namespace 是否在允许列表中 +pub fn is_valid_namespace(namespace: &str) -> bool { + ALLOWED_MEMORY_NAMESPACES.iter().any(|(name, _)| *name == namespace) +} + +/// 获取 namespace 的中文描述 +pub fn get_namespace_description(namespace: &str) -> Option<&'static str> { + ALLOWED_MEMORY_NAMESPACES + .iter() + .find(|(name, _)| *name == namespace) + .map(|(_, desc)| *desc) +} + +/// 获取所有允许的 namespace 名称列表(用于 JSON schema enum) +pub fn allowed_namespace_names() -> Vec<&'static str> { + ALLOWED_MEMORY_NAMESPACES.iter().map(|(name, _)| *name).collect() +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SkillEventRecord { pub id: String, diff --git a/src/tools/memory_manage.rs b/src/tools/memory_manage.rs index d854d58..8cae0f1 100644 --- a/src/tools/memory_manage.rs +++ b/src/tools/memory_manage.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use async_trait::async_trait; use serde_json::json; -use crate::storage::{MemoryRecord, MemoryRepository, MemoryUpsert}; +use crate::storage::{is_valid_namespace, MemoryRecord, MemoryRepository, MemoryUpsert}; use crate::tools::traits::{Tool, ToolContext, ToolResult}; pub struct MemoryManageTool { @@ -27,25 +27,27 @@ impl Tool for MemoryManageTool { } fn parameters_schema(&self) -> serde_json::Value { + let namespaces = crate::storage::allowed_namespace_names(); json!({ "type": "object", "properties": { "action": { "type": "string", "enum": ["put", "update", "delete"], - "description": "Management action to perform. Use 'put' to create or overwrite, 'update' to modify an existing record, and 'delete' to remove one. Use memory_search for retrieval." + "description": "管理操作。put 用于创建或覆盖,update 用于修改已有记录,delete 用于删除。检索请使用 memory_search。" }, "namespace": { "type": "string", - "description": "Optional memory namespace filter, such as profile, preferences, or tasks" + "enum": namespaces, + "description": "记忆命名空间分类" }, "key": { "type": "string", - "description": "Exact memory key within the namespace" + "description": "命名空间内的记忆键名" }, "content": { "type": "string", - "description": "Memory content for put/update" + "description": "put/update 时的记忆内容" } }, "required": ["action"] @@ -145,6 +147,14 @@ fn build_memory_upsert( Some(namespace) => namespace, None => return Err(error_result("Missing required parameter: namespace")), }; + // 验证 namespace 是否在允许列表中 + if !is_valid_namespace(namespace) { + let allowed = crate::storage::allowed_namespace_names().join(", "); + return Err(error_result(&format!( + "Invalid namespace '{}'. Allowed namespaces: {}", + namespace, allowed + ))); + } let key = match args.get("key").and_then(|value| value.as_str()) { Some(key) => key, None => return Err(error_result("Missing required parameter: key")), @@ -237,7 +247,7 @@ mod tests { &context, json!({ "action": "put", - "namespace": "profile", + "namespace": "user", "key": "language", "content": "Rust" }), @@ -282,7 +292,7 @@ mod tests { &context, json!({ "action": "get", - "namespace": "profile", + "namespace": "user", "key": "language" }), ) diff --git a/src/tools/memory_search.rs b/src/tools/memory_search.rs index d2621f2..f0e5863 100644 --- a/src/tools/memory_search.rs +++ b/src/tools/memory_search.rs @@ -28,33 +28,35 @@ impl Tool for MemorySearchTool { } fn parameters_schema(&self) -> serde_json::Value { + let namespaces = crate::storage::allowed_namespace_names(); json!({ "type": "object", "properties": { "action": { "type": "string", "enum": ["search", "get", "list"], - "description": "Retrieval action. Use 'search' for multi-keyword recall, 'get' for an exact namespace/key read, and 'list' to browse recent memories." + "description": "检索操作。search 用于多关键词召回,get 用于精确 namespace/key 读取,list 用于浏览最近记忆。" }, "namespace": { "type": "string", - "description": "Optional namespace filter, such as profile, preferences, tasks, or decisions. Required for get." + "enum": namespaces, + "description": "可选的命名空间过滤。get 操作时必填。" }, "queries": { "type": "array", "items": { "type": "string" }, - "description": "Keyword queries for memory search. Provide multiple concise bilingual keywords, English aliases, and likely snake_case memory_key terms when known. Search matches any of the provided entries. Required for search.", + "description": "搜索关键词数组。建议提供多个简洁的双语关键词、英文别名和可能的 snake_case memory_key。search 操作时必填。", "minItems": 1 }, "key": { "type": "string", - "description": "Exact memory key within the namespace. Required for get." + "description": "命名空间内的记忆键名。get 操作时必填。" }, "limit": { "type": "integer", - "description": "Maximum number of memories to return", + "description": "返回记忆的最大数量", "minimum": 1, "default": 10 } @@ -233,7 +235,7 @@ mod tests { .put_memory(&crate::storage::MemoryUpsert { scope_kind: "user".to_string(), scope_key: TEST_CHANNEL.to_string(), - namespace: "preferences".to_string(), + namespace: "user".to_string(), memory_key: "language".to_string(), content: "User prefers Chinese responses".to_string(), source_type: "message".to_string(), @@ -274,7 +276,7 @@ mod tests { &context, json!({ "action": "get", - "namespace": "preferences", + "namespace": "user", "key": "language" }), )