构建并评估检索流水线
组合 Embedding Model 与 Vector Store,保存文档身份与归因,执行租户过滤,并衡量检索质量。
流水线边界
Runifold 提供服务商中立的 EmbeddingModel、VectorStore、Retriever、Document、
Query、Usage、Deadline、取消与 Capability 契约。它不负责 Chunking、Document 授权、
Source-of-truth Sync、Reranking 或一套固定 RAG 流水线。
明确区分 Ingestion 与 Query Path;它们通常需要不同 Authority、Budget、Task Hint 与运维 计划。
构建并查询一个可运行的 Index
[dependencies]
runifold = "=0.9.0"
runifold-providers = { version = "=0.9.0", features = ["openai"] }
runifold-retrieval-text = "=0.9.0"use std::sync::Arc;
use runifold::{
Document, InMemoryVectorIndex, RetrievalContext, RetrievalQuery, Retriever,
};
use runifold_providers::openai::OpenAiClient;
let client = OpenAiClient::from_api_key(std::env::var("OPENAI_API_KEY")?)?;
let embedder = Arc::new(client.embedding_model("text-embedding-3-small")?);
let built = InMemoryVectorIndex::build(
"product-help",
embedder,
vec![
Document::new("returns", "Returns are accepted within 30 days.")?,
Document::new("shipping", "Standard shipping takes 3–5 business days.")?,
],
RetrievalContext::new(),
)
.await?;
let response = built
.index
.retrieve(
RetrievalQuery::new("How long can I return an order?", 2)?,
RetrievalContext::new(),
)
.await?;
for hit in response.documents {
println!("{}: {}", hit.document.id, hit.document.text);
}built.usage 是摄取 Embedding 用量,response.usage 属于查询。内存 Index 不可变,
适合测试或小型静态语料;文档变化后需要重建。持久 Upsert 和共享访问请使用
pgvector 或 Qdrant。
使用 .dynamic_context(4, built.index) 把它连接到 Agent。这个 Limit 只限制单次模型
轮次可注入的文档数,不能替代 Adapter 自己的 Top-K、Tenant Filter、Score Threshold
与 Context Size Limit。
Embedding Model
OpenAI-Compatible、Gemini 与 Ollama Client 提供 Embedding Adapter。Request 保存 Batch 顺序并区分 Document 与 Query Task。Adapter 拒绝空 Model Name、无效 Vector 与默认静默 截断,同时报告可归因 Usage。
记录 Provider、Model、Dimension、Task Mode、Normalization 与 Index Version。不要在一个 Search Space 混合不兼容配置产生的 Vector。
索引与查询
VectorRetriever 可以组合任意 Embedding Model 与 Vector Store。内存 Index 是确定性
参考。Qdrant 把应用 Document ID 映射到稳定 Point ID;pgvector 使用显式 Setup 与参数化
Query。
Upsert 稳定 Document Identity、Text、Source Metadata、Tenant Namespace 与 Embedding Version。在 Adapter 与组合层都限制 Top-K。
安全与归因
Retrieval 是外部 ReadOnly Authority。显式授权,在 Similarity Ranking 前执行 Tenant 与 ACL Filter,保存 Source Attribution,并把检索文本标为不可信数据。它不能创建 System Instruction。
Checkpointed Execution 在首次模型调用前持久化成功准备的 Context,因此 Resume 不会重复 检索或静默改变证据。
检索评测
把检索与答案生成分开评测。稳定 Relevance Judgment 可以衡量 Precision@K、Recall@K、 Reciprocal Rank、nDCG、Usage 与 Host-observed Latency,并保存 Case Order 与 Dataset Version。
之后再评测 Grounded Answer Quality、Citation Correctness 与 Refusal。好 Answer Score 可能掩盖低 Recall;高 Recall 仍可能给 Agent 提供恶意或无关文本。
生产摄取顺序
- 读取正文前先授权数据源;
- 确定性地归一化和切块,同时保留 Source 与 ACL Identity;
- 分配稳定 Document / Chunk ID 与明确 Index Version;
- 使用
RetrievalDocumentTask Mode 分批 Embedding; - 写入前拒绝数量或维度不匹配;
- 一起 Upsert 正文、归因、Tenant Namespace、ACL、模型与版本;
- 验证完成后才发布新 Index Version;
- 按 Source Identity 删除旧 Chunk,不要按相似度删除。
查询时,从已认证应用状态派生 Tenant 与 ACL Filter,创建带 Deadline 的
RetrievalContext,用 RetrievalQuery 模式生成 Embedding,执行有界 Top-K,按需
Rerank,再把带归因证据作为不可信 Context 交给 Agent。
检索故障排查
排查生产质量前,先验证 0.9 的摄取与排序契约。使用 runifold-retrieval-text 完成有界
UTF-8 加载,并生成稳定、保留 Unicode 与来源的 Chunk ID:
use std::num::NonZeroUsize;
use runifold_retrieval_text::{
DEFAULT_MAX_TEXT_BYTES, TextChunkPolicy, chunk_document, load_text,
split_markdown_sections,
};
let source = load_text("handbook", markdown_bytes, DEFAULT_MAX_TEXT_BYTES)?;
let sections = split_markdown_sections(&source)?;
let policy = TextChunkPolicy::new(NonZeroUsize::new(800).expect("non-zero"), 80)?;
let chunks = sections
.iter()
.map(|section| chunk_document(section, policy))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>();HybridRetriever 会并发查询两个独立 Retriever,再进行有界、带权的 Reciprocal Rank
Fusion。需要 Provider-neutral Reranker 对扩展 Candidate 集重排时,把它包进
RerankingRetriever。Chunk Policy、Embedding Model、Fusion Weight 与 Reranker
Descriptor 必须一起版本化,否则一次 Index 重建就可能静默让评测数据失效。
| 现象 | 常见原因 | 修复 |
|---|---|---|
| 结果为空 | Filter 太严、Namespace 错误或阈值过高 | 记录各阶段安全计数并检查 Index Version |
| Dimension Mismatch | Query 与 Corpus 使用不同模型 | 重建或路由到匹配 Index |
| Duplicate Document ID | Chunk Identity 不稳定 | 从 Source Identity + Chunk 位置/版本派生 |
| 相关来源排名低 | 切块或 Embedding 不匹配 | 修改 Answer Prompt 前先单独评估 Retrieval |
| 跨租户证据 | Ranking 后才过滤 | 在 Store Query 内绑定 Tenant / ACL |
| 延迟高 | Top-K 太大、串行 Rerank 或 Provider 慢 | 分别测量 Embed / Search / Rerank |
| 文档 Prompt Injection | 把证据当成指令 | 保留信任标签,并把 System Policy 分开 |