44 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 可自主编译和维护知识页面
- 知识图谱(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),供淘汰策略使用 |
| 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()裁剪 - 复用现有
CompactConfig(context_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自动更新 indexsearch基于页面标题/摘要/标签的关键词匹配(不依赖 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/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 — 关键词提取
检索前先从输入中提取关键词,作为两个通道共享的检索入口:
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 │
流程说明:
- 关键词提取 — 先用
KeywordExtractor从输入中提取候选关键词,两个通道共享 - KnowledgeStore 检索 — 用关键词匹配页面的
title(精准/前缀)、tags(完全)、summary(包含) - KnowledgeGraph 检索 — 用关键词匹配实体
name和tags,命中后按depth图遍历获取关联实体 - 评分 — 对每条召回结果计算
score ∈ [0.0, 1.0] - 过滤排序 — 丢弃低分、按分排序、截取 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.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 / Hybrid) InMemoryStore.save()内部实现淘汰检查- 单元测试:TTL 过期淘汰、容量上限淘汰、不淘汰(None)、Hybrid 组合
- 验收:
cargo build+cargo test通过
依赖:chrono(日期时间)、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类型 - 定义
KnowledgeStoretrait - 实现
InMemoryKnowledgeStore- index 自动维护(add/update/delete 时同步)
- search 基于标题/摘要/标签的关键词匹配
- 单元测试:页面 CRUD、index 一致性、搜索
- 验收:
cargo build+cargo test通过
Step 4:KnowledgeGraph(含标签匹配)
文件:src/memory/graph.rs
- 定义
GraphEntity(含tags: Vec<String>)、GraphRelation类型 - 定义
KnowledgeGraphtrait - 实现
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 5:MemoryRetriever(关键词提取 + 评分机制)+ 模块整合
文件:src/memory/retriever.rs、src/memory.rs
- 定义
KeywordExtractortrait +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. 验收标准
MemoryStoretrait 定义清晰的 4 个方法,InMemoryStore通过单元测试EvictionConfig支持 None / Ttl / Capacity / RecallBased / Hybrid 五种策略EvictionPolicy::RecallBased按记忆价值公式正确计算并淘汰低价值记忆InMemoryStore在 save() 后正确执行 TTL 淘汰、容量淘汰、基于召回价值淘汰InMemoryStore::record_recall()记录召回次数和评分MemoryRetriever每次检索后调用record_recall()更新召回统计ConversationMemory支持 sliding window 和 full 两种策略ConversationMemorysliding window 模式下达到上限后删除最旧消息条目ConversationMemory正确复用llm::compact的压缩逻辑KnowledgeStoretrait +InMemoryKnowledgeStore支持页面 CRUD 和 index 维护KnowledgeGraphtrait +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 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 混合检索 | 文件系统搜索不适合所有后端 |