# 记忆系统设计方案 > 设计日期:2026-06-07 > 状态:待实现 --- ## 1. 背景与目标 ### 1.1 背景 AG Core 已完成 Phase 0(LLM 调用周期)、Phase 1(提示词工程)、Phase 2(工具系统)。Phase 3 的目标是构建记忆系统,为 Phase 4(Agent 运行时)提供记忆存储、管理与检索能力。 ### 1.2 目标 提供一套可插拔的记忆抽象层,支持以下记忆形态: - **对话记忆(ConversationMemory)** — 管理多轮对话消息历史,支持 sliding window / 全量策略 - **知识库(KnowledgeStore)** — 基于 LLM Wiki 模式的结构化知识管理,Agent 可自主编译和维护知识页面 - **检索器(MemoryRetriever)** — 单通道关键词检索,提供统一的记忆查找入口 ### 1.3 设计原则 - **不引入 embedding 依赖** — 采用 Karpathy's LLM Wiki 模式的 index + keyword 检索,替代传统向量检索 - **trait + 轻量默认实现** — 存储抽象接口提供纯内存默认实现(InMemoryStore),满足原型和测试需求 - **模块间松耦合** — 记忆系统与 LlmCycle 的集成推迟到 Phase 4 Agent Runtime,Phase 3 只定义接口和数据操作 --- ## 2. 需求分析 ### 2.1 功能需求 | ID | 需求 | 优先级 | 说明 | |----|------|--------|------| | F1 | MemoryStore 通用键值存储 | P0 | save/get/delete/list | | F2 | 对话消息管理 | P0 | 按 session 管理,支持 sliding window / full | | F3 | 知识页面 CRUD | P1 | 创建/更新/删除/检索知识页面 | | F4 | 知识页面关键词检索 | P1 | 基于标题/摘要/标签的关键词匹配 | | F5 | 知识页面索引维护 | P1 | 维护可遍历的内容目录(index) | | F6 | 可插拔后端 | P0 | MemoryStore 通过 trait 抽象,下游可实现自定义后端 | | F7 | 记忆淘汰 | P1 | 支持 TTL 过期淘汰、容量上限淘汰 | | F8 | 消息条目级淘汰 | P1 | ConversationMemory 达到上限后删除最旧消息 | ### 2.2 非功能需求 | ID | 需求 | 说明 | |----|------|------| | NF1 | 零 embedding 依赖 | 核心库不引入任何向量数据库或 embedding 模型依赖 | | NF2 | 错误体系完善 | MemoryError 枚举,支持 is_recoverable() 分类 | | NF3 | 线程安全 | 所有存储实现满足 Send + Sync | | NF4 | 异步 API | 所有 IO 操作为 async | | NF5 | 模块化 | 各组件独立可替换 | --- ## 3. 方案设计 ### 3.1 总体架构 ```mermaid graph TB subgraph Retrieval["检索层"] MR["MemoryRetriever"] end subgraph Logic["逻辑层"] CM["ConversationMemory"] KS["KnowledgeStore"] end subgraph Storage["存储层"] MS["MemoryStore (trait)"] IMS["InMemoryStore (默认)"] end MR --> KS CM --> MS KS --> MS MS --> IMS ``` ### 3.2 模块结构 ``` src/ memory.rs # 模块根:pub mod + pub use 重导出 memory/ store.rs # MemoryStore trait + InMemoryStore conversation.rs # ConversationMemory(对话管理) knowledge.rs # KnowledgeStore(具体 struct) retriever.rs # MemoryRetriever(单通道检索) error.rs # MemoryError types.rs # 核心数据类型 ``` ### 3.3 接口定义 #### MemoryStore — 底层存储抽象 ```rust #[async_trait] pub trait MemoryStore: Send + Sync { async fn save(&self, item: MemoryItem) -> Result<(), MemoryError>; async fn get(&self, id: &str) -> Result, MemoryError>; async fn delete(&self, id: &str) -> Result<(), MemoryError>; async fn list(&self, filter: &MemoryFilter) -> Result, MemoryError>; } ``` #### InMemoryStore — 默认实现 ```rust pub struct InMemoryStore { items: Mutex>, } ``` 基于 `HashMap` + `Mutex`,纯内存,线程安全。 #### ConversationMemory — 对话记忆 ```rust pub struct ConversationMemory { store: Arc, session_id: String, config: ConversationMemoryConfig, } pub struct ConversationMemoryConfig { pub strategy: MemoryStrategy, // sliding_window | full pub max_turns: usize, // sliding window 的最大轮数 pub compact_config: Option, // 复用现有压缩配置 } ``` - 基于 `MemoryStore` 持久化消息 - `add_message()` 写入,`get_history()` 读取 - sliding window 模式:超出 `max_turns` 后,调用 `llm::compact::microcompact()` 裁剪 - 复用现有 `CompactConfig`(context_window, reserved_tokens, keep_recent) #### KnowledgeStore — 具体 struct ```rust pub struct KnowledgeStore { store: Arc, index: Mutex>, } impl KnowledgeStore { pub fn new(store: Arc) -> Self { ... } pub async fn add_page(&self, page: KnowledgePage) -> Result<(), MemoryError> { ... } pub async fn get_page(&self, id: &str) -> Result, MemoryError> { ... } pub async fn update_page(&self, page: KnowledgePage) -> Result<(), MemoryError> { ... } pub async fn delete_page(&self, id: &str) -> Result<(), MemoryError> { ... } pub async fn search(&self, query: &str) -> Result, MemoryError> { ... } pub async fn get_index(&self) -> Result, MemoryError> { ... } } ``` - `search` 使用简单的字符串包含匹配标题/摘要/标签 - index 在 add/update/delete 时自动维护 #### MemoryRetriever — 简化版检索器 ```rust pub struct MemoryRetriever { knowledge_store: KnowledgeStore, config: RetrieverConfig, } pub struct RetrieverConfig { pub max_results: usize, // 默认 20 pub min_score: f32, // 默认 0.1 } pub struct RetrievalResult { pub items: Vec, pub query: String, } pub struct ScoredItem { pub page: KnowledgePage, pub score: f32, // TextOverlap 评分 [0.0, 1.0] } ``` 检索流程: ```mermaid flowchart TD A["输入: query"] B["1. 关键词提取(split + 过滤停用词)"] C["2. KnowledgeStore.search(keywords)"] D["3. TextOverlap 评分"] E["4. 过滤 score < min_score"] F["5. 降序排序 → 截取 top-N"] G["6. 返回 RetrievalResult"] A --> B B --> C C --> D D --> E E --> F F --> G ``` 关键词提取在 MemoryRetriever 内部简单实现:按空格/标点分割 → 过滤单字符和停用词 → 返回关键词列表。TextOverlap 计算 query 与页面标题/摘要/内容的 n-gram 重叠度(基于 Dice 系数)。 ### 3.4 核心数据类型 ```rust pub struct MemoryItem { pub id: String, pub content: String, pub metadata: serde_json::Value, pub created_at: chrono::DateTime, } pub struct MemoryFilter { pub prefix: Option, pub since: Option>, pub limit: Option, } pub struct KnowledgePage { pub id: String, pub title: String, pub summary: String, pub content: String, pub tags: Vec, pub references: Vec, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, } pub struct PageIndexEntry { pub id: String, pub title: String, pub summary: String, pub tags: Vec, pub updated_at: chrono::DateTime, } pub enum MemoryStrategy { SlidingWindow, Full, } ``` ### 3.5 错误类型 ```rust #[derive(Debug, thiserror::Error)] pub enum MemoryError { #[error("Item not found: {0}")] NotFound(String), #[error("Item already exists: {0}")] AlreadyExists(String), #[error("Storage error: {0}")] Storage(String), #[error("Serialization error: {0}")] Serialization(String), #[error("Invalid input: {0}")] InvalidInput(String), #[error("Retrieval error: {0}")] RetrievalError(String), } impl MemoryError { pub fn is_recoverable(&self) -> bool { matches!(self, Self::NotFound(_) | Self::RetrievalError(_)) } } ``` ### 3.6 ConversationMemory 与 compact 模块的集成 ```mermaid flowchart TD subgraph CM["ConversationMemory (src/memory/conversation.rs)"] A["add_message()"] A1["store.save(message) ← 写到底层存储"] C{"sliding_window && over_limit?"} D["messages = store.list(session)"] E["compact_config.should_compact() ← 复用 CompactConfig"] F["microcompact(&mut messages) ← 复用 microcompact()"] G["store.save(pruned) ← 写回"] H["return"] I["get_history()"] J["store.list(session)"] A --> A1 A1 --> C C -->|是| D D --> E E --> F F --> G G --> H C -->|否| H I --> J end K["imports: llm::compact::{CompactConfig, microcompact, should_compact}"] ``` --- ## 4. 物理存储策略 ### 4.1 存储层次 ```mermaid graph TB subgraph RetrievalLayer["检索层"] MR["MemoryRetriever (检索 + 评分,无状态)"] end subgraph AbstractionLayer["存储抽象层"] ABS["MemoryStore\n(存储抽象接口,不感知存储介质)"] end subgraph ImplementationLayer["实现层"] IMS["InMemoryStore (HashMap)\n进程内 volatile\n测试/原型适用"] CUSTOM["下游自定义实现\nFileStore / SqliteStore / RedisStore / ...\n生产环境适用"] end MR --> ABS ABS --> IMS ABS --> CUSTOM ``` ### 4.2 InMemoryStore 的物理存储 | 组件 | 数据结构 | 存储位置 | 持久化 | 生命周期 | |------|---------|---------|--------|---------| | `InMemoryStore` | `HashMap` | 进程堆内存 | ❌ | 随进程销毁 | | `KnowledgeStore` | 基于 `InMemoryStore` + `Vec` | 进程堆内存 | ❌ | 随进程销毁 | **适用场景:** - 单元测试和集成测试 - 本地快速原型开发 - 单次会话的临时 Agent **不适合场景:** - 生产部署 - 需要跨进程/跨会话共享记忆 - 需要数据持久化和恢复 ### 4.3 持久化存储方案(下游实现) agcore 核心库**不内置**持久化实现,用户通过实现 `MemoryStore` trait 对接所需后端: | 后端 | 实现建议 | 适用场景 | 复杂度 | |------|---------|---------|--------| | **JSON 文件** | MemoryStore trait → 序列化为单文件 JSON | 单机、轻量持久化 | 低 | | **SQLite** | MemoryStore → 关系表 | 单机、中小规模 | 中 | | **PostgreSQL** | MemoryStore → 关系表 | 多进程共享、中等规模 | 中 | | **Redis** | MemoryStore → Hash/JSON 类型 | 高速缓存、会话共享 | 低 | ### 4.4 对下游实现的约束 `MemoryStore` trait 对持久化实现无特殊约束: - 方法签名不涉及文件路径、连接字符串等存储细节 - 所有方法均为 `async`,持久化实现可自由选择同步(`spawn_blocking`)或异步 driver - 初始化参数在具体实现的构造函数中注入 ### 4.5 序列化 核心类型均实现 `Serialize` / `Deserialize`(通过 `#[derive(serde)]`),便于持久化实现直接复用: ```rust #[derive(Serialize, Deserialize)] pub struct MemoryItem { ... } #[derive(Serialize, Deserialize)] pub struct KnowledgePage { ... } // ... ``` 所有类型基于 `serde_json::Value` 作为 metadata 类型,不引入 protobuf/msgpack 等序列化框架。 --- ## 5. 淘汰策略 ### 5.1 问题 所有存储组件如果不设上限,会随运行时间无限增长。ConversationMemory 当前的 sliding window 只做 tool result 压缩,不删除消息条目。 ### 5.2 淘汰策略 在 `MemoryStore` trait 层提供可选的淘汰配置,上层组件按需设置: ```rust pub struct EvictionConfig { pub policy: EvictionPolicy, pub check_interval: usize, // 每写入 N 条后检查一次淘汰条件 } pub enum EvictionPolicy { None, // 不淘汰(默认) Ttl { ttl_secs: u64 }, // 超过存活时间淘汰 Capacity { max_items: usize },// 超过容量淘汰最旧 } ``` `InMemoryStore` 在 `save()` 后检查淘汰条件: ```mermaid flowchart TD A["save(item)"] B["items.insert(id, item)"] C{"writes_since_last_check >= check_interval?"} D{"policy 类型"} E["Ttl → items.retain(created_at > cutoff)"] F["Capacity → 按 created_at 升序排列,截断到 max_items"] G["None → 不淘汰"] A --> B B --> C C -->|是| D C -->|否| G D -->|Ttl| E D -->|Capacity| F D -->|None| G ``` ### 5.3 各组件淘汰策略 | 组件 | 推荐策略 | 理由 | |------|---------|------| | **ConversationMemory** | `Capacity { max_items }` | 对话是流式的,旧消息价值递减,达到上限后淘汰最旧的消息条目 | | **MemoryStore**(通用) | `Ttl { ttl_secs }` | 通用存储由调用方按场景决定 | | **KnowledgeStore** | `None`(默认不淘汰) | 知识是累积的,新增不淘汰旧 | ### 5.4 ConversationMemory 淘汰行为 ```mermaid flowchart TD A["add_message(msg)"] B["store.save(msg)"] C{"eviction.policy == Capacity?"} D["history = store.list(session)"] E{"while history.len() > max_items"} F["oldest = history.remove(0) ← 删除最旧消息"] G["store.delete(oldest.id)"] H["maybe_compact() ← 复用 microcompact() 做内容压缩"] I["return"] A --> B B --> C C -->|是| D D --> E E -->|是| F F --> G G --> E E -->|否| H C -->|否| H H --> I ``` 两种机制分层: - **淘汰(eviction)**:删除整条消息,控制条目总数上限 - **压缩(compaction)**:压缩剩余消息的 tool result 内容,节省 token ### 5.5 InMemoryStore 的淘汰实现 ```rust impl InMemoryStore { pub fn with_eviction(config: EvictionConfig) -> Self { ... } } // 在 save() 内部: async fn save(&self, item: MemoryItem) -> Result<(), MemoryError> { self.items.lock().insert(item.id.clone(), item); self.maybe_evict().await; } async fn maybe_evict(&self) { match &self.eviction.policy { EvictionPolicy::Ttl { ttl_secs } => { let cutoff = Utc::now() - Duration::seconds(*ttl_secs as i64); self.items.lock().retain(|_, v| v.created_at > cutoff); } EvictionPolicy::Capacity { max_items } => { let mut items = self.items.lock(); if items.len() > *max_items { let mut vec: Vec<_> = items.drain().collect(); vec.select_nth_unstable_by( *max_items, |a, b| b.1.created_at.cmp(&a.1.created_at), ); vec.truncate(*max_items); *items = vec.into_iter().collect(); } } EvictionPolicy::None => {} } } ``` --- ## 6. 实现计划 ### Step 1:基础类型 + MemoryStore(含淘汰机制) **文件**:`src/memory.rs`、`src/memory/types.rs`、`src/memory/error.rs`、`src/memory/store.rs` - 创建 `memory.rs` + `memory/` 目录 - 定义 `MemoryItem`、`MemoryFilter`、`MemoryStrategy` 类型 - 定义 `MemoryError` 枚举 - 定义 `MemoryStore` trait 与 `InMemoryStore` 实现 - 定义 `EvictionConfig`、`EvictionPolicy`(None / Ttl / Capacity) - `InMemoryStore.save()` 内部实现淘汰检查 - 单元测试:TTL 过期淘汰、容量上限淘汰、不淘汰(None) - 验收:`cargo build` + `cargo test` 通过 **依赖**:chrono(日期时间)、serde(序列化) ### Step 2:ConversationMemory(含消息淘汰) **文件**:`src/memory/conversation.rs` - 定义 `ConversationMemoryConfig`、`MemoryStrategy` - 实现 `ConversationMemory` - `add_message()` → 写入 MemoryStore → 触发容量淘汰(删除最旧消息) - 复用 `llm::compact` 的 `CompactConfig` 和 `microcompact()` 做内容压缩 - 单元测试:sliding window 消息淘汰、tool result 内容压缩、full 模式、空 session - 验收:`cargo build` + `cargo test` 通过 **依赖**:MemoryStore(含 EvictionConfig)+ llm::compact ### Step 3:KnowledgeStore **文件**:`src/memory/knowledge.rs` - 定义 `KnowledgePage`、`PageIndexEntry` 类型 - 实现 `KnowledgeStore` 具体 struct(非 trait) - 内部使用 `Arc` 存储数据 - index 自动维护(add/update/delete 时同步) - search 基于标题/摘要/标签的关键词匹配 - 单元测试:页面 CRUD、index 一致性、搜索 - 验收:`cargo build` + `cargo test` 通过 ### Step 4:MemoryRetriever + 模块整合 **文件**:`src/memory/retriever.rs`、`src/memory.rs` - 实现内存检索器 `MemoryRetriever` - 内部关键词提取:split + 过滤停用词 - TextOverlap 评分:基于 Dice 系数计算 query 与页面的文本重叠度 - 阈值过滤 → 排序 → 截取 top-N - 在 `memory.rs` 中用 `pub use` 重导出所有公共类型 - 在 `src/lib.rs` 中声明 `pub mod memory` - 单元测试:关键词提取、TextOverlap 评分正确性、阈值过滤、排序正确性 - 集成测试:端到端检索流程 - 验收:`cargo build` + `cargo test` 通过 --- ## 7. 风险评估 | 风险 | 概率 | 影响 | 缓解措施 | |------|------|------|---------| | KnowledgeStore 的 keyword 检索在大规模下效率低 | 中 | 中 | MemoryStore 实现可替换——下游可使用 SQLite FTS 等更高效的后端 | | ConversationMemory 与 compact 耦合引入循环依赖 | 低 | 高 | 仅引用 `CompactConfig`(纯数据结构)和 `microcompact()`(纯函数),不引用 cycle.rs | | chrono 增加依赖体积 | 低 | 低 | chrono 已是 Rust 生态标准,且仅用于时间戳 | | Phase 4 集成时发现 Memory 设计不合理 | 低 | 高 | 按最小可行接口设计,预留扩展空间 | --- ## 8. 验收标准 - [ ] `MemoryStore` trait + `InMemoryStore` 通过单元测试 - [ ] `EvictionConfig` 支持 None / Ttl / Capacity 三种策略 - [ ] `InMemoryStore` 在 save() 后正确执行 TTL 淘汰和容量淘汰 - [ ] `ConversationMemory` 支持 sliding window 和 full 两种策略 - [ ] `ConversationMemory` sliding window 模式下达到上限后删除最旧消息条目 - [ ] `ConversationMemory` 正确复用 `llm::compact` 的压缩逻辑 - [ ] `KnowledgeStore` 支持页面 CRUD 和 index 维护 - [ ] `MemoryRetriever` 支持基于 TextOverlap 的知识检索 - [ ] 无 embedding 相关依赖 - [ ] 模块结构:`memory.rs` + `memory/` 目录 + `pub use` 重导出 - [ ] `MemoryError` 枚举完善,支持 `is_recoverable()` - [ ] 所有公开 API 有文档注释(`///`) - [ ] `cargo build` 和 `cargo test` 通过 - [ ] 单个文件不超过 300 行 --- ## 附录:与 Karpathy's LLM Wiki 的关系 本方案受 Karpathy's LLM Wiki 模式启发,但做了一些调整以适应 Agent 核心库的定位: | Karpathy LLM Wiki | AG Core Memory System | 差异原因 | |-------------------|----------------------|---------| | 三层:Raw → Wiki → Schema | 三组件:MemoryStore → ConversationMemory + KnowledgeStore + MemoryRetriever | Agent 场景需要区分对话记忆和知识记忆 | | index.md + log.md | PageIndexEntry(同 index.md)+ 无 log(Phase 4 Agent 负责) | 日志是工作流层职责,非存储层 | | LLM Agent 全权维护 | KnowledgeStore 提供数据接口,Phase 4 Agent 编排工作流 | core 只提供存储能力,不编排 | | 文件系统为后端 | MemoryStore trait 抽象后端 | 可插拔设计需要 trait 抽象 | | 基于文件系统搜索 | index + keyword 检索 | 文件系统搜索不适合所有后端 |