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

This commit is contained in:
徐涛
2026-06-07 23:01:28 +08:00
parent 6dc7ee492f
commit 1fe7f02281
+87 -34
View File
@@ -99,6 +99,9 @@ src/
```rust
#[async_trait]
pub trait MemoryStore: Send + Sync {
/// 保存/覆盖一个 MemoryItemupsert 语义)。
/// - 如果 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>;
@@ -123,6 +126,8 @@ pub struct ConversationMemory {
store: Arc<dyn MemoryStore>,
session_id: String,
config: ConversationMemoryConfig,
messages: Vec<OpenaiChatMessage>, // 热缓存,供 compact 直接操作
compact_state: CompactState, // 断路器状态
}
pub struct ConversationMemoryConfig {
@@ -132,10 +137,12 @@ pub struct ConversationMemoryConfig {
}
```
- 基于 `MemoryStore` 持久化消息
- `add_message()` 写入,`get_history()` 读取
- sliding window 模式:超出 `max_turns` 后,调用 `llm::compact::microcompact()` 裁剪
- 复用现有 `CompactConfig`context_window, reserved_tokens, keep_recent
- `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
@@ -156,8 +163,39 @@ impl KnowledgeStore {
}
```
- `search` 使用简单的字符串包含匹配标题/摘要/标签
- index 在 add/update/delete 时自动维护
- `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 的不同步问题:
```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 — 简化版检索器
@@ -205,6 +243,19 @@ flowchart TD
关键词提取在 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 核心数据类型
```rust
@@ -212,12 +263,13 @@ pub struct MemoryItem {
pub id: String,
pub content: String,
pub metadata: serde_json::Value,
pub created_at: chrono::DateTime<chrono::Utc>,
pub created_at: time::OffsetDateTime,
}
pub struct MemoryFilter {
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>,
}
@@ -228,8 +280,8 @@ pub struct KnowledgePage {
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 created_at: time::OffsetDateTime,
pub updated_at: time::OffsetDateTime,
}
pub struct PageIndexEntry {
@@ -237,7 +289,7 @@ pub struct PageIndexEntry {
pub title: String,
pub summary: String,
pub tags: Vec<String>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub updated_at: time::OffsetDateTime,
}
pub enum MemoryStrategy {
@@ -254,9 +306,6 @@ pub enum MemoryError {
#[error("Item not found: {0}")]
NotFound(String),
#[error("Item already exists: {0}")]
AlreadyExists(String),
#[error("Storage error: {0}")]
Storage(String),
@@ -281,29 +330,31 @@ impl MemoryError {
```mermaid
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)"]
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 --> C
C -->|是| D
A1 --> A2
A2 --> B
B -->|是| C
C --> D
D --> E
E --> F
F --> G
B -->|否| F
G --> H
C -->|| H
I --> J
H -->|缓存未命中| I
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)
- 验收:`cargo build` + `cargo test` 通过
**依赖**chrono(日期时间)、serde(序列化)
**依赖**time(日期时间,启用 `serde` feature)、serde(序列化)
### Step 2ConversationMemory(含消息淘汰)
@@ -551,7 +602,9 @@ async fn maybe_evict(&self) {
- 内部关键词提取:split + 过滤停用词
- TextOverlap 评分:基于 Dice 系数计算 query 与页面的文本重叠度
- 阈值过滤 → 排序 → 截取 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`
- 单元测试:关键词提取、TextOverlap 评分正确性、阈值过滤、排序正确性
- 集成测试:端到端检索流程
@@ -565,7 +618,7 @@ async fn maybe_evict(&self) {
|------|------|------|---------|
| KnowledgeStore 的 keyword 检索在大规模下效率低 | 中 | 中 | MemoryStore 实现可替换——下游可使用 SQLite FTS 等更高效的后端 |
| ConversationMemory 与 compact 耦合引入循环依赖 | 低 | 高 | 仅引用 `CompactConfig`(纯数据结构)和 `microcompact()`(纯函数),不引用 cycle.rs |
| chrono 增加依赖体积 | 低 | 低 | chrono 已是 Rust 生态标准,且仅用于时间戳 |
| time 增加依赖体积 | 低 | 低 | time 是 Rust 官方维护的时间库,体积小于 chrono |
| Phase 4 集成时发现 Memory 设计不合理 | 低 | 高 | 按最小可行接口设计,预留扩展空间 |
---