Files
agcore/design/pdd/21-phase15-vector-store-persistence.md
徐涛 28ca43ccb2 chore(docs): 将设计文档从 docs 移至 design 目录
将 note、pdd、prd、roadmap 四类文档分别归入 `design/` 下对应子目录中,并新增 `.gitkeep` 占位文件
2026-07-23 05:45:53 +08:00

64 KiB
Raw Permalink Blame History

Phase 15: 向量存储持久化(SqliteStore 后端)

  • 文档编号21
  • 标题Phase 15 — 向量存储持久化
  • 日期2026-07-09
  • 状态:待实施
  • 涉及模块memory/vector_store.rs(新文件)、memory/vector.rsmemory.rsexamples/document_demo.rs
  • 关联文档roadmap.md(§Phase 15)、18-phase11-testing-and-retrieval.mdVectorRetriever trait)、20-phase14-document-and-embedding.mdDocument 系统 + Embedding 抽象)
  • 对应Roadmap §Phase 15v0.3.0 第三阶段)
  • 预估规模:新增约 400 行核心代码 + 约 30 行示例修改

1. 背景与目标

1.1 背景

agcore v0.3.0 的目标是从「LLM 调用工具箱」升级为「能构建多 Agent 协作、RAG、长记忆 Agent 产品的基础系统」。Roadmap §Phase 15 定位为 v0.3.0 的第三阶段,在 Phase 14 交付了 Document 类型和 Embedding 抽象之后,补齐 RAG 管线的核心存储与检索层

当前 agcore 已有:

  • VectorRetriever trait(Phase 11)——语义向量检索抽象,支持 index(id, embeddings)search(query, k)
  • InMemoryVectorRetrieverPhase 11)——基于 HashMap 的内存引用实现
  • Document 类型 + RecursiveCharacterSplitterPhase 14)——文档分割
  • Embedding trait + MockEmbeddingPhase 14)——文本向量化抽象

这四个组件构成了 RAG 管线的「前置」和「后置」环节,但缺少中间的 向量存储 环节——一个能持久化保存向量和文档、支持进程重启后恢复数据的核心存储抽象。

1.2 目标

  1. 定义 VectorStore trait——统一向量存储的 add / search / remove 接口 [高]
  2. 实现 InMemoryVectorStore——基于 HashMap 的内存引用实现,支持余弦相似度全量扫描 [高]
  3. 实现 PersistentVectorStore——基于 SqliteStore 的持久化包装,R/W 时双向同步 [高]
  4. 实现 RagPipeline——组合器,封装 split → embed → store.addingest)和 embed → store.searchretrieve [高]
  5. 标记 VectorRetriever 为废弃(#[deprecated]),引导迁移到 VectorStore [中]
  6. 更新 document_demo 示例,从手动循环迁移到 RagPipeline 两行调用 [中]
  7. 代码零新增外部依赖 [高]

1.3 适用范围

纳入 排除
VectorStore trait 定义(add / search / remove ANN 近似最近邻索引(O(n) 扫描,<10K 向量足够)
InMemoryVectorStoreHashMap + 余弦全量扫描) update 方法(remove + add 即可)
PersistentVectorStoreSqliteStore JSON blob 后端读写) namespace 运行时 CRUD(构造参数固定)
RagPipeline(具体 struct,非 trait 维度校验——调用方保证同一 VectorStore 实例写入的所有向量维度一致(通过 Embedding::dim() 确认);VectorStore 不做运行时校验
零新增外部依赖 RagPipeline trait 抽象(等第二种 pipeline 实现)
VectorRetriever 废弃标记(#[deprecated] pgvector / Qdrant 后端

2. 当前状态分析

2.1 现有 RAG 能力矩阵

维度 已实现 缺失
文档分割 RecursiveCharacterSplitterPhase 14
文本向量化 Embedding trait + MockEmbeddingPhase 14 真实 ProviderPhase 15 不解决)
向量检索抽象 VectorRetriever traitPhase 11 持久化语义检索
向量检索内存实现 InMemoryVectorRetrieverPhase 11
向量持久化 PersistentVectorStore(本 Phase
RAG 管线编排 RagPipeline(本 Phase
SqliteStore 后端 Phase 7

2.2 现有检索抽象对比

VectorRetriever traitPhase 11,将被废弃)

#[async_trait]
pub trait VectorRetriever: Send + Sync {
    async fn index(&self, id: String, embeddings: Vec<f32>) -> Result<(), MemoryError>;
    async fn search(&self, query: Vec<f32>, k: usize) -> Result<Vec<(String, f32)>, MemoryError>;
}

index 只接受单个 (id, embeddings) 对,不支持批量添加;search 返回 (id, score),不附带文档内容。调用方必须自行维护 id → Document 的映射。

VectorStore trait(本 Phase

#[async_trait]
pub trait VectorStore: Send + Sync {
    async fn add(&self, documents: &[Document], embeddings: &[Vec<f32>]) -> Result<(), MemoryError>;
    async fn search(&self, query: &[f32], k: usize) -> Result<Vec<(Document, f32)>, MemoryError>;
    async fn remove(&self, ids: &[String]) -> Result<(), MemoryError>;
}

add 接受批量 (doc, embedding) 对;search 直接返回 (Document, score),调用方无需额外映射。消除 Phase 11 设计中 id → Document 的隐式耦合。

2.3 模块依赖关系

当前(Phase 14 完成后):

Document + RecursiveCharacterSplitter (src/document.rs)
    ↓ output: Vec<Document>
RagPipeline::ingest (本 Phase) ← Embedding trait (src/llm/embedding.rs)
    ├── splitter.split()     → Vec<Document> chunks
    ├── embedder.embed()     → Vec<Vec<f32>>
    └── store.add()          → 持久化
                ↓
RagPipeline::retrieve (本 Phase)
    ├── embedder.embed(query) → Vec<Vec<f32>>
    └── store.search()        → Vec<(Document, f32)>

VectorRetriever trait (src/memory/vector.rs) → #[deprecated]
    ↓ 消费端迁移
VectorStore trait (src/memory/vector_store.rs) ← 新
    ├── InMemoryVectorStore        ← 新
    ├── PersistentVectorStore      ← 新 (依赖 SqliteStore)
    └── 被 RagPipeline 消费         ← 新

2.4 风险识别

风险 等级 缓解措施
PersistentVectorStore 构造时全量加载阻塞 MemoryStore::list() 由 SqliteStore 内部 spawn_blocking 卸载;10K 条预估 <500ms
JSON 序列化/反序列化性能 每条向量存储为独立 MemoryItem,写入/读取均单条操作,无全量 JSON
大向量集(>10K 条)搜索 O(n) 性能 文档标注当前为全量扫描,提供升级路径到 ANN 索引
SqliteStore 非向量专用后端,查询不支持向量索引 设计目标即「小数据量够用」,超出规模换专用后端
add() 不等长输入部分写入已发生 截断处理 + 返回 Err(InvalidInput),调用方可 let _ = 忽略
向量维度不一致(不同 Embedding provider 混用) VectorStore 不做运行时校验,调用方自行通过 Embedding::dim() 确认维度一致性
调用方传入重复 doc.id 静默覆盖 HashMap::insert 自然行为;等价于 upsert 语义,需在 doc comment 中注明
MemoryStore 淘汰策略误删向量条目 VectorEntry 存储 created_at 保留原始时间,避免 TTL 误判;默认 EvictionPolicy::None 无害

3. 可选方案

3.1 模块归属:vector_store.rs 放在哪里?

方案 描述 优点 缺点
A. memory/vector_store.rs(推荐) memory/vector.rs(现有 VectorRetriever)同目录 语义一致:向量存储属于记忆系统;复用现有 MemoryErrorMemoryStore 依赖;文件路径短 目录已存在,初始化工作量小
B. 新顶层模块 vector/ 创建 src/vector/mod.rs + vector_store.rs 语义独立,与 Document 顶层模块对等 增加顶层入口,增加模块声明开销;VectorStore 依赖 MemoryStore/MemoryError,放在 memory 外部反而增加耦合

结论:选择 方案 A。Roadmap 原定 src/vector/ 新模块,但实际分析发现 VectorStore 是记忆系统的自然扩展(类似 KnowledgeStore 放在 memory/),且 VectorRetriever 已在 memory/vector.rs。放在同一模块可以减少跨模块引用和 use 路径复杂度。 [高]

3.2 VectorStore trait:批量 vs 单条

方案 描述 优点 缺点
A. 批量 add(推荐) add(&[Document], &[Vec<f32>]) RagPipeline 一次 ingest 处理多个 chunk 时只需要一次 async call;批量语义明确 需要处理不等长输入
B. 单条 add add(Document, Vec<f32>) 签名简单,没有不等长问题 RagPipeline 遍历每个 chunk 逐个 awaitN 个 chunk 产生 N 次 async roundtrip

结论:选择 方案 A。提供 add_one 便捷方法作为语法糖。 [高]

3.3 search 返回值:元组 vs 命名结构体

方案 描述 优点 缺点
A. 元组(推荐) Vec<(Document, f32)> 零新类型;符合 Rust 惯用风格(与 VecRetriever::search 一致);字段访问 result.0.content 直观 .score 不如命名结构体 readable
B. ScoredDocument 结构体 struct ScoredDocument { doc: Document, score: f32 } 语义清晰;可扩展字段 YAGNI:当前没有需要扩展的额外字段;增加类型结构复杂度

结论:选择 方案 A。保持与 Phase 11 VectorRetriever::search 返回 Vec<(String, f32)> 一致的设计语言,只是把 Stringid)替换为 Document。 [高]

3.4 错误类型:复用 vs 新建

方案 描述 优点 缺点
A. 复用 MemoryError(推荐) 所有 VectorStore API 返回 Result<(), MemoryError>Result<Vec<...>, MemoryError> 零新类型;已有 InvalidInput(不等长)、NotFound(删除不存在 id)、SerializationJSON 反序列化)变体 MemoryError::Serialization 的语义略宽,但 JSON 反序列化失败确实属于序列化错误
B. 新建 VectorError 独立枚举:InvalidInput / DimensionMismatch / Serialization / Storage 类型精确;不与 memory 错误耦合 Phase 15 RagPipeline 需要同时处理 MemoryError + VectorError + LlmError;组合复杂度高

结论:选择 方案 A。VectorStore 是 memory/ 模块的直接组成部分,复用 MemoryError 自然合理。 [高]

3.5 RagPipelinestruct vs trait

方案 描述 优点 缺点
A. 具体 struct(推荐) RagPipeline { embedder, store, splitter } 代码量最少;当前只有一个实现 如果将来出现第二流水线(如多模态嵌入),需要抽象
B. RagPipeline trait 定义 ingest / retrieve 接口 可插拔 YAGNI:当前没有第二种 pipeline 需求;trait 一旦发布,签名变动是 breaking change

结论:选择 方案 A。YAGNI 原则。当出现第二种 pipeline 实现时,再从 struct 中提取 trait。 [高]

3.6 持久化格式:JSON blob vs 结构化列

方案 描述 优点 缺点
A. JSON blob(推荐) 每条向量存为 MemoryItemcontent 为 JSON.stringify(VectorEntry) 零 schema 迁移;利用现有 MemoryStore APIsave/get/list/delete 无法用 SQL 直接查询向量内容(业务上也不需要有此需求)
B. SqliteStore 专用表 在 sqlite_store.rs 中新增 vectors 查询效率高 突破 MemoryStore trait 抽象层,增加跨模块依赖;PersistentVectorStore 丧失后端无关性

结论:选择 方案 AMemoryStorelist(prefix) 可以按 namespace 过滤,足以支持构造时的全量加载。JSON blob 的序列化/反序列化成本在 <10K 向量场景下可以忽略。 [高]

3.7 模块扁平 vs 子目录

方案 描述 优点 缺点
A. 扁平(推荐) src/memory/vector_store.rs 单文件 约 400 行,一个文件包含 trait + 三个 struct + 内联测试 如果后期需要拆分为 search/persist/ 等子模块,需重构为目录
B. 目录预置 src/memory/vector_store/ 子目录 预留扩展空间 推测性设计;当前内容不足以分割

结论:选择 方案 A。所有代码在一个文件中,结构清晰:trait 定义在前,实现在后,测试在 #[cfg(test)] 中。 [高]


4. 推荐方案

4.1 整体架构

Phase 15 交付
┌─────────────────────────────────────────────────────┐
│  src/memory/vector_store.rs                          │
│                                                      │
│  ┌──────────────────────────────────────────────┐   │
│  │ VectorStore trait                             │   │
│  │  add(docs, embeddings) -> Result<(), ME>      │   │
│  │  search(query, k) -> Result<Vec<(Doc,f32)>,ME>│   │
│  │  remove(ids) -> Result<(), ME>                │   │
│  │  add_one(doc, emb) -> Result<(), ME>          │   │
│  └──────────────┬───────────────────────────────┘   │
│                 │ implements                         │
│  ┌──────────────┴───────────────┐                    │
│  │ InMemoryVectorStore         │                    │
│  │  entries: HashMap            │                    │
│  │  dot() / cosine_similarity() │                    │
│  └──────────────┬───────────────┘                    │
│                 │ wraps                              │
│  ┌──────────────┴───────────────┐                    │
│  │ PersistentVectorStore       │                    │
│  │  inner: InMemoryVectorStore │                    │
│  │  store: Arc<dyn MemoryStore> │                    │
│  │  namespace: String          │                    │
│  │  add(): 内存 + SqliteStore 双写  │                    │
│  └──────────────────────────────┘                    │
│                                                      │
│  ┌──────────────────────────────┐                    │
│  │ RagPipeline                  │                    │
│  │  embedder: Arc<dyn Embedding>│                    │
│  │  store: Arc<dyn VectorStore> │                    │
│  │  splitter: Option<Splitter>  │                    │
│  │  ingest: split→embed→store   │                    │
│  │  retrieve: embed→search      │                    │
│  └──────────────────────────────┘                    │
└─────────────────────────────────────────────────────┘

存储格式(SqliteStore 视角):
  MemoryItem.id = "vec:{namespace}:{doc_id}"
  MemoryItem.content = JSON(VectorEntry { doc_id, content, metadata, mime_type, embedding })
  MemoryItem.metadata = {}

4.2 核心类型设计

VectorStore trait

/// 向量存储抽象 —— 语义检索的核心接口。
///
/// 提供文档-向量的批量添加、余弦相似度搜索、批量删除三个核心操作。
/// 所有实现必须满足 Send + Sync 以支持跨 .await 调用。
///
/// # 并发安全
///
/// 实现内部必须使用线程安全的容器(如 Mutex<HashMap> 或 RwLock),
/// 允许跨多个 tokio task 共享 `&VectorStore` 引用。
#[async_trait]
pub trait VectorStore: Send + Sync {
    /// 批量添加文档及其向量。
    ///
    /// `documents` 和 `embeddings` 必须等长。不等长时:
    /// - 截取 `min(len)` 对处理(部分写入已发生)
    /// - 返回 `Err(MemoryError::InvalidInput)` 告知截断
    /// - 调用方可以 `let _ = store.add(...)` 忽略错误
    async fn add(&self, documents: &[Document], embeddings: &[Vec<f32>])
        -> Result<(), MemoryError>;

    /// 检索与 `query` 向量最相似的 `k` 条记录。
    ///
    /// 返回 `Vec<(Document, f32)>`,其中 `f32` 为余弦相似度分数,
    /// 取值范围 `[0.0, 1.0]`(对单位向量),按分数降序排列。
    ///
    /// # 守卫
    ///
    /// - 空索引 → 返回 `vec![]`
    /// - `k == 0` → 返回 `vec![]`
    /// - 零向量(norm ≈ 0)→ 返回 `vec![]`
    async fn search(&self, query: &[f32], k: usize)
        -> Result<Vec<(Document, f32)>, MemoryError>;

    /// 批量删除文档(幂等)。
    ///
    /// 不存在的 id 静默忽略,不会返回错误。
    async fn remove(&self, ids: &[String]) -> Result<(), MemoryError>;

    /// 便捷方法:单条添加。
    ///
    /// 等价于 `self.add(&[doc], &[emb]).await`。
    async fn add_one(&self, doc: Document, emb: Vec<f32>)
        -> Result<(), MemoryError> {
        self.add(&[doc], &[emb]).await
    }
}

设计决策

  • documentsembeddings 的输入使用切片引用(&[Document]&[Vec<f32>])——零拷贝,调用方可以传入 Vec 的引用或数组切片 [高]
  • search 返回 Vec<(Document, f32)> 而非 Vec<(String, f32)>——消除调用方维护 id → Document 映射的负担 [高]
  • remove 幂等设计——不存在的 id 静默忽略,简化调用方逻辑(无需在删除前检查 id 是否存在) [高]
  • 提供默认实现的 add_one 便捷方法——&[x] 语法足够精简,但 add_one 在链式调用中更清晰 [中]

InMemoryVectorStore

/// 内存向量存储 —— VectorStore 的引用实现。
///
/// 内部使用 `Mutex<HashMap<String, (Document, Vec<f32>)>>` 存储,
/// `search()` 执行 O(n) 全量余弦相似度扫描,适用于 ≤10K 条向量的场景。
///
/// # 并发安全
///
/// 使用 `std::sync::Mutex`(非 tokio Mutex)。
///
/// **锁持有时间评估**
/// - `add()` / `remove()`:微秒级(HashMap 插入/删除操作)
/// - `search()`:毫秒级(O(n) 全量扫描 + 余弦计算),对 10K 条 1536 维向量预估 1-10ms。
///   实现时在锁内克隆数据快照到 Vec 后立即释放锁,在锁外进行余弦相似度计算,
///   避免长时间持有锁阻塞并发写操作。
pub struct InMemoryVectorStore {
    pub(crate) entries: Mutex<HashMap<String, (Document, Vec<f32>)>>,
}

impl InMemoryVectorStore {
    /// 创建一个空存储。
    pub fn new() -> Self { /* ... */ }

    /// 从预填充的 entries 构造(供 PersistentVectorStore 使用)。
    pub(crate) fn with_entries(
        entries: HashMap<String, (Document, Vec<f32>)>,
    ) -> Self { /* ... */ }
}

add 实现逻辑

add(documents, embeddings):
    if documents.len() == embeddings.len():
        for i in 0..len(doc): insert(doc[i], emb[i])
        return Ok(())
    else:
        tracing::warn!(
            docs = documents.len(), embs = embeddings.len(),
            "InMemoryVectorStore::add 长度不匹配,截断到 min"
        )
        min_len = min(len(doc), len(emb))
        for i in 0..min_len: insert(doc[i], emb[i])
        return Err(InvalidInput)

    insert(doc, emb):
        entries.lock() → map.insert(doc.id.clone(), (doc, emb))

InMemoryVectorRetriever 的行为差异:旧 VectorRetriever::index 对不等长输入使用 zip 静默截断(无返回值),新 VectorStore::add 显式检查并返回 Err。迁移时调用方应检查返回的 Err 或确保输入等长。

search 实现逻辑

search(query, k):
    if k == 0: return vec![]
    tracing::trace!("InMemoryVectorStore::search, k={k}")
    对整个 query 向量做零向量检测(所有分量 ≈ 0.0

    lock entries → 获取所有值的快照 Vec<(Document, Vec<f32>)>

    for each (doc, emb):
        score = cosine_similarity(query, emb)
        if score < 1e-10: continue (排除无关或空向量)
        if score < 1e-10: continue  # 浮点除零保护:余弦相似度分母含 1e-10,正交或零向量产生趋零值,不构成有效结果
        results.push((doc, score))

    results.sort_by(|a, b| b.1.partial_cmp(&a.1))  // 降序
    results.truncate(k)
    return results

remove 实现逻辑

remove(ids):
    lock entries → map.retain(|key, _| !ids.contains(key))
    // retain 在 Rust 中是对传入闭包返回 true 的元素保留
    // 所以我们保留「不在 ids 中」的元素
    tracing::debug!(count = ids.len(), "InMemoryVectorStore::remove")

余弦相似度辅助函数

/// 点积。
fn dot(a: &[f32], b: &[f32]) -> f32 {
    debug_assert_eq!(a.len(), b.len(), "dot: 不等长向量 ({}/{}), 维度不匹配将导致静默截断", a.len(), b.len());
    a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}

/// 余弦相似度,加 1e-10 防除零。
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
    let dot_product = dot(a, b);
    let norm_a = dot(a, a).sqrt();
    let norm_b = dot(b, b).sqrt();
    dot_product / (norm_a * norm_b + 1e-10)
}

设计决策

  • dot()cosine_similarity()pub(crate) 私有辅助函数——不暴露为公共 API,如需在其他模块使用,可通过 InMemoryVectorStore::search 间接调用 [中]
  • 使用 std::sync::Mutex 而非 tokio::sync::Mutexadd/remove 锁持有微秒级(HashMap 插入/删除),search 在锁内克隆数据快照后立即释放锁,在锁外计算余弦相似度(预估 1-10ms/10K 条),避免长时间持有锁阻塞并发写操作 [高]
  • 零向量守卫在 search() 中而非 cosine_similarity() 中——避免每次相似度计算都检测,只在入口做一次 [中]

PersistentVectorStore

/// 持久化向量存储 —— 基于 MemoryStore 的持久化包装。
///
/// # 架构
///
/// 运行时全量加载到 InMemoryVectorStore 做余弦搜索,
/// 写操作(add/remove)同时同步到内存和后端 MemoryStore。
///
/// # 存储格式
///
/// 每条向量存为一条 MemoryItem
/// - `id`: `"vec:{namespace}:{doc_id}"`colon-separated namespace 前缀)
/// - `content`: JSON 序列化的 VectorEntry
/// - `metadata`: 空 HashMap
///
/// # 构造开销
///
/// `new()` 通过 `store.list(prefix)` 全量加载已有条目,
/// 时间复杂度 O(N)(N 为已有向量数),适用于 ≤10K 条的场景。
pub struct PersistentVectorStore {
    inner: InMemoryVectorStore,
    store: Arc<dyn MemoryStore>,
    namespace: String,
}

私有 VectorEntry 结构体

#[derive(Serialize, Deserialize)]
struct VectorEntry {
    doc_id: String,
    content: String,
    metadata: HashMap<String, String>,
    mime_type: String,
    embedding: Vec<f32>,
    /// ISO 8601 创建时间(UTC),持久化 roundtrip 重建时保持原时间,
    /// 避免 MemoryStore 的 TTL 淘汰策略误判。
    created_at: String,
}

构造方法 new()(异步):

pub async fn new(store: Arc<dyn MemoryStore>, namespace: &str) -> Result<Self, MemoryError> {
    let prefix = format!("vec:{}:", namespace);
    let filter = MemoryFilter { prefix: Some(prefix.clone()), ..Default::default() };

    // MemoryStore::list 内部由 SqliteStore 使用 spawn_blocking 卸载到阻塞线程池
    tracing::debug!(namespace = %namespace, "PersistentVectorStore::new — 开始全量加载");
    let items = store.list(&filter).await?;
    tracing::info!(count = items.len(), "PersistentVectorStore::new — 加载完成");

    let mut entries = HashMap::new();
    for item in items {
        let entry: VectorEntry = serde_json::from_str(&item.content)
            .map_err(|e| MemoryError::Serialization(e.to_string()))?;
        let doc = Document {
            id: entry.doc_id,
            content: entry.content,
            metadata: entry.metadata,
            mime_type: entry.mime_type,
        };
        // created_at 保留在 VectorEntry 中用于 roundtrip 重建,TTL 淘汰由 MemoryStore 管理
        // VectorEntry.created_at 字段在反序列化时已自动填充)
        entries.insert(doc.id.clone(), (doc, entry.embedding));
    }

    tracing::info!(entries = entries.len(), "PersistentVectorStore — 内存索引重建完成");
    Ok(Self {
        inner: InMemoryVectorStore::with_entries(entries),
        store,
        namespace: namespace.to_string(),
    })
}

spawn_blocking 说明MemoryStore::list() 是 async trait 方法,SqliteStore 实现内部使用 tokio::task::spawn_blocking 将 SQLite 查询卸载到阻塞线程池。PersistentVectorStore::new() 本身在 async context 中调用即可,无需额外 spawn_blocking。加载耗时 ≈ SQLite 查询时间 + JSON 反序列化时间,10K 条向量预估在 200ms-500ms 范围内(O(n) 反序列化,每条 JSON <2KB)。

add 双写路径(先持久化后内存):

add(documents, embeddings):
    tracing::debug!(count = docs.len(), "PersistentVectorStore::add")
    1. for each (doc, emb):
        entry = VectorEntry {
            doc_id, content, metadata, mime_type,
            embedding: emb,
            created_at: OffsetDateTime::now_utc()
                .format(&Rfc3339)          // ISO 8601 字符串
        }
        json = serde_json::to_string(&entry)
        key = format!("vec:{}:{}", self.namespace, doc.id)
        self.store.save(MemoryItem { id: key, content: json, metadata: {}, .. })
            .await?                               // 先写持久化(失败时不污染内存)
    2. self.inner.add(docs, embs).await?          // 再写内存
    3. Ok(())

search 完全委托

search(query, k):
    tracing::trace!("PersistentVectorStore::search, k={k}")
    self.inner.search(query, k).await   // 纯内存搜索

remove 双删(先删持久化后删内存):

remove(ids):
    tracing::debug!(count = ids.len(), "PersistentVectorStore::remove")
    1. for each id:
        key = format!("vec:{}:{}", self.namespace, id)
        self.store.delete(&key).await?     // 先删持久化(幂等)
    2. self.inner.remove(ids).await?      // 再删内存
    3. Ok(())

设计决策

  • new() 使用 async 而非同步构造——MemoryStore::list() 是异步操作;即使调用方想立即 .await,异步构造也比同步构造 + 显式 init() 调用更安全(不会忘记调用 init() [高]
  • 使用 MemoryFilter { prefix: Some(...) } 而非 list_all + 客户端过滤——SqliteStore 支持 prefix 索引扫描,减少网络传输量 [高]
  • VectorEntry.doc_id 使用 doc.id 的值而非重新生成——删除时可通过 id 反推 MemoryItem key [高]
  • 写入顺序:先持久化后内存——持久化写入失败时内存不被污染,下次构造 PersistentVectorStore 从持久化全量加载,自动忽略不完整的写入;内存写入失败时持久化已写入(产生「持久化有、内存无」的中间状态),但 search 读到的是持久化已确认子集而非超集,不会返回幽灵数据;addremove 双写均遵守此顺序 [高]
  • 批量 add 的非原子性:add 的持久化阶段对 N 条文档执行 N 次独立 store.save(),非原子操作。第 i 条失败时前 i-1 条已持久化。这是已知限制,未来可通过引入 MemoryStore::save_batch 批量 API 优化。当前 PersistentVectorStore::new() 全量加载时能正确跳过缺失条目 [中]

remove 幽灵数据窗口:先 store.delete() 成功后、inner.remove() 前的窗口内,search() 委托到 inner 可能返回已被删除的条目(幽灵数据)。该窗口有限——重启后从持久化全量加载时幽灵条目自然消失。调用方如需精确状态,可在 remove() 返回后主动调用 search() 验证。 [中]

键命名示例

namespace doc.id MemoryItem id
"default" "doc_001:chunk:0000" "vec:default:doc_001:chunk:0000"
"reports" "rpt_42" "vec:reports:rpt_42"
"research" "paper_xyz:chunk:0005" "vec:research:paper_xyz:chunk:0005"

注意doc.id 中如果包含冒号(如 doc_001:chunk:0000),拼接后的 key 为 vec:namespace:id 三层结构。MemoryFilter 的 prefix 匹配使用 prefix = format!("vec:{}:", namespace),即 "vec:default:",可以匹配 "vec:default:doc_001:chunk:0000""vec:default:other_id:with:colons"。因为 prefix 只是字符串前缀匹配,中间包含冒号不影响匹配的正确性。 [高]

RagPipeline

/// RAG 管线组合器 —— 封装 split → embed → store 和 embed → search 两个核心流程。
///
/// # 使用方式
///
/// ```rust
/// let pipeline = RagPipeline::new(embedder, store);
/// pipeline.ingest(&documents).await?;
/// let results = pipeline.retrieve("what is agcore?", 5).await?;
/// ```
///
/// # 分割器
///
/// `splitter` 字段为 `Option<RecursiveCharacterSplitter>`
/// - `Some(splitter)` → `ingest()` 先分割再嵌入(调用方传入原始文档)
/// - `None` → `ingest()` 跳过分割,直接嵌入(调用方已分好 chunk
pub struct RagPipeline {
    embedder: Arc<dyn Embedding>,
    store: Arc<dyn VectorStore>,
    splitter: Option<RecursiveCharacterSplitter>,
}

impl RagPipeline {
    /// 创建新的 RAG 管线。
    ///
    /// 不设置分割器时,`ingest()` 跳过分割阶段,
    /// 调用方传入的 Document 应已是分割好的 chunk。
    pub fn new(
        embedder: Arc<dyn Embedding>,
        store: Arc<dyn VectorStore>,
        splitter: Option<RecursiveCharacterSplitter>,
    ) -> Self { /* ... */ }

    /// 摄取文档:分割 → 向量化 → 存储。
    ///
    /// 流程:
    /// 1. 如果 splitter 存在,先分割文档为 chunks
    /// 2. 提取所有 chunk 的 content 为 Vec<String>
    /// 3. embedder.embed() 批量向量化
    /// 4. store.add() 批量存储
    ///
    /// # 边界
    ///
    /// - 空文档切片 → Ok(()), 无操作
    /// - 分割后 chunk 为空 → Ok(()), 无操作
    pub async fn ingest(&self, documents: &[Document]) -> Result<(), MemoryError> {
        // 分割(可选)
        let chunks = match &self.splitter {
            Some(splitter) => splitter.split(documents),
            None => documents.to_vec(),
        };
        if chunks.is_empty() {
            return Ok(());
        }

        // 向量化
        let texts: Vec<String> = chunks.iter().map(|d| d.content.clone()).collect();
        let embeddings = self.embedder.embed(&texts)
            .await
            .map_err(|e| MemoryError::Storage(e.to_string()))?;

        // 存储
        self.store.add(&chunks, &embeddings).await?;
        Ok(())
    }

    /// 检索:向量化查询 → 向量相似度搜索。
    ///
    /// # 边界
    ///
    /// - 空字符串查询 → 返回 vec![]embed 产生零向量 → search 零向量守卫)
    pub async fn retrieve(&self, query: &str, k: usize)
        -> Result<Vec<(Document, f32)>, MemoryError> {
        let embeddings = self.embedder.embed(&[query.to_string()])
            .await
            .map_err(|e| MemoryError::Storage(e.to_string()))?;
        self.store.search(&embeddings[0], k).await
    }
}

设计决策

  • embedder.embed() 返回 LlmErrorstore.add/search 返回 MemoryError——在两个错误类型之间通过 MemoryError::Storage(e.to_string()) 桥接 [高]
  • splitterOption——兼顾「调用方已分割」和「需要管线自动分割」两种场景 [高]
  • ingestretrieve 均返回 MemoryError——上层统一处理一种错误类型,简化调用方代码 [高]
  • retrieve 内部构造 &[query.to_string()]——接受 &str 而非 &[String],接口更友好 [中]

4.3 边界情况处理

场景 行为 原因
add() 输入 documents.len() ≠ embeddings.len() 截取 min(len) 对处理,返回 Err(InvalidInput) 不等长属于调用方错误;截取保证至少部分写入有效
add() 输入不等长截断后 调用方可 let _ = store.add(...).await 忽略 Err(部分数据已写入),或检查 Err 的 Display 文案了解截断数量 不等长时无法区分"全部写入"和"部分写入",调用方应评估业务容忍度
add() 重复 doc.id 静默覆盖(upsert 语义)——后写入的 Document 和 embedding 替换前者 HashMap insert 的自然行为,等价于 update
add() 输入空切片 (&[], &[]) 空操作,返回 Ok(()) 合法输入
remove() 不存在的 id 静默忽略(幂等) 简化调用方逻辑
remove() 空切片 (&[]) 空操作,返回 Ok(()) 合法输入
search() 空索引 返回 vec![] 无数据可查
search() k = 0 返回 vec![] 请求 0 条结果
search() 零向量查询 返回 vec![](零向量守卫) 零向量无法做有意义的相似度比较
search() 索引中所有向量与查询正交 返回 vec![](相似度 ≤ 1e-10 全部过滤) 无相关内容
PersistentVectorStore::new() 加载失败 返回 Err(MemoryError::Storage) 依赖后端异常,不可恢复
JSON 反序列化失败 返回 Err(MemoryError::Serialization) 数据损坏或 schema 变更
ingest() 空文档切片 Ok(()),无操作 合法输入
ingest() 分割后 chunk 为空 Ok(()),无操作 所有文档均为空内容
add_one() 内部委托 等价于 add(&[doc], &[emb]) 保持一致性

4.4 错误处理策略

场景 处理方式 原因
add() 不等长 Err(MemoryError::InvalidInput)(含截断说明) 调用方可选择 let _ = 忽略或检查错误
PersistentVectorStore::add 持久化写入失败 Err 传播(内存未写入——先写持久化后写内存) 先持久化后内存,持久化失败时内存不被污染,重启后自动忽略不完整写入
PersistentVectorStore::remove 持久化删除失败 Err 传播(内存未删除——先删持久化后删内存) 先持久化后内存,持久化失败时内存不受影响;尚未删除的条目可安全重试
PersistentVectorStore 构造加载失败 Err 传播 启动失败应尽早暴露
JSON 序列化/反序列化失败 Err(MemoryError::Serialization) 数据损坏属不可恢复错误
MemoryStore 后端 IO 错误 Err 传播 依赖底层错误,不转换
remove() 不存在的 id 静默忽略(幂等) 符合 MemoryStore::delete 的幂等语义
RagPipeline ingest 空文档列表 Ok(()),无操作 合法输入
RagPipeline 中 LlmError 桥接为 MemoryError::Storage(原始错误 via .to_string() 统一错误类型降低调用方复杂度

4.5 与现有系统的关系

前 Phase 11 (memory/vector.rs):
  VectorRetriever trait          → #[deprecated(since = "0.3.0")]
  InMemoryVectorRetriever        → #[deprecated(since = "0.3.0")]

本 Phase (memory/vector_store.rs):
  VectorStore trait              → 新
  InMemoryVectorStore            → 新(语义等价于 InMemoryVectorRetriever
  PersistentVectorStore          → 新
  RagPipeline                    → 新

Phase 14 消费:
  Document                        → VectorStore::add 输入类型
  Embedding trait                 → RagPipeline::ingest/retrieve 调用
  RecursiveCharacterSplitter      → RagPipeline::ingest 可选前置

5. 实施计划

5.1 文件与代码量估计

文件 角色 估计行数 新增/修改
src/memory/vector_store.rs VectorStore trait + InMemoryVectorStore + PersistentVectorStore + RagPipeline + ~19 测试 ~400 新增
src/memory/vector.rs VectorRetriever + InMemoryVectorRetriever 加 deprecation 标注 +4 修改
src/memory.rs 追加 pub mod vector_store + re-export +3 修改
examples/document_demo.rs 从手动循环迁移到 RagPipeline 两行调用 ~-30 修改
合计 ~377 1 新增 + 3 修改

5.2 实施步骤

Step 15.1 ──→ Step 15.2 ──→ Step 15.3 ──→ Step 15.4 ──→ Step 15.5 ──→ Step 15.6
vector_store  追加           追加            标记废弃 +      简化             全量验证
.rs           Persistent    RagPipeline    更新 re-export  document_demo    cargo test
(trait +      VectorStore   + 测试           + 测试                           cargo clippy
 InMemory)                                                                   cargo doc

Step 15.1 — 创建 vector_store.rstrait + InMemoryVectorStore

涉及文件src/memory/vector_store.rs(新增)

前置依赖:无(独立新增文件)

工作量M1-2h

验收条件cargo build --all-targets 通过

内容

  1. VectorStore trait 定义(#[async_trait]3 个必选方法 + add_one 默认实现)
  2. InMemoryVectorStore struct + new() / with_entries() 构造器
  3. VectorStore for InMemoryVectorStore 的实现:
    • add:等长校验 → 遍历 zip → insert(不等长截断 + Err
    • searchlock → 空/k=0/零向量守卫 → 全量余弦扫描 → sort → truncate
    • removelock → HashMap::retain
  4. 私有辅助函数 dot(a, b) → f32cosine_similarity(a, b) → f32
  5. 内联测试(10 个):basic_add_and_search / search_empty_store / search_zero_vector / search_k_is_zero / search_orthogonal_vectors / add_mismatched_lengths / add_duplicate_id_upsert / remove_items / remove_nonexistent_id / concurrent_operations

验证cargo build --all-targets 编译通过。

Step 15.2 — 追加 PersistentVectorStore

涉及文件src/memory/vector_store.rs

前置依赖Step 15.1

工作量M1-2h

验收条件:持久化 roundtrip 测试(write → close → reopen → read)通过

内容

  1. 私有 VectorEntry 结构体(#[derive(Serialize, Deserialize)]
  2. PersistentVectorStore struct + async fn new(store, namespace) 构造方法
    • store.list(prefix="vec:{namespace}:") 全量加载
    • serde_json::from_str 反序列化
    • InMemoryVectorStore::with_entries() 注入
  3. VectorStore for PersistentVectorStore 实现:
    • add:序列化每个 VectorEntry → 先逐个 store.save()(先持久化)→ 再 inner.add()(后内存)
    • search:完全委托 inner.search()
    • remove:逐个 store.delete()(先删持久化)→ 再 inner.remove()(后删内存)

验证cargo test -- memory::vector_store::persistent 全部通过。

Step 15.3 — 追加 RagPipeline

涉及文件src/memory/vector_store.rs

前置依赖Step 15.2

工作量S< 1h

验收条件:端到端 ingest_and_retrieve 集成测试通过

内容

  1. RagPipeline struct3 个字段:embedder / store / splitter
  2. new() 构造函数
  3. ingest()split(可选)→ embed → store.add
  4. retrieve()embed → store.search
  5. 桥接 LlmError → MemoryError::Storage
  6. 内联测试(4 个):ingest_and_retrieve + retrieve_empty_store + ingest_empty_docs(空文档切片返回 Ok(())+ ingest_empty_split(分割后 chunk 为空返回 Ok(())

验证cargo test -- memory::vector_store::rag 全部通过。

Step 15.4 — 标记 VectorRetriever 废弃 + 更新 re-export

涉及文件src/memory/vector.rs(修改)、src/memory.rs(修改)

前置依赖Step 15.1VectorStore trait 存在)

工作量S< 0.5h

验收条件cargo build 项目内部零 deprecation warning(已使用 #[allow(deprecated)] 的测试除外)

内容

  • src/memory/vector.rs

    • VectorRetriever trait 前加 #[deprecated(since = "0.3.0", note = "请使用 memory::VectorStore")]
    • InMemoryVectorRetriever struct 前加 #[deprecated(since = "0.3.0", note = "请使用 memory::InMemoryVectorStore")]
    • 内联测试前加 #[allow(deprecated)]
  • src/memory.rs

    • 追加 pub mod vector_store;
    • 追加 re-export
      • pub use vector_store::VectorStore;
      • pub use vector_store::InMemoryVectorStore;
      • pub use vector_store::PersistentVectorStore;
      • pub use vector_store::RagPipeline;

验证cargo build --all-targets 编译通过,cargo clippy 0 警告。

Step 15.5 — 简化 document_demo.rs

涉及文件examples/document_demo.rs

前置依赖Step 15.3RagPipeline 存在)

工作量S< 0.5h

验收条件cargo run --example document_demo exit 0

迁移概要:移除手动 InMemoryVectorRetriever 循环 + id 映射,替换为 RagPipeline::new(embedder, store, splitter)ingest()retrieve() 两行调用。文件从 ~74 行缩减为 ~40 行。

验证cargo run --example document_demo → exit 0。

Step 15.6 — 全量验证

验证标准

检查项 指标
cargo build --all-targets 通过
cargo test --all-targets 全绿(313+ → 332+~19 个新测试)
cargo doc --no-deps 0 warning
零新外部依赖 Cargo.toml 无修改
cargo run --example document_demo exit 0
性能基准验证 PersistentVectorStore::new 10K 加载 <500msInMemoryVectorStore::search 10K 搜索 <100ms

6. 测试计划

分组 测试 场景 验证点
InMemory basic_add_and_search 3 个 3 维向量,query 匹配 Top1 最相似文档的 id 和 score 正确
InMemory search_empty_store 空索引 + 任意 query 返回 vec![]
InMemory search_zero_vector 零向量查询(norm ≈ 0 零向量守卫 → vec![]
InMemory add_mismatched_lengths 3 docs + 2 embs(不等长) 截取 2 对处理 + 返回 Err
InMemory remove_items add 2 → remove 1 → search 已删除 id 不在结果中
InMemory remove_nonexistent_id remove 从未添加的 id 静默忽略,返回 Ok(())
InMemory concurrent_operations 10 并发生成 + 混合搜索 无 panic
InMemory search_k_is_zero k=0 + 非空索引 k=0 守卫 → 返回 vec![]
InMemory add_duplicate_id_upsert 同一 doc.id 写入两次 第二次写入覆盖前者,search 返回 1 条
InMemory search_orthogonal_vectors 索引中向量与查询正交 相似度 < 1e-10 过滤 → 返回 vec![]
Persistent persistent_roundtrip write 3 条 → 重建 store → read 数据完整不丢
Persistent search_after_reload write → 重建 store → search 重启后检索结果匹配
Persistent namespace_isolation 两个 namespace 各自写入 互不干扰,search 正确
Persistent concurrent_access 多 task 写入 + 搜索 无死锁
Persistent partial_add_recovery 写入 5 条,模拟第 3 条持久化失败,重建 store 前 2 条存活,后 3 条缺失
Persistent new_empty_store 空 MemoryStore 构造 PersistentVectorStore search 返回空结果
RagPipeline ingest_and_retrieve Document → embed → add → search 端到端 检索结果包含目标文档
RagPipeline ingest_empty_docs 空文档切片传入 ingest 返回 Ok(()) 不报错
RagPipeline ingest_empty_split 分割后 chunk 为空 返回 Ok(()) 不报错
RagPipeline retrieve_empty_store 空 store + 任意查询 返回 vec![]

7. 文件变更清单

文件 操作 说明
src/memory/vector_store.rs 新增 ~400 行:VectorStore trait + InMemoryVectorStore + PersistentVectorStore + RagPipeline + 内联测试
src/memory/vector.rs 修改 +4 行:两个 deprecated 标注 + 测试 #[allow(deprecated)]
src/memory.rs 修改 +3 行:module 声明 + 4 个 pub use re-export
examples/document_demo.rs 修改 ~74 → ~40 行:移除手动 VectorRetriever,改为 RagPipeline 两行调用

验证标准

| 检查项 | 指标 | | cargo test --all-targets | 全绿(313+ → 332+~19 新测试) | | cargo build --all-targets | 通过 | | cargo clippy --all-targets -- -D warnings | 0 警告 | | cargo doc --no-deps | 0 warning | | cargo run --example document_demo | exit 0 | | 零新外部依赖 | Cargo.toml 无修改 | | 性能基准验证 | PersistentVectorStore::new 10K 加载 <500msInMemoryVectorStore::search 10K 搜索 <100ms |

8. 回滚方案

8.1 代码回滚

场景 操作
vector_store.rs 编译/测试失败 git checkout -- src/memory/vector_store.rs
deprecation 标注问题 git checkout -- src/memory/vector.rs src/memory.rs
document_demo 不工作 git checkout -- examples/document_demo.rs
全量回滚 git revert <phase15-commit>

8.2 数据回滚

PersistentVectorStore 使用 vec:{namespace}: 前缀写入 SqliteStore,回退到旧版本时需清理这些数据:

-- 清理所有 Phase 15 写入的向量数据
DELETE FROM memory_items WHERE id LIKE 'vec:%';

-- 按 namespace 精确清理(如果只回滚特定向量集合)
DELETE FROM memory_items WHERE id LIKE 'vec:my_knowledge:%';

不可逆决策声明

  • VectorEntry JSON 格式(字段名、embedding 序列化方式、created_at 格式)一旦发布,后续修改需考虑旧数据兼容性
  • vec:{namespace}:{doc_id} 键命名约定一旦外部消费者依赖此模式,更改需迁移
  • VectorStore trait 的 add/search/remove 签名一旦发布,修改是 breaking change

缓解措施:若需在 v0.4 中变更 JSON 格式,建议在 VectorEntry 中引入 version 字段(v1),反序列化时按版本路由解析逻辑。

9. 不做的事

  • ANN 索引(O(n) 扫描,<10K 向量足够)
  • update 方法(remove + add 即可)
  • namespace 运行时 CRUD(构造参数固定)
  • 向量维度校验(同现有策略,错误维度不 panic 但结果异常)
  • RagPipeline trait 抽象(等第二种 pipeline 实现时再提取)
  • pgvector / Qdrant 等专用向量数据库后端

11. Roadmap 同步要点

实施完成后需同步更新 docs/roadmap.md

  1. Phase 15 状态⏳ 待实施✅ Phase 15 全部交付物已完成
  2. 依赖关系图P15["..."]:::pending:::done
  3. 里程碑表M11 状态从 ✅ 2026-07-09
  4. 顶部当前状态:追加 Phase 15 完成
  5. 下一步行动:指向 Phase 16 摘要自动生成
  6. 已完成列表:追加 - ✅ Phase 15 向量存储持久化 — VectorStore trait + InMemoryVectorStore + PersistentVectorStore + RagPipeline
  7. 模块位置修正Phase 15 交付物描述中 src/vector/ 修正为 src/memory/vector_store.rs
  8. 文档链接:追加 docs/21-phase15-vector-store-persistence.md

12. 参考来源

  1. Phase 11 VectorRetriever traitdocs/18-phase11-testing-and-retrieval.md

    • 现有 VectorRetriever trait 设计,本 Phase 标记废弃并迁移到 VectorStore
  2. Phase 14 Document 系统 + Embedding 抽象docs/20-phase14-document-and-embedding.md

    • Document 类型和 Embedding trait 是本 Phase RagPipeline 的前置依赖
  3. Phase 7 SqliteStoredocs/14-phase7-sqlite-store.md

    • MemoryStore trait 的 SQLite 后端,PersistentVectorStore 的持久化依赖
  4. Roadmap Phase 15 定义docs/roadmap.md §Phase 15v0.3.0 第三阶段)

    • 交付物描述、设计要点、依赖关系
  5. MemoryError 错误类型src/memory/error.rs

    • 复用错误类型的设计依据(InvalidInput / Serialization / Storage / NotFound
  6. MemoryStore traitsrc/memory/store/mod.rs

    • save / get / delete / list 接口签名,PersistentVectorStore 构造时依赖 list(prefix),写入时依赖 save / delete

13. 详细实施计划

13.1 任务依赖总图

flowchart LR
    classDef step fill:#e1f5fe,stroke:#0288d1
    classDef verify fill:#e8f5e9,stroke:#388e3c

    subgraph Step15_1["Step 15.1 — vector_store.rstrait + InMemory"]
        15_1_1["15.1.1 定义 VectorStore trait"] --> 15_1_2["15.1.2 实现 InMemoryVectorStore struct"]
        15_1_2 --> 15_1_4["15.1.4 实现 dot / cosine_similarity"]
        15_1_4 --> 15_1_3["15.1.3 实现 VectorStore for InMemoryVectorStore"]
        15_1_3 --> 15_1_5["15.1.5 编写 10 个内联测试"]
    end

    subgraph Step15_2["Step 15.2 — 追加 PersistentVectorStore"]
        15_2_1["15.2.1 定义 VectorEntry 结构体"] --> 15_2_2["15.2.2 实现 PersistentVectorStore + new()"]
        15_2_2 --> 15_2_3["15.2.3 实现 VectorStore for PersistentVectorStore"]
        15_2_3 --> 15_2_4["15.2.4 编写 6 个内联测试"]
    end

    subgraph Step15_3["Step 15.3 — 追加 RagPipeline"]
        15_3_1["15.3.1 实现 RagPipeline struct + new()"] --> 15_3_2["15.3.2 实现 ingest()"]
        15_3_1 --> 15_3_3["15.3.3 实现 retrieve()"]
        15_3_2 --> 15_3_4["15.3.4 编写 4 个内联测试"]
        15_3_3 --> 15_3_4
    end

    subgraph Step15_4["Step 15.4 — 标记废弃 + 更新 re-export"]
        15_4_1["15.4.1 VectorRetriever 加 #[deprecated]"] --> 15_4_2["15.4.2 InMemoryVectorRetriever 加 #[deprecated]"]
        15_4_2 --> 15_4_3["15.4.3 memory.rs 追加 re-export"]
    end

    subgraph Step15_5["Step 15.5 — 简化 document_demo"]
        15_5_1["15.5.1 迁移至 RagPipeline 两行调用"]
    end

    subgraph Step15_6["Step 15.6 — 全量验证"]
        15_6_1["15.6.1 cargo build --all-targets"]
        15_6_2["15.6.2 cargo test --all-targets"]
        15_6_3["15.6.3 cargo clippy"]
        15_6_4["15.6.4 cargo doc --no-deps"]
        15_6_5["15.6.5 性能基准验证"]
        15_6_6["15.6.6 cargo run --example document_demo"]
        15_6_7["15.6.7 验证 Cargo.toml 零变更"]
    end

    15_1_5 --> 15_2_1
    15_2_4 --> 15_3_1
    15_3_4 --> 15_5_1
    15_1_5 --> 15_4_1
    15_5_1 --> 15_6_1

    class 15_1_1,15_1_2,15_1_3,15_1_4,15_1_5,15_2_1,15_2_2,15_2_3,15_2_4,15_3_1,15_3_2,15_3_3,15_3_4 step
    class 15_4_1,15_4_2,15_4_3,15_5_1 step
    class 15_6_1,15_6_2,15_6_3,15_6_4,15_6_5,15_6_6,15_6_7 verify

13.2 Step 15.1 — 创建 vector_store.rstrait + InMemoryVectorStore

涉及文件src/memory/vector_store.rs(新增) 前置依赖:无 工作量M1-2h

# 任务描述 涉及文件 前置依赖 工作量 风险 验收条件
15.1.1 定义 VectorStore trait3 个必选方法(add / search / remove+ add_one 默认实现 + doc comments + #[async_trait] src/memory/vector_store.rs S cargo build 通过;cargo doc --no-deps 0 warning(确认 intra-doc link 正确)
15.1.2 实现 InMemoryVectorStore structMutex<HashMap<String, (Document, Vec<f32>)>> 内部存储 + new()pub(crate) with_entries() 构造器 src/memory/vector_store.rs 15.1.1 S 编译通过;with_entries 对外不可见
15.1.4 实现私有辅助函数 dot(a, b) → f32(含 debug_assert_eq!(a.len(), b.len()))和 cosine_similarity(a, b) → f32(加 1e-10 防除零),pub(crate) 可见性 src/memory/vector_store.rs 15.1.2 S 编译通过;零向量与零向量 cosine_similarity 不 panic,返回 0.0(因 1e-10 防除零);正交向量接近 0.0
15.1.3 实现 VectorStore for InMemoryVectorStoreadd()(等长校验/截断+Err+tracing::warn!/Mutex insert)、search()(锁内克隆快照→释放锁→外余弦扫描→sort+truncate,含空/k=0/零向量守卫)、remove()HashMap::retain src/memory/vector_store.rs 15.1.4(依赖 dot/cosine_similarity 辅助函数) M 单元测试通过;不等长输入返回 Err(InvalidInput) 且已写入部分数据;空索引返回 vec![]k=0 返回 vec![]
15.1.5 编写 10 个内联测试:basic_add_and_search / search_empty_store / search_zero_vector / search_k_is_zero / search_orthogonal_vectors / add_mismatched_lengths / add_duplicate_id_upsert / remove_items / remove_nonexistent_id / concurrent_operations src/memory/vector_store.rs 15.1.3 S cargo test -- InMemory 全部通过;并发测试验证 10 个并发无 panic 且 search 结果计数正确(并发 add N 条后 search 返回 N 条);remove_nonexistent_id 验证删除不存在的 id 幂等静默忽略

说明Step 15.1 执行顺序:15.1.1 → 15.1.2 → 15.1.4(辅助函数) → 15.1.3(依赖辅助函数的 impl → 15.1.5(测试),严格串行。add_mismatched_lengths 测试需验证不等长截断语义——截取 min(len) 对处理并返回 Err(InvalidInput)remove_nonexistent_id 测试验证幂等性——删除不存在的 id 应返回 Ok(())

13.3 Step 15.2 — 追加 PersistentVectorStore

涉及文件src/memory/vector_store.rs 前置依赖Step 15.1InMemoryVectorStore 存在) 工作量M1-2h

# 任务描述 涉及文件 前置依赖 工作量 风险 验收条件
15.2.1 定义私有 VectorEntry 结构体:#[derive(Serialize, Deserialize)],字段含 doc_id / content / metadata / mime_type / embedding / created_at: String src/memory/vector_store.rs 15.1.5 S 编译通过;结构体不可见(无 pub),仅文件内使用
15.2.2 实现 PersistentVectorStore struct + async fn new(store, namespace)prefix 过滤全量加载 → serde_json 反序列化 → InMemoryVectorStore::with_entries 注入 + tracing 埋点;在 doc comment 中标注 spawn_blocking 说明 src/memory/vector_store.rs 15.2.1 S 持久化 roundtrip 测试通过;加载完成后 tracing::info! 输出条目计数
15.2.3 实现 VectorStore for PersistentVectorStoreadd(序列化→逐个 store.save() 先写持久化→inner.add() 后写内存→tracing::debug!)、search(完全委托 inner.search())、remove(逐个 store.delete() 先删持久化→inner.remove() 后删内存) src/memory/vector_store.rs 15.2.2 M 单元测试通过;add 写入后搜索返回预期结果;remove 后搜索不再返回已删条目
15.2.4 编写 6 个内联测试:persistent_roundtripwrite→close→reopen→read)、search_after_reloadnamespace_isolationconcurrent_accesspartial_add_recovery(部分写入后重建验证存活数据)、new_empty_store(空存储构造后可用) src/memory/vector_store.rs 15.2.3 S cargo test -- persistent 全部通过;namespace_isolation 验证两个 namespace 互不干扰;partial_add_recovery 验证前 N 条持久化成功条目在重建后可检索;new_empty_store 验证构造后 search 返回空结果

说明PersistentVectorStore::new() 依赖 SqliteStore 的 list(prefix) 支持。若 store.list() 返回空 vec,构造过程仍应成功(空索引)。tracing 埋点需区分 debug(构造过程)和 info(加载完成)级别。 注意PersistentVectorStore::add非原子操作(N 条文档执行 N 次独立 store.save()),第 i 条失败时前 i-1 条已持久化。需追加 partial_add_recovery 测试验证此恢复路径:写入 5 条 → 模拟第 3 条持久化失败 → 重建 store → 验证前 2 条存活。同时追加 new_empty_store 测试验证空 MemoryStore 构造后可用、search 返回空结果。

13.4 Step 15.3 — 追加 RagPipeline

涉及文件src/memory/vector_store.rs 前置依赖Step 15.2 工作量S<1h

# 任务描述 涉及文件 前置依赖 工作量 风险 验收条件
15.3.1 实现 RagPipeline struct3 个字段(embedder: Arc<dyn Embedding> / store: Arc<dyn VectorStore> / splitter: Option<RecursiveCharacterSplitter>+ new() 构造函数 + doc comment 说明用法 src/memory/vector_store.rs 15.2.4 S 编译通过;new() 中 splitter 为 None 合法
15.3.2 实现 ingest()splitter.split(可选)→ embedder.embed → store.add;空文档守卫(空切片和空 chunk 均返回 Ok(()));LlmError→MemoryError::Storage 桥接。已知限制:当前将所有 chunk 一次性传入 embedder.embed(),真实 Embedding Provider(如 OpenAI)有批量大小限制,调用方需自行控制单次 ingest 的文档数(如 20 条/批) src/memory/vector_store.rs 15.3.1 S 端到端 ingest 测试通过;空文档输入返回 Ok(()) 不报错
15.3.3 实现 retrieve()embedder.embed(query) → store.search(query_vec, k);空字符串查询路径(零向量守卫→vec![] src/memory/vector_store.rs 15.3.1 S 端到端 retrieve 测试通过;空字符串返回 vec![]
15.3.4 编写 4 个内联测试:ingest_and_retrieveretrieve_empty_storeingest_empty_docs(空文档切片返回 Ok(()))、ingest_empty_split(分割后 chunk 为空返回 Ok(()) src/memory/vector_store.rs 15.3.2 + 15.3.3 S cargo test -- rag 全部通过;ingest_and_retrieve 使用 MockEmbedding 保持确定性,验证检索结果包含目标文档;空输入测试验证边界守卫正确

说明ingest()retrieve() 都在 impl RagPipeline 块中实现,非 trait 方法。错误桥接使用 MemoryError::Storage(e.to_string()),丢失原始错误类型信息——这是已知取舍,调用方可从 .to_string() 获取原始错误信息。 注意:需追加 ingest_empty_docsingest_empty_split 测试验证空文档切片和分割后 chunk 为空均返回 Ok(()) 不报错(§4.3 边界定义)。

13.5 Step 15.4 — 标记 VectorRetriever 废弃 + 更新 re-export

涉及文件src/memory/vector.rssrc/memory.rs 前置依赖Step 15.1VectorStore trait 存在即可标记废弃) 工作量S<0.5h

# 任务描述 涉及文件 前置依赖 工作量 风险 验收条件
15.4.1 VectorRetriever trait 前加 #[deprecated(since = "0.3.0", note = "请使用 memory::VectorStore")] src/memory/vector.rs 15.1.5 S 编译通过;使用 VectorRetriever 的地方产生 deprecation warning
15.4.2 InMemoryVectorRetriever struct 前加 #[deprecated(since = "0.3.0", note = "请使用 memory::InMemoryVectorStore")];内联测试前加 #[allow(deprecated)] src/memory/vector.rs 15.4.1 S deprecation warning 仅在测试中出现;cargo build --all-targets 零业务代码 warning
15.4.3 memory.rs 追加 pub mod vector_store; + 4 行 pub use vector_store::{VectorStore, InMemoryVectorStore, PersistentVectorStore, RagPipeline}; src/memory.rs 15.1.5 S cargo build --all-targets 编译通过;cargo run --example document_demo 编译通过

说明Step 15.4 只需 Step 15.1 完成即可开始(不依赖 15.2 和 15.3)。但为减少分支冲突,建议在 15.3 之后执行。#[deprecated] 标注使用 since = "0.3.0" 而非具体日期,与 Rust 惯例保持一致。#[allow(deprecated)] 只应出现在 vector.rs 的内联测试 #[cfg(test)] 块中,项目其他位置不应使用 #[allow(deprecated)]

13.6 Step 15.5 — 简化 document_demo.rs

涉及文件examples/document_demo.rs 前置依赖Step 15.3RagPipeline 存在) 工作量S<0.5h

# 任务描述 涉及文件 前置依赖 工作量 风险 验收条件
15.5.1 document_demo.rs 从手动 VectorRetriever 循环(index→search 逐条操作 + id→Document 映射)改为 RagPipeline 两行调用(pipeline.ingest()pipeline.retrieve());文件从 ~74 行缩减为 ~40 行;移除所有 deprecation 相关 #[allow] examples/document_demo.rs 15.3.4 S cargo run --example document_demo exit 0;输出不包含 deprecation warning

说明:迁移后示例不再直接引用 VectorRetriever,依赖于 memory::VectorStorememory::RagPipeline。示例需保持可读性——保留必要注释说明 ingest 和 retrieve 流程。文件中不再需要 #[allow(deprecated)]

13.7 Step 15.6 — 全量验证

前置依赖Step 15.5 工作量S<0.5h

# 检查项 前置 验收条件
15.6.1 cargo build --all-targets 15.5.1 编译通过,零 warning(测试内 deprecation warning 除外)
15.6.2 cargo test --all-targets 15.6.1 全绿,预计 313+ → 332+(新增 ~19 个:9 InMemory + 6 Persistent + 4 RagPipeline);检查 flaky 测试重跑
15.6.3 cargo clippy --all-targets -- -D warnings 15.6.2 0 警告
15.6.4 cargo doc --no-deps 15.6.3 0 warningdoc comments 中引用的类型(如 MemoryStoreEmbedding)有正确的 intra-doc links
15.6.5 性能基准验证 15.6.4 PersistentVectorStore::new 加载 10K 条模拟数据耗时 <500msstd::time::Instant 断言);InMemoryVectorStore::search 在 10K 条索引上搜索耗时 <100ms
15.6.6 cargo run --example document_demo 15.6.5 exit 0,输出不包含错误或 panic
15.6.7 验证 Cargo.toml 零变更 15.6.6 git diff Cargo.toml 无输出,无新增外部依赖

13.8 并行机会

  • Step 15.1 内部串行:任务粒度不适合并行,每个任务的输出作为下一个的输入
  • Step 15.2 必须在 Step 15.1 之后(依赖 InMemoryVectorStore
  • Step 15.3 必须在 Step 15.2 之后(依赖 PersistentVectorStore 的写入——虽然 RagPipeline 用 dyn VectorStore 理论上可绑定 InMemoryVectorStore,但方案顺序决定了先实现持久化后实现组合器)
  • Step 15.4 只需 Step 15.1VectorStore trait 存在即可标记废弃),理论上可与 Step 15.2 并行——但为减少编辑冲突(二者都修改 memory/ 目录下的文件但不同文件),建议在 Step 15.3 之后执行
  • Step 15.5 必须在 Step 15.3 之后(依赖 RagPipeline
  • 结论:无有效并行机会

13.9 风险与应对

风险 等级 应对
InMemoryVectorStore::search 的 Mutex 锁竞争 add/remove 锁持有微秒级;search 锁内克隆数据快照后立即释放锁,余弦计算在锁外执行(10K 条预估 1-10ms),不存在死锁场景
PersistentVectorStore::new 全量加载 10K+ 向量耗时 SqliteStore::list 内部使用 spawn_blocking + WAL,预估 ≤500ms10K 条 JSON 反序列化约 200ms
JSON 序列化/反序列化 Embedding 大量 float 数据 serde_json 序列化 Vec<f32> 是 O(n) 扫描,每条 <2KB10K 条约 20MB JSON——在可接受范围内
ingest_and_retrieve 测试可能 flaky 测试中使用 MockEmbedding(确定性),结果可预测,非 flaky
store.list(prefix) 未按 created_at 排序 SqliteStore 按 ORDER BY created_at ASC 排序;全量加载到 HashMap 后顺序无关
15.4.1 标记 #[deprecated] 后项目外部依赖方产生 warning v0.3.0 发布说明中列明废弃清单,提供迁移指南;保持两个 trait 并行可用一个 minor 版本
不等长输入截断语义存在分歧 已通过单元测试(add_mismatched_lengths)锁定行为:截取 min(len) 对处理 + 返回 Err(InvalidInput)
remove 幽灵数据窗口(持久化已删、内存未删) 重启后从持久化全量加载,幽灵条目自然消失;已知限制已在 §4.2 文档中声明
先写持久化后写内存的 add 路径中,内存写入失败 持久化已成功写入,内存失败导致搜索不包含该条目;重启后全量加载可恢复正常

13.10 验收检查清单

□ 15.6.1  cargo build --all-targets       │ 编译通过
□ 15.6.2  cargo test --all-targets         │ 全绿(313+ → 332+,新增 ~19 个)
□ 15.6.3  cargo clippy --all-targets       │ 0 警告
□ 15.6.4  cargo doc --no-deps              │ 0 warning
□ 15.6.5  性能基准验证                    │ PV::new 10K 加载 <500ms + search <100ms
□ 15.6.6  cargo run --example document_demo│ exit 0
□ 15.6.7  Cargo.toml 零变更               │ 无新增外部依赖

各 Step 验证速查

Step 快速验证命令
15.1 cargo build --all-targets
15.2 cargo test -- persistent
15.3 cargo test -- rag
15.4 cargo build --all-targetscargo clippy --all-targets
15.5 cargo run --example document_demo
15.6 全部检查项