docs(记忆系统): 添加记忆系统设计方案
This commit is contained in:
@@ -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<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 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 混合检索 | 文件系统搜索不适合所有后端 |
|
||||
Reference in New Issue
Block a user