docs(memory): 更新记忆系统方案文档
This commit is contained in:
+90
-37
@@ -99,6 +99,9 @@ src/
|
|||||||
```rust
|
```rust
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait MemoryStore: Send + Sync {
|
pub trait MemoryStore: Send + Sync {
|
||||||
|
/// 保存/覆盖一个 MemoryItem(upsert 语义)。
|
||||||
|
/// - 如果 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 2:ConversationMemory(含消息淘汰)
|
### Step 2:ConversationMemory(含消息淘汰)
|
||||||
|
|
||||||
@@ -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 设计不合理 | 低 | 高 | 按最小可行接口设计,预留扩展空间 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user