# 记忆系统设计方案 > 设计日期: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 可自主编译和维护知识页面 - **知识图谱(KnowledgeGraph)** — 实体-关系图存储,支持关联检索与图遍历 - **检索器(MemoryRetriever)** — 组合多种检索策略,提供统一的记忆查找入口 ### 1.3 设计原则 - **不引入 embedding 依赖** — 采用 Karpathy's LLM Wiki 模式的 index + keyword 检索,替代传统向量检索 - **trait + 轻量默认实现** — 所有抽象接口均提供纯内存默认实现(InMemory),满足原型和测试需求 - **模块间松耦合** — 记忆系统与 LlmCycle 的集成推迟到 Phase 4 Agent Runtime,Phase 3 只定义接口和数据操作 --- ## 2. 需求分析 ### 2.1 功能需求 | ID | 需求 | 优先级 | 说明 | |----|------|--------|------| | F1 | 存储任意键值对 | P0 | MemoryStore 提供通用的 save/get/delete/list | | F2 | 对话消息管理 | P0 | 按 session 管理多轮对话历史,支持滑动窗口裁剪 | | F3 | 知识页面管理 | P1 | 创建/更新/删除/检索知识页面(标题+摘要+内容+标签+交叉引用) | | F4 | 知识页面索引 | P1 | 维护可遍历的内容目录(index) | | F5 | 知识页面关键词检索 | P1 | 基于标题/摘要/标签的关键词匹配 | | F6 | 实体-关系图存储 | P1 | 实体节点增删查 + 关系边管理 + 图遍历 | | F7 | 组合检索 | P2 | 综合 KnowledgeStore + KnowledgeGraph 的多策略检索 | | F8 | 可插拔后端 | P0 | 所有存储抽象提供 trait,下游可实现自定义后端(文件/SQLite/图数据库等) | | F9 | 记忆淘汰 | P1 | 支持 TTL 过期淘汰、容量上限淘汰、Hybrid 组合策略 | | F10 | 消息条目级淘汰 | P1 | ConversationMemory 达到上限后删除最旧消息,而非仅压缩内容 | | F11 | 基于召回价值的淘汰 | P2 | 根据召回频率、匹配评分、时效性综合计算"记忆价值",淘汰价值最低的记忆 | | F12 | 召回统计记录 | P2 | MemoryStore 记录每次召回事件(recall_count + score),供淘汰策略使用 | ### 2.2 非功能需求 | ID | 需求 | 说明 | |----|------|------| | NF1 | 零 embedding 依赖 | 核心库不引入任何向量数据库或 embedding 模型依赖 | | NF2 | 错误体系完善 | MemoryError 枚举,支持 is_recoverable() 分类 | | NF3 | 线程安全 | 所有存储实现满足 Send + Sync | | NF4 | 异步 API | 所有 IO 操作为 async | | NF5 | 模块化 | 各组件独立可替换(通过 trait) | --- ## 3. 方案设计 ### 3.1 总体架构 ``` ┌─────────────────────────────────────────────────────────────┐ │ MemoryRetriever │ │ (组合检索:index + keyword + graph) │ ├──────────────────┬──────────────────┬───────────────────────┤ │ ConversationMemory│ KnowledgeStore │ KnowledgeGraph │ │ (对话消息管理) │ (知识页面管理) │ (实体-关系图) │ ├──────────────────┴──────────────────┴───────────────────────┤ │ MemoryStore │ │ (底层存储抽象:save/get/delete/list) │ │ InMemoryStore (默认) │ └─────────────────────────────────────────────────────────────┘ ``` ### 3.2 模块结构 ``` src/ memory.rs # 模块根:pub mod + pub use 重导出 memory/ store.rs # MemoryStore trait + InMemoryStore conversation.rs # ConversationMemory(对话管理) knowledge.rs # KnowledgeStore trait + InMemoryKnowledgeStore graph.rs # KnowledgeGraph trait + InMemoryGraph 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 — 知识库 ```rust #[async_trait] pub trait KnowledgeStore: Send + Sync { async fn add_page(&self, page: KnowledgePage) -> Result<(), MemoryError>; async fn get_page(&self, id: &str) -> Result, MemoryError>; async fn update_page(&self, page: KnowledgePage) -> Result<(), MemoryError>; async fn delete_page(&self, id: &str) -> Result<(), MemoryError>; async fn search(&self, query: &str) -> Result, MemoryError>; async fn get_index(&self) -> Result, MemoryError>; } ``` - `add_page` 自动更新 index - `search` 基于页面标题/摘要/标签的关键词匹配(不依赖 embedding) - `get_index` 返回所有页面的摘要目录(类似 LLM Wiki 的 index.md) #### InMemoryKnowledgeStore — 知识库默认实现 ```rust pub struct InMemoryKnowledgeStore { pages: Mutex>, index: Mutex>, } ``` - `search()` 使用简单的字符串包含匹配(可替换为更复杂的检索算法) #### KnowledgeGraph — 知识图谱 图谱检索的核心流程:**输入 → 关键词提取 → 实体标签匹配 → 图遍历** ```rust #[async_trait] pub trait KnowledgeGraph: Send + Sync { async fn add_entity(&self, entity: GraphEntity) -> Result<(), MemoryError>; async fn get_entity(&self, id: &str) -> Result, MemoryError>; async fn remove_entity(&self, id: &str) -> Result<(), MemoryError>; async fn add_relation(&self, relation: GraphRelation) -> Result<(), MemoryError>; async fn remove_relation(&self, source_id: &str, target_id: &str, relation_type: &str) -> Result<(), MemoryError>; async fn get_related(&self, entity_id: &str, depth: usize) -> Result, MemoryError>; async fn find_by_keywords(&self, keywords: &[String]) -> Result, MemoryError>; } ``` - `find_by_keywords()` — 用关键词匹配实体的 `name`(精准/前缀匹配)和 `tags`(完全匹配),不模糊搜索全字段 - `get_related()` — 找到匹配实体后,按 `depth` 遍历关联实体,同时返回关联关系 - `get_related()` 按指定深度遍历邻居节点 - `search_entities()` 按实体名称/类型搜索 #### InMemoryGraph — 知识图谱默认实现 ```rust pub struct InMemoryGraph { entities: Mutex>, relations: Mutex>, } ``` - 图遍历使用 BFS/DFS,`depth` 控制搜索深度 - 适合原型和小规模图 #### MemoryRetriever — 组合检索器(含关联度评分) 两种检索通道会产出大量结果,因此需要**关联度评分机制**来排序和过滤。 ##### ScoringStrategy — 评分策略 不依赖 embedding,基于可计算的文本和图结构特征: | 策略 | 基准 | 计算方式 | 适用场景 | |------|------|---------|---------| | `TextOverlap` | **原始 query** | query 与召回结果的文本重叠度(n-gram Jaccard / Dice 系数),标题 > 摘要 > 内容 | 默认推荐,衡量内容契合度 | | `GraphDistance` | 实体关系 | 实体间最短路径长度的倒数 → `1 / (1 + distance)` | 图遍历结果排序 | | `TemporalWeight` | 时间 | 时间衰减:`1.0 / (1 + days_since_update)` | 时间敏感的知识(如新闻) | | `ReferenceCount` | 页面权威性 | 被其他页面引用的次数归一化(简化版 PageRank) | 权威页面自然排在前面 | | `Hybrid` | 综合 | 以上加权组合:`w1*overlap + w2*graph + w3*temporal + w4*reference` | 需要多维度权衡的场景 | ##### KeywordExtractor — 关键词提取 检索前先从输入中提取关键词,作为两个通道共享的检索入口: ```rust pub trait KeywordExtractor: Send + Sync { fn extract(&self, query: &str) -> Vec; } // 默认实现:按非字母数字字符分割,过滤停用词和单字符词 pub struct SimpleKeywordExtractor { stop_words: HashSet, } ``` ##### RetrievalResult — 统一召回结果 ```rust pub struct MemoryRetriever { knowledge_store: Arc, knowledge_graph: Arc, keyword_extractor: Arc, config: RetrieverConfig, } pub struct RetrieverConfig { pub scoring: ScoringStrategy, pub max_results: usize, // 最大返回条数,默认 20 pub min_score: f32, // 最低分数阈值 [0.0, 1.0],默认 0.1 pub graph_depth: usize, // 图遍历深度,默认 2 pub weights: ScoreWeights, // Hybrid 策略的权重分配 } pub struct ScoreWeights { pub overlap: f32, // 默认 0.5 — 文本重叠度,以原始 query 为基准 pub graph: f32, // 默认 0.2 — 图距离 pub temporal: f32, // 默认 0.1 — 时间衰减 pub reference: f32, // 默认 0.2 — 引用计数 } pub enum ScoringStrategy { TextOverlap, // 以原始 query 为准绳的文本重叠度(默认) GraphDistance, TemporalWeight, ReferenceCount, Hybrid(ScoreWeights), } pub struct RetrievalResult { pub items: Vec, pub total_found: usize, pub query: String, } pub struct ScoredItem { pub source: RetrievalSource, // KnowledgeStore | KnowledgeGraph pub score: f32, // [0.0, 1.0] 最终评分 pub breakdown: ScoreBreakdown, // 各维度评分明细(可选) } pub enum RetrievalSource { KnowledgePage(KnowledgePage), GraphEntity(GraphEntity), } pub struct ScoreBreakdown { pub overlap_score: f32, // 与原始 query 的文本重叠度 pub graph_score: f32, // 图距离 pub temporal_score: f32, // 时间衰减 pub reference_score: f32, // 引用计数 } pub enum RetrievalStrategy { Hybrid, // 结合所有通道 + 评分排序(默认) KnowledgeOnly, // 仅 KnowledgeStore GraphOnly, // 仅 KnowledgeGraph } ``` 检索流程: ``` 输入: "LangGraph 的 StateGraph 状态管理原理?" │ ├─ 1. 关键词提取 (KeywordExtractor) │ → ["LangGraph", "StateGraph", "状态管理"] │ ├─ 2. 并行召回 ──────────────────────────────────────┐ │ │ │ │ ├─ KnowledgeStore.find_by_keywords(keywords) │ │ │ → 匹配页面标题/摘要/标签 │ │ │ → 返回 KnowledgePage 列表 │ │ │ │ │ └─ KnowledgeGraph.find_by_keywords(keywords) ──┐ │ │ → 匹配实体 name / tags │ │ │ → 命中: "LangGraph" (framework), │ │ │ "StateGraph" (concept) │ │ │ → get_related("LangGraph", depth=2) │ │ │ → 返回匹配实体 + 关联实体列表 │ │ │ │ │ ├─ 3. 逐条评分 (ScoringStrategy) ←───────────────────┘ │ │ // 基准:原始 query,而非中间关键词 │ │ │ → overlap_score: 结果内容与原始 query 的文本重叠度 │ │ │ (n-gram Jaccard 系数,标题权重大于正文) │ │ │ → graph_score: 实体在图中的距离(如有) │ │ │ → temporal_score: 时间衰减 │ │ │ → reference_score: 引用次数 │ │ │ → score = Σ(weight_i * score_i) │ │ │ │ ├─ 4. 过滤: score < min_score → 丢弃 │ ├─ 5. 排序: score 降序 → 截取 max_results 条 │ └─ 6. 返回: RetrievalResult │ ``` 流程说明: 1. **关键词提取** — 先用 `KeywordExtractor` 从输入中提取候选关键词,两个通道共享 2. **KnowledgeStore 检索** — 用关键词匹配页面的 `title`(精准/前缀)、`tags`(完全)、`summary`(包含) 3. **KnowledgeGraph 检索** — 用关键词匹配实体 `name` 和 `tags`,命中后按 `depth` 图遍历获取关联实体 4. **评分** — 对每条召回结果计算 `score ∈ [0.0, 1.0]` 5. **过滤排序** — 丢弃低分、按分排序、截取 top N 这样设计的好处: - 关键词生成在两个通道间共享,避免重复计算 - 图谱检索从"模糊搜索"变为"标签精确匹配 → 图遍历",更精准 - 每个步骤职责单一,可独立替换实现 ### 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, // 交叉引用的页面 ID 列表 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 struct GraphEntity { pub id: String, pub name: String, pub entity_type: String, // "person" | "concept" | "project" | ... pub description: String, } pub struct GraphRelation { pub source_id: String, pub target_id: String, pub relation_type: String, // "works_on" | "part_of" | "related_to" | ... pub weight: f32, // 关系强度 [0.0, 1.0] } pub enum MemoryStrategy { SlidingWindow, Full, } pub enum ScoringStrategy { KeywordDensity, GraphDistance, TemporalWeight, ReferenceCount, Hybrid(ScoreWeights), } pub struct ScoreWeights { pub keyword: f32, // 默认 0.4 pub graph: f32, // 默认 0.3 pub temporal: f32, // 默认 0.1 pub reference: f32, // 默认 0.2 } pub struct RetrieverConfig { pub scoring: ScoringStrategy, pub max_results: usize, pub min_score: f32, pub graph_depth: usize, pub weights: ScoreWeights, } pub struct RetrievalResult { pub items: Vec, pub total_found: usize, pub query: String, } pub struct ScoredItem { pub source: RetrievalSource, pub score: f32, pub breakdown: ScoreBreakdown, } pub struct ScoreBreakdown { pub keyword_score: f32, pub graph_score: f32, pub temporal_score: f32, pub reference_score: f32, } pub enum RetrievalSource { KnowledgePage(KnowledgePage), GraphEntity(GraphEntity), } pub enum RetrievalStrategy { Hybrid, KnowledgeOnly, GraphOnly, } ``` ### 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("Graph error: {0}")] GraphError(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 模块的集成 ``` ConversationMemory (src/memory/conversation.rs) │ │ add_message() │ ├─ store.save(message) ← 写到底层存储 │ ├─ if sliding_window && over_limit: │ │ messages = store.list(session) │ │ compact_config.should_compact() ← 复用 CompactConfig │ │ microcompact(&mut messages) ← 复用 microcompact() │ │ store.save(pruned) ← 写回 │ └─ return │ │ get_history() │ └─ store.list(session) │ └─ imports: llm::compact::{CompactConfig, microcompact, should_compact} ``` --- --- ## 4. 物理存储策略 ### 4.1 存储层次 ``` ┌─────────────────────────────────────────────────┐ │ MemoryRetriever │ │ (检索 + 评分,无状态) │ ├─────────────────────────────────────────────────┤ │ MemoryStore / KnowledgeStore / KnowledgeGraph │ │ (存储抽象接口,不感知存储介质) │ ├──────────────────┬──────────────────────────────┤ │ InMemoryStore │ 下游自定义实现 │ │ (HashMap) │ (FileStore / SqliteStore │ │ 进程内 volatile│ / RedisStore / ... ) │ │ 测试/原型适用 │ 生产环境适用 │ └──────────────────┴──────────────────────────────┘ ``` ### 4.2 InMemoryStore 的物理存储 **当前默认实现:** | 组件 | 数据结构 | 存储位置 | 持久化 | 生命周期 | |------|---------|---------|--------|---------| | `InMemoryStore` | `HashMap` | 进程堆内存 | ❌ | 随进程销毁 | | `InMemoryKnowledgeStore` | `HashMap` + `Vec` | 进程堆内存 | ❌ | 随进程销毁 | | `InMemoryGraph` | `HashMap` + `Vec` | 进程堆内存 | ❌ | 随进程销毁 | | `InMemoryStore.recall_stats` | `HashMap` | 进程堆内存 | ❌ | 随进程销毁 | **适用场景:** - 单元测试和集成测试 - 本地快速原型开发 - 单次会话的临时 Agent(不期望跨会话持久化) - Phase 4 开发阶段的验证 **不适合场景:** - 生产部署 - 需要跨进程/跨会话共享记忆 - 需要数据持久化和恢复 ### 4.3 持久化存储方案(下游实现) agcore 核心库**不内置**持久化实现,用户通过实现 `MemoryStore` / `KnowledgeStore` / `KnowledgeGraph` trait 对接所需后端: | 后端 | 实现建议 | 适用场景 | 复杂度 | |------|---------|---------|--------| | **JSON 文件** | MemoryStore trait → 序列化为单文件 JSON | 单机、轻量持久化 | 低 | | **SQLite** | MemoryStore + KnowledgeStore → 关系表 | 单机、中小规模 | 中 | | **PostgreSQL** | MemoryStore + KnowledgeStore → 关系表 | 多进程共享、中等规模 | 中 | | **Redis** | MemoryStore → Hash/JSON 类型 | 高速缓存、会话共享 | 低 | | **Neo4j** | KnowledgeGraph → 图数据库 | 大规模图谱检索 | 高 | **示例:FileStore 结构示意(由用户自行实现)** ``` ~/.agcore/memory/ store.json # MemoryStore 数据 knowledge.json # KnowledgeStore 数据 graph_entities.json # KnowledgeGraph 实体 graph_relations.json # KnowledgeGraph 关系 ``` ### 4.4 对下游实现的约束 MemoryStore / KnowledgeStore / KnowledgeGraph 三个 trait 对持久化实现没有任何特殊约束: - 方法签名不涉及文件路径、连接字符串等存储细节 - 所有方法均为 `async`,持久化实现可以自由选择同步(`spawn_blocking`)或异步 driver - 初始化参数在具体实现的构造函数中注入(如 `SqliteStore::new("path/to/db").await`) ### 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 等序列化框架。 --- ### 4.1 问题 所有存储组件(MemoryStore、KnowledgeStore、KnowledgeGraph)如果不设上限,会随运行时间无限增长。ConversationMemory 当前的 sliding window 只做 tool result 压缩,不删除消息条目。 ### 4.2 淘汰策略 在 `MemoryStore` trait 层提供可选的淘汰配置,上层组件按需设置: ```rust pub struct EvictionConfig { pub policy: EvictionPolicy, pub check_interval: usize, // 每写入 N 条后检查一次淘汰条件 } pub enum EvictionPolicy { None, // 不淘汰(默认,KnowledgeStore/KnowledgeGraph 适用) Ttl { ttl_secs: u64 }, // 超过存活时间淘汰(MemoryStore 通用) Capacity { max_items: usize }, // 超过容量淘汰最旧(ConversationMemory 适用) RecallBased { max_items: usize, recall_weight: f32, // 召回频率权重,默认 0.3 score_weight: f32, // 平均匹配评分权重,默认 0.5 recency_weight: f32, // 最近召回时间权重,默认 0.2 }, Hybrid { ttl_secs: Option, max_items: Option, recall: Option, }, } ``` ### 4.3 RecallBased 淘汰策略 基于记忆的实际使用价值决定淘汰优先级,而非简单的写入时间。 #### 记忆价值评分 ```rust pub struct RecallStats { pub recall_count: u64, // 累计被召回的次数 pub total_score: f64, // 累计评分(平均分 = total_score / recall_count) pub last_recall_at: i64, // 最后一次被召回的时间戳(秒) } // 记忆价值 = recall_freq * recall_weight + avg_score * score_weight + recency * recency_weight impl RecallStats { pub fn value(&self, now: i64, config: &RecallBasedConfig) -> f64 { let recency = 1.0 / (1.0 + (now - self.last_recall_at) as f64 / 86400.0); let freq = (self.recall_count as f64).ln_1p(); let avg = if self.recall_count > 0 { self.total_score / self.recall_count as f64 } else { 0.0 }; freq * config.recall_weight as f64 + avg * config.score_weight as f64 + recency * config.recency_weight as f64 } } ``` 淘汰时按 `value()` 升序排列,淘汰价值最低的记忆。 #### 召回统计的更新时机 在 `MemoryStore` trait 中增加可选方法 `record_recall()`: ```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>; /// 记录一次召回事件(可选,仅 RecallBased 淘汰策略需要) async fn record_recall(&self, id: &str, score: f32) -> Result<(), MemoryError> { // 默认空实现,不强制实现 Ok(()) } } ``` #### 召回统计的存储 `InMemoryStore` 内部额外维护一个 `HashMap`: ```rust pub struct InMemoryStore { items: Mutex>, recall_stats: Mutex>, // 召回统计 eviction: EvictionConfig, created_at: Mutex>, // 写入时间(用于 TTL) } ``` 淘汰时的完整流程: ``` save(item) │ ├─ items.insert(id, item) ├─ created_at.insert(id, now) │ └─ if writes_since_last_check >= check_interval: switch policy: Ttl → items.retain(created_at > cutoff) Capacity → items: 按 created_at 升序排列,截断到 max_items recall_stats: 同步清理 RecallBased → 每条计算 value(now) 按 value 升序排列,截断到 max_items recall_stats: 同步清理 Hybrid → 先 TTL → 再 Capacity 或 RecallBased ``` ### 4.4 MemoryRetriever 对召回统计的联动 `MemoryRetriever` 每次成功检索后,对被选中的结果调用 `store.record_recall(id, score)`: ``` retrieve(query) │ ├─ 关键词提取 → 并行召回 ├─ 评分 → 过滤 → 排序 ├─ return results │ └─ 后台(非阻塞): for item in results: store.record_recall(item.id, item.score).await; graph.record_recall(entity_id, item.score).await; ``` 这样,被频繁召回且评分高的记忆会获得高价值分,自然保留;长期不被使用或评分低的会被优先淘汰。 ### 4.3 各组件淘汰策略 | 组件 | 推荐策略 | 理由 | |------|---------|------| | **ConversationMemory** | `Capacity { max_items }` | 对话是流式的,旧消息价值递减,达到上限后淘汰最旧的消息条目 | | **MemoryStore**(通用) | `Ttl { ttl_secs }` 或 `Hybrid` | 通用存储由调用方按场景决定 | | **KnowledgeStore** | `None`(默认不淘汰) | 知识是累积的,新增不淘汰旧;如需要可按 `Hybrid` 配置 | | **KnowledgeGraph** | `None`(默认不淘汰) | 实体关系是长期资产,不应自动淘汰 | ### 4.4 ConversationMemory 淘汰行为 当前方案的 sliding window 仅压缩 tool result 内容,但不删除消息。升级后: ``` add_message(msg) │ ├─ store.save(msg) │ ├─ if eviction.policy == Capacity: │ history = store.list(session) │ while history.len() > max_items: │ oldest = history.remove(0) ← 删除最旧消息 │ store.delete(oldest.id) │ ├─ maybe_compact() ← 复用 microcompact() 做内容压缩 │ └─ return ``` 两种机制分层: - **淘汰(eviction)**:删除整条消息,控制条目总数上限 - **压缩(compaction)**:压缩剩余消息的 tool result 内容,节省 token ### 4.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.sort_by_key(|(_, v)| v.created_at); vec.truncate(*max_items); *items = vec.into_iter().collect(); } } EvictionPolicy::Hybrid { ttl_secs, max_items } => { // 先按 TTL 淘汰,再按容量淘汰 } EvictionPolicy::None => {} } } ``` --- ### 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 / Hybrid) - `InMemoryStore.save()` 内部实现淘汰检查 - 单元测试:TTL 过期淘汰、容量上限淘汰、不淘汰(None)、Hybrid 组合 - 验收:`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` trait - 实现 `InMemoryKnowledgeStore` - index 自动维护(add/update/delete 时同步) - search 基于标题/摘要/标签的关键词匹配 - 单元测试:页面 CRUD、index 一致性、搜索 - 验收:`cargo build` + `cargo test` 通过 ### Step 4:KnowledgeGraph **文件**:`src/memory/graph.rs` - 定义 `GraphEntity`、`GraphRelation` 类型 - 定义 `KnowledgeGraph` trait - 实现 `InMemoryGraph` - BFS/DFS 图遍历 - search_entities 按名称/类型匹配 - 单元测试:实体 CRUD、关系添加/删除、图遍历、搜索 - 验收:`cargo build` + `cargo test` 通过 ### Step 5:MemoryRetriever(关键词提取 + 评分机制)+ 模块整合 **文件**:`src/memory/retriever.rs`、`src/memory.rs` - 定义 `KeywordExtractor` trait + `SimpleKeywordExtractor` 默认实现(分割 + 停用词过滤) - 定义 `ScoringStrategy` 枚举(TextOverlap / GraphDistance / TemporalWeight / ReferenceCount / Hybrid) - 定义 `ScoreWeights`、`ScoredItem`、`ScoreBreakdown`、`RetrieverConfig` - 实现评分函数: - `TextOverlap`: 以原始 query 为基准,计算召回内容与 query 的 n-gram Jaccard/Dice 系数 - 图距离、时间衰减、引用计数 - 实现 `MemoryRetriever`: - `retrieve(query)`: 关键词提取 → 并行召回 → 逐条评分(以原始 query 为基准)→ 阈值过滤 → 排序截取 - 支持 `RetrievalStrategy` 切换检索通道 - 可选:启用 `ScoreBreakdown` 明细(默认 off,减少计算开销) - 在 `memory.rs` 中用 `pub use` 重导出所有公共类型 - 在 `src/lib.rs` 中声明 `pub mod memory` - 单元测试:关键词提取、各评分策略的正确性、阈值过滤、排序正确性 - 集成测试:端到端检索流程 - 验收:`cargo build` + `cargo test` 通过 --- ## 5. 风险评估 | 风险 | 概率 | 影响 | 缓解措施 | |------|------|------|---------| | KnowledgeStore 的 keyword 检索在大规模下效率低 | 中 | 中 | search 算法可替换——下游通过实现 trait 使用 BM25/TF-IDF | | InMemoryGraph 不支持复杂图查询 | 中 | 中 | `depth` 参数限制遍历深度;生产场景替换为专用图数据库 | | ConversationMemory 与 compact 耦合引入循环依赖 | 低 | 高 | 仅引用 `CompactConfig`(纯数据结构)和 `microcompact()`(纯函数),不引用 cycle.rs | | chrono 增加依赖体积 | 低 | 低 | chrono 已是 Rust 生态标准,且仅用于时间戳 | | Phase 4 集成时发现 Memory trait 设计不合理 | 低 | 高 | 按最小可行接口设计,预留扩展空间 | --- ## 6. 验收标准 - [ ] `MemoryStore` trait 定义清晰的 4 个方法,`InMemoryStore` 通过单元测试 - [ ] `EvictionConfig` 支持 None / Ttl / Capacity / RecallBased / Hybrid 五种策略 - [ ] `EvictionPolicy::RecallBased` 按记忆价值公式正确计算并淘汰低价值记忆 - [ ] `InMemoryStore` 在 save() 后正确执行 TTL 淘汰、容量淘汰、基于召回价值淘汰 - [ ] `InMemoryStore::record_recall()` 记录召回次数和评分 - [ ] `MemoryRetriever` 每次检索后调用 `record_recall()` 更新召回统计 - [ ] `ConversationMemory` 支持 sliding window 和 full 两种策略 - [ ] `ConversationMemory` sliding window 模式下达到上限后删除最旧消息条目 - [ ] `ConversationMemory` 正确复用 `llm::compact` 的压缩逻辑 - [ ] `KnowledgeStore` trait + `InMemoryKnowledgeStore` 支持页面 CRUD 和 index 维护 - [ ] `KnowledgeGraph` trait + `InMemoryGraph` 支持实体 CRUD、关系管理和图遍历 - [ ] `MemoryRetriever` 组合 KnowledgeStore + KnowledgeGraph 返回统一检索结果 - [ ] 无 embedding 相关依赖(不引入 fastembed、pgvector、qdrant 等 crate) - [ ] 模块结构符合项目惯例:`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 + KnowledgeGraph + 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 + graph 混合检索 | 文件系统搜索不适合所有后端 |