RAGToolkit.AI.DataRetrieval
1.2.0
dotnet add package RAGToolkit.AI.DataRetrieval --version 1.2.0
NuGet\Install-Package RAGToolkit.AI.DataRetrieval -Version 1.2.0
<PackageReference Include="RAGToolkit.AI.DataRetrieval" Version="1.2.0" />
<PackageVersion Include="RAGToolkit.AI.DataRetrieval" Version="1.2.0" />
<PackageReference Include="RAGToolkit.AI.DataRetrieval" />
paket add RAGToolkit.AI.DataRetrieval --version 1.2.0
#r "nuget: RAGToolkit.AI.DataRetrieval, 1.2.0"
#:package RAGToolkit.AI.DataRetrieval@1.2.0
#addin nuget:?package=RAGToolkit.AI.DataRetrieval&version=1.2.0
#tool nuget:?package=RAGToolkit.AI.DataRetrieval&version=1.2.0
RAGToolkit.AI.DataRetrieval
管线的默认实现与内置处理器。
核心类型
| 类型 | 说明 |
|---|---|
RetrievalPipeline |
管线:持有 QueryProcessors / ResultProcessors 两个列表,ProcessAsync 把它们串起来 |
VectorStoreRetriever |
直接面向 MEVD VectorStoreCollection<TKey, TRecord> 的检索实现 |
RetrievalPipelineExtensions |
AsRetriever(collection, contentSelector) —— 把管线绑成 IRetriever 供 DI 使用 |
ReciprocalRankFusion |
RRF 融合(internal,多 variant 时由管线调用) |
强类型结果与去重身份
ProcessAsync 把命中放成 RetrievalChunk<TRecord>,其 Record 就是源记录本身(TRecord,
无需字典键、无需逐字段强转)。结果集 RetrievalResults 本身是非泛型的(IList<T> 不变,
泛型视图只能是会过期的副本),所以知道记录形状的调用方显式收回强类型:
RetrievalResults results = await pipeline.ProcessAsync(collection, query, topK: 5);
foreach (var chunk in results.Chunks.Cast<RetrievalChunk<ChunkVector>>())
{
Console.WriteLine(chunk.Record.UserId); // 强类型,无字典键
}
两条路径的字典填充不一样:集合重载知道记录形状,会把记录成员 + ParentId/ChunkId 别名
写进基类字典,供记录无关的处理器(ParentContextResultProcessor 等)按名读;自定义搜索重载的
chunk 由调用方自己 new,映射是调用方的决定,管线不会反射回填(对齐微软 DataIngestion:
IngestionChunk<T> 的强类型成员与 Metadata 严格分开,要落进字典由消费者显式写)。
多 variant 融合时按什么判定"同一个块"由调用方声明 —— 传 keySelector(Func<TRecord, object?>)。
直接给键值本身即可,不必 ToString():字典按值相等比较,Guid/int 这类键会按值去重而不是按引用。
不传时回落到记录自身的 Key → Id → 正文;在自定义搜索重载上字典是空的,实际就是回落到正文,
所以那条路径应当显式传:
var results = await pipeline.ProcessAsync(
query, topK: 5, searchAsync: SearchBranchAsync,
keySelector: r => r.Id); // Guid 键,无需字符串化
元数据过滤(MEVD VectorSearchOptions.Filter)
管线把 MEVD 的检索选项透传给连接器,因此你可以直接用强类型表达式做列级元数据过滤——不用自己拼 SQL,也不用走委托重载去重写映射:
// 按真实列过滤:某用户的某个知识库
var options = new VectorSearchOptions<ChunkVector>
{
Filter = r => r.UserId == userId && r.KnowledgeBaseId == kbId,
};
// 主入口直接带 options(向量检索)
var results = await pipeline.ProcessAsync(collection, query, topK: 5, options: options);
// 走 DI / IRetriever 也一样
var retriever = pipeline.AsRetriever(collection, contentSelector: r => r.Content, options: options);
Filter是Expression<Func<TRecord, bool>>。连接器(如 PgVector)把它翻译成参数化 SQLWHERE(值走$参数,防注入)。支持==、&&、||、!=、>/<,以及.Contains()(→@> ARRAY[..])和Enumerable.Any(→&& ARRAY[..])。- 混合检索(集合实现
IKeywordHybridSearchable时)同样会把Filter转成HybridSearchOptions透传,稠密侧和关键词侧都生效。 - 顺带:
VectorSearchOptions.ScoreThreshold能把相似度阈值原生下推到融合前的每个分支,比ScoreThresholdResultProcessor的后过滤更对位——融合后分数是 rank 尺度(1 / (k + rank)),不能再按相似度卡。 options是可选参数,不传时行为与 1.0.0 完全一致(向后兼容)。
内置处理器
Query Processors(检索前)
| 处理器 | 作用 | 依赖 |
|---|---|---|
NormalizationQueryProcessor |
Unicode NFKC 归一化(全角折半角)+ 空白折叠 + 可选大小写折叠 | 无 |
SynonymExpansionQueryProcessor |
按术语表替换,每命中一次产出一个 variant | 无 |
HydeQueryProcessor |
让模型写一段"假答案",用它去检索稠密侧 | 一个生成模型 |
MultiQueryQueryProcessor |
让模型给几种不同说法,各自检索后 RRF 融合 | 一个生成模型 |
RewritingQueryProcessor |
让模型改写查询(稠密侧和关键词侧都变) | 一个生成模型 |
Result Processors(检索后)
| 处理器 | 作用 | 元数据标志 |
|---|---|---|
ScoreThresholdResultProcessor |
按分数阈值过滤 | score_filtered |
TopNResultProcessor |
截断到前 N 条 | trimmed |
RerankingResultProcessor |
cross-encoder 重排 | reranked |
ParentContextResultProcessor |
用相邻块扩展命中块 | parent_context_expanded |
LongContextReorderResultProcessor |
最相关的放头尾、次要的放中间 | reordered |
每个处理器都会往 RetrievalResults.Metadata 写一个"本次是否起了作用"的布尔,链路里哪一环是空转的一眼可见。
处理器顺序
三条顺序是有意义的,写错了会静默失效:
1. HydeQueryProcessor 要放在任何 variant 扩展器之前。
它写的是 RetrievalProviderOptions.EmbeddingQueryText —— 那是每个查询一份的覆写,不是每 variant 一份。
查询已经被扩成多个 variant 时它不会动手,Metadata["hyde_skipped"] 会报 "several_variants"。
2. LongContextReorderResultProcessor 要放在最后。
它的输出故意不再按分数排序(最相关的在头尾)。后面再挂 TopNResultProcessor 会把顺序排回去,等于白做。
3. 融合之后不能再套相似度阈值。
多 variant 走 RRF 后,RetrievalChunk.Score 变成 rank 尺度 —— 顶部只有 1 / 61 ≈ 0.016,不再是相似度。
此时 ScoreThresholdResultProcessor(0.5) 会把结果全部清空。
Metadata["fused"] 报是否融合过,fusion_k 报 k 值。
接 LLM
三个 LLM 处理器都不挑模型,任意 IChatClient 实现(OpenAI / Azure / Ollama / DeepSeek / 通义 / 智谱 …)都能接:
using Microsoft.Extensions.AI;
IChatClient chat = /* 你的 chat client */;
// HyDE:让模型先写一段"假答案",再用它去检索稠密侧。
// shouldApply 可选 —— 短查询才值得扩写;长查询本来就具体,扩写反而会把
// 向量拉向模型的通用措辞,离语料更远。
pipeline.QueryProcessors.Add(new HydeQueryProcessor(
async (question, cancellationToken) =>
(await chat.GetResponseAsync(
$"写一段能回答下面这个问题的文字,直接写,不要解释:{question}",
cancellationToken: cancellationToken)).Text,
shouldApply: query => query.Text.Length < 20));
// Multi-Query:让模型给几种不同说法,各自检索后用 RRF 融合。
pipeline.QueryProcessors.Add(new MultiQueryQueryProcessor(
async (question, cancellationToken) =>
{
var response = await chat.GetResponseAsync(
$"用 3 种不同的说法改写这个问题,每行一条,不要编号:{question}",
cancellationToken: cancellationToken);
return (response.Text ?? string.Empty)
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}));
零依赖的两个不需要模型:
pipeline.QueryProcessors.Add(new NormalizationQueryProcessor(foldCase: true));
pipeline.QueryProcessors.Add(new SynonymExpansionQueryProcessor(
new Dictionary<string, IReadOnlyList<string>>
{
["AI"] = ["Artificial Intelligence"],
["款号"] = ["商品编号"],
}));
依赖
RAGToolkit.AI.DataRetrieval.AbstractionsMicrosoft.Extensions.VectorData.AbstractionsMicrosoft.Extensions.Logging.Abstractions
许可证
MIT —— 见 LICENSE。
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Extensions.VectorData.Abstractions (>= 10.10.0)
- RAGToolkit.AI.DataRetrieval.Abstractions (>= 1.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
1.2.0: Generic chunks. ProcessAsync now produces RetrievalChunk<TRecord> chunks whose Record is the strongly typed source record — read chunk.Record.Id instead of chunk.Record["Id"]. The result set stays non-generic (RetrievalResults): IList<T> is invariant, so a typed Chunks view could only be a stale copy; cast the chunks with chunks.Cast<RetrievalChunk<TRecord>>(). A keySelector parameter (Func<TRecord, object?>) states how fusion deduplicates; return the key value itself — no ToString() needed, since keys are compared by their own equality. By default it reads the record's Key/Id, falling back to the chunk text. Only the collection overload fills the supplementary dictionary (record members + parent/child aliases) that the record-agnostic result processors read — on the custom-search overload the caller owns the chunks and maps its own record. ChunkIdentity was removed. The non-generic ProcessAsync overloads and the base RetrievalChunk type are unchanged, so existing callers and the record-agnostic result processors keep working.