22 KiB
记忆系统设计方案
设计日期: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 可自主编译和维护知识页面
- 检索器(MemoryRetriever) — 单通道关键词检索,提供统一的记忆查找入口
1.3 设计原则
- 不引入 embedding 依赖 — 采用 Karpathy's LLM Wiki 模式的 index + keyword 检索,替代传统向量检索
- trait + 轻量默认实现 — 存储抽象接口提供纯内存默认实现(InMemoryStore),满足原型和测试需求
- 模块间松耦合 — 记忆系统与 LlmCycle 的集成推迟到 Phase 4 Agent Runtime,Phase 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 总体架构
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 — 底层存储抽象
#[async_trait]
pub trait MemoryStore: Send + Sync {
/// 保存/覆盖一个 MemoryItem(upsert 语义)。
/// - 如果 id 不存在,则插入新条目
/// - 如果 id 已存在,则覆盖旧条目
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 — 默认实现
pub struct InMemoryStore {
items: Mutex<HashMap<String, MemoryItem>>,
}
基于 HashMap<String, MemoryItem> + Mutex,纯内存,线程安全。
ConversationMemory — 对话记忆
pub struct ConversationMemory {
store: Arc<dyn MemoryStore>,
session_id: String,
config: ConversationMemoryConfig,
messages: Vec<OpenaiChatMessage>, // 热缓存,供 compact 直接操作
compact_state: CompactState, // 断路器状态
}
pub struct ConversationMemoryConfig {
pub strategy: MemoryStrategy, // sliding_window | full
pub max_turns: usize, // sliding window 的最大轮数
pub compact_config: Option<CompactConfig>, // 复用现有压缩配置
}
add_message(msg)写入热缓存self.messages,同时通过store.save()持久化到后端get_history()优先从热缓存返回,缓存未命中时从 store 恢复compact直接在self.messages上调用should_compact()和microcompact()- 压缩后同步回
store - 使用了
llm::types::OpenaiChatMessage作为内部消息类型 - 复用现有
CompactConfig(context_window, reserved_tokens, keep_recent)和CompactState
KnowledgeStore — 具体 struct
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(标题/摘要/标签),全文 content 搜索走 MemoryStore- index 在 add/update/delete 时自动维护,也支持通过
rebuild_index()手动重建 KnowledgeStore以"knowledge_{page_id}"格式作为MemoryItem.id前缀,前缀字符串提取为常量const KNOWLEDGE_PREFIX: &str = "knowledge_"
提供 rebuild_index() 方法修复 index 与 store 的不同步问题:
impl KnowledgeStore {
/// 从 MemoryStore 重建 index(修复 index 与 store 的不同步问题)
pub async fn rebuild_index(&self) -> Result<(), MemoryError> {
let items = self.store.list(&MemoryFilter {
prefix: Some("knowledge_".into()),
since: None,
offset: None,
limit: None,
}).await?;
let mut index = self.index.lock();
index.clear();
for item in items {
let page: KnowledgePage = serde_json::from_str(&item.content)
.map_err(|e| MemoryError::Serialization(e.to_string()))?;
index.push(PageIndexEntry {
id: page.id.clone(),
title: page.title,
summary: page.summary,
tags: page.tags,
updated_at: page.updated_at,
});
}
Ok(())
}
}
MemoryRetriever — 简化版检索器
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]
}
检索流程:
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 系数)。
TextOverlap 评分基于 Dice 系数(字符 bigram):
dice(query, text) = 2 × |bigrams(query) ∩ bigrams(text)| / (|bigrams(query)| + |bigrams(text)|)
多字段加权: score = title_dice × 0.5 + summary_dice × 0.3 + content_dice × 0.2
中文场景退化:当前版本按字符级 bigram 处理中文,不依赖分词器。
已知限制:关键词提取基于空格/标点分割,对中文不做语义分词。 中文场景按字符 bigram 参与 TextOverlap 计算,精度低于专业分词方案。 如有更高精度需求,可替换 MemoryRetriever 的关键词提取逻辑。
3.4 核心数据类型
pub struct MemoryItem {
pub id: String,
pub content: String,
pub metadata: serde_json::Value,
pub created_at: time::OffsetDateTime,
}
pub struct MemoryFilter {
pub prefix: Option<String>,
pub since: Option<time::OffsetDateTime>,
pub offset: Option<usize>, // 跳过前 N 条
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: time::OffsetDateTime,
pub updated_at: time::OffsetDateTime,
}
pub struct PageIndexEntry {
pub id: String,
pub title: String,
pub summary: String,
pub tags: Vec<String>,
pub updated_at: time::OffsetDateTime,
}
pub enum MemoryStrategy {
SlidingWindow,
Full,
}
3.5 错误类型
#[derive(Debug, thiserror::Error)]
pub enum MemoryError {
#[error("Item not found: {0}")]
NotFound(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 模块的集成
flowchart TD
subgraph CM["ConversationMemory"]
A["add_message(msg)"]
A1["self.messages.push(msg) ← 热缓存"]
A2["store.save(to_item(msg)) ← 冷持久化"]
B{"len(messages) > max_turns?"}
C["should_compact(&messages, &config, &state) ← 直接在热缓存上操作"]
D["microcompact(&mut messages, keep_recent) ← 复用 microcompact()"]
E["sync_to_store() ← 压缩后同步回 store"]
F["return"]
G["get_history()"]
H["从 self.messages 返回"]
I["从 store 恢复 → 重建热缓存"]
A --> A1
A1 --> A2
A2 --> B
B -->|是| C
C --> D
D --> E
E --> F
B -->|否| F
G --> H
H -->|缓存未命中| I
end
J["依赖: llm::compact::{CompactConfig, CompactState, should_compact, microcompact}"]
4. 物理存储策略
4.1 存储层次
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)]),便于持久化实现直接复用:
#[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 层提供可选的淘汰配置,上层组件按需设置:
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() 后检查淘汰条件:
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 淘汰行为
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 的淘汰实现
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枚举 - 定义
MemoryStoretrait 与InMemoryStore实现 - 定义
EvictionConfig、EvictionPolicy(None / Ttl / Capacity) InMemoryStore.save()内部实现淘汰检查- 单元测试:TTL 过期淘汰、容量上限淘汰、不淘汰(None)
- 验收:
cargo build+cargo test通过
依赖:time(日期时间,启用 serde feature)、serde(序列化)
Step 2:ConversationMemory(含消息淘汰)
文件:src/memory/conversation.rs
- 定义
ConversationMemoryConfig、MemoryStrategy - 实现
ConversationMemoryadd_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具体 struct(非 trait)- 内部使用
Arc<dyn MemoryStore>存储数据 - index 自动维护(add/update/delete 时同步)
- search 基于标题/摘要/标签的关键词匹配
- 内部使用
- 单元测试:页面 CRUD、index 一致性、搜索
- 验收:
cargo build+cargo test通过
Step 4:MemoryRetriever + 模块整合
文件:src/memory/retriever.rs、src/memory.rs
- 实现内存检索器
MemoryRetriever- 内部关键词提取:split + 过滤停用词
- TextOverlap 评分:基于 Dice 系数计算 query 与页面的文本重叠度
- 阈值过滤 → 排序 → 截取 top-N
- 在
memory.rs中用pub use分层重导出:- 高频类型(大多数下游需要):
MemoryStore、InMemoryStore、ConversationMemory、KnowledgeStore、MemoryRetriever、MemoryError - 低频类型(配置/高级使用):
MemoryItem、MemoryFilter、MemoryStrategy、KnowledgePage、PageIndexEntry、EvictionConfig、EvictionPolicy、ConversationMemoryConfig、RetrieverConfig、RetrievalResult、ScoredItem
- 高频类型(大多数下游需要):
- 在
src/lib.rs中声明pub mod memory - 单元测试:关键词提取、TextOverlap 评分正确性、阈值过滤、排序正确性
- 集成测试:端到端检索流程
- 验收:
cargo build+cargo test通过
7. 风险评估
| 风险 | 概率 | 影响 | 缓解措施 |
|---|---|---|---|
| KnowledgeStore 的 keyword 检索在大规模下效率低 | 中 | 中 | MemoryStore 实现可替换——下游可使用 SQLite FTS 等更高效的后端 |
| ConversationMemory 与 compact 耦合引入循环依赖 | 低 | 高 | 仅引用 CompactConfig(纯数据结构)和 microcompact()(纯函数),不引用 cycle.rs |
| time 增加依赖体积 | 低 | 低 | time 是 Rust 官方维护的时间库,体积小于 chrono |
| Phase 4 集成时发现 Memory 设计不合理 | 低 | 高 | 按最小可行接口设计,预留扩展空间 |
8. 验收标准
MemoryStoretrait +InMemoryStore通过单元测试EvictionConfig支持 None / Ttl / Capacity 三种策略InMemoryStore在 save() 后正确执行 TTL 淘汰和容量淘汰ConversationMemory支持 sliding window 和 full 两种策略ConversationMemorysliding 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)+ 无 log(Phase 4 Agent 负责) | 日志是工作流层职责,非存储层 |
| LLM Agent 全权维护 | KnowledgeStore 提供数据接口,Phase 4 Agent 编排工作流 | core 只提供存储能力,不编排 |
| 文件系统为后端 | MemoryStore trait 抽象后端 | 可插拔设计需要 trait 抽象 |
| 基于文件系统搜索 | index + keyword 检索 | 文件系统搜索不适合所有后端 |