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

19 KiB
Raw Blame History

记忆系统设计方案

设计日期: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 总体架构

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 {
    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,
}

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() 裁剪
  • 复用现有 CompactConfigcontext_window, reserved_tokens, keep_recent

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 在 add/update/delete 时自动维护

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 系数)。

3.4 核心数据类型

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 错误类型

#[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 模块的集成

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 存储层次

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 },// 超过容量淘汰最旧
}

InMemoryStoresave() 后检查淘汰条件:

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.rssrc/memory/types.rssrc/memory/error.rssrc/memory/store.rs

  • 创建 memory.rs + memory/ 目录
  • 定义 MemoryItemMemoryFilterMemoryStrategy 类型
  • 定义 MemoryError 枚举
  • 定义 MemoryStore trait 与 InMemoryStore 实现
  • 定义 EvictionConfigEvictionPolicyNone / Ttl / Capacity
  • InMemoryStore.save() 内部实现淘汰检查
  • 单元测试:TTL 过期淘汰、容量上限淘汰、不淘汰(None)
  • 验收:cargo build + cargo test 通过

依赖chrono(日期时间)、serde(序列化)

Step 2ConversationMemory(含消息淘汰)

文件src/memory/conversation.rs

  • 定义 ConversationMemoryConfigMemoryStrategy
  • 实现 ConversationMemory
    • add_message() → 写入 MemoryStore → 触发容量淘汰(删除最旧消息)
    • 复用 llm::compactCompactConfigmicrocompact() 做内容压缩
  • 单元测试:sliding window 消息淘汰、tool result 内容压缩、full 模式、空 session
  • 验收:cargo build + cargo test 通过

依赖MemoryStore(含 EvictionConfig+ llm::compact

Step 3KnowledgeStore

文件src/memory/knowledge.rs

  • 定义 KnowledgePagePageIndexEntry 类型
  • 实现 KnowledgeStore 具体 struct(非 trait
    • 内部使用 Arc<dyn MemoryStore> 存储数据
    • index 自动维护(add/update/delete 时同步)
    • search 基于标题/摘要/标签的关键词匹配
  • 单元测试:页面 CRUD、index 一致性、搜索
  • 验收:cargo build + cargo test 通过

Step 4MemoryRetriever + 模块整合

文件src/memory/retriever.rssrc/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 buildcargo 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 检索 文件系统搜索不适合所有后端