把一个 AI 功能接进产品后,最难回答的问题常常不是"它能不能生成内容",而是"这一版真的比上一版好吗"。换模型、改提示词、调温度、加知识库、压缩上下文,都可能让一类问题变好、另一类问题变坏。只拿三五个演示问题试一遍,很容易在上线后才发现:JSON 格式更稳了,但拒答变差;平均准确率提高了,但重要客户的流程问答退步了。
本文给出一套适合工程团队起步的大模型应用评测方法:如何选样本、写判定标准、组合程序评分与人工评分、定位失败类型,以及把结果转成可执行的发布门槛。文末附一段只用 Python 标准库的完整代码,用虚构样本比较两个候选版本。代码可以直接运行,但字符匹配只是确定性检查的示范,不能代替语义与事实评审。
一、先分清:评测的是模型,还是整个应用
公开基准评测通常比较模型在一组标准任务上的能力。应用评测则要覆盖整个链路:用户输入、提示词、知识检索、工具调用、模型输出、后处理和界面展示。用户看到的错误可能来自模型,也可能来自应用层。
例如"退款要多久"答错,至少有四种可能:规则文档没同步;检索命中旧规则;模型读到了新规则却漏掉适用条件;后处理把"工作日"三个字截掉。只看模型最终文本,无法知道是哪一环。评测数据最好因此保存链路版本和必要的中间证据,而不是只有一列 prompt 和一列 answer。
HELM 研究强调跨场景、多指标的评估思路;NIST AI 风险管理框架的 Measure 部分也把定量、定性和持续监测放在同一套过程里。工程上可以把它落成一个简单原则:按真实使用场景分组,再为每组选择能说明问题的指标。不要把所有质量压成一个"AI 得分"。
一个应用评测对象可以写成:
text
输入样本 + 预期行为 + 应用版本 + 模型/提示词/知识版本
→ 实际响应与轨迹 → 自动检查 + 人工复核 → 失败归因
二、评测集不是"随便攒一批问题"
建议先从真实用户请求和线上失败记录里抽样,再补齐那些低频但代价高的边界情况。选样本时,至少覆盖以下几类:
| 样本切片 | 典型问题 | 主要观察点 |
|---|---|---|
| 高频正常请求 | 常见规则、常见代码解释 | 基础正确率和响应速度 |
| 长尾表达 | 缩写、口语、错别字、跨语言 | 鲁棒性 |
| 多条件请求 | "2025 版、一线城市、外包人员" | 条件是否被完整保留 |
| 无答案请求 | 知识库没有资料 | 是否诚实表示无法确认 |
| 结构化输出 | 返回 JSON 或固定字段 | 解析成功率、字段约束 |
| 权限与安全边界 | 请求越权数据或危险操作 | 是否按应用规则拒绝/交接 |
| 历史失败样本 | 曾经线上答错的问题 | 回归是否复发 |
不要只保留"当前版本答错的题"。那会让评测集不断朝修过的错误倾斜,却看不到正常场景被新改动破坏。一个实用起点是维护三层集合:
- 固定回归集:短期内不随意改动,用来比较版本;
- 新增失败集:每周收集线上误答、用户反馈和人工审核发现的问题;
- 保留集:不参与日常提示词调优,用来检查是否对回归集过拟合。
数据来自真实用户时,应去除无关个人信息、密钥和业务秘密,并按权限控制访问。对于敏感或高风险领域,标注规则还需要领域人员审核。测试集的质量首先取决于"预期行为"写得是否清楚,而不是样本数量是否好看。
给每个样本写可判断的预期行为
"回答得好""尽量准确"很难稳定评分。更可用的写法是:
json
{
"id": "refund-2025-01",
"slice": "versioned_policy",
"input": "2025版退款多久到账?",
"expected_behavior": "给出2025版规则,注明工作日;没有依据时不猜测",
"required_facts": ["7个工作日"],
"forbidden_facts": ["30个自然日"],
"source_version": "refund-policy-2025"
}
这仍不是完美金标准,但它让标注人员知道要判断什么,也让失败分析能落到具体字段。若问题本身有多种合理答案,不要硬写唯一参考句子;改为列出必须满足的事实、允许的表达差异和明确不能出现的内容。
三、评分要分层:程序能判的先程序判
程序评分适合有清晰规则的部分:JSON 能否解析、必填字段是否齐全、引用 ID 是否存在、是否出现不允许的工具名、输出是否超长、金额或日期是否匹配业务数据。它的优点是便宜、可重复、能快速进入持续集成;缺点是容易漏判语义错误。
例如一个回答包含"7 个工作日"并不意味着它正确。如果原文只适用于已审核通过的订单,而答案说"任何订单都在 7 个工作日内退款",字符检查会通过,事实判断却应失败。反过来,"一周工作日内"可能表达接近,但简单字符串匹配会判错。
因此建议把检查分成三层:
- 硬约束:格式、权限、字段、引用存在性、危险操作拦截;这类失败通常可以直接阻止发布。
- 事实与任务完成度:核心事实、条件、单位、步骤是否正确;用人工标注或经过校准的评分方法判断。
- 体验指标:清晰度、冗长度、语气、延迟和成本;根据业务目标权衡。
不要将"格式通过率 99%"误读为"回答正确率 99%"。每个数字都要说明分母、样本来源和判定规则。
四、何时用 LLM 当裁判,如何减少偏差
开放式回答很难靠精确匹配评分。可以让另一个模型根据明确量表检查:是否回答了问题、是否有证据支撑、是否遗漏条件、是否凭空添加信息。对一部分样本,这能帮助人工审核更快定位可疑答案。
但"让模型给模型打分"也有局限。MT-Bench/Chatbot Arena 的相关研究讨论了位置偏差、篇幅偏差等问题:比较 A/B 两个回答时,谁放前面、谁写得更长,都可能影响模型裁判。它的分数应被看作一个测量工具的输出,而不是天然真相。
实操上可这样降低风险:
- 先写量表,例如"事实准确 0--2、条件完整 0--2、证据支持 0--2",每档给判定例子;
- A/B 比较时盲化版本名,并交换顺序评分;
- 要求裁判指出具体错误证据,而不只输出一个数字;
- 抽样让人类复核,记录模型裁判与人工意见不一致的类型;
- 对发布门槛较重要的失败项,以人工或确定性校验为准。
G-Eval 等研究探索了模型辅助文本评估,但具体效果仍依赖任务与量表。不要把公开论文里的相关性数字直接当成自己业务上的准确率。
五、可运行的最小回归器:比较两个版本
下面脚本使用虚构的客服场景,不调用任何外部模型。你可以把 baseline 和 candidate 理解为两次已保存的模型输出。程序检查必含文本、禁含文本和 JSON 必填字段,并按切片汇总失败。它适合验证发布流水线里的确定性条件,开放式质量仍需人工复核。
将代码保存为 eval_demo.py,使用 Python 3.10+ 运行:
python
import json
from dataclasses import dataclass
@dataclass(frozen=True)
class Case:
id: str
slice_name: str
required: tuple[str, ...] = ()
forbidden: tuple[str, ...] = ()
json_keys: tuple[str, ...] = ()
@dataclass(frozen=True)
class Result:
id: str
slice_name: str
passed: bool
reasons: tuple[str, ...]
def check(case: Case, answer: str) -> Result:
reasons: list[str] = []
for text in case.required:
if text not in answer:
reasons.append(f"missing required text: {text}")
for text in case.forbidden:
if text in answer:
reasons.append(f"contains forbidden text: {text}")
if case.json_keys:
try:
parsed = json.loads(answer)
except json.JSONDecodeError:
reasons.append("invalid JSON")
else:
if not isinstance(parsed, dict):
reasons.append("JSON root is not an object")
else:
for key in case.json_keys:
if key not in parsed:
reasons.append(f"missing JSON key: {key}")
return Result(case.id, case.slice_name, not reasons, tuple(reasons))
def evaluate(cases: list[Case], answers: dict[str, str]) -> list[Result]:
expected_ids = {case.id for case in cases}
if len(expected_ids) != len(cases):
raise ValueError("duplicate case ID")
if set(answers) != expected_ids:
raise ValueError("answer IDs must match case IDs exactly")
return [check(case, answers[case.id]) for case in cases]
def report(label: str, results: list[Result]) -> None:
passed = sum(result.passed for result in results)
print(f"{label}: {passed}/{len(results)} passed")
for slice_name in sorted({result.slice_name for result in results}):
subset = [r for r in results if r.slice_name == slice_name]
ok = sum(r.passed for r in subset)
print(f" {slice_name}: {ok}/{len(subset)}")
for result in results:
if not result.passed:
print(f" FAIL {result.id}: {'; '.join(result.reasons)}")
cases = [
Case("refund", "versioned_policy", required=("7个工作日",),
forbidden=("30个自然日",)),
Case("invoice", "normal", required=("电子发票",)),
Case("classification", "structured", json_keys=("category", "priority")),
Case("unknown", "no_answer", required=("无法确认",),
forbidden=("已经生效",)),
Case("admin", "safety", required=("不能直接执行",),
forbidden=("已删除",)),
]
baseline = {
"refund": "退款预计30个自然日到账。",
"invoice": "可在订单页下载电子发票。",
"classification": '{"category":"billing"}',
"unknown": "目前无法确认这项新政策。",
"admin": "不能直接执行该操作,请联系管理员。",
}
candidate = {
"refund": "审核通过后,退款预计7个工作日到账。",
"invoice": "订单页可以下载电子发票。",
"classification": '{"category":"billing","priority":"normal"}',
"unknown": "新政策已经生效,我无法确认具体日期。",
"admin": "不能直接执行该操作,请联系管理员。",
}
old = evaluate(cases, baseline)
new = evaluate(cases, candidate)
report("baseline", old)
report("candidate", new)
old_by_id = {result.id: result for result in old}
for result in new:
if old_by_id[result.id].passed and not result.passed:
print("REGRESSION:", result.id)
assert sum(r.passed for r in old) == 3
assert sum(r.passed for r in new) == 4
预期重点不是"候选版 4/5 比旧版 3/5 高,所以可以上线"。候选版修好了退款规则和 JSON 字段,却把"无答案"样本回答成"新政策已经生效"。如果无答案误答属于关键风险,就算总分提高,也应阻止上线或要求修复后再测。切片和单个高风险失败比平均分更有决策价值。
这段代码还有刻意保留的限制:required 和 forbidden 是字符匹配,无法判断上下文是否支持,也无法识别同义表达。真实工程可以先把它用于硬约束,再结合人工标注、业务规则和受控的模型裁判评估开放式回答。不要把脚本输出命名为"准确率"而不解释评分范围。
六、比较两个版本时,先锁住实验条件
要判断改动的效果,尽量只改一个主要变量。若同时换模型、换提示词、换知识库版本,还调整了采样参数,分数变化就很难归因。至少记录这些信息:
text
应用代码版本 / 提示词版本 / 模型标识与配置
知识库与工具版本 / 测试集版本 / 运行时间
每题输入与输出 / 检索和工具轨迹 / 延迟与成本
生成式模型可能有随机性;即使参数保持一致,外部服务与动态知识也可能让结果变化。对于重要决策,可对代表性样本重复运行,并同时看均值、波动和失败样本。若同一题时好时坏,把"一次通过"当成稳健结果就会过于乐观。
分数差异很小时,也别忽略样本量。50 道题里多答对 1 道,可能只是偶然波动;同样的变化若发生在高风险切片,则仍值得处理。是否做置信区间或显著性检验取决于样本量和决策成本,但至少应展示分母和逐题变化,而不是只报百分比。
七、失败分析比调一个总分更有用
每次评测后,把失败样本按原因归类。例如:
- 事实错误:数字、日期、单位或适用范围错误;
- 证据错误:引用不存在、引用旧版本、结论不被证据支持;
- 任务遗漏:多条件请求漏掉一个条件;
- 格式错误:JSON 无法解析、字段缺失、类型不符;
- 拒答错误:该答不答、该拒不拒;
- 工具错误:选错工具、重复执行、参数越权;
- 交互问题:过长、含糊、需要澄清却直接猜测。
一题可能同时有多个问题,分类不必互斥。记录"修复负责人"和"可能的链路环节",后续才能把问题转成行动:补资料、改检索、修后处理、限制工具权限,或修改提示词。把所有失败都写成"模型幻觉",基本帮不上排障。
历史失败题修好后应进入回归集,但也要保留未见过的新样本。持续对同一批公开答案调提示词,会逐渐把系统调成"会做题",却不一定更会服务真实用户。
八、上线门槛应包括"不可退步项"
发布门槛可以分三层,而不只看总分:
- 硬性拦截:权限越界、凭空执行高影响操作、敏感信息泄漏、关键格式无法解析等,一旦出现就不发布。
- 关键切片下限:例如无答案正确处理率、版本规则准确率,不能低于已确认的基线。
- 整体收益与成本:主要任务完成率提高,同时延迟、调用成本在产品可接受范围内。
门槛应写在评测运行前。若每次看到结果再改门槛,很容易把评测变成对结论的事后解释。对于分歧样本,建立明确的人工复核流程;复核结论和量表版本一起保存。
一个简化的发布记录可以包含:
| 项目 | 记录示例 |
|---|---|
| 对比对象 | 旧提示词 v12 / 新提示词 v13 |
| 样本版本 | eval-set-2026-10-15 |
| 总体变化 | 任务通过 82% → 86% |
| 关键切片 | 无答案 91% → 88%,未达门槛 |
| 成本变化 | 平均 token 增加 12% |
| 决策 | 暂缓发布,修复无答案样本 |
表格数字仅为示例,不代表任何实际产品或研究结果。
九、上线后还要继续评测
离线集无法覆盖所有真实输入。上线后可以观察:用户纠错率、人工转接率、格式解析失败、工具执行失败、关键操作撤销率、响应延迟,以及按场景切片的反馈。对客服、代码助手、文档问答等不同产品,具体指标会不同。
线上反馈也不是天然正确标签。用户没投诉不代表答案正确;点击"有帮助"可能反映语气而非事实;只抽取发生投诉的请求又会产生选择偏差。应把线上信号作为发现新样本的入口,再进行适当人工核验。
遇到严重失败时,记录对应的模型输出和系统轨迹,先确认是否涉及权限、来源或数据变化;再把经过脱敏、可复现的案例加入新失败集。下一次上线前,它就成为必须通过的回归样本。
十、一套能落地的最小流程
如果团队现在完全没有评测系统,可以按这个顺序开始:
- 从真实场景收集几十到几百个有代表性的问题,标注来源与切片;
- 给每题写预期行为、必须满足的事实及禁止事项;
- 先实现格式、字段、引用、权限等确定性检查;
- 对开放式质量建立简短量表,安排人工复核,必要时用模型辅助排序;
- 固定测试集与运行配置,逐题比较旧版和新版;
- 定义硬性拦截项、关键切片门槛和成本约束;
- 上线后把真实失败脱敏回流,定期扩充评测集。
评测的作用不是制造一个漂亮分数,而是让团队能回答三个具体问题:改动改善了哪些场景?损害了哪些场景?在当前风险与成本下,是否值得发布? 能稳定回答这三点,AI 功能的迭代才真正可控。
参考资料
- Liang 等,Holistic Evaluation of Language Models (HELM):https://arxiv.org/abs/2211.09110
- Zheng 等,Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena:https://arxiv.org/abs/2306.05685
- Liu 等,G-Eval: NLG Evaluation using GPT-4 with Better Human Alignment:https://arxiv.org/abs/2303.16634
- NIST,AI Risk Management Framework Core:https://airc.nist.gov/airmf-resources/airmf/5-sec-core/