docs(memory): 更新记忆系统方案文档

This commit is contained in:
徐涛
2026-06-07 23:01:28 +08:00
parent 6dc7ee492f
commit 1fe7f02281
+90 -37
View File
@@ -99,6 +99,9 @@ src/
```rust ```rust
#[async_trait] #[async_trait]
pub trait MemoryStore: Send + Sync { pub trait MemoryStore: Send + Sync {
/// 保存/覆盖一个 MemoryItemupsert 语义)。
/// - 如果 id 不存在,则插入新条目
/// - 如果 id 已存在,则覆盖旧条目
async fn save(&self, item: MemoryItem) -> Result<(), MemoryError>; async fn save(&self, item: MemoryItem) -> Result<(), MemoryError>;
async fn get(&self, id: &str) -> Result<Option<MemoryItem>, MemoryError>; async fn get(&self, id: &str) -> Result<Option<MemoryItem>, MemoryError>;
async fn delete(&self, id: &str) -> Result<(), MemoryError>; async fn delete(&self, id: &str) -> Result<(), MemoryError>;
@@ -123,19 +126,23 @@ pub struct ConversationMemory {
store: Arc<dyn MemoryStore>, store: Arc<dyn MemoryStore>,
session_id: String, session_id: String,
config: ConversationMemoryConfig, config: ConversationMemoryConfig,
messages: Vec<OpenaiChatMessage>, // 热缓存,供 compact 直接操作
compact_state: CompactState, // 断路器状态
} }
pub struct ConversationMemoryConfig { pub struct ConversationMemoryConfig {
pub strategy: MemoryStrategy, // sliding_window | full pub strategy: MemoryStrategy, // sliding_window | full
pub max_turns: usize, // sliding window 的最大轮数 pub max_turns: usize, // sliding window 的最大轮数
pub compact_config: Option<CompactConfig>, // 复用现有压缩配置 pub compact_config: Option<CompactConfig>, // 复用现有压缩配置
} }
``` ```
- 基于 `MemoryStore` 持久化消息 - `add_message(msg)` 写入热缓存 `self.messages`,同时通过 `store.save()` 持久化到后端
- `add_message()` 写入,`get_history()` 读取 - `get_history()` 优先从热缓存返回,缓存未命中时从 store 恢复
- sliding window 模式:超出 `max_turns` 后,调用 `llm::compact::microcompact()` 裁剪 - `compact` 直接在 `self.messages` 调用 `should_compact()``microcompact()`
- 复用现有 `CompactConfig`context_window, reserved_tokens, keep_recent - 压缩后同步回 `store`
- 使用了 `llm::types::OpenaiChatMessage` 作为内部消息类型
- 复用现有 `CompactConfig`context_window, reserved_tokens, keep_recent)和 `CompactState`
#### KnowledgeStore — 具体 struct #### KnowledgeStore — 具体 struct
@@ -156,8 +163,39 @@ impl KnowledgeStore {
} }
``` ```
- `search` 使用简单的字符串包含匹配标题/摘要/标签 - `search()` 优先搜索 index(标题/摘要/标签),全文 content 搜索走 MemoryStore
- index 在 add/update/delete 时自动维护 - index 在 add/update/delete 时自动维护,也支持通过 `rebuild_index()` 手动重建
- `KnowledgeStore``"knowledge_{page_id}"` 格式作为 `MemoryItem.id` 前缀,前缀字符串提取为常量 `const KNOWLEDGE_PREFIX: &str = "knowledge_"`
提供 `rebuild_index()` 方法修复 index 与 store 的不同步问题:
```rust
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 — 简化版检索器 #### MemoryRetriever — 简化版检索器
@@ -205,6 +243,19 @@ flowchart TD
关键词提取在 MemoryRetriever 内部简单实现:按空格/标点分割 → 过滤单字符和停用词 → 返回关键词列表。TextOverlap 计算 query 与页面标题/摘要/内容的 n-gram 重叠度(基于 Dice 系数)。 关键词提取在 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 核心数据类型 ### 3.4 核心数据类型
```rust ```rust
@@ -212,12 +263,13 @@ pub struct MemoryItem {
pub id: String, pub id: String,
pub content: String, pub content: String,
pub metadata: serde_json::Value, pub metadata: serde_json::Value,
pub created_at: chrono::DateTime<chrono::Utc>, pub created_at: time::OffsetDateTime,
} }
pub struct MemoryFilter { pub struct MemoryFilter {
pub prefix: Option<String>, pub prefix: Option<String>,
pub since: Option<chrono::DateTime<chrono::Utc>>, pub since: Option<time::OffsetDateTime>,
pub offset: Option<usize>, // 跳过前 N 条
pub limit: Option<usize>, pub limit: Option<usize>,
} }
@@ -228,8 +280,8 @@ pub struct KnowledgePage {
pub content: String, pub content: String,
pub tags: Vec<String>, pub tags: Vec<String>,
pub references: Vec<String>, pub references: Vec<String>,
pub created_at: chrono::DateTime<chrono::Utc>, pub created_at: time::OffsetDateTime,
pub updated_at: chrono::DateTime<chrono::Utc>, pub updated_at: time::OffsetDateTime,
} }
pub struct PageIndexEntry { pub struct PageIndexEntry {
@@ -237,7 +289,7 @@ pub struct PageIndexEntry {
pub title: String, pub title: String,
pub summary: String, pub summary: String,
pub tags: Vec<String>, pub tags: Vec<String>,
pub updated_at: chrono::DateTime<chrono::Utc>, pub updated_at: time::OffsetDateTime,
} }
pub enum MemoryStrategy { pub enum MemoryStrategy {
@@ -254,9 +306,6 @@ pub enum MemoryError {
#[error("Item not found: {0}")] #[error("Item not found: {0}")]
NotFound(String), NotFound(String),
#[error("Item already exists: {0}")]
AlreadyExists(String),
#[error("Storage error: {0}")] #[error("Storage error: {0}")]
Storage(String), Storage(String),
@@ -281,29 +330,31 @@ impl MemoryError {
```mermaid ```mermaid
flowchart TD flowchart TD
subgraph CM["ConversationMemory (src/memory/conversation.rs)"] subgraph CM["ConversationMemory"]
A["add_message()"] A["add_message(msg)"]
A1["store.save(message) ← 写到底层存储"] A1["self.messages.push(msg) ← 热缓存"]
C{"sliding_window && over_limit?"} A2["store.save(to_item(msg)) ← 冷持久化"]
D["messages = store.list(session)"] B{"len(messages) > max_turns?"}
E["compact_config.should_compact() ← 复用 CompactConfig"] C["should_compact(&messages, &config, &state) ← 直接在热缓存上操作"]
F["microcompact(&mut messages) ← 复用 microcompact()"] D["microcompact(&mut messages, keep_recent) ← 复用 microcompact()"]
G["store.save(pruned) ← 写回"] E["sync_to_store() ← 压缩后同步回 store"]
H["return"] F["return"]
I["get_history()"] G["get_history()"]
J["store.list(session)"] H["从 self.messages 返回"]
I["从 store 恢复 → 重建热缓存"]
A --> A1 A --> A1
A1 --> C A1 --> A2
C -->|是| D A2 --> B
B -->|是| C
C --> D
D --> E D --> E
E --> F E --> F
F --> G B -->|否| F
G --> H G --> H
C -->|| H H -->|缓存未命中| I
I --> J
end end
K["imports: llm::compact::{CompactConfig, microcompact, should_compact}"] J["依赖: llm::compact::{CompactConfig, CompactState, should_compact, microcompact}"]
``` ```
--- ---
@@ -516,7 +567,7 @@ async fn maybe_evict(&self) {
- 单元测试:TTL 过期淘汰、容量上限淘汰、不淘汰(None) - 单元测试:TTL 过期淘汰、容量上限淘汰、不淘汰(None)
- 验收:`cargo build` + `cargo test` 通过 - 验收:`cargo build` + `cargo test` 通过
**依赖**chrono(日期时间)、serde(序列化) **依赖**time(日期时间,启用 `serde` feature)、serde(序列化)
### Step 2ConversationMemory(含消息淘汰) ### Step 2ConversationMemory(含消息淘汰)
@@ -551,7 +602,9 @@ async fn maybe_evict(&self) {
- 内部关键词提取:split + 过滤停用词 - 内部关键词提取:split + 过滤停用词
- TextOverlap 评分:基于 Dice 系数计算 query 与页面的文本重叠度 - TextOverlap 评分:基于 Dice 系数计算 query 与页面的文本重叠度
- 阈值过滤 → 排序 → 截取 top-N - 阈值过滤 → 排序 → 截取 top-N
-`memory.rs` 中用 `pub use` 重导出所有公共类型 -`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` -`src/lib.rs` 中声明 `pub mod memory`
- 单元测试:关键词提取、TextOverlap 评分正确性、阈值过滤、排序正确性 - 单元测试:关键词提取、TextOverlap 评分正确性、阈值过滤、排序正确性
- 集成测试:端到端检索流程 - 集成测试:端到端检索流程
@@ -565,7 +618,7 @@ async fn maybe_evict(&self) {
|------|------|------|---------| |------|------|------|---------|
| KnowledgeStore 的 keyword 检索在大规模下效率低 | 中 | 中 | MemoryStore 实现可替换——下游可使用 SQLite FTS 等更高效的后端 | | KnowledgeStore 的 keyword 检索在大规模下效率低 | 中 | 中 | MemoryStore 实现可替换——下游可使用 SQLite FTS 等更高效的后端 |
| ConversationMemory 与 compact 耦合引入循环依赖 | 低 | 高 | 仅引用 `CompactConfig`(纯数据结构)和 `microcompact()`(纯函数),不引用 cycle.rs | | ConversationMemory 与 compact 耦合引入循环依赖 | 低 | 高 | 仅引用 `CompactConfig`(纯数据结构)和 `microcompact()`(纯函数),不引用 cycle.rs |
| chrono 增加依赖体积 | 低 | 低 | chrono 已是 Rust 生态标准,且仅用于时间戳 | | time 增加依赖体积 | 低 | 低 | time 是 Rust 官方维护的时间库,体积小于 chrono |
| Phase 4 集成时发现 Memory 设计不合理 | 低 | 高 | 按最小可行接口设计,预留扩展空间 | | Phase 4 集成时发现 Memory 设计不合理 | 低 | 高 | 按最小可行接口设计,预留扩展空间 |
--- ---