
上一课改提示词时,判断效果的办法是「多试几条,看着还行」。这有两个问题:你试的几条往往是自己想出来的、偏简单的输入;而且每次试的都不一样,没法比较。改了一处规则,可能修好了你盯着的那条,却把另外三条弄坏了,你根本看不到。
普通软件靠测试防止改坏。AI 应用的输出不固定,不能用 assert output == "...",但思路一样:固定一组输入,定好什么算对,每次改动都自动跑一遍,看分数。这就是评估(eval)。
第一步:金标准案例集

金标准(golden set)是一组你认可「答案应该是这样」的案例。刚开始 20–50 条就够用,关键在于挑得好:
- 来自真实输入。从日志、客服记录里挑,不要自己编。真实留言的错别字、语序混乱、一句话两件事,是自己想不出来的。
- 覆盖各类情况。常见情况占大头,再专门留出边界情况:信息缺失、和业务无关、超长、夹带「忽略上面的要求」的输入。
- 写要点,不写标准答案原文。模型每次措辞不同,逐字比对没有意义。记下「必须包含什么」和「不能出现什么」:
CASES = [
{
"id": "quality-01",
"input": "上周买的耳机,订单 20240917-8812,左耳没声音,要么换要么退,周末前解决。",
"points": ["问题类型是质量", "订单号 20240917-8812", "诉求包含换货或退款", "期限是周末前"],
},
{
"id": "no-order-01",
"input": "衣服洗一次就掉色,我要退货退款!",
"points": ["问题类型是质量", "诉求是退货退款", "订单号为空,不能编造"],
},
# ……共 20–50 条
]- 线上出过错的输入,修好后加进来。这样同一个错误就不会悄悄再出现。
第二步:先做规则检查
很多错误根本不需要模型来判断。下面假设上一课的工单助手已经按建议改成了 JSON Schema 输出:
import json
TYPES = {"退款", "换货", "物流", "质量", "账号", "其他"}
BANNED = ["亲", "保证", "100%"]
def rule_check(output):
"""返回问题列表,空列表表示通过。"""
try:
data = json.loads(output)
except json.JSONDecodeError:
return ["不是合法 JSON"]
problems = []
for field in ["type", "order_id", "request", "summary"]:
if field not in data:
problems.append(f"缺少字段 {field}")
if data.get("type") not in TYPES:
problems.append(f"类型不合法:{data.get('type')}")
if len(data.get("summary", "")) > 50:
problems.append("摘要超过 50 字")
for word in BANNED:
if word in output:
problems.append(f"出现禁用词:{word}")
return problems规则检查快、免费、结果确定,应该放在最前面。能写成规则的就写成规则,例如还可以检查:订单号是否真的出现在输入原文里。规则没通过的,就不用再花钱让模型评审了。
第三步:用模型当评审

「诉求概括得对不对」「回答有没有漏掉要点」这类问题写不成规则,可以交给另一次模型调用来判断,这叫 LLM-as-judge。要让评审稳定,有三点:
- 给评分标准(rubric)。不要问「这个回答好不好」,而是写清楚每个分数对应什么情况。
- 给参考要点。让评审对照金标准里的要点检查,而不是凭它自己的理解。
- 用 JSON Schema 输出判定。先写理由,再给分数,程序直接读取分数和是否通过。
动手:给一个回答打分
下面是一个评审:系统提示词是评分标准,输入里有问题、参考要点和待评的回答。这个回答漏掉了「大杯加 4 元」,看评审能不能发现。再试试把回答改对,或者改成一个很长但内容错误的回答。
系统提示词(这次请求一起发送,点开查看)
{
"reasons": "要点1未覆盖:回答按每杯 28 元计算,漏掉大杯加 4 元。要点2错误:应为 96 元,回答为 84 元。要点3正确:200 积分抵扣 10 元。要点4错误:应付 86 元,回答为 74 元。",
"missing": ["大杯每杯 32 元", "3 杯共 96 元", "应付 86 元"],
"score": 2,
"pass": false
}Schema 里把 reasons 放在 score 前面,是为了让模型先写出判断过程再给分;先给分再找理由,理由往往只是在为分数辩护。
一个最小的评估脚本
把被测应用、规则检查和评审串起来:
import json, os
from openai import OpenAI
client = OpenAI(base_url="https://hivegpt.cn/v1", api_key=os.environ["HIVEGPT_API_KEY"])
JUDGE_SYSTEM = open("prompts/judge.md", encoding="utf-8").read() # 就是上面运行框里的评分标准
VERDICT_SCHEMA = {
"name": "verdict",
"strict": True,
"schema": {
"type": "object",
"properties": {
"reasons": {"type": "string"},
"missing": {"type": "array", "items": {"type": "string"}},
"score": {"type": "integer", "enum": [1, 2, 3, 4, 5]},
"pass": {"type": "boolean"},
},
"required": ["reasons", "missing", "score", "pass"],
"additionalProperties": False,
},
}
def judge(question, points, answer):
points_text = "\n".join(f"{i}. {p}" for i, p in enumerate(points, 1))
resp = client.chat.completions.create(
model="gpt-5.5",
messages=[
{"role": "system", "content": JUDGE_SYSTEM},
{"role": "user", "content": f"【问题】{question}\n【参考要点】\n{points_text}\n【待评回答】{answer}"},
],
response_format={"type": "json_schema", "json_schema": VERDICT_SCHEMA},
)
return json.loads(resp.choices[0].message.content)
def run_eval(app, cases, out_file="eval_results.json"):
results = []
for case in cases:
output = app(case["input"]) # app:被测的函数,比如上一课的工单助手
problems = rule_check(output)
if problems:
results.append({"id": case["id"], "pass": False, "score": 0, "why": problems, "output": output})
continue
v = judge(case["input"], case["points"], output)
results.append({"id": case["id"], "pass": v["pass"], "score": v["score"], "why": v["missing"], "output": output})
passed = sum(r["pass"] for r in results)
print(f"通过 {passed}/{len(results)},平均分 {sum(r['score'] for r in results) / len(results):.2f}")
for r in results:
if not r["pass"]:
print(f" 失败 {r['id']}: {r['why']}")
with open(out_file, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2) # 留档,和下次的结果对比
return results输出里不只看总分,更要看哪几条失败了、为什么。总分从 42/50 变成 43/50,可能是修好了两条、又弄坏了一条,逐条对比才能看出来。
对比两版提示词
有时你不需要绝对分数,只想知道新版是不是比旧版好。这时让评审在两个回答之间选一个(pairwise),比分别打分更稳定。关键是交换位置各评一次:
def compare(question, answer_old, answer_new):
first = pick(question, answer_old, answer_new) # pick 返回 "A" 或 "B",A 是先给的那个
second = pick(question, answer_new, answer_old) # 换个位置再评一次
if first == "B" and second == "A":
return "new"
if first == "A" and second == "B":
return "old"
return "tie" # 两次结论不一致:差别不明显,或者评审在看位置而不是内容pick 就是一次评审调用,Schema 改成 {"reason": ..., "winner": "A" 或 "B"}。对整个案例集统计「新版赢 / 旧版赢 / 平」的条数,再决定要不要上线。
评审也会出错
模型评审很方便,但它有已知的偏差:
| 偏差 | 表现 | 怎么应对 |
|---|---|---|
| 位置偏好 | 两两比较时偏向第一个或第二个 | 交换位置各评一次,不一致算平 |
| 长度偏好 | 回答越长、越客气,分数越高 | 在评分标准里写明「不因长度加分」,给参考要点 |
| 自我偏好 | 偏爱和自己风格相近的回答 | 有条件时评审和被测用不同模型 |
| 和人的判断不一致 | 评审给 5 分,业务同事觉得不行 | 定期人工抽查 |
人工抽查的做法:从评审过的结果里随机抽 20–30 条,请懂业务的人独立打「通过 / 不通过」,再算和评审结论一致的比例。一致率低(比如不到八成),先别急着改被测应用,先看分歧在哪,改评分标准或参考要点。评审本身也需要被评估,否则你只是把「看着还行」换成了「模型说还行」。
什么时候跑
- 每次改提示词、换模型、改检索逻辑,都跑一遍。这是回归测试:确认修好了想修的,也没弄坏别的。把它放进 CI,或者至少写成一条命令。
- 定期抽样线上日志。每天抽一部分真实请求(去掉手机号、地址等个人信息),跑规则检查和评审。线上请求没有参考要点,评审只按通用标准判断(格式、是否答非所问、是否编造),分数低的交给人看。
- 把发现的坏例子加进金标准。案例集会随着应用一起长大,越来越接近真实的使用情况。
评估本身也要花钱:50 条案例,每条一次被测调用加一次评审调用,就是 100 次请求。规则检查放前面、评审只跑必要的,成本是可控的。
小结
- 「试了几条看着还行」不算测试:先整理 20–50 条真实案例,记下每条的要点和禁止出现的内容。
- 先用规则检查过滤格式、字段、长度、禁用词,再用带评分标准、参考要点和 JSON Schema 判定的模型评审;两版对比时交换位置各评一次。
- 评审有位置、长度等偏差,要定期人工抽查;每次改动都跑回归,并抽样线上日志,把坏例子补进案例集。
下一课:对话一长,上下文装不下、模型开始忘事。我们来看上下文工程:长对话、记忆和压缩。