将 note、pdd、prd、roadmap 四类文档分别归入 `design/` 下对应子目录中,并新增 `.gitkeep` 占位文件
28 KiB
Phase 7 — SqliteStore 持久化实现方案
- 文档编号:14
- 标题:Phase 7 — SqliteStore 持久化实现方案
- 日期:2026-07-05
- 状态:已定稿
- 涉及模块:memory/store
- 关联文档:roadmap.md, 6-memory-system.md
背景与目标
Phase 7 的核心任务是完成 MemoryStore trait 的 SQLite 后端实现,使 Agent 进程重启后记忆数据不丢失。这是 v0.2.0 从"内存玩具"走向"可用工具"的关键门槛,也是后续 Phase 8(MVP 出口)和 Phase 10(ContextSlot)的前置依赖。
成功标准:
- SqliteStore 完整实现 MemoryStore trait(4 个方法:save/get/delete/list)
- 进程关闭后重新打开同一数据库文件,数据完整可读
- 与现有 InMemoryStore 通过 MemoryStore trait 可互换,消费者零改动
- 所有现有测试保持通过,clippy 0 警告
Scope & Non-goals
| 范围 | 内容 |
|---|---|
| 包含 | 单表 CRUD + prefix/since/offset/limit 查询 + WAL 并发 + Mutex 串行化 + 错误映射 |
| 不包含(Phase 7) | 淘汰策略(EvictionPolicy,仅 InMemoryStore 持有,需 v0.3 纳入 SqliteStore) |
| 不包含(Phase 7) | Schema 迁移框架(PRAGMA user_version 足矣,不引入 refinery/sea-query) |
| 不包含(Phase 7) | 批量写入 / 事务 API(N+1 clear 延迟可接受,优化后置) |
| 不包含(Phase 7) | 跨进程共享同一数据库文件(Mutex 为单进程设计) |
当前状态分析
现有实现
- MemoryStore trait 已在 v0.1 Phase 3 就绪,定义 4 个异步方法
- InMemoryStore 实现稳定运行,使用
Mutex<HashMap>作为后端 - 全量测试 191 个通过,clippy 0 警告
- 项目当前无 SQLite 或其他数据库依赖
现有消费者
通过 crate::memory::store::MemoryStore 路径引用的模块:
| 模块 | 文件 | 使用方式 |
|---|---|---|
| Agent Builder | agent/builder.rs |
RuntimeBundle 中引用 MemoryStore |
| Session Memory | agent/session_memory.rs |
SessionMemory 实现 |
| Agent Runtime | agent/runtime.rs |
类型标注 |
| Agent Session | agent/session.rs |
默认 InMemoryStore 兜底 |
| Conversation | memory/conversation.rs |
ConversationMemory 测试 |
| Knowledge | memory/knowledge.rs |
KnowledgeStore 测试 |
| Retriever | memory/retriever.rs |
MemoryRetriever 测试 |
所有消费者均通过 MemoryStore trait 访问,不依赖具体实现类型,因此新增 SqliteStore 不会产生编译或运行时影响。
目录结构现状
src/memory/
├── mod.rs
├── store.rs ← 包含 MemoryStore trait + InMemoryStore + EvictionPolicy
├── conversation.rs
├── knowledge.rs
├── retriever.rs
└── vector.rs
store.rs 目前是一个单体文件,同时承载 trait 定义和 InMemoryStore 实现。
调研发现
MemoryStore trait 定义
#[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>;
}
关键类型
| 类型 | 定义 |
|---|---|
MemoryItem |
{ id: String, content: String, metadata: Value, created_at: OffsetDateTime } |
MemoryFilter |
{ prefix: Option<String>, since: Option<OffsetDateTime>, offset: Option<usize>, limit: Option<usize> } |
MemoryError |
变体:NotFound / Storage / Serialization / InvalidInput / RetrievalError |
EvictionPolicy |
None / Ttl { ttl_secs } / Capacity { max_items } |
EvictionConfig |
{ policy, check_interval } |
并发模型参考
InMemoryStore 当前使用 Mutex<HashMap> 实现 Send + Sync。SqliteStore 将遵循相同模式,使用 Arc<Mutex<Connection>> + spawn_blocking 满足异步 trait 约束。
Schema 设计考虑
created_at使用 TEXT(ISO 8601) 存储——.to_string()零转换,字典序与时间序一致(前提:所有时间戳归一化到 UTC;OffsetDateTime::to_string()在 UTC 下输出"2026-07-05T12:00:00Z"格式,字典序与时间序严格对应)- 初始 schema 即创建
created_at索引,避免后续大数据量全表排序 - Schema 版本管理通过
PRAGMA user_version实现,零外部依赖,后续加字段只需追加if version < N { ALTER TABLE }
可选方案
A. rusqlite + Mutex<Connection>(推荐)
| 维度 | 评估 |
|---|---|
| 新增依赖 | 1 个(rusqlite 0.32 + bundled features) |
| 实现量 | ~200 行 |
| SQL 支持 | 原生支持 prefix LIKE 过滤 + ORDER BY 排序 |
| 性能 | 有索引时查询 O(log n),写入串行化 |
| 并发 | WAL 模式 + Mutex 串行化写入,适合单进程 Agent |
| 事务支持 | 完整 ACID |
| 崩溃安全 | WAL 模式,崩溃恢复有保障 |
适用场景:单进程 Agent 本地持久化、嵌入式场景、需要关系查询能力的通用存储。
外部依赖评估:
- rusqlite 0.32 — 最新稳定版(2025-12 发布),维护活跃(月均 2+ 次提交),Apache-2.0 许可证
bundledfeature 编译 SQLite 源码(Public Domain)进二进制,无系统级 SQLite 依赖,零外部 C 库安装步骤- 供应链风险:bundled 模式依赖 crate 发布节奏同步 SQLite 安全更新;SQLite 安全公告频率极低(年均 <3 例),此风险可接受
B. JSONL 文件
| 维度 | 评估 |
|---|---|
| 新增依赖 | 0 |
| 实现量 | ~150 行 |
| get() 复杂度 | O(n) 全量扫描 |
| delete() 复杂度 | O(n) 全量重写 |
| 并发 | 需文件锁(flock) |
| 事务支持 | 无 |
| 崩溃安全 | 无保障,写入中断可能丢失或损坏数据 |
否决理由:核心的 get() 查询场景不可接受 O(n) 性能;在 Agent 运行时频繁读写记忆的场景下,全量扫描的成本会随着数据积累线性增长,不符合可用性要求。
C. sled 嵌入式 KV
| 维度 | 评估 |
|---|---|
| 新增依赖 | 1 个(纯 Rust) |
| 实现量 | ~150 行 |
| prefix scan | 原生支持 |
| 排序 | 需手动实现 |
| 关系模型 | 不如 SQL 匹配当前查询模式 |
| 社区成熟度 | 较新,API 仍在演进 |
否决理由:当前查询模式(prefix 过滤 + 按 created_at 排序)在关系模型中用一条 SQL 即可表达,引入 KV 存储反而需要手动处理排序逻辑。非必要不引入新存储范式。
推荐方案
总体方向:方案 A(rusqlite + Mutex<Connection>)
选择理由:
- 最少依赖,最高匹配:1 个新增依赖即可完整支持 MemoryFilter 的所有查询维度(prefix LIKE、created_at 范围、offset/limit)
- 生产就绪:rusqlite 是 SQLite 的 Rust 绑定事实标准,bundled 模式免去系统 SQLite 依赖
- Schema 演进简单:PRAGMA user_version + 逐版本迁移,零外部迁移工具依赖
- 与 InMemoryStore 语义一致:Mutex 串行化 + spawn_blocking 适配 async trait,与现有并发模型同构
关键设计决策
| 决策 | 选择 | 理由 |
|---|---|---|
| Schema 版本管理 | PRAGMA user_version | 零外部依赖,~20 行,后续 ALTER TABLE 即可 |
| created_at 存储格式 | TEXT(ISO 8601) + UTC 归一化 | 零转换代码;UTC 下输出 "2026-07-05T12:00:00Z",字典序与时间序严格一致 |
| Upsert SQL 策略 | INSERT ... ON CONFLICT(id) DO UPDATE SET ... |
保留调用方传入的 created_at,避免被 DEFAULT 覆盖 |
| 性能索引 | 初始 schema 加 created_at 索引 | 避免大数据量全表排序 |
| 配置参数 | 仅 path、busy_timeout=5s |
其余内置默认值;5s 超时避免 SQLITE_BUSY 快速失败 |
| Mutex 中毒恢复 | .lock().unwrap_or_else(|e| e.into_inner()) |
不 panic,恢复执行 |
| 批量操作 | 不加 | N+1 clear ~250ms(N=50),可接受,优化后置 |
| spawn_blocking 取消安全性 | 短事务模式(auto-commit) | 每个操作独立事务,取消时后台 task 自然完成/panic,不 Cross 操作持有 Mutex |
我们放弃了什么(集中 Trade-off 记录):
- 写入串行化:
Mutex<Connection>确保 SQLite 写入安全,代价是同一时刻只能有一个写入者。Agent 场景下写入频率低(每次 LLM 调用触发 1-2 次),串行化不构成瓶颈 - 单进程锁:无法跨进程共享同一数据库文件。多进程场景需要网络后端(PostgreSQL/Redis)
- 无横向扩展:单文件 SQLite 无分片能力。需扩展时切换到分布式后端
Schema 定义(初始版本)
CREATE TABLE IF NOT EXISTS memory_items (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
metadata TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_memory_items_created_at
ON memory_items(created_at);
数据完整性防御
| 异常场景 | 防御措施 | 错误映射 |
|---|---|---|
| 数据库文件损坏 | migrate() 中执行 PRAGMA quick_check;失败时 open() 返回 MemoryError::Storage |
Storage |
| created_at 解析失败 | get()/list() 中 OffsetDateTime::parse 失败不 panic,返回 MemoryError::Serialization |
Serialization |
| content / metadata 为 NULL | get() 中检测 SQLite 返回值,NULL 时返回 MemoryError::Storage |
Storage |
| 约束冲突(PRIMARY KEY / NOT NULL) | 映射为 MemoryError::InvalidInput |
InvalidInput |
| 序列化/反序列化失败 | serde_json::to_string/from_str 错误映射为 MemoryError::Serialization |
Serialization |
性能预算(目标延迟,单条操作)
| 操作 | 目标延迟 | 说明 |
|---|---|---|
save(1KB item) |
< 5ms | 含 serde_json 序列化 + spawn_blocking + SQLite INSERT |
get(1KB item) |
< 3ms | 含 SQLite SELECT + 反序列化 |
list(prefix 匹配 100 行) |
< 20ms | 含索引 B-tree 遍历 + ORDER BY + LIMIT |
| 并发 10 writer | p99 < 50ms | Mutex 串行化排队,每 writer 等待 9×5ms 内 |
实施后通过 Step C 测试验证以上预算。未达标时不阻塞发布,但记录为可观测告警阈值。
实施建议
实施计划
Step A — 目录重构(纯搬移,零行为变化)
目标:将单体 store.rs 拆分为模块目录架构,为新增 SqliteStore 做准备。
src/memory/
├── store.rs ← 模块根:MemoryStore trait + EvictionPolicy/EvictionConfig
│ + pub mod in_memory;
│ + pub mod sqlite_store;
│ + pub use in_memory::InMemoryStore;
├── store/
│ ├── in_memory.rs ← InMemoryStore 提取至此(struct + impl + 6 个内联测试)
│ └── sqlite_store.rs ← 新增 SqliteStore
模式参考:llm/provider.rs → llm/provider/{openai,anthropic,ollama}.rs
外部消费者的导入路径不变(crate::memory::store::MemoryStore),零改动风险。
重构步骤:
- 创建
src/memory/store/目录 - 创建
src/memory/store/in_memory.rs,从原store.rs提取 InMemoryStore 全部代码(struct + impl + Default + 6 个测试) - 修改
src/memory/store.rs:保留 MemoryStore trait + EvictionPolicy/EvictionConfig,加pub mod in_memory;+pub use in_memory::InMemoryStore; - 验证:
cargo test --all-targets全绿,测试数量不变(191 pass)
Step B — SqliteStore 实现
Cargo.toml添加rusqlite = { version = "0.32", features = ["bundled"] }- 创建
src/memory/store/sqlite_store.rs,实现:SqliteStore结构体:{ conn: Arc<Mutex<Connection>> }SqliteStore::open(path)构造函数,支持":memory:"lock_conn()辅助方法(Mutex 中毒恢复)migrate()Schema 初始化 + 版本管理MemoryStoretrait 的 4 个方法From<rusqlite::Error> for MemoryError
- 修改
src/memory/store.rs:加pub mod sqlite_store;+pub use sqlite_store::SqliteStore; - 修改
src/memory.rs:加pub use store::SqliteStore; - 编写测试覆盖:
- CRUD 基本操作
- Upsert(同 id 重复 save 覆盖)
- prefix 过滤
- 由于/until 时间范围过滤
- 并发 10 个 writer × 10 次操作
- 持久化恢复(write → drop → reopen → read)
- 验证:
cargo test --all-targets全绿 +cargo clippy --all-targets -- -D warnings0 警告
Step C — 验证确认
- 确认现有 memory 模块内测试全部通过
- 确认 agent/llm/tools/prompt 模块不受影响
- 确认 SqliteStore 与 InMemoryStore 通过 MemoryStore trait 可互换
- 确认 clippy 无新增警告
Commit 安排
| 顺序 | 类型 | Scope | 描述 |
|---|---|---|---|
| 1 | refactor | memory | 将 store.rs 拆分为模块目录,仅结构搬移 |
| 2 | feat | memory | 实现 SqliteStore 持久化 |
风险与缓解
| 风险 | 严重度 | 缓解措施 |
|---|---|---|
| Mutex 中毒导致后续操作全部失败 | 中 | lock_conn() 使用 .lock().unwrap_or_else(|e| e.into_inner()) 恢复模式,不 panic |
| spawn_blocking 取消后连接状态不一致 | 中 | 每个操作使用短事务(auto-commit),不跨操作持有 Mutex;取消时遗留 task 自然完成或 panic,Mutex 通过 .into_inner() 恢复 |
| WAL 文件无限增长 | 低 | 内置 auto-checkpoint 阈值 + 启动时执行 PRAGMA wal_checkpoint(TRUNCATE) |
| list 无索引导致全表扫描 | 中(大数据量) | 初始 schema 即创建 idx_memory_items_created_at 索引 |
| 父目录不存在导致 open 失败 | 低 | open() 内部调用 fs::create_dir_all() 确保目录存在 |
| clear() N+1 删除性能 | 低 | 不走 trait 接口的逐条删除,可后续优化为直接 DELETE FROM memory_items |
| 数据库文件损坏 | 低 | migrate() 中执行 PRAGMA quick_check;失败时返回 MemoryError::Storage,调用方可切换 InMemoryStore |
可观测性(实施时落实)
- 所有 MemoryStore 方法通过
tracing::instrument记录延迟和结果(info!正常完成,warn!超过性能预算阈值,error!操作失败) list()返回行数通过tracing::debug记录(调优参考)- WAL 文件大小在
migrate()后检查一次,超过 100MB 时记录warn! - 操作计数(读写次数、错误率)暂不暴露为独立 metrics,v0.3 按需添加
已知假设
| 假设 | 验证状态 | Fallback |
|---|---|---|
| 单进程独享 SQLite 文件,无跨进程竞争 | ✅ 设计前提(Mutex 为单进程设计) | 多进程场景使用网络后端(PostgreSQL/Redis,v0.3+) |
| ISO 8601 TEXT 字典序等价于时间序 | ✅ 条件成立(需 UTC 归一化) | 若时区异常,切换 INTEGER(unix_timestamp) 存储后重建索引 |
| SqliteStore 初始化失败可 fallback 到 InMemoryStore | ✅ 调用方自行控制 | open() 返回 MemoryError,消费者 catch 后改用 InMemoryStore::new() |
| N+1 clear ~250ms(N=50) 可接受 | 🟡 未实测(基于 N×5ms 推算) | 若成为瓶颈,SqliteStore 内部加 delete_by_prefix() 方法(不走 trait 接口) |
| rusqlite bundled SQLite 版本足够新 | ✅ 0.32 版内置 SQLite 3.46 | 如需特定版本,切换 bundled 为指定版本或使用系统 SQLite |
| busy_timeout=5s 覆盖所有竞争场景 | 🟡 未实测(WAL 下写写冲突概率低) | 若观测到 SQLITE_BUSY,增大超时或在重试逻辑中处理 |
| spawn_blocking 线程池不会被耗尽 | ✅ 默认 512 线程,Agent 场景占用 ≤10 | 若观测到阻塞任务排队,启动时 tokio::task::spawn_blocking 已有兜底排队机制 |
参考来源
- rusqlite crate — 官方文档
- SQLite PRAGMA user_version — Schema 版本管理机制
- SQLite WAL mode — 并发读写性能优化
docs/6-memory-system.md— Phase 3 MemoryStore trait 原始设计docs/roadmap.md— 项目里程碑规划(Phase 7/8/10 依赖关系)src/llm/provider.rs→src/llm/provider/— 目录重构模式参考
实施计划
任务总览
3 个阶段、8 个任务单元、2 个 Commit。
阶段一:目录重构
Task A1 — 创建 store/ 目录并提取 InMemoryStore
| 项目 | 内容 |
|---|---|
| 任务描述 | 创建 src/memory/store/ 目录,新建 store/in_memory.rs,从 store.rs 完整提取 InMemoryStore 结构体、impl MemoryStore、impl Default、6 个内联测试 |
| 涉及文件 | src/memory/store.rs → 分割到 src/memory/store/in_memory.rs(新增) |
| 前置依赖 | 无 |
| 预估工作量 | S(< 1h) |
| 风险等级 | 低 — 纯搬移,编译器可验证 |
| 验收条件 | cargo build 通过(此时 store.rs 尚未修改,store/in_memory.rs 应被 crate 忽略) |
注意:需要先在 store.rs 顶部添加 pub mod in_memory; 声明,否则子模块不会被编译。或者可以先创建目录和文件,等 Task A2 再统一加声明路径。
实际做法:先创建文件但不声明,A2 统一声明。这样 A1 和 A2 之间可以有一个无编译的中间状态。
Task A2 — 修改 store.rs 模块根
| 项目 | 内容 |
|---|---|
| 任务描述 | 修改 store.rs 为纯模块根:保留 MemoryStore trait、EvictionPolicy、EvictionConfig;添加 pub mod in_memory; + pub use in_memory::InMemoryStore;;删除已提取到 in_memory.rs 中的代码 |
| 涉及文件 | src/memory/store.rs(修改) |
| 前置依赖 | Task A1(文件已存在) |
| 预估工作量 | S(< 1h) |
| 风险等级 | 低 — 保留部分不变,提取部分在子模块中 |
| 验收条件 | cargo test --all-targets 全绿,测试数量不变(191 pass),clippy 0 warning |
Task A3 — 验证阶段一
| 项目 | 内容 |
|---|---|
| 任务描述 | 运行全量测试链确认目录重构零行为变化 |
| 涉及文件 | 全量 |
| 前置依赖 | Task A2 |
| 预估工作量 | XS(验证) |
| 风险等级 | 低 |
| 验收条件 | cargo test --all-targets 191 pass、cargo clippy --all-targets -- -D warnings 0 警告、cargo build 通过 |
阶段二:SqliteStore 实现
Task B1 — 添加 rusqlite 及 dev-dependencies
| 项目 | 内容 |
|---|---|
| 任务描述 | 在 Cargo.toml 中添加依赖:[dependencies] 加 rusqlite = { version = "0.32", features = ["bundled"] },[dev-dependencies] 加 tempfile = "3"(用于测试隔离);运行 cargo build 确认编译通过,cargo test --no-run 验证 dev-dependencies 可用 |
| 涉及文件 | Cargo.toml(修改)、Cargo.lock(自动更新) |
| 前置依赖 | 无(可与阶段一并行) |
| 预估工作量 | XS(< 15min) |
| 风险等级 | 低 — 标准依赖添加 |
| 验收条件 | cargo build 成功,cargo test --no-run 成功,Cargo.lock 中生成 rusqlite 和 tempfile 条目 |
Task B2 — 实现 SqliteStore 核心
| 项目 | 内容 |
|---|---|
| 任务描述 | 创建 src/memory/store/sqlite_store.rs,实现以下 8 个子模块: |
(1)SqliteStore 结构体 { conn: Arc<Mutex<Connection>> } |
|
(2)SqliteStore::open(path) — 支持 ":memory:",内部调用 fs::create_dir_all 确保父目录存在 |
|
(3)lock_conn() — 内部辅助方法,.lock().unwrap_or_else(|e| e.into_inner()) 处理 Mutex 中毒 |
|
(4)migrate() — 按版本递增执行迁移:PRAGMA user_version 检查(初始版本号=1)→ 建表 memory_items + 索引 idx_memory_items_created_at + 设置 WAL 模式 + busy_timeout=5s + PRAGMA synchronous = NORMAL + PRAGMA wal_autocheckpoint=1000 + PRAGMA quick_check(检测数据库损坏)+ PRAGMA wal_checkpoint(TRUNCATE) |
|
(5)MemoryStore trait 的 4 个方法实现(save/get/delete/list),全部使用 spawn_blocking 包裹;生命周期注意:spawn_blocking 闭包前先 .conn.clone() 取 Arc<Connection>,参数调 .clone() 取 owned 值,再传入 spawn_blocking(move |{ ... }) |
|
— save: INSERT INTO memory_items (id, content, metadata, created_at) VALUES (?1, ?2, ?3, ?4) ON CONFLICT(id) DO UPDATE SET content=excluded.content, metadata=excluded.metadata, created_at=excluded.created_at(全字段覆盖 upsert,与 InMemoryStore 行为一致) |
|
— get: SELECT content, metadata, created_at FROM memory_items WHERE id = ?1 → Ok(None) 当无结果 |
|
— delete: DELETE FROM memory_items WHERE id = ?1(幂等,不返回 NotFound) |
|
— list: 根据 filter 字段(prefix/since/offset/limit)组合动态构造 WHERE 子句 + ORDER BY created_at ASC + LIMIT ? OFFSET ?,全部使用参数化查询防注入 |
|
(6)序列化转换层:time::OffsetDateTime 存为 TEXT(ISO 8601),通过 .to_string() 绑定 String 参数;serde_json::Value 存为 TEXT,通过 serde_json::to_string() 绑定 String 参数;读取时通过 OffsetDateTime::parse 和 serde_json::from_str 反序列化。在 SqliteStore 内部实现 to_sql_params() / from_sql_row() 辅助方法集中处理 |
|
| (7)错误映射:不在 blanket impl From 中处理所有 rusqlite Error,而是在每个方法内部按数据完整性防御表的映射规则逐类处理: | |
— 数据库文件损坏 / IO 错误 → MemoryError::Storage |
|
— created_at 解析失败 → MemoryError::Serialization(不 panic) |
|
— content/metadata 为 NULL → MemoryError::Storage |
|
— serde_json 序列化/反序列化失败 → MemoryError::Serialization |
|
— 约束冲突 → MemoryError::InvalidInput |
|
(8)可观测性:在 4 个 trait 方法和 open() 上添加 #[tracing::instrument(skip(self))];正常完成记录 trace!,超过性能预算阈值记录 warn!,操作失败记录 error! |
|
| 涉及文件 | src/memory/store/sqlite_store.rs(新增) |
| 前置依赖 | Task B1(rusqlite + tempfile 依赖)、Task A1(store/ 目录存在) |
| 预估工作量 | M(1-4h) |
| 风险等级 | 高 — 3 个技术点需留意:time::OffsetDateTime 无 rusqlite::ToSql 实现,需显式处理 String 绑定;spawn_blocking + &self 生命周期需 clone 后才能传闭包;错误映射需精细匹配 rusqlite::Error 嵌套变体(SqliteFailure 内含 ErrorCode) |
| 验收条件 | 单元测试通过(见 Task B4)、cargo build 通过 |
Task B3 — 注册模块并重导出
| 项目 | 内容 |
|---|---|
| 任务描述 | 在 store.rs 添加 pub mod sqlite_store; + pub use sqlite_store::SqliteStore;;在 memory.rs 添加 pub use store::SqliteStore; |
| 涉及文件 | src/memory/store.rs(修改)、src/memory.rs(修改) |
| 前置依赖 | Task B2(sqlite_store.rs 文件存在) |
| 预估工作量 | XS(< 15min) |
| 风险等级 | 低 |
| 验收条件 | cargo build 通过,SqliteStore 可从 agcore::memory::SqliteStore 路径访问 |
Task B4 — 编写测试
| 项目 | 内容 |
|---|---|
| 任务描述 | 在 sqlite_store.rs 中编写 #[cfg(test)] mod tests,覆盖: |
| (1)CRUD 基本操作(save → get → list → delete → get None) | |
| (2)Upsert 语义(同 id 重复 save 覆盖内容,created_at 保持调用方传入值) | |
| (3)prefix 过滤(MemoryFilter.prefix) | |
| (4)时间范围过滤(MemoryFilter.since) | |
| (5)offset/limit 分页 | |
| (6)并发 10 个 writer × 10 次写入,验证无数据丢失 | |
| (7)持久化恢复(write → drop store → reopen 同一文件 → read) | |
(8)错误路径:open("/nonexistent_dir/ag.db") 返回 Storage 错误 |
|
辅助函数:make_item(id) 创建 MemoryItem,使用 tempfile::TempDir(或自定义 tmp 路径)隔离测试数据库文件 |
|
| 涉及文件 | src/memory/store/sqlite_store.rs(修改追加 test mod) |
| 前置依赖 | Task B2(实现完成) |
| 预估工作量 | M(1-4h) |
| 风险等级 | 中 — 并发测试的时序控制、持久化恢复测试的 TempDir 管理 |
| 验收条件 | 全量测试通过,新增测试 ≥ 8 个 |
Task B5 — 验证阶段二
| 项目 | 内容 |
|---|---|
| 任务描述 | 运行全量测试链确认 SqliteStore 实现正确,不影响已有模块 |
| 涉及文件 | 全量 |
| 前置依赖 | Task B3、Task B4 |
| 预估工作量 | S(< 1h) |
| 风险等级 | 低 |
| 验收条件 | cargo test --all-targets 全绿(191 + 新增测试)、cargo clippy --all-targets -- -D warnings 0 警告、cargo build 通过 |
阶段三:验证收尾
Task C1 — 跨模块兼容性验证
| 项目 | 内容 |
|---|---|
| 任务描述 | (1)确认 ConversationMemory / KnowledgeStore / MemoryRetriever / AgentSession / SessionMemory 以 Arc<dyn MemoryStore> 接受 SqliteStore 时编译通过且测试全绿 |
(2)确认 SqliteStore 与 InMemoryStore 可互换——修改一个现有测试将后端从 InMemoryStore 换为 SqliteStore(使用 ":memory:"),测试全绿 |
|
| (3)验证性能预算:在测试环境下测量 save(1KB)/get(1KB)/list(100行) 的单次延迟,确认 < 5ms / < 3ms / < 20ms | |
| 涉及文件 | 测试文件(memory/ 模块内各 test mod) |
| 前置依赖 | Task B5 |
| 预估工作量 | S(< 1h) |
| 风险等级 | 低 |
| 验收条件 | 全量测试通过 + 互换测试通过 + 性能预算大致满足(未达标不阻塞发布) |
依赖关系图
阶段一(目录重构) 阶段二(SqliteStore 实现)
┌──────────────┐ ┌──────────────┐
│ Task A1 │ │ Task B1 │ ← 无依赖,可与 A 并行
│ 创建目录+提取 │ │ Cargo.toml │
└──────┬───────┘ └──────┬───────┘
↓ ↓
┌──────────────┐ ┌──────────────┐
│ Task A2 │ │ Task B2 │
│ 修改store.rs │← A1 ───→│ 核心实现 │← B1 + A1
└──────┬───────┘ └──────┬───────┘
↓ ↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Task A3 │ │ Task B3 │← B2 ──→│ Task B4 │
│ 验证阶段一 │ │ 注册+重导出 │ │ 编写测试 │
└──────────────┘ └──────┬───────┘ └──────┬───────┘
↓ ↓
┌──────────────┐────────────────┘
│ Task B5 │← B3 + B4
│ 验证阶段二 │
└──────┬───────┘
↓
阶段三(验证收尾)
┌──────────────┐
│ Task C1 │
│ 跨模块兼容性 │
└──────────────┘
Commit 安排
| 顺序 | Commit 类型 | Scope | 描述 | 包含 Task |
|---|---|---|---|---|
| 1 | refactor | memory | 将 store.rs 拆分为模块目录,仅结构搬移 | A1 → A2 → A3 |
| 2 | feat | memory | 实现 SqliteStore 持久化(含错误映射、测试、WAL 模式) | B1 → B2 → B3 → B4 → B5 → C1 |
注意:Task B1 与阶段一无依赖,可以在 Commit 1 合并进行或在 Commit 2 开头。建议在 Commit 2 开头,因为 Cargo.toml 变更属于功能变更而非重构。
验证全链
实施完毕后整体认证链路:
cargo test --all-targets— 全量测试通过cargo clippy --all-targets -- -D warnings— 0 警告cargo build --release— release 构建通过- 确认
cargo doc --no-deps无 warning(新增公开类型文档注释) - 确认测试数量:191 + (8 个 new sqlite_store tests) = 199+ pass