feat: 限定记忆命名空间为7种分类

- 新增 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: 其他记忆
This commit is contained in:
ooodc 2026-05-30 13:06:55 +08:00
parent 7d9355fd78
commit 1288ba268f
7 changed files with 110 additions and 45 deletions

View File

@ -38,10 +38,20 @@
### 记忆写入
#### 命名空间分类
记忆必须使用以下命名空间之一:
- `user` - 用户记忆:用户长期偏好、身份背景和历史协作信息
- `semantic` - 语义记忆:结构化或非结构化知识内容
- `episodic` - 情景记忆:历史对话、任务执行过程及关键事件
- `skill` - 技能记忆:技能定义、工作流、工具调用策略及最佳实践
- `environment` - 环境记忆:外部系统状态、运行环境配置和实时资源信息
- `reflection` - 反思记忆:成功经验、失败原因和优化建议
- `other` - 其他记忆:不属于以上分类的其他内容
#### 写入规则
- 写入或修改记忆时使用 memory_manage。
- 遇到未来仍有用的信息时写入记忆:用户长期偏好、稳定事实、用户对你的纠正、持续任务或项目上下文、明确决策等。
- 写入时优先使用规范 namespacepreferences、profile、tasks、decisions。
- 写入时必须使用允许的命名空间user、semantic、episodic、skill、environment、reflection、other
- 优先调用 memory_manage(action='put');同一 namespace/key 可直接覆盖更新。
#### 【重要注意!】以下场景视为高价值加分,必须记录记忆

View File

@ -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` - 其他记忆:不属于以上分类的其他内容
组织原则:
- 根据记忆的语义内容自然分组

View File

@ -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(),

View File

@ -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(),

View File

@ -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,

View File

@ -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"
}),
)

View File

@ -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"
}),
)