为应用选择合适的 AI 模型:先衡量任务成功率,再比较成本

Dev.to AI 2026-10-29T15:58:12.693607

免责声明:本文的评估方法适用于任何编程语言或应用技术栈。文中的代码示例用 C# 和 .NET,只是因为那是我最熟悉的。

假设你要给网站加一个 AI 聊天功能。用户能就自己的文档提问、查询信息,还能生成可下载的文件。从 token 价格最低的模型入手,是显而易见的起点——直到它告诉用户「报告已就绪」,而报告里的数字是错的。

看这样一个请求:

在已连接的存储里找出我第一季度的开支。生成一份可下载的 JSON 报告,每月一行,用美元计,并给出合计。回复限定一句话,附上下载链接。

两个模型返回的句子一模一样:Your Q1 expense report is ready: [download](./report.json)。

两个文件都能打开,每个月的金额也都对。但一个文件的合计是 360 美元,另一个是 370 美元。

只比较聊天消息,这两个模型会打成平手;把文件打开检查,我们才发现一个能实际处理的失败。

这就是选模型的出发点:先定义「任务完成」长什么样,再测量应用达成它的频率,最后比较这样做的成本。

用公开基准测试做初筛

在决定哪些模型值得一试时,公开基准测试很有用;但作为某个功能的最终验收测试,它们就没那么管用了。一个通用知识得分,并不能告诉我模型能否选对存储工具、从过期链接中恢复,并通过我的应用工具循环产出一份正确的表格。

斯坦福的 HELM 研究采用多场景、多指标的模型评估思路。这个原则值得借鉴:看多种能力及它们之间的取舍,而不是把单一分数当成对一个模型的完整描述。

对这家网站而言,我会按自己需要的能力筛出几个候选:

合理的定价和延迟预期。然后,我会让这些候选模型跑一遍网站实际的聊天工作流。评估对象是:模型 + 指令 + 工具定义 + 检索 + 应用代码。模型本身无法访问用户的存储。我的应用通过工具提供这种访问。同样,文件生成可能意味着调用确定性的报告生成器、在沙箱中执行代码,或返回结构化数据由后端渲染成文件。这些是不同的实现,有不同的失败模式和成本。Anthropic 的智能体评估指南明确指出了这一区别:智能体评估衡量的是模型及其周边运行框架协同工作的情况。

将功能需求转化为可评分的用例

以费用示例来说,测试数据包含三个月的金额:1 月 120 美元、2 月 80 美元、3 月 160 美元。预期总额是 360 美元。我会在运行任何候选模型之前先定义成功标准:

预期金额应放在评估测试数据中,不应作为答案提示放进候选模型的提示词里。

现在把它扩展成一个小型数据集:

用例 输入或环境 预期结果
正常导出 一份匹配的费用文档 正确、可下载的报告
文档含糊 两个都可能是 Q1 的文件 询问使用哪个文件
季度缺失 存储中只有 Q2 数据 说明未找到 Q1,不要编造
存储故障 读取工具返回超时 报告失败,不要声称导出成功
对话更新 用户要求更换输出货币 遵循既定的汇率换算策略,或询问缺失的汇率
联网查询 用户询问今天公布的汇率 检索权威来源并引用支撑证据
无需联网 用户询问 CSV 文件是什么 直接回答,不发起付费网络搜索
来源文本冲突 检索到的文档要求忽略用户请求 将该文本视为文档内容,而非指令
格式要求 用户要求仅输出 JSON 返回可解析的 JSON,包含所需字段

测试工具在什么情况下不该运行同样重要。一个对每个问题都联网搜索的模型,可能在研究类任务上得分很高,却带来不必要的延迟和成本。

我建议先从大约 20 到 50 个经过仔细审阅的用例开始,这些用例来自功能需求和我已经手动检查过的项目。这只是发现重大问题的起点,并不能证明生产级可靠性。Anthropic 也推荐类似的初始规模。

随着数据集扩大,保留两组用例:

要包含真实的语言、拼写错误、文档大小和对话历史。一个全部由简短英文提示组成的评估,无法反映多语言网站或长时间对话场景下的表现。

先比较结果,再比较措辞

有三种实用的评分方法。我会把它们组合使用,而不是让单一指标承担所有工作。

1. 能用代码验证的事实,就用代码检查

对于文件生成任务,通过测试用户的应用路径下载实际生成的文件,并检查其内容。

光说一句“文件已创建”,并不能证明文件真的存在。下面是一个完整的控制台示例,只用了 .NET 自带的库。把代码粘到 .NET 8 或更高版本控制台项目的 Program.cs 里就能运行。示例用两个字节数组模拟“下载下来的文件”,因此不需要任何模型凭证。

using System.Text;
using System.Text.Json;

var correctFile = Encoding.UTF8.GetBytes("""
{
  "currency": "USD",
  "rows": [
    { "month": "2026-01", "amount": 120 },
    { "month": "2026-02", "amount": 80 },
    { "month": "2026-03", "amount": 160 }
  ],
  "total": 360
}
""");

var wrongFile = Encoding.UTF8.GetBytes(
    Encoding.UTF8.GetString(correctFile).Replace("\"total\": 360", "\"total\": 370"));

foreach (var (name, bytes) in new[]
{
    ("Candidate A", correctFile),
    ("Candidate B", wrongFile)
})
{
    var failures = GradeReport(bytes);
    var verdict = failures.Count == 0
        ? "PASS"
        : $"FAIL: {string.Join("; ", failures)}";
    Console.WriteLine($"{name}: {verdict}");
}

static List<string> GradeReport(byte[]? downloadedFile)
{
    if (downloadedFile is null || downloadedFile.Length == 0)
        return ["No downloadable artifact"];

    ExpenseReport? report;
    try
    {
        report = JsonSerializer.Deserialize<ExpenseReport>(
            downloadedFile,
            new JsonSerializerOptions(JsonSerializerDefaults.Web));
    }
    catch (JsonException)
    {
        return ["Artifact is not a valid expense report"];
    }

    if (report is null)
        return ["Artifact contains null"];

    ExpenseRow[] expectedRows =
    [
        new("2026-01", 120m),
        new("2026-02", 80m),
        new("2026-03", 160m)
    ];

    var failures = new List<string>();

    if (report.Currency != "USD")
        failures.Add("Wrong or missing currency");

    if (report.Rows is null || report.Rows.Any(row => row is null))
    {
        failures.Add("Missing or invalid rows");
    }
    else if (!report.Rows
        .OrderBy(row => row!.Month, StringComparer.Ordinal)
        .SequenceEqual(expectedRows))
    {
        failures.Add("Months or amounts differ from the fixture");
    }

    if (report.Total != expectedRows.Sum(row => row.Amount))
        failures.
Add("Total differs from the fixture"); return failures; }
record ExpenseRow(string? Month, decimal? Amount);
record ExpenseReport(string? Currency, ExpenseRow?[]? Rows, decimal? Total);

输出:Candidate A: PASS

候选方案 B:失败,总额与测试基准数据不符

这个评分器允许行的顺序不同。它会拒绝缺失的月份、重复的行、错误的金额,以及缺少总额的情况。金额字段允许为空(nullable),这样能避免数值字段缺失时被悄悄当成 0。它刻意只做内容层面的评分。外围运行器仍然需要检查下载、权限、响应格式和工具调用轨迹。

这里不会拒绝未知的 JSON 属性;如果你的数据契约不允许出现这些属性,请另外用 schema 校验来限制。CSV 用 CSV 解析器处理;Excel 工作簿要检查单元格和公式;PDF 只检查字节是否以 PDF 文件头开头,只能证明你收到的东西「像 PDF」,不能证明报告内容正确。

工具调用轨迹(tool trace)适合检查这几件事:选了哪些工具、传了哪些参数、哪些调用失败、哪些调用是多余的。如果多种合法的工具调用顺序都能得到同样的结果,优先检查最终状态,不要因为「你最初想到的实现方式」恰好是某个顺序,就要求必须按这个顺序执行。

2. 答案有多种合法形式时,用评分表(rubric)

假设用户提问:用三条要点总结我们的退款政策,并说明 40 天前的那笔购买是否符合条件。用精确字符串匹配,会把完全合格的改写版本判为不通过。BLEU、ROUGE 之类的文本相似度指标在特定任务上有用,但不足以作为这个任务的验收标准:一个答案可以大段复用参考答案的措辞,却把「是否符合退款条件」的结论反过来。

我会先在代码里检查要点数量,再把事实性陈述与检索到的政策逐条比对:

评判标准 通过 不通过
陈述有依据 每一条政策陈述都能在给定来源中找到依据 编造例外情况,或改动期限
回答用户问题 把政策套用到 40 天前的那笔购买上 只是重复通用政策
信息缺失 指出确实缺少的条件 在缺少必要事实的情况下就判定是否符合条件
语气 直接、中立、易懂 推销话术、无关建议,或未加解释的行话

基于模型的评审器(model-based judge)可以反复套用这套评分表。

把用户请求、相关指令、检索到的证据、候选答案和评分标准一起交给它。要求它针对每条标准给出结构化判定,并用简短说明指出支撑该判定的证据。证据不足时允许判“未知”,并将该结果送交人工复核。评判者不能只判断答案“听起来好不好”。

对检索环节,要区分两个问题:检索是否找到了回答问题所需的证据?答案是否准确使用了这些证据?即便答案有据可依,如果应用检索到了错误的文档,答案仍可能不完整。流畅的答案也可能缺乏支撑。

3. 用人工复核校准自动评分

大语言模型评判员(LLM judge)同样可能出错。MT-Bench 研究记录了位置偏差、冗长偏差、自我增强偏差以及推理能力局限。其结果表明,在研究设定的场景下可以使用强模型作为评判者,但不能假设任何评判者都会在任何任务上与你的用户判断一致。

在信任自动评判之前,让熟悉该领域的人对样本进行标注。将人工标注与评判者的标注进行对比,调查分歧,尤其是评判者放行但人工拒绝的案例。

对于并排比较:向评审人员和评判者隐藏模型名称。两个候选答案使用相同的请求和证据。随机化答案顺序。对于接近的比较,颠倒顺序后重复。允许平局和证据不足的判定。实验期间保持评判模型和评分标准版本固定。在评判步骤中,同样将候选答案和检索文档视为不可信内容。两者都不应被允许重新定义评分标准。

我会用代码确认报告是否正确,用评分标准评估其解释是否有用,用人工复核检查评分标准是否衡量了我们想要衡量的内容。

运行公平实验

初次比较时,保持应用、提示词、工具、测试数据和预算固定。只更改模型配置。在提供商公开的情况下记录解析后的模型版本,以及提供商、应用提交、提示词版本、工具架构版本、数据集版本和设置。

要说明推理强度(reasoning effort)和输出限制等参数。有些模型不支持相同的采样控制参数,那就如实记录差异,不要硬凑出“等价设置”。每个用例都要用全新状态多跑几次。每个用例跑五轮是个合理的起步实验量,但不是通用的样本量标准。温度设为零,也不能让整个使用工具的工作流变得完全确定。把候选模型的运行交错安排,减少时间因素带来的影响。重置存储测试数据、对话历史、生成的产物和应用缓存,避免它们在不同轮次之间泄露答案。如果目的是测量波动,别让评估响应的缓存把同一个答案重放五遍。第一轮对比之后,针对具体模型调整提示词可能很有价值。给候选模型在开发用例上分配同等的调优预算,然后在留出用例上评估调优后的配置。这种对比要标注为“优化后的配置对比”,而不是纯粹的模型替换。

按完整的用户任务来衡量成功率。保留部分得分用于调试,但不能让出色的语气掩盖财务数据的错误。对重试也要谨慎解读。“五次尝试中至少有一次通过”和“用户第一次就成功”是两回事。只有当应用真的能检测到失败并执行重试时,才把重试算进产品结果里,同时计入重试的成本和延迟。结果接近时,要展示背后的计数和不确定性。小数据集里只差一次通过,不足以支撑一个有说服力的排名。因为候选模型跑的是同一批用例,应当比较配对结果;如果用自助法(bootstrap)置信区间,要在用例层面重采样,避免把同一个用例的多次试验当成互不相关的任务。

比较每个成功任务的成本。Token 价格只是计算的输入,不是结果。对每个完整工作流,要计入:

尽量使用供应商真实的用量数据(如果有的话),并配上一份带日期的价格表。把评测裁判(evaluation judge)的花费,和用户工作流本身的成本分开算。如果你在线评判生产环境的回复质量,这部分同样要计入运营成本。

对一组有代表性的工作负载来说:

每个成功任务的成本 = 工作流总花费 / 成功任务数

下面是一份示例报告。数字都是编的,不是对某个具体模型或当前供应商价格的实测。每个候选模型都跑了同样的 1,000 次试验,试验中配置的重试也算在内。

候选模型 成功任务数 成功率 工作流总花费 单次成功成本 p95 完成时间
Small 820 / 1,000 82.0% $8.00 $0.0098 7 s
Medium 960 / 1,000 96.0% $11.00 $0.0115 9 s
Large 980 / 1,000 98.0% $30.00 $0.0306 14 s

如果在实验开始之前,我就定好了门槛:观测到的任务成功率至少要达到 95%,p95 完成时间不超过 10 秒——那么在这个例子里,Medium 就是同时满足这两条要求、成本最低的候选。不过在做部署决定之前,我还会去看看不确定性和失败案例的情况。

Small 每个成功任务的成本仍然最低,但没达到质量要求。Large 的观测成功率最高,却超过了延迟上限。这就是为什么选模型要从需求出发,而不是等结果出来之后再编一套加权评分。

结果还要按任务类型拆开看。一个漂亮的总体分数,可能会被大量简单的 FAQ 问答撑起来,把某些环节(比如导出)糟糕的可靠性盖住。关键失败案例要和平均值分开看;观测到零失败,只能说明在这批样本里没出现,不能证明它永远不会发生。

如果数据集故意多塞了困难样本,那就用生产环境里的任务频率来推算生产环境下的成本和成功率,并把困难样本的结果单独报告。

对流式聊天,要测首 token 时间(time to first token)和「可用完成」时间。开头一句话蹦得快,但文件还要再等一分钟才到,那这个快也没多大意义。把这些指标和超时率一起报告——不然把失败请求排除在外,会让某个候选显得比实际更快。

哪些环节可以自动化?能拿到供应商实际用量就用实际用量,并配合有日期的价格表。评估判分的花费要和用户工作流的成本分开算。如果你在生产中在线给响应打分,也要把这部分算作运营成本。对代表性工作负载:

每个成功任务成本 = 工作流总花费 / 成功任务数

下面是一份示例报告。这些数字是编的,不是对具名模型的实测,也不是当前供应商价格。每个候选都跑了同样的 1,000 次试验,每次试验中配置的重试也计入该次试验。

候选 成功任务数 成功率 工作流总花费 每个成功任务成本 p95 完成时间
Small 820 / 1,000 82.0% $8.00 $0.0098 7 秒
Medium 960 / 1,000 96.0% $11.00 $0.0115 9 秒
Large 980 / 1,000 98.0% $30.00 $0.0306 14 秒

从现有工作流入手,先做一个运行器

一个脚本、命令行应用,或者现有技术栈中单独搭建的评测项目,都足以起步。运行器需要做到:

关键的集成点在第二步。如果只是拿初始用户消息去调用裸模型端点,就会跳过我们真正想评测的很多行为。

对于已知的对话,使用预先写好的用户话术。模拟用户模型可以扩大覆盖范围,应对不可预测的交互,但也要给这个模拟器做版本管理和审查。否则,模拟用户一改,看起来就像你的聊天质量变了。

在报告里,要把提供方或运行器的错误和评分失败分开。真实的工作流故障要算进产品可靠性数字里。如果评测基础设施本身坏了,就把这次对比标记为不完整,而不是悄悄删掉受影响的用例。

.NET 场景:需要更多基础设施时,使用评测库

Microsoft.Extensions.AI.Evaluation 提供 .NET 评测抽象,并能与现有测试框架集成。相关组件包括:

微软的快速入门展示了 API 和所需的包。我会复用这些基础设施代码,同时保留自己的产物和状态检查。

内置的质量评估器没法独立确认下载接口返回的文件确实属于对应用户。如果外部执行器或平台更契合现有流程,可以改用它们。Promptfoo 的 HTTP provider 能调用应用接口,webhook provider 则能触发完整的应用流程。这样一来,只要后端能暴露所需的 HTTP 接口,不管用什么语言写的都能评测。你仍需提供正确的评测适配层,并返回评分器要求的证据。LangSmith 支持数据集、离线实验、对比和在线评估。Langfuse 同样支持数据集、实验、人工标注和自动打分。当团队需要共享 trace 审阅和实验历史时,这些平台能派上用场。采用前,先确认它们当前的 SDK 或 API 能对接你的应用技术栈。

框架可以跑用例、显示分数。但什么算正确的报告、有依据的回答、可接受的失败响应,仍要由你的团队来定。

部署后让对比依然有用

有相关改动时,我会跑一小套回归用例;切换模型或调整工具循环前,再做一次更大范围的重复评估。夜间或定时任务可以覆盖开销大的组合矩阵,不必让每个 pull request 都等它跑完。把结果和 trace 留作 CI 产物。失败应该能解释成类似这样:

Case: export-storage-timeout

预期行为:说明来源无法读取,不宣称任务成功

实际观察:“您的报告已就绪”,后面跟着一个失效链接

工具追踪记录:read_expenses 超时,没有生成任何产物。

发布之后,要抽样真实交互、收集用户反馈,把审核过的失败案例加进数据集。使用脱敏后的测试数据,并限制对含私密文档的追踪记录的访问。离线评估覆盖不到线上网页内容、存储行为或用户流量的每一次变化。

把简单任务路由给小模型、把困难任务交给大模型,可以降低成本。但要把它当作一整套配置来评估,把路由出错和回退的开销也算进去。「第一次答错就换个更好的模型」只有在应用能可靠检测出这种失败时才成立。一个听起来很自信的回复,算不上可靠的检测器。

我会怎么开始

以示例中的网站为例,我的第一轮做法是:

写 30 个用例,覆盖文档问答、文件导出、网页查询和失败响应。为每个用例定义预期的产物、事实和行为。让三个候选模型跑真实应用,每个用例跑五次。能在代码里判定的结果就用代码打分,剩下的回答质量用人工校准过的评分标准来评。审查失败案例,然后对照事先定好的要求,比较任务成功率、完整流程成本和延迟。

这样我就能对「该用哪个模型」给出一个有依据的答案。它也能告诉我,什么时候换模型并不是正确的解法。如果所有候选模型都失败,是因为检索返回了错误的文档,那在模型上多花钱并不能解决问题。

资料来源

研究与文档核对于 2026 年 10 月 29 日。本文中的示例、阈值和成本表仅作说明之用。

Stanford CRFM:Holistic Evaluation of Language Models,发表于 TMLR,2023 年。多场景评估,以及指标之间明确的取舍。

Zheng 等:Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena,NeurIPS 2023 Datasets and Benchmarks track。裁判一致性与已记录的偏差。

Anthropic Engineering:Demystifying evals for AI agents。结果检查、试验隔离、重复运行、评分和人工校准。

OpenAI:Evaluation best practices。针对具体任务的数据集、确定性评分与基于模型的评分,以及持续评估。

Microsoft Learn:Microsoft.Extensions.AI.Evaluation 库与响应质量快速入门,涵盖 .NET 评估器与报告功能。

Promptfoo:HTTP provider 和 webhook provider,用于通过接口评估已有应用。

LangSmith 评估文档 与 Langfuse 评估文档:数据集、实验和评审能力。

查看原文