RAGToolkit.AI.Evaluation 0.2.0

dotnet add package RAGToolkit.AI.Evaluation --version 0.2.0
                    
NuGet\Install-Package RAGToolkit.AI.Evaluation -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RAGToolkit.AI.Evaluation" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RAGToolkit.AI.Evaluation" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="RAGToolkit.AI.Evaluation" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RAGToolkit.AI.Evaluation --version 0.2.0
                    
#r "nuget: RAGToolkit.AI.Evaluation, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RAGToolkit.AI.Evaluation@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RAGToolkit.AI.Evaluation&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=RAGToolkit.AI.Evaluation&version=0.2.0
                    
Install as a Cake Tool

RAGToolkit.AI.Evaluation

RAG 检索质量的离线评估。纯 IR 指标(不调模型、同一份数据永远算出同一个数)+ 一层薄薄的 Microsoft.Extensions.AI.Evaluation 适配,让它能和微软自带的 LLM 裁判(Groundedness / Retrieval / Completeness …) 并排跑、进同一份报告。

类型

类型 说明
RetrievalMetrics 纯函数指标:IsHit / RecallAtK / PrecisionAtK / ReciprocalRank / NdcgAtK / AveragePrecisionAtK
RetrievalMetricsSummary 跨用例取平均(Summarize);自动排除「没标期望目标」的用例
PrecisionDenominator 查准率分母口径:Cutoff(固定 K,默认)/ Retrieved(实际返回条数)
RetrievalMetricsEvaluator IEvaluator 实现,一个评估器出 6 个指标(Hit@K / Recall@K / Precision@K / MRR@K / nDCG@K / AP@K)
RetrievalEvaluationContext 把检索结果带进 MEAI 的评估管线(派生自 EvaluationContext)
EvaluationCase / RetrievedItem / CaseOutcome 数据模型:期望目标、一条检索结果(含正文)、一条用例的结果
IRetrievalUnderTest 被测系统的唯一契约:RetrieveAsync(query, topK, …)
RetrievalEvaluationRunner 把评估集跑一遍(串行),产出 RetrievalEvaluationReport
RetrievalEvaluationReport 汇总 + 逐条明细;ToText() 出纯文本
RetrievalComparison A/B 对比:配对 bootstrap 出每个指标的差值和置信区间(Compare)
RetrievalComparisonReport 对比结果;ToText() 出一张带赢/平/输和判定列的表
RetrievalMetricComparison 单个指标的两侧均值、差值、区间、是否显著、赢/平/输计数
RetrievalTally 逐条差值分档计数(B 赢 / 持平 / A 赢)

快速开始

一、只算指标(不接任何模型)

EvaluationCase[] cases = [new EvaluationCase("这套服务怎么部署?", ["deploy.md", "ops.md"])];

RetrievalEvaluationRunner runner = new(new MyRetrieval(), k: 5);
RetrievalEvaluationReport report = await runner.RunAsync(cases);

Console.WriteLine(report.ToText());

被测系统包一层就行:

sealed class MyRetrieval : IRetrievalUnderTest
{
    public async Task<IReadOnlyList<RetrievedItem>> RetrieveAsync(
        string query, int topK, CancellationToken cancellationToken = default)
    {
        // 调你自己的检索。Rank 从 1 开始;Text 是这条结果的正文(可以留空)。
        return [.. hits.Select((hit, index) =>
            new RetrievedItem(hit.Id, index + 1, hit.Score, hit.Text))];
    }
}

评估集从哪来,包不掺和 —— 它只认 EvaluationCase。想放 JSON 文件里就自己读, 字段名跟 record 一致,加个大小写不敏感就能容忍手写文件:

JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true };

List<EvaluationCase> cases =
    JsonSerializer.Deserialize<List<EvaluationCase>>(json, options) ?? [];

// [ { "query": "蓝鲸的心脏有多大?", "expectedIds": ["bluewhale.md"] },
//   { "query": "这套服务怎么部署?", "expectedIds": ["deploy.md", "ops.md"] } ]

按知识库分几个文件、文件叫什么名字、要不要进数据库 —— 都是你的决定。

二、接微软的 Reporting(出 HTML 报告)

本包不依赖 Microsoft.Extensions.AI.Evaluation.Reporting —— 接入点是 IEvaluator 接口本身。 加一个包引用,然后把评估器塞进去:

ReportingConfiguration configuration = DiskBasedReportingConfiguration.Create(
    storageRootPath: "eval-results",
    evaluators: [new RetrievalMetricsEvaluator(k: 5)],
    chatConfiguration: null,          // ★ 指标评估器不调模型,这里可以省
    enableResponseCaching: false,
    executionName: "baseline");

// ★ 一个 ScenarioRun 只能 EvaluateAsync 一次 → 一条用例开一个
await using ScenarioRun run = await configuration.CreateScenarioRunAsync(
    scenarioName: query, iterationName: "baseline");

await run.EvaluateAsync(
    userRequest: query,
    modelResponse: "(仅检索指标,未调用模型)",
    additionalContext: [new RetrievalEvaluationContext(outcome)]);

三、接大模型当裁判

chatConfiguration 填上即可 —— 任何 IChatClient 都行(OpenAI / Azure / Ollama / 自建代理):

ChatConfiguration chatConfiguration = new(chatClient);

ReportingConfiguration configuration = DiskBasedReportingConfiguration.Create(
    storageRootPath: "eval-results",
    evaluators:
    [
        new RetrievalMetricsEvaluator(k: 5),   // 我们的:不调模型
        new RetrievalEvaluator(),              // 微软的 LLM 裁判
        new GroundednessEvaluator(),           // 同上
    ],
    chatConfiguration: chatConfiguration,
    enableResponseCaching: false,
    executionName: "with-judges");

await run.EvaluateAsync(
    userRequest: query,
    modelResponse: answer,
    additionalContext:
    [
        new RetrievalEvaluationContext(outcome),                      // 喂我们的指标
        new RetrievalEvaluatorContext(chunks),                        // 喂 Retrieval 裁判
        new GroundednessEvaluatorContext(string.Join("\n", chunks)),   // 喂 Groundedness 裁判
    ]);

两边的指标并排进同一份报告,名字不冲突(我们的带 @K,裁判的用词不同),实测可以共存于同一个 CompositeEvaluator:

Hit@5 = 1    Recall@5 = 1    Precision@5 = 0.2    MRR@5 = 1    nDCG@5 = 1    AP@5 = 1
Retrieval = 4        ← LLM 裁判
Groundedness = 4     ← LLM 裁判

★ 喂给 LLM 裁判的正文就是 RetrievedItem.Text —— 想让裁判能用,检索结果里得带上 chunk 正文。

四、A/B 对比(改了检索策略,是不是真变好了)

同一批用例跑两遍,逐条配对求差,再对差值做 bootstrap:

RetrievalEvaluationReport baseline = await new RetrievalEvaluationRunner(retrievalOld, k: 3)
    .RunAsync(cases);
RetrievalEvaluationReport candidate = await new RetrievalEvaluationRunner(retrievalNew, k: 3)
    .RunAsync(cases);

// K 和口径直接从两份报告里取;两边不一致会抛,免得比出口径不同的假结论。
// 还能传 confidenceLevel / resamples / seed / tieEpsilon,默认值够用。
RetrievalComparisonReport comparison = RetrievalComparison.Compare(baseline, candidate);

Console.WriteLine(comparison.ToText());

实测输出(候选版把 8 条的目标从第 3 位提到第 1 位,但把另外 4 条挤出了前 3):

K = 3 | 用例 12(配对 12 / 未标期望 0) | 查准率口径 Cutoff
置信水平 95% | 配对 bootstrap 重采样 1000 次(种子 0)

指标              基线        候选        差值        95% 区间                赢/平/输       判定
Hit@3           1.000     0.667     -0.333    [-0.583, -0.083]      0/8/4       显著变差
Recall@3        1.000     0.667     -0.333    [-0.583, -0.083]      0/8/4       显著变差
Precision@3     0.333     0.222     -0.111    [-0.194, -0.028]      0/8/4       显著变差
MRR@3           0.333     0.667     +0.333    [+0.083, +0.583]      8/0/4       显著变好
nDCG@3          0.500     0.667     +0.167    [-0.083, +0.417]      8/0/4       无显著差异
AP@3            0.333     0.667     +0.333    [+0.083, +0.583]      8/0/4       显著变好

赢/平/输 按逐条差值分档:超出 ±1E-09 才算赢或输,中间算持平 —— 均值区间只说平均差多少,这里说多少条真的动了。
显著 ≠ 有用:区间 [+0.001, +0.002] 也叫显著,值不值得改看差值大小。
各指标各自算 95% 区间,没有做多重比较校正 —— 比六个指标时「至少一个假阳性」的概率约 26%。

这就是为什么不能只看一个汇总分:MRR 和 AP 显著变好、Hit 和 Recall 显著变差, 「变好了吗」取决于你更在乎排得准还是找得全。

赢/平/输那一列是另一件事。 上表 Hit@3 是 0/8/4 —— 八条用例压根没动(基线本来就命中、 候选也命中),只有四条被挤出去。均值 -0.333 只说「平均掉了三分之一」,它说不出「八条原地不动」。 对照 MRR@3 的 8/0/4:这一项 12 条全动了(8 条从第 3 位提到第 1 位、4 条被挤出去)。 两份差值的绝对值一样大,一条只影响四分之一的用例,另一条影响全部。

差值在 ±tieEpsilon(默认 1e-9)内的算持平。这个阈值只吃浮点噪声 —— IR 指标最小的非零步长是 1/K 量级(K 上千也有 1e-3),所以真实的改善不会被误判成持平。

它怎么算的:

决策 做法 为什么
配对还是独立 配对:按用例算 B_i − A_i 再汇总 同一批用例里难题对两边都难,逐条作差把这部分方差消掉,同样的用例数能看出更小的差别
逐条分档 差值在 ±tieEpsilon(默认 1e-9)内算持平,另给赢/平/输计数 均值只说平均差多少。「50 条里 30 条变好」和「50 条里 3 条大改」是同一份均值、完全不同的结论。阈值取 1e-9 是因为 IR 指标最小非零步长是 1/K 量级,它只吃浮点噪声
统计方法 百分位 bootstrap(默认 1000 次) 不假设正态 —— 命中率是 0/1 的,t 检验的前提不成立
随机性 固定种子(默认 0) 同一份输入永远算出同一个区间,报告才拿得去跟上一版比
进不进 MEAI 不进(不做 IEvaluator) MEAI 的 IEvaluator 是单条评估,A/B 是跨用例的,层级不同
多重比较 不校正,但在报告里把假阳性风险写出来 校正会让人以为「不显著 = 没差别」,把判断权留给读报告的人

两边必须是同一份评估集、同样的顺序 —— 用例数、每条的查询、每条的期望目标都对得上,否则直接抛。 静默错配会算出一份看着很正式的假显著性,比失败危险得多。

指标口径

用之前先看清楚,否则跟别人的数字对不上。

约定 做法
去重 召回率、nDCG、AP 按去重后的期望目标算。文档级标注下,同一个文档的多个分块同时被召回只算命中一个目标 —— 不去重会让 DCG 超过理想值,算出大于 1 的 nDCG。
查准率分母 默认固定 K(PrecisionDenominator.Cutoff);切成 Retrieved 则除以实际返回条数。
nDCG 的理想排序 按 min(期望目标数, K) 截断 —— 只惩罚排序,不惩罚召回不足。

三个指标的分工:nDCG@K 说「排得准不准」,Recall@K 说「找得全不全」, MAP@K 两者都管(AP 的分母是期望目标总数,漏召回会被罚)。

跟 LlamaIndex 对不上数,多半是这两条:

  • 它的 nDCG 理想排序不截断 → 期望 10 个、K=3、前 3 条全中时它给 0.469,我们给 1.000(我们跟 trec_eval 的 ndcg_cut 一致)。
  • 它的 DCG / AP 不去重 → 同一个 id 返回两次会算出 nDCG > 1。

设计约定

  • 指标层不调模型。 同一份标注配同一份检索结果,永远算出同一个数 —— 这正是它跟 LLM 裁判最本质的区别(LLM 打分需要校准)。
  • 一个用例抛异常,整个跑批失败。 刻意如此:静默跳过会印出一份「召回 0.6」的假报告。
  • 跑批串行、不并发。 评估要的是可复现,不是快。
  • 缺上下文不抛异常,而是返回空值指标 + 一条 error 诊断(MEAI 惯例,报告里能直接看到为什么没有值)。
  • 指标名带截断位置(Recall@3)—— 不带 K 会让 topK=3 / topK=5 两次运行撞成同一个名字。
  • A/B 对比是跨用例的,不是 MEAI 的 IEvaluator。 它比的是「两套系统在同一批用例上的平均表现」, 一条用例的评估结果撑不起一个置信区间。

依赖

  • .NET 10
  • Microsoft.Extensions.AI.Evaluation

许可证

MIT —— 见 LICENSE。

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 79 10/10/2026
0.1.0 84 10/10/2026