# 记忆系统设计方案 > 设计日期: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 { /// 保存/覆盖一个 MemoryItem(upsert 语义)。 /// - 如果 id 不存在,则插入新条目 /// - 如果 id 已存在,则覆盖旧条目 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, messages: Vec, // 热缓存,供 compact 直接操作 compact_state: CompactState, // 断路器状态 } pub struct ConversationMemoryConfig { pub strategy: MemoryStrategy, // sliding_window | full pub max_turns: usize, // sliding window 的最大轮数 pub compact_config: Option, // 复用现有压缩配置 } ``` - `add_message(msg)` 写入热缓存 `self.messages`,同时通过 `store.save()` 持久化到后端 - `get_history()` 优先从热缓存返回,缓存未命中时从 store 恢复 - `compact` 直接在 `self.messages` 上调用 `should_compact()` 和 `microcompact()` - 压缩后同步回 `store` - 使用了 `llm::types::OpenaiChatMessage` 作为内部消息类型 - 复用现有 `CompactConfig`(context_window, reserved_tokens, keep_recent)和 `CompactState` #### 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(标题/摘要/标签),全文 content 搜索走 MemoryStore - index 在 add/update/delete 时自动维护,也支持通过 `rebuild_index()` 手动重建 - `KnowledgeStore` 以 `"knowledge_{page_id}"` 格式作为 `MemoryItem.id` 前缀,前缀字符串提取为常量 `const KNOWLEDGE_PREFIX: &str = "knowledge_"` 提供 `rebuild_index()` 方法修复 index 与 store 的不同步问题: ```rust impl KnowledgeStore { /// 从 MemoryStore 重建 index(修复 index 与 store 的不同步问题) pub async fn rebuild_index(&self) -> Result<(), MemoryError> { let items = self.store.list(&MemoryFilter { prefix: Some("knowledge_".into()), since: None, offset: None, limit: None, }).await?; let mut index = self.index.lock(); index.clear(); for item in items { let page: KnowledgePage = serde_json::from_str(&item.content) .map_err(|e| MemoryError::Serialization(e.to_string()))?; index.push(PageIndexEntry { id: page.id.clone(), title: page.title, summary: page.summary, tags: page.tags, updated_at: page.updated_at, }); } Ok(()) } } ``` #### 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 系数)。 TextOverlap 评分基于 Dice 系数(字符 bigram): dice(query, text) = 2 × |bigrams(query) ∩ bigrams(text)| / (|bigrams(query)| + |bigrams(text)|) 多字段加权: score = title_dice × 0.5 + summary_dice × 0.3 + content_dice × 0.2 中文场景退化:当前版本按字符级 bigram 处理中文,不依赖分词器。 > **已知限制**:关键词提取基于空格/标点分割,对中文不做语义分词。 > 中文场景按字符 bigram 参与 TextOverlap 计算,精度低于专业分词方案。 > 如有更高精度需求,可替换 MemoryRetriever 的关键词提取逻辑。 ### 3.4 核心数据类型 ```rust pub struct MemoryItem { pub id: String, pub content: String, pub metadata: serde_json::Value, pub created_at: time::OffsetDateTime, } pub struct MemoryFilter { pub prefix: Option, pub since: Option, pub offset: Option, // 跳过前 N 条 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: time::OffsetDateTime, pub updated_at: time::OffsetDateTime, } pub struct PageIndexEntry { pub id: String, pub title: String, pub summary: String, pub tags: Vec, pub updated_at: time::OffsetDateTime, } pub enum MemoryStrategy { SlidingWindow, Full, } ``` ### 3.5 错误类型 ```rust #[derive(Debug, thiserror::Error)] pub enum MemoryError { #[error("Item not found: {0}")] NotFound(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"] A["add_message(msg)"] A1["self.messages.push(msg) ← 热缓存"] A2["store.save(to_item(msg)) ← 冷持久化"] B{"len(messages) > max_turns?"} C["should_compact(&messages, &config, &state) ← 直接在热缓存上操作"] D["microcompact(&mut messages, keep_recent) ← 复用 microcompact()"] E["sync_to_store() ← 压缩后同步回 store"] F["return"] G["get_history()"] H["从 self.messages 返回"] I["从 store 恢复 → 重建热缓存"] A --> A1 A1 --> A2 A2 --> B B -->|是| C C --> D D --> E E --> F B -->|否| F G --> H H -->|缓存未命中| I end J["依赖: llm::compact::{CompactConfig, CompactState, should_compact, microcompact}"] ``` --- ## 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` 通过 **依赖**:time(日期时间,启用 `serde` feature)、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` 分层重导出: - 高频类型(大多数下游需要):`MemoryStore`、`InMemoryStore`、`ConversationMemory`、`KnowledgeStore`、`MemoryRetriever`、`MemoryError` - 低频类型(配置/高级使用):`MemoryItem`、`MemoryFilter`、`MemoryStrategy`、`KnowledgePage`、`PageIndexEntry`、`EvictionConfig`、`EvictionPolicy`、`ConversationMemoryConfig`、`RetrieverConfig`、`RetrievalResult`、`ScoredItem` - 在 `src/lib.rs` 中声明 `pub mod memory` - 单元测试:关键词提取、TextOverlap 评分正确性、阈值过滤、排序正确性 - 集成测试:端到端检索流程 - 验收:`cargo build` + `cargo test` 通过 --- ## 7. 风险评估 | 风险 | 概率 | 影响 | 缓解措施 | |------|------|------|---------| | KnowledgeStore 的 keyword 检索在大规模下效率低 | 中 | 中 | MemoryStore 实现可替换——下游可使用 SQLite FTS 等更高效的后端 | | ConversationMemory 与 compact 耦合引入循环依赖 | 低 | 高 | 仅引用 `CompactConfig`(纯数据结构)和 `microcompact()`(纯函数),不引用 cycle.rs | | time 增加依赖体积 | 低 | 低 | time 是 Rust 官方维护的时间库,体积小于 chrono | | 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 检索 | 文件系统搜索不适合所有后端 |