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

603 lines
19 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 可自主编译和维护知识页面
- **检索器(MemoryRetriever)** — 单通道关键词检索,提供统一的记忆查找入口
### 1.3 设计原则
- **不引入 embedding 依赖** — 采用 Karpathy's LLM Wiki 模式的 index + keyword 检索,替代传统向量检索
- **trait + 轻量默认实现** — 存储抽象接口提供纯内存默认实现(InMemoryStore),满足原型和测试需求
- **模块间松耦合** — 记忆系统与 LlmCycle 的集成推迟到 Phase 4 Agent RuntimePhase 3 只定义接口和数据操作
---
## 2. 需求分析
### 2.1 功能需求
| ID | 需求 | 优先级 | 说明 |
|----|------|--------|------|
| F1 | MemoryStore 通用键值存储 | P0 | save/get/delete/list |
| F2 | 对话消息管理 | P0 | 按 session 管理,支持 sliding window / full |
| F3 | 知识页面 CRUD | P1 | 创建/更新/删除/检索知识页面 |
| F4 | 知识页面关键词检索 | P1 | 基于标题/摘要/标签的关键词匹配 |
| F5 | 知识页面索引维护 | P1 | 维护可遍历的内容目录(index) |
| F6 | 可插拔后端 | P0 | MemoryStore 通过 trait 抽象,下游可实现自定义后端 |
| F7 | 记忆淘汰 | P1 | 支持 TTL 过期淘汰、容量上限淘汰 |
| F8 | 消息条目级淘汰 | P1 | ConversationMemory 达到上限后删除最旧消息 |
### 2.2 非功能需求
| ID | 需求 | 说明 |
|----|------|------|
| NF1 | 零 embedding 依赖 | 核心库不引入任何向量数据库或 embedding 模型依赖 |
| NF2 | 错误体系完善 | MemoryError 枚举,支持 is_recoverable() 分类 |
| NF3 | 线程安全 | 所有存储实现满足 Send + Sync |
| NF4 | 异步 API | 所有 IO 操作为 async |
| NF5 | 模块化 | 各组件独立可替换 |
---
## 3. 方案设计
### 3.1 总体架构
```mermaid
graph TB
subgraph Retrieval["检索层"]
MR["MemoryRetriever"]
end
subgraph Logic["逻辑层"]
CM["ConversationMemory"]
KS["KnowledgeStore"]
end
subgraph Storage["存储层"]
MS["MemoryStore (trait)"]
IMS["InMemoryStore (默认)"]
end
MR --> KS
CM --> MS
KS --> MS
MS --> IMS
```
### 3.2 模块结构
```
src/
memory.rs # 模块根:pub mod + pub use 重导出
memory/
store.rs # MemoryStore trait + InMemoryStore
conversation.rs # ConversationMemory(对话管理)
knowledge.rs # KnowledgeStore(具体 struct
retriever.rs # MemoryRetriever(单通道检索)
error.rs # MemoryError
types.rs # 核心数据类型
```
### 3.3 接口定义
#### MemoryStore — 底层存储抽象
```rust
#[async_trait]
pub trait MemoryStore: Send + Sync {
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 — 具体 struct
```rust
pub struct KnowledgeStore {
store: Arc<dyn MemoryStore>,
index: Mutex<Vec<PageIndexEntry>>,
}
impl KnowledgeStore {
pub fn new(store: Arc<dyn MemoryStore>) -> Self { ... }
pub async fn add_page(&self, page: KnowledgePage) -> Result<(), MemoryError> { ... }
pub async fn get_page(&self, id: &str) -> Result<Option<KnowledgePage>, MemoryError> { ... }
pub async fn update_page(&self, page: KnowledgePage) -> Result<(), MemoryError> { ... }
pub async fn delete_page(&self, id: &str) -> Result<(), MemoryError> { ... }
pub async fn search(&self, query: &str) -> Result<Vec<KnowledgePage>, MemoryError> { ... }
pub async fn get_index(&self) -> Result<Vec<PageIndexEntry>, MemoryError> { ... }
}
```
- `search` 使用简单的字符串包含匹配标题/摘要/标签
- index 在 add/update/delete 时自动维护
#### MemoryRetriever — 简化版检索器
```rust
pub struct MemoryRetriever {
knowledge_store: KnowledgeStore,
config: RetrieverConfig,
}
pub struct RetrieverConfig {
pub max_results: usize, // 默认 20
pub min_score: f32, // 默认 0.1
}
pub struct RetrievalResult {
pub items: Vec<ScoredItem>,
pub query: String,
}
pub struct ScoredItem {
pub page: KnowledgePage,
pub score: f32, // TextOverlap 评分 [0.0, 1.0]
}
```
检索流程:
```mermaid
flowchart TD
A["输入: query"]
B["1. 关键词提取(split + 过滤停用词)"]
C["2. KnowledgeStore.search(keywords)"]
D["3. TextOverlap 评分"]
E["4. 过滤 score < min_score"]
F["5. 降序排序 → 截取 top-N"]
G["6. 返回 RetrievalResult"]
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
```
关键词提取在 MemoryRetriever 内部简单实现:按空格/标点分割 → 过滤单字符和停用词 → 返回关键词列表。TextOverlap 计算 query 与页面标题/摘要/内容的 n-gram 重叠度(基于 Dice 系数)。
### 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>,
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 enum MemoryStrategy {
SlidingWindow,
Full,
}
```
### 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("Retrieval error: {0}")]
RetrievalError(String),
}
impl MemoryError {
pub fn is_recoverable(&self) -> bool {
matches!(self, Self::NotFound(_) | Self::RetrievalError(_))
}
}
```
### 3.6 ConversationMemory 与 compact 模块的集成
```mermaid
flowchart TD
subgraph CM["ConversationMemory (src/memory/conversation.rs)"]
A["add_message()"]
A1["store.save(message) ← 写到底层存储"]
C{"sliding_window && over_limit?"}
D["messages = store.list(session)"]
E["compact_config.should_compact() ← 复用 CompactConfig"]
F["microcompact(&mut messages) ← 复用 microcompact()"]
G["store.save(pruned) ← 写回"]
H["return"]
I["get_history()"]
J["store.list(session)"]
A --> A1
A1 --> C
C -->|是| D
D --> E
E --> F
F --> G
G --> H
C -->|否| H
I --> J
end
K["imports: llm::compact::{CompactConfig, microcompact, should_compact}"]
```
---
## 4. 物理存储策略
### 4.1 存储层次
```mermaid
graph TB
subgraph RetrievalLayer["检索层"]
MR["MemoryRetriever (检索 + 评分,无状态)"]
end
subgraph AbstractionLayer["存储抽象层"]
ABS["MemoryStore\n(存储抽象接口,不感知存储介质)"]
end
subgraph ImplementationLayer["实现层"]
IMS["InMemoryStore (HashMap)\n进程内 volatile\n测试/原型适用"]
CUSTOM["下游自定义实现\nFileStore / SqliteStore / RedisStore / ...\n生产环境适用"]
end
MR --> ABS
ABS --> IMS
ABS --> CUSTOM
```
### 4.2 InMemoryStore 的物理存储
| 组件 | 数据结构 | 存储位置 | 持久化 | 生命周期 |
|------|---------|---------|--------|---------|
| `InMemoryStore` | `HashMap<String, MemoryItem>` | 进程堆内存 | ❌ | 随进程销毁 |
| `KnowledgeStore` | 基于 `InMemoryStore` + `Vec<PageIndexEntry>` | 进程堆内存 | ❌ | 随进程销毁 |
**适用场景:**
- 单元测试和集成测试
- 本地快速原型开发
- 单次会话的临时 Agent
**不适合场景:**
- 生产部署
- 需要跨进程/跨会话共享记忆
- 需要数据持久化和恢复
### 4.3 持久化存储方案(下游实现)
agcore 核心库**不内置**持久化实现,用户通过实现 `MemoryStore` trait 对接所需后端:
| 后端 | 实现建议 | 适用场景 | 复杂度 |
|------|---------|---------|--------|
| **JSON 文件** | MemoryStore trait → 序列化为单文件 JSON | 单机、轻量持久化 | 低 |
| **SQLite** | MemoryStore → 关系表 | 单机、中小规模 | 中 |
| **PostgreSQL** | MemoryStore → 关系表 | 多进程共享、中等规模 | 中 |
| **Redis** | MemoryStore → Hash/JSON 类型 | 高速缓存、会话共享 | 低 |
### 4.4 对下游实现的约束
`MemoryStore` trait 对持久化实现无特殊约束:
- 方法签名不涉及文件路径、连接字符串等存储细节
- 所有方法均为 `async`,持久化实现可自由选择同步(`spawn_blocking`)或异步 driver
- 初始化参数在具体实现的构造函数中注入
### 4.5 序列化
核心类型均实现 `Serialize` / `Deserialize`(通过 `#[derive(serde)]`),便于持久化实现直接复用:
```rust
#[derive(Serialize, Deserialize)]
pub struct MemoryItem { ... }
#[derive(Serialize, Deserialize)]
pub struct KnowledgePage { ... }
// ...
```
所有类型基于 `serde_json::Value` 作为 metadata 类型,不引入 protobuf/msgpack 等序列化框架。
---
## 5. 淘汰策略
### 5.1 问题
所有存储组件如果不设上限,会随运行时间无限增长。ConversationMemory 当前的 sliding window 只做 tool result 压缩,不删除消息条目。
### 5.2 淘汰策略
`MemoryStore` trait 层提供可选的淘汰配置,上层组件按需设置:
```rust
pub struct EvictionConfig {
pub policy: EvictionPolicy,
pub check_interval: usize, // 每写入 N 条后检查一次淘汰条件
}
pub enum EvictionPolicy {
None, // 不淘汰(默认)
Ttl { ttl_secs: u64 }, // 超过存活时间淘汰
Capacity { max_items: usize },// 超过容量淘汰最旧
}
```
`InMemoryStore``save()` 后检查淘汰条件:
```mermaid
flowchart TD
A["save(item)"]
B["items.insert(id, item)"]
C{"writes_since_last_check >= check_interval?"}
D{"policy 类型"}
E["Ttl → items.retain(created_at > cutoff)"]
F["Capacity → 按 created_at 升序排列,截断到 max_items"]
G["None → 不淘汰"]
A --> B
B --> C
C -->|是| D
C -->|否| G
D -->|Ttl| E
D -->|Capacity| F
D -->|None| G
```
### 5.3 各组件淘汰策略
| 组件 | 推荐策略 | 理由 |
|------|---------|------|
| **ConversationMemory** | `Capacity { max_items }` | 对话是流式的,旧消息价值递减,达到上限后淘汰最旧的消息条目 |
| **MemoryStore**(通用) | `Ttl { ttl_secs }` | 通用存储由调用方按场景决定 |
| **KnowledgeStore** | `None`(默认不淘汰) | 知识是累积的,新增不淘汰旧 |
### 5.4 ConversationMemory 淘汰行为
```mermaid
flowchart TD
A["add_message(msg)"]
B["store.save(msg)"]
C{"eviction.policy == Capacity?"}
D["history = store.list(session)"]
E{"while history.len() > max_items"}
F["oldest = history.remove(0) ← 删除最旧消息"]
G["store.delete(oldest.id)"]
H["maybe_compact() ← 复用 microcompact() 做内容压缩"]
I["return"]
A --> B
B --> C
C -->|是| D
D --> E
E -->|是| F
F --> G
G --> E
E -->|否| H
C -->|否| H
H --> I
```
两种机制分层:
- **淘汰(eviction)**:删除整条消息,控制条目总数上限
- **压缩(compaction**:压缩剩余消息的 tool result 内容,节省 token
### 5.5 InMemoryStore 的淘汰实现
```rust
impl InMemoryStore {
pub fn with_eviction(config: EvictionConfig) -> Self { ... }
}
// 在 save() 内部:
async fn save(&self, item: MemoryItem) -> Result<(), MemoryError> {
self.items.lock().insert(item.id.clone(), item);
self.maybe_evict().await;
}
async fn maybe_evict(&self) {
match &self.eviction.policy {
EvictionPolicy::Ttl { ttl_secs } => {
let cutoff = Utc::now() - Duration::seconds(*ttl_secs as i64);
self.items.lock().retain(|_, v| v.created_at > cutoff);
}
EvictionPolicy::Capacity { max_items } => {
let mut items = self.items.lock();
if items.len() > *max_items {
let mut vec: Vec<_> = items.drain().collect();
vec.select_nth_unstable_by(
*max_items,
|a, b| b.1.created_at.cmp(&a.1.created_at),
);
vec.truncate(*max_items);
*items = vec.into_iter().collect();
}
}
EvictionPolicy::None => {}
}
}
```
---
## 6. 实现计划
### Step 1:基础类型 + MemoryStore(含淘汰机制)
**文件**`src/memory.rs``src/memory/types.rs``src/memory/error.rs``src/memory/store.rs`
- 创建 `memory.rs` + `memory/` 目录
- 定义 `MemoryItem``MemoryFilter``MemoryStrategy` 类型
- 定义 `MemoryError` 枚举
- 定义 `MemoryStore` trait 与 `InMemoryStore` 实现
- 定义 `EvictionConfig``EvictionPolicy`None / Ttl / Capacity
- `InMemoryStore.save()` 内部实现淘汰检查
- 单元测试:TTL 过期淘汰、容量上限淘汰、不淘汰(None)
- 验收:`cargo build` + `cargo test` 通过
**依赖**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` 具体 struct(非 trait
- 内部使用 `Arc<dyn MemoryStore>` 存储数据
- index 自动维护(add/update/delete 时同步)
- search 基于标题/摘要/标签的关键词匹配
- 单元测试:页面 CRUD、index 一致性、搜索
- 验收:`cargo build` + `cargo test` 通过
### Step 4MemoryRetriever + 模块整合
**文件**`src/memory/retriever.rs``src/memory.rs`
- 实现内存检索器 `MemoryRetriever`
- 内部关键词提取:split + 过滤停用词
- TextOverlap 评分:基于 Dice 系数计算 query 与页面的文本重叠度
- 阈值过滤 → 排序 → 截取 top-N
-`memory.rs` 中用 `pub use` 重导出所有公共类型
-`src/lib.rs` 中声明 `pub mod memory`
- 单元测试:关键词提取、TextOverlap 评分正确性、阈值过滤、排序正确性
- 集成测试:端到端检索流程
- 验收:`cargo build` + `cargo test` 通过
---
## 7. 风险评估
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|---------|
| KnowledgeStore 的 keyword 检索在大规模下效率低 | 中 | 中 | MemoryStore 实现可替换——下游可使用 SQLite FTS 等更高效的后端 |
| ConversationMemory 与 compact 耦合引入循环依赖 | 低 | 高 | 仅引用 `CompactConfig`(纯数据结构)和 `microcompact()`(纯函数),不引用 cycle.rs |
| chrono 增加依赖体积 | 低 | 低 | chrono 已是 Rust 生态标准,且仅用于时间戳 |
| Phase 4 集成时发现 Memory 设计不合理 | 低 | 高 | 按最小可行接口设计,预留扩展空间 |
---
## 8. 验收标准
- [ ] `MemoryStore` trait + `InMemoryStore` 通过单元测试
- [ ] `EvictionConfig` 支持 None / Ttl / Capacity 三种策略
- [ ] `InMemoryStore` 在 save() 后正确执行 TTL 淘汰和容量淘汰
- [ ] `ConversationMemory` 支持 sliding window 和 full 两种策略
- [ ] `ConversationMemory` sliding window 模式下达到上限后删除最旧消息条目
- [ ] `ConversationMemory` 正确复用 `llm::compact` 的压缩逻辑
- [ ] `KnowledgeStore` 支持页面 CRUD 和 index 维护
- [ ] `MemoryRetriever` 支持基于 TextOverlap 的知识检索
- [ ] 无 embedding 相关依赖
- [ ] 模块结构:`memory.rs` + `memory/` 目录 + `pub use` 重导出
- [ ] `MemoryError` 枚举完善,支持 `is_recoverable()`
- [ ] 所有公开 API 有文档注释(`///`
- [ ] `cargo build``cargo test` 通过
- [ ] 单个文件不超过 300 行
---
## 附录:与 Karpathy's LLM Wiki 的关系
本方案受 Karpathy's LLM Wiki 模式启发,但做了一些调整以适应 Agent 核心库的定位:
| Karpathy LLM Wiki | AG Core Memory System | 差异原因 |
|-------------------|----------------------|---------|
| 三层:Raw → Wiki → Schema | 三组件:MemoryStore → ConversationMemory + KnowledgeStore + MemoryRetriever | Agent 场景需要区分对话记忆和知识记忆 |
| index.md + log.md | PageIndexEntry(同 index.md+ 无 logPhase 4 Agent 负责) | 日志是工作流层职责,非存储层 |
| LLM Agent 全权维护 | KnowledgeStore 提供数据接口,Phase 4 Agent 编排工作流 | core 只提供存储能力,不编排 |
| 文件系统为后端 | MemoryStore trait 抽象后端 | 可插拔设计需要 trait 抽象 |
| 基于文件系统搜索 | index + keyword 检索 | 文件系统搜索不适合所有后端 |