原创

怎样给业务选择大模型:用 Node.js 建立质量、成本和延迟评测集

不要只看公开榜单选模型。本文用一个可运行的 Node.js 评测框架,把真实业务样本、质量量表、Token 成本、端到端延迟和回归门禁放进同一套可复现流程,并说明如何保存证据、灰度发布与安全回滚。

大模型评测方法专题 · 第 5/5 篇查看专题目录 →

公开排行榜能帮助你缩小候选范围,却不能替你决定哪个模型适合客服、代码审查、知识库问答或内容归纳。同一个模型可能在通用知识题上领先,却因为 JSON 不稳定、首字等待过长、引用不可核验,无法满足你的生产约束。

业务选型真正要回答的是:在同一批真实任务、同一套 Prompt 和同一计分规则下,候选模型能否守住质量底线,并以可接受的成本和延迟持续运行? 本文给出一套 Node.js 最小框架,帮助你生成证据,而不是给出一张脱离业务的模型名次表。

如果你正在评估 AI 内容系统,可先阅读AI 总结质量回归测试;准备把候选模型接入生产时,再参考Node.js 多模型轮询与故障切换。

一、先写决策规则,再调用模型

评测最常见的错误,是先跑一批模型,再从结果里挑对自己有利的指标。更可靠的方式是预先登记评测协议:

  • 业务任务、用户群体与不可接受的失败;
  • 测试集版本、样本来源、纳入和排除规则;
  • 固定的系统提示词、用户模板、工具和结构化输出约束;
  • 模型版本、采样参数、服务区域与运行日期;
  • 自动检查、人工量表、模型裁判及其权重;
  • 质量硬门槛、统计比较方法、成本与延迟预算;
  • 超时、拒答、空响应、限流和重试怎样计分。

OpenAI Evals 的官方文档也把评测拆为测试数据与测试逻辑,并提醒开发者先确认是否已有适用模板。无论使用哪个框架,这两个部分都需要版本化;只保存最终平均分无法复现一次决策。

二、建立贴近生产分布的测试集

测试集应来自脱敏后的真实任务,并保留困难样本,不能只选模型容易回答的问题。一个最小夹具可以这样描述:

{"id":"support-001","segment":"refund","input":"用户请求退款……","mustInclude":["退款时限"],"forbiddenClaims":["保证当天到账"],"expectedSchema":{"required":["answer","citations"]},"risk":"high"}

每条样本至少应包含稳定 ID、输入、任务分层、判分依据和风险等级。涉及检索时,还要冻结知识库快照或记录索引版本,否则候选模型读到的证据不同,结果便不可比。

样本分层比单纯增加数量更重要。例如客服评测可以按退款、物流、账户、安全和未知问题划分;代码任务可以按语言、缺陷类型和仓库规模划分。汇总时同时报告总体结果与各分层结果,避免大量简单样本掩盖高风险场景退化。

测试集不要长期不变。线上出现的新失败应在脱敏、人工确认后加入回归集,但最终验收集要限制访问,防止团队反复针对固定答案调 Prompt,得到无法泛化的高分。

三、把质量拆成可检查的信号

“回答不错”不能作为发布门禁。业务质量通常至少包含以下三层:

层级 典型指标 判定方式
程序契约 非空、JSON 可解析、字段完整、类型正确 自动断言
任务正确性 关键点覆盖、答案匹配、代码测试通过、引用可定位 确定性规则或人工核验
风险与体验 无依据陈述、安全边界、语气、可操作性 量表评分与人工复核

硬性错误不要被平均分抵消。例如高风险样本出现一条无依据的退款承诺,即使其他样本语言流畅,也应阻断发布。模型裁判可以降低初筛成本,但裁判的 Prompt、模型版本和分数映射同样需要记录,并用人工标注样本验证;不能把裁判给出的 0.9 直接称为准确率。

四、同时记录成本与延迟

模型调用应至少记录:

  • 端到端延迟:从客户端发出请求到收到完整响应;
  • 首 Token 延迟(TTFT):流式响应中从请求发出到首个内容片段;
  • 输出速度:首个内容片段后,每秒生成的 Token;
  • 输入、输出和缓存 Token 数;
  • 超时、限流、服务错误与重试次数;
  • 按运行时价格快照计算的单请求成本。

非流式接口无法测得真实 TTFT,只能得到完整响应耗时。不要用总耗时除以 Token 数反推 TTFT,也不要比较运行在不同地区、不同并发和不同网络条件下的数据。

若供应商按每百万 Token 报价,单次请求的估算成本可以写成:

cost = inputTokens / 1,000,000 × inputPrice
     + cachedInputTokens / 1,000,000 × cachedInputPrice
     + outputTokens / 1,000,000 × outputPrice

价格必须带供应商、币种、生效日期和计费口径。模型深度推理、工具调用、批处理或缓存可能采用不同规则,应以运行时实际账单与供应商官方价格页校准,不能把文章里的历史报价写死为结论。

除“平均每请求成本”外,还应计算每个成功任务的成本:总成本除以通过业务门禁的样本数。低价模型若频繁失败、重试或转人工,实际完成成本可能更高。

五、一个无第三方依赖的 Node.js 评测骨架

下面的示例只依赖 Node.js 内置模块。invokeModel 是适配器:实际项目中由它调用候选模型,并统一返回文本、usage 和可选的 TTFT。示例默认不发起任何外部请求,因此不会生成虚假的实测数字。

// evaluate.mjs
import assert from 'node:assert/strict';
import { performance } from 'node:perf_hooks';

function estimateCost(usage, price) {
  const input = usage.inputTokens ?? 0;
  const cached = usage.cachedInputTokens ?? 0;
  const output = usage.outputTokens ?? 0;
  return (
    input * price.inputPerMillion
    + cached * price.cachedInputPerMillion
    + output * price.outputPerMillion
  ) / 1_000_000;
}

function percentile(values, p) {
  assert.ok(values.length > 0, 'percentile 需要至少一个样本');
  const sorted = [...values].sort((a, b) => a - b);
  const index = Math.ceil((p / 100) * sorted.length) - 1;
  return sorted[Math.max(0, index)];
}

function grade(fixture, responseText) {
  const text = responseText.trim();
  const missing = fixture.mustInclude.filter(term => !text.includes(term));
  const forbidden = fixture.forbiddenClaims.filter(term => text.includes(term));
  return {
    passed: text.length > 0 && missing.length === 0 && forbidden.length === 0,
    missing,
    forbidden
  };
}

export async function evaluate({ fixtures, candidate, invokeModel }) {
  const rows = [];
  for (const fixture of fixtures) {
    const started = performance.now();
    try {
      const result = await invokeModel({ candidate, fixture });
      const latencyMs = performance.now() - started;
      const quality = grade(fixture, result.text);
      rows.push({
        fixtureId: fixture.id,
        segment: fixture.segment,
        risk: fixture.risk,
        ok: quality.passed,
        quality,
        latencyMs,
        ttftMs: result.ttftMs ?? null,
        usage: result.usage,
        estimatedCost: estimateCost(result.usage, candidate.price),
        error: null
      });
    } catch (error) {
      rows.push({
        fixtureId: fixture.id,
        segment: fixture.segment,
        risk: fixture.risk,
        ok: false,
        latencyMs: performance.now() - started,
        estimatedCost: 0,
        error: error instanceof Error ? error.message : String(error)
      });
    }
  }

  const latencies = rows.map(row => row.latencyMs);
  const passed = rows.filter(row => row.ok).length;
  const totalCost = rows.reduce((sum, row) => sum + row.estimatedCost, 0);
  return {
    protocol: candidate.protocol,
    candidate: candidate.id,
    sampleCount: rows.length,
    passRate: passed / rows.length,
    failureCount: rows.length - passed,
    p50LatencyMs: percentile(latencies, 50),
    p95LatencyMs: percentile(latencies, 95),
    totalCost,
    costPerPassedTask: passed > 0 ? totalCost / passed : null,
    rows
  };
}

这里的字符串包含检查只是演示。事实问答可用人工参考答案和证据核验,代码任务应运行隔离测试,结构化输出应使用 JSON Schema。不同任务不要硬套同一个评分器。

六、候选模型配置必须是不可变快照

候选配置不能只保存一个容易变化的营销名称。建议至少固定以下字段:

const candidate = {
  id: 'provider:model-version:prompt-v4',
  provider: 'replace-me',
  model: 'replace-with-pinned-version',
  protocol: 'business-eval-2026-08-v1',
  parameters: { temperature: 0, topP: 1, maxOutputTokens: 800 },
  promptHash: 'sha256:replace-me',
  endpointRegion: 'replace-me',
  price: {
    currency: 'replace-me',
    effectiveAt: 'YYYY-MM-DD',
    inputPerMillion: 0,
    cachedInputPerMillion: 0,
    outputPerMillion: 0
  }
};

这里的零价格是明显的占位值,运行前必须替换并校验,不能进入正式报告。生产实现可以增加断言,拒绝币种、生效日期或价格缺失的配置。API Key 只从密钥管理系统或环境变量读取,绝不能写入评测记录。

对于无法固定底层版本的模型别名,也要记录每次响应返回的模型标识和运行时间。候选模型、Prompt、工具定义、检索索引和价格快照共同构成一个可发布单元。

七、用基线对比,而不是孤立看候选分数

模型升级时,应让生产基线与候选版本在同一批夹具、相近时间和相同并发条件下运行。输出按 fixtureId 配对后,重点查看:

  1. 候选新增了哪些失败,而不只是总通过率变化;
  2. 高风险分层是否退化;
  3. P50 与 P95 延迟是否变化;
  4. 每个成功任务成本是否变化;
  5. 失败是否集中在某类输入、语言或长度;
  6. 多次重复运行时,结论是否稳定。

平均值会隐藏长尾。交互式产品通常更关心 P95 延迟;批处理任务可能更关心吞吐和总成本。样本较少时,一个案例就可能显著改变比例,因此报告必须同时显示分子、分母和逐样本差异,必要时给出置信区间,而不是只发布两位小数的“胜率”。

采样输出具有随机性。即使设置相同参数,服务端实现更新也可能改变结果。对关键样本重复运行,并报告每个候选的运行次数与波动,比只选择最好的一次更可信。

八、设置质量优先的回归门禁

门禁应由业务风险决定。下面只展示结构,不提供可直接照抄的阈值:

function checkGate({ baseline, candidate, limits }) {
  const blockers = [];
  if (candidate.failureCount > limits.maxFailures) {
    blockers.push('失败样本超过上限');
  }
  if (candidate.passRate < baseline.passRate - limits.maxPassRateDrop) {
    blockers.push('通过率相对基线退化');
  }
  if (candidate.p95LatencyMs > limits.maxP95LatencyMs) {
    blockers.push('P95 延迟超过预算');
  }
  if (
    candidate.costPerPassedTask == null
    || candidate.costPerPassedTask > limits.maxCostPerPassedTask
  ) {
    blockers.push('成功任务成本超过预算或无法计算');
  }
  const highRiskFailure = candidate.rows.some(row => row.risk === 'high' && !row.ok);
  if (highRiskFailure) blockers.push('存在高风险样本失败');
  return { passed: blockers.length === 0, blockers };
}

limits 必须在看候选结果前,由产品、工程、安全和财务共同确认。高风险任务可以采用“一条关键失败即阻断”;低风险文案任务则可允许少量变化进入人工抽查。切勿为了让某个候选通过而事后放宽门槛。

门禁通过也不等于自动全量上线。先小流量灰度,监控真实错误、人工转接、用户反馈、延迟和账单,再逐步扩大流量。Prompt 和配置的灰度、回滚方法可继续阅读生产环境 Prompt 版本管理。

九、评测报告应能回答“为什么选它”

一份可审核的选型报告至少包含:

  • 决策日期、负责人和评测协议版本;
  • 候选模型、Prompt、参数、工具和知识库版本;
  • 测试集规模、分层、数据时间范围与脱敏方式;
  • 总体及分层质量结果、失败案例和人工复核结论;
  • P50/P95 延迟、超时率、重试率和测试并发;
  • Token 用量、价格快照、总成本与成功任务成本;
  • 与生产基线的逐样本退化和改善;
  • 已知限制、灰度方案、监控指标和回退条件。

对外发布时应说明数据来自哪个版本和什么条件。不要把开发环境单次运行包装成生产 SLA,也不要将内部业务集结果外推为“全行业最强”。

十、推荐实施顺序

  1. 选择一个价值明确、边界清楚的业务任务;
  2. 从真实流量中脱敏抽样,并补充高风险与边界案例;
  3. 固定基线模型、Prompt、参数和检索快照;
  4. 先实现格式、字段、禁用声明等确定性检查;
  5. 为主观质量建立人工量表和双人校准样本;
  6. 统一采集 usage、错误、端到端延迟和流式 TTFT;
  7. 在同一环境运行基线与候选,保留逐样本输出;
  8. 按预先登记的门禁决定拒绝、复核或灰度;
  9. 上线后把新失败沉淀为回归夹具,并定期复跑。

FAQ

公开榜单第一名可以直接进入生产吗?

不可以。公开榜单衡量的是特定数据集、Prompt、评分器和运行条件下的能力。你的业务可能更看重结构化输出、私有知识、长尾语言、延迟或失败成本,必须用真实任务复测。

质量、价格和速度应该怎样加权?

先设置不可妥协的质量与安全门槛,再在通过门槛的候选中比较延迟和成功任务成本。把三项直接加成一个总分,可能让低价抵消关键事实错误,通常不适合高风险任务。

为什么看每个成功任务成本,而不是每百万 Token 单价?

Token 单价只描述计费单位。模型失败、超时、重试、生成过长或需要人工修订,都会增加完成一个业务任务的真实成本。成功任务成本能把质量损失和调用费用放进同一决策视角。

测试集需要多少样本?

没有适用于所有业务的固定数字。样本要先覆盖关键分层与高风险失败,再根据结果波动和决策风险扩充。报告样本数、分层和不确定性,比引用一个脱离业务的最低数量更可靠。

可以只用模型裁判完成自动评测吗?

不建议。模型裁判适合扩大初筛范围,但需要用人工标注数据验证其一致性与偏差。高风险判断、裁判分歧和新型失败仍应进入人工复核。

总结

业务模型评测不是寻找一张永远有效的排行榜,而是建立一个可以重复回答决策问题的工程系统:冻结真实样本和配置,用清晰规则衡量质量,统一记录成本与延迟,以生产基线识别退化,再通过预先登记的门禁和灰度发布控制风险。

先用少量、覆盖关键风险的夹具跑通整条证据链,再持续吸收线上失败。这样得出的模型选择未必能被概括成一句“谁最强”,却能清楚说明:在什么任务、什么版本和什么约束下,为什么选择它,以及出现问题时如何回退。

实测与内容说明

实测记录

  • 本文提供的是无第三方依赖的 Node.js 评测骨架和演示夹具,未调用真实模型,也未产生任何模型准确率、延迟或成本排名。
  • 示例价格、质量阈值和延迟阈值均为必须由团队按供应商当前报价、业务风险和生产数据替换的配置位,不代表本站实测结论。
  • 示例代码按 Node.js ESM 语法设计;接入生产前仍需补充鉴权、限流、重试、脱敏、持久化和人工复核。

参考资料

内容版本 1.0 · 审核:推荐智能手记 · 计划复审:2026-11-29