Files
agcore/docs/6-memory-system.md
T

924 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 记忆系统设计方案
> 设计日期:2026-06-07
> 状态:待实现
---
## 1. 背景与目标
### 1.1 背景
AG Core 已完成 Phase 0LLM 调用周期)、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 RuntimePhase 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<Option<MemoryItem>, MemoryError>;
async fn delete(&self, id: &str) -> Result<(), MemoryError>;
async fn list(&self, filter: &MemoryFilter) -> Result<Vec<MemoryItem>, MemoryError>;
}
```
#### InMemoryStore — 默认实现
```rust
pub struct InMemoryStore {
items: Mutex<HashMap<String, MemoryItem>>,
}
```
基于 `HashMap<String, MemoryItem>` + `Mutex`,纯内存,线程安全。
#### ConversationMemory — 对话记忆
```rust
pub struct ConversationMemory {
store: Arc<dyn MemoryStore>,
session_id: String,
config: ConversationMemoryConfig,
}
pub struct ConversationMemoryConfig {
pub strategy: MemoryStrategy, // sliding_window | full
pub max_turns: usize, // sliding window 的最大轮数
pub compact_config: Option<CompactConfig>, // 复用现有压缩配置
}
```
- 基于 `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<Option<KnowledgePage>, 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<Vec<KnowledgePage>, MemoryError>;
async fn get_index(&self) -> Result<Vec<PageIndexEntry>, MemoryError>;
}
```
- `add_page` 自动更新 index
- `search` 基于页面标题/摘要/标签的关键词匹配(不依赖 embedding)
- `get_index` 返回所有页面的摘要目录(类似 LLM Wiki 的 index.md
#### InMemoryKnowledgeStore — 知识库默认实现
```rust
pub struct InMemoryKnowledgeStore {
pages: Mutex<HashMap<String, KnowledgePage>>,
index: Mutex<Vec<PageIndexEntry>>,
}
```
- `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<Option<GraphEntity>, 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<Vec<ScoredEntity>, MemoryError>;
async fn find_by_keywords(&self, keywords: &[String]) -> Result<Vec<GraphEntity>, MemoryError>;
}
```
- `find_by_keywords()` — 用关键词匹配实体的 `name`(精准/前缀匹配)和 `tags`(完全匹配),不模糊搜索全字段
- `get_related()` — 找到匹配实体后,按 `depth` 遍历关联实体,同时返回关联关系
- `get_related()` 按指定深度遍历邻居节点
- `search_entities()` 按实体名称/类型搜索
#### InMemoryGraph — 知识图谱默认实现
```rust
pub struct InMemoryGraph {
entities: Mutex<HashMap<String, GraphEntity>>,
relations: Mutex<Vec<GraphRelation>>,
}
```
- 图遍历使用 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<String>;
}
// 默认实现:按非字母数字字符分割,过滤停用词和单字符词
pub struct SimpleKeywordExtractor {
stop_words: HashSet<String>,
}
```
##### RetrievalResult — 统一召回结果
```rust
pub struct MemoryRetriever {
knowledge_store: Arc<dyn KnowledgeStore>,
knowledge_graph: Arc<dyn KnowledgeGraph>,
keyword_extractor: Arc<dyn KeywordExtractor>,
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<ScoredItem>,
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<chrono::Utc>,
}
pub struct MemoryFilter {
pub prefix: Option<String>,
pub since: Option<chrono::DateTime<chrono::Utc>>,
pub limit: Option<usize>,
}
pub struct KnowledgePage {
pub id: String,
pub title: String,
pub summary: String,
pub content: String,
pub tags: Vec<String>,
pub references: Vec<String>, // 交叉引用的页面 ID 列表
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
pub struct PageIndexEntry {
pub id: String,
pub title: String,
pub summary: String,
pub tags: Vec<String>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
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<ScoredItem>,
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<String, MemoryItem>` | 进程堆内存 | ❌ | 随进程销毁 |
| `InMemoryKnowledgeStore` | `HashMap<String, KnowledgePage>` + `Vec<PageIndexEntry>` | 进程堆内存 | ❌ | 随进程销毁 |
| `InMemoryGraph` | `HashMap<String, GraphEntity>` + `Vec<GraphRelation>` | 进程堆内存 | ❌ | 随进程销毁 |
| `InMemoryStore.recall_stats` | `HashMap<String, RecallStats>` | 进程堆内存 | ❌ | 随进程销毁 |
**适用场景:**
- 单元测试和集成测试
- 本地快速原型开发
- 单次会话的临时 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<u64>,
max_items: Option<usize>,
recall: Option<RecallBasedConfig>,
},
}
```
### 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<Option<MemoryItem>, MemoryError>;
async fn delete(&self, id: &str) -> Result<(), MemoryError>;
async fn list(&self, filter: &MemoryFilter) -> Result<Vec<MemoryItem>, MemoryError>;
/// 记录一次召回事件(可选,仅 RecallBased 淘汰策略需要)
async fn record_recall(&self, id: &str, score: f32) -> Result<(), MemoryError> {
// 默认空实现,不强制实现
Ok(())
}
}
```
#### 召回统计的存储
`InMemoryStore` 内部额外维护一个 `HashMap<String, RecallStats>`
```rust
pub struct InMemoryStore {
items: Mutex<HashMap<String, MemoryItem>>,
recall_stats: Mutex<HashMap<String, RecallStats>>, // 召回统计
eviction: EvictionConfig,
created_at: Mutex<HashMap<String, i64>>, // 写入时间(用于 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 2ConversationMemory(含消息淘汰)
**文件**`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 3KnowledgeStore
**文件**`src/memory/knowledge.rs`
- 定义 `KnowledgePage``PageIndexEntry` 类型
- 定义 `KnowledgeStore` trait
- 实现 `InMemoryKnowledgeStore`
- index 自动维护(add/update/delete 时同步)
- search 基于标题/摘要/标签的关键词匹配
- 单元测试:页面 CRUD、index 一致性、搜索
- 验收:`cargo build` + `cargo test` 通过
### Step 4KnowledgeGraph
**文件**`src/memory/graph.rs`
- 定义 `GraphEntity``GraphRelation` 类型
- 定义 `KnowledgeGraph` trait
- 实现 `InMemoryGraph`
- BFS/DFS 图遍历
- search_entities 按名称/类型匹配
- 单元测试:实体 CRUD、关系添加/删除、图遍历、搜索
- 验收:`cargo build` + `cargo test` 通过
### Step 5MemoryRetriever(关键词提取 + 评分机制)+ 模块整合
**文件**`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+ 无 logPhase 4 Agent 负责) | 日志是工作流层职责,非存储层 |
| LLM Agent 全权维护 | KnowledgeStore 提供数据接口,Phase 4 Agent 编排工作流 | core 只提供存储能力,不编排 |
| 文件系统为后端 | MemoryStore trait 抽象后端 | 可插拔设计需要 trait 抽象 |
| 基于文件系统搜索 | index + keyword + graph 混合检索 | 文件系统搜索不适合所有后端 |