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

44 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 可自主编译和维护知识页面
  • 知识图谱(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),供淘汰策略使用
F13 标签复用查询 P1 KnowledgeGraph 提供 find_tags() 接口,供调用方在创建实体时复用已有标签,避免同义标签膨胀

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 — 底层存储抽象

#[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 — 知识库

#[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 — 知识库默认实现

pub struct InMemoryKnowledgeStore {
    pages: Mutex<HashMap<String, KnowledgePage>>,
    index: Mutex<Vec<PageIndexEntry>>,
}
  • search() 使用简单的字符串包含匹配(可替换为更复杂的检索算法)

KnowledgeGraph — 知识图谱

图谱检索的核心流程:输入 → 关键词提取 → 实体标签匹配 → 图遍历

#[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>;

    // 标签管理
    async fn find_tags(&self, prefix: &str) -> Result<Vec<String>, MemoryError>;
    async fn entity_count_by_tag(&self, tag: &str) -> Result<usize, MemoryError>;
    async fn set_entity_tags(&self, entity_id: &str, tags: Vec<String>) -> Result<(), MemoryError>;
    fn tag_constraints(&self) -> TagConstraints;
}
  • find_by_keywords() — 用关键词匹配实体的 name(前缀匹配)和 tags(完全匹配),不模糊搜索全字段
  • get_related() — 找到匹配实体后,按 depth 遍历关联实体,同时返回关联关系
  • find_tags() — 前缀匹配已有标签,供 Phase 4 Agent 做标签复用决策
  • entity_count_by_tag() — 查看某标签的实体关联数,判断标签的通用程度
  • set_entity_tags() — 替换实体的全部标签,调用方按关联度降序传入,内部自动截断到 max_tags_per_entity

标签规范与生成策略

标签是连接 KnowledgeStore(内容)和 KnowledgeGraph(实体)的检索桥梁。两条检索通道共享同一组关键词,通过标签匹配实现交叉发现。

标签复用原则

标签不应随意增长。优先复用已有标签,确无合适标签时才创建新标签。这保证:

  • 同一概念永远使用同一标签("state-graph" 不会出现 "state-graph" + "state-graph-framework" 并存)
  • 共享标签的实体自然形成概念关联,增强图检索的连通性
  • 标签空间收敛,检索效率不随实体数量增长而退化
标签注册与复用流程

KnowledgeGraph 提供标签查询接口供调用方(Phase 4 Agent)做复用决策:

#[async_trait]
pub trait KnowledgeGraph: Send + Sync {
    // ... 实体/关系管理方法 ...

    /// 查询已有标签(前缀匹配),用于标签复用决策
    async fn find_tags(&self, prefix: &str) -> Result<Vec<String>, MemoryError>;

    /// 查询某标签关联的实体数量
    async fn entity_count_by_tag(&self, tag: &str) -> Result<usize, MemoryError>;
}

Phase 4 Agent 在 Ingest 时遵从这个流程创建实体标签:

LLM 提取候选标签 → ["LangGraph", "StateGraph", "state-machine", "graph-framework"]
                          │
    ┌─────────────────────┼──────────────────────┐
    │ for each candidate: │                       │
    │                     │                       │
    │ graph.find_tags(candidate.lowercase())      │
    │   │                                         │
    │   ├─ 命中已有标签 → 复用(用已有标签字符串)  │
    │   │   "state-machine" → 已有 "state-machine" │
    │   │   用 "state-machine" 而非创建新标签       │
    │   │                                         │
    │   └─ 无匹配 → 注册新标签                     │
    │       "graph-framework" → 全新标签,注册      │
    │                                             │
    └─────────────────────────────────────────────┘

最终 entity.tags = ["langgraph", "state-graph", "state-machine", "graph-framework"]
                                          ↑ 复用了已有标签
标签容量与精炼

每个实体的标签数量有上限,不能无限增长。超出上限时,保留关联度最高的标签。

pub struct GraphEntity {
    pub id: String,
    pub name: String,
    pub entity_type: String,
    pub description: String,
    pub tags: Vec<String>,          // 检索标签,按关联度降序排列(索引越小越关键)
}

pub struct TagConstraints {
    pub max_tags_per_entity: usize,  // 每个实体最多标签数,默认 8
}

KnowledgeGraph 提供标签集替换接口,由调用方(Phase 4 Agent)按关联度排序后写入,内部自动截断:

#[async_trait]
pub trait KnowledgeGraph: Send + Sync {
    // ... 其他方法 ...

    /// 替换实体的全部标签
    /// 调用方确保 tags 已按关联度降序排列
    /// 内部自动截断到 max_tags_per_entity
    async fn set_entity_tags(&self, entity_id: &str, tags: Vec<String>) -> Result<(), MemoryError>;

    /// 获取标签容量约束
    fn tag_constraints(&self) -> TagConstraints;
}

标签精炼流程(Phase 4 Agent 负责编排):

LLM 从内容中提取候选标签(可能有 15-20 个)
  │
  ├─ 1. 评估每个标签与实体的关联度
  │     → "langgraph": 0.95 | "graph-framework": 0.3 | "computer": 0.1
  │
  ├─ 2. 按关联度降序排列
  │     → ["langgraph", "state-graph", "state-machine", "graph", ...]
  │
  ├─ 3. 复用已有标签(find_tags)
  │     → 将同义候选替换为已有标签
  │
  ├─ 4. 保留 top-8(受 max_tags_per_entity 约束)
  │     → ["langgraph", "state-graph", "state-machine", "graph",
  │        "agent", "workflow", "llm", "orchestration"]
  │
  └─ 5. set_entity_tags(entity_id, top_8)
        → KnowledgeGraph 内部截断到 max_tags_per_entity
标签规范
规则 说明 示例
原子性 每个标签是一个独立的关键词,不包含空格或标点 "state-graph" / "State Graph framework"
小写化 统一小写存储,匹配时大小写不敏感 "langgraph"
单数优先 优先使用单数形式 "agent" 而非 "agents"
领域受限 3-8 个标签/实体,聚焦可检索性而非描述性 ["langgraph", "state-graph", "graph", "state-machine"]

匹配行为(find_by_keywords):

字段 匹配方式 说明
name 前缀匹配 "lang" 匹配 "LangGraph""LangChain",支持大小写不敏感
tags 完全匹配 "state-graph" 只匹配 "state-graph",不匹配 "state"
entity_type 完全匹配(精确值) "framework" 匹配 "framework"

标签生成方式(Phase 4 Agent 层面):

Ingest 新内容
  │
  ├─ LLM 提取标签(主要方式)
  │   → 分析内容 → 提取 3-8 个关键概念作为标签
  │   → 同时写入 KnowledgePage.tags 和 GraphEntity.tags
  │
  ├─ 规则提取(辅助方式)
  │   → 从 entity.name 中按 CamelCase / kebab-case 拆分
  │   → "StateGraph" → ["state", "graph", "state-graph"]
  │
  └─ 标签继承(关联同步)
      → KnowledgePage 的标签自动同步到关联的 GraphEntity
      → 页面标签变更时,同步更新关联实体的标签

标签在检索中的作用:

检索关键词: ["langgraph", "state-graph"]
  │
  ├─ KnowledgeStore 匹配: 页面 title(前缀) / tags(完全) / summary(包含)
  │   → 命中 KnowledgePage { tags: ["langgraph", "graph", "state-machine"] } ← tags 完全匹配
  │   → 命中 KnowledgePage { title: "LangGraph StateGraph Guide" } ← title 前缀匹配
  │
  └─ KnowledgeGraph 匹配: 实体 name(前缀) / tags(完全)
      → 命中 GraphEntity { name: "LangGraph", tags: ["langgraph", "agent-framework"] } ← 都匹配
      → 命中 GraphEntity { name: "StateGraph", tags: ["langgraph", "state-graph"] } ← tags 完全匹配
      → get_related("StateGraph") → 遍历关联实体 "StateMachine", "Graph" 等

tags 定位为精准的检索锚点name 作为松散的检索入口,两者配合确保召回既有广度又有精度。

InMemoryGraph — 知识图谱默认实现

pub struct InMemoryGraph {
    entities: Mutex<HashMap<String, GraphEntity>>,
    relations: Mutex<Vec<GraphRelation>>,
}
  • 图遍历使用 BFS/DFSdepth 控制搜索深度
  • 适合原型和小规模图

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 — 关键词提取

检索前先从输入中提取关键词,作为两个通道共享的检索入口:

pub trait KeywordExtractor: Send + Sync {
    fn extract(&self, query: &str) -> Vec<String>;
}

// 默认实现:按非字母数字字符分割,过滤停用词和单字符词
pub struct SimpleKeywordExtractor {
    stop_words: HashSet<String>,
}
RetrievalResult — 统一召回结果
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 检索 — 用关键词匹配实体 nametags,命中后按 depth 图遍历获取关联实体
  4. 评分 — 对每条召回结果计算 score ∈ [0.0, 1.0]
  5. 过滤排序 — 丢弃低分、按分排序、截取 top N

这样设计的好处:

  • 关键词生成在两个通道间共享,避免重复计算
  • 图谱检索从"模糊搜索"变为"标签精确匹配 → 图遍历",更精准
  • 每个步骤职责单一,可独立替换实现

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>,    // 交叉引用的页面 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 tags: Vec<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 错误类型

#[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)]),便于持久化实现直接复用:

#[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 层提供可选的淘汰配置,上层组件按需设置:

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 淘汰策略

基于记忆的实际使用价值决定淘汰优先级,而非简单的写入时间。

记忆价值评分

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()

#[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>

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 的淘汰实现

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

  • 创建 memory.rs + memory/ 目录
  • 定义 MemoryItemMemoryFilterMemoryStrategy 类型
  • 定义 MemoryError 枚举
  • 定义 MemoryStore trait 与 InMemoryStore 实现
  • 定义 EvictionConfigEvictionPolicyNone / Ttl / Capacity / Hybrid
  • InMemoryStore.save() 内部实现淘汰检查
  • 单元测试:TTL 过期淘汰、容量上限淘汰、不淘汰(None)、Hybrid 组合
  • 验收: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 trait
  • 实现 InMemoryKnowledgeStore
    • index 自动维护(add/update/delete 时同步)
    • search 基于标题/摘要/标签的关键词匹配
  • 单元测试:页面 CRUD、index 一致性、搜索
  • 验收:cargo build + cargo test 通过

Step 4KnowledgeGraph(含标签匹配)

文件src/memory/graph.rs

  • 定义 GraphEntity(含 tags: Vec<String>)、GraphRelation 类型
  • 定义 KnowledgeGraph trait
  • 实现 InMemoryGraph
    • BFS/DFS 图遍历
    • find_by_keywords():关键词前缀匹配 name,完全匹配 tags,大小写不敏感
    • find_tags(prefix):标签复用查询,前缀匹配已有标签
    • set_entity_tags():替换实体的全部标签,按传入顺序保留前 max_tags_per_entity
    • tag_constraints():返回 TagConstraints { max_tags_per_entity: 8 }
    • 内部维护 tag_index: HashMap<String, HashSet<String>> 实现标签到实体的快速检索
  • 标签规范在源码中以常量或文档注释形式明确(小写化、原子性、优先复用已有、每实体 ≤8 个)
  • 单元测试:实体 CRUD、关系添加/删除、标签匹配(大小写不敏感、前缀/完全区分)、标签复用查询、标签容量截断、图遍历
  • 验收:cargo build + cargo test 通过

Step 5MemoryRetriever(关键词提取 + 评分机制)+ 模块整合

文件src/memory/retriever.rssrc/memory.rs

  • 定义 KeywordExtractor trait + SimpleKeywordExtractor 默认实现(分割 + 停用词过滤)
  • 定义 ScoringStrategy 枚举(TextOverlap / GraphDistance / TemporalWeight / ReferenceCount / Hybrid
  • 定义 ScoreWeightsScoredItemScoreBreakdownRetrieverConfig
  • 实现评分函数:
    • 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、关系管理和图遍历
  • KnowledgeGraph::find_tags(prefix) 支持前缀匹配查询已有标签
  • KnowledgeGraph::set_entity_tags() 替换全部标签,截断到 max_tags_per_entity
  • 每个实体的标签数不超过 TagConstraints::max_tags_per_entity(默认 8
  • 标签复用原则在方案文档和源码规范中明确,下游实现可据此遵守
  • MemoryRetriever 组合 KnowledgeStore + KnowledgeGraph 返回统一检索结果
  • 无 embedding 相关依赖(不引入 fastembed、pgvector、qdrant 等 crate
  • 模块结构符合项目惯例: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 + 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 混合检索 文件系统搜索不适合所有后端