From 8573c6eb92d65ac5c7013c17a558f6feb3c10c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=B6=9B?= Date: Sun, 7 Jun 2026 13:24:49 +0800 Subject: [PATCH] =?UTF-8?q?docs(=E8=AE=B0=E5=BF=86=E7=B3=BB=E7=BB=9F):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AE=B0=E5=BF=86=E7=B3=BB=E7=BB=9F=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/6-memory-system.md | 923 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 923 insertions(+) create mode 100644 docs/6-memory-system.md diff --git a/docs/6-memory-system.md b/docs/6-memory-system.md new file mode 100644 index 0000000..55a185a --- /dev/null +++ b/docs/6-memory-system.md @@ -0,0 +1,923 @@ +# 记忆系统设计方案 + +> 设计日期: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 混合检索 | 文件系统搜索不适合所有后端 |