LLM 当裁判省的是人力,费的是心思:评分标准写不清楚,同一个输出能被同一个模型打出两种分。这一期我围绕 LLM-as-a-Judge 的评分标准设计拆出四条可执行的经验,并在 4sapi(https://4sapi.com)上把整套评测流程跑通了一遍。

一、开篇痛点:评测结果时灵时不灵,先怀疑评分标准

把打分交给 LLM 之后,我遇到的第一个问题不是模型不行,而是分数不稳定。具体症状我列过四类:

排查到最后,问题几乎都出在评分标准上。模糊的提示词等于放手让评判模型自由发挥,评估不一致只是表象,真正被浪费的还有每一次评测的 token——让一个 LLM 对着含糊的标准写长篇评语,等于花钱买噪音,买回来的还是不可复现的结果。

这一期我把评分标准设计拆成四条经验:问题原子化、只评客观事实、只评 prompt 明确要求的内容、用 golden set 校准。每一条我都放在 4sapi(https://4sapi.com)的评测流水线上实测过,下面逐个拆开。

二、原理速览:LLM-as-a-Judge 到底在做什么

LLM-as-a-Judge 的思路不复杂:把"评估另一个模型输出质量"这件事,从人肉打分变成一次普通的 LLM 调用。评测脚本把评分标准和待评输出拼进 prompt,发给评判模型,拿回分数,再汇总统计。

我的评测脚本
    │  拼入评分标准 + 待评输出
    v
4sapi 网关(https://4sapi.com)
    │  统一格式 / 鉴权 / 限流 / 计费
    v
评判模型
    │  输出结构化评分 JSON
    v
评测脚本汇总分数 → 一致性报告

这条链路里,评判模型的能力只决定评测上限,评分标准才决定实际水平。标准写得含糊,模型就只能靠感觉打分,感觉这个东西恰恰是最不稳定的;标准写得可判定,分数才有统计意义,结果才能进 CI、进上线门禁。

我最开始踩的坑就在这里:背景里全是"高质量""清晰""有用"这类模糊提示,评判模型不知道这些词指什么,就会自己脑补一套标准,换一个会话脑补得还不一样——这就是评测不一致的根源,也是 token 浪费的根源。

三、经验一:问题原子化,互不重叠

第一条经验:评分标准里的每个问题,只问一件事,并且问题之间不重叠。

反例是这样的标准:"请评估回答是否清晰、完整、有帮助。"这一句话里至少混着三个维度,而"清晰""完整""有帮助"之间边界模糊——一个回答可以逻辑清晰但漏了关键步骤,评判模型会卡在"该给哪个维度打分"上反复横跳。

我现在的写法是把每个维度拆成独立的问题,每个问题只对应一个可以判真伪的事实:

拆分前(模糊) 拆分后(原子化)
回答是否清晰、完整、有帮助 Q1:回答是否包含完整的接入步骤?
Q2:回答中的代码示例能否直接运行?
Q3:回答是否解释了每一步的作用?

原子化带来的收益是实打实的三个:

判断标准是否够原子,我有一个简单的检查办法:如果回答某个问题需要同时参考两个独立事实,就说明它还能再拆。拆到每个问题只有一个谓词为止。

四、经验二:只评客观事实,用 MUST 表述

第二条经验:只让评判模型评估客观事实,不要让它做主观审美。

"语气是否自然""读起来是否舒服"这类标准,人都不一定能达成一致,让模型评更是各说各话。能进评分标准的,必须是"可以被判定为真或假"的事实,判定结果不依赖打分者的偏好。

把标准写成可判定命题,我借用 RFC 2119 的术语体系,用 MUST / MUST NOT 来表述:

对比一下两种写法:

"应该"是主观的,"MUST"是可验证的。评判模型对 MUST 条款的判定只有两种结果:满足或不满足,分数不再依赖它当时的心情。把标准里的"应该""尽量""最好"全部替换成 MUST / MUST NOT 之后,我的评测一致率明显回升,因为留给模型自由裁量的空间几乎没有了。

五、经验三:只评 prompt 中明确要求的内容

第三条经验:评分标准里出现的每一项,都必须在 prompt 里能找到出处,绝不添加 prompt 之外的要求。

评判模型有一个坏习惯:它会自己脑补评价维度。我遇到过最典型的例子:评测任务里只要求模型"输出一份 JSON 配置",评分标准里却写着"是否包含 Markdown 标题"——这一条 prompt 根本没要求,评判模型却会当真去检查,然后给输出扣分。这就是脑补,脑补出来的分数和任务本身毫无关系。

我用来拦截脑补的做法有四条:

只评明确要求的内容,评测结果才是在回答"这个输出是否满足任务要求",而不是在回答"模型觉得这个输出怎么样"。前者是验收,后者是观后感,评测体系要的是前者。

六、经验四:用 golden set 校准评判模型

第四条经验:评分标准写得好不好,用专家标注的 golden set 来验收,而不是靠感觉。

golden set 是一组已经由专家标好判定的样本:输入、输出、标准答案、专家评分,一一对应。校准流程是:

专家标注 golden set(输入 / 输出 / 专家分)
    │
    v
用评分标准驱动评判模型打分
    │
    v
对比模型分与专家分
    │
    v
不一致 → 修改评分标准或更换评判模型 → 重新评测
    │
    v
一致率达到目标 → 标准验收通过,可进入批量评测

一致性怎么量化?最朴素的是准确率:模型分与专家分完全一致的样本占比。样本量大之后,还可以用加权一致率,把"错一档"和"错两档"区分开,错得离谱的样本权重更高。

校准不是一次性的。换了评判模型、改了评分标准、任务描述有调整,都要重跑一遍 golden set。我的实测经验是:多数不一致并不是模型笨,而是标准里混进了主观表述——把主观词改写成 MUST 条款之后,一致率通常立刻回升,这说明校准真正校准的是评分标准,而不是模型。

七、把评测提示词工程化:评分标准进配置

四条经验落到工程上,是让评测提示词变成可维护的资产,而不是散落在脚本里的一段话。我的做法有五点:

第四、五点合起来解决评测场景最核心的两件事:结果可直接统计,token 消耗被压到最低。评判模型不写小作文,账单上省下来的部分,在批量评测里非常可观。

八、接入 4sapi 跑 judge 评测:Python 示例

实操环节。评测脚本用 OpenAI 兼容客户端接入 4sapi(https://4sapi.com),base_url 指向统一网关,一次 API 调用完成一次判定:

import json
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["4SAPI_API_KEY"],
    base_url="https://4sapi.com/v1",  # 统一网关地址
)

# 评分标准:原子化 + MUST 表述,独立于脚本维护
RUBRIC = [
    "Q1: 回答 MUST 包含 base_url 的完整配置示例。",
    "Q2: 回答 MUST NOT 遗漏鉴权参数的说明。",
    "Q3: 回答 MUST 输出合法的 JSON 结构。",
]

JUDGE_SYSTEM = (
    "系统指令:作为评测员,只依据评分标准判定客观事实,"
    "不得引入标准之外的要求。只输出 JSON,格式:"
    '{"results": [{"question": "...", "pass": true}]}'
)

def parse_judge_json(raw: str) -> dict:
    """去掉可能的代码围栏后解析 JSON。"""
    raw = raw.strip()
    if raw.startswith("```"):
        raw = raw.split("\n", 1)[1].rsplit("```", 1)[0].strip()
    return json.loads(raw)

def judge_once(output_text: str) -> dict:
    prompt = (
        "以下是待评输出:\n\n"
        f"{output_text}\n\n"
        "请逐条判定:\n"
        + "\n".join(RUBRIC)
    )
    resp = client.chat.completions.create(
        model="judge-model",        # 换成实际使用的评判模型
        messages=[
            {"role": "system", "content": JUDGE_SYSTEM},
            {"role": "user", "content": prompt},
        ],
        temperature=0,              # 评测场景固定为 0,不做随机采样
        max_tokens=512,             # 收紧输出上限,防止写长篇评语
    )
    return parse_judge_json(resp.choices[0].message.content)

def run_eval(outputs: list[str]) -> dict:
    summary = {"pass": 0, "fail": 0, "samples": len(outputs)}
    for text in outputs:
        result = judge_once(text)
        for item in result["results"]:
            summary["pass" if item["pass"] else "fail"] += 1
    return summary

# 用法:run_eval([model_a_output, model_b_output, ...])

几个容易踩的细节:

九、评测模式对比与成本控制

把三种评测方式放在一起看,差异一目了然:

评测方式 一致性 单样本成本 可追溯性
人工评测 高,但费时 人力成本高 依赖评审记录
裸 LLM 打分(无结构化标准) 低,波动大 中,评语冗长烧 token 差,无法解释分数
结构化标准 + golden set 校准 高,可复现 低,只输出 JSON 好,逐条标准可定位

成本控制的核心是别让评判模型"发挥"。同样一个样本,模糊标准可能让模型输出 500 个 token 的评语,结构化标准只要 50 个 token 的 JSON——批量评测上万条样本时,差距直接反映在账单上。

另外三个降本手段是我常用的:固定系统提示,让同一段上下文反复命中缓存读取价;批量样本合并提交,减少请求次数;评测任务与正式业务调用分开计费标签,成本归因一目了然。配合 4sapi 网关的用量统计,评测成本可以按标准、按模型、按批次拆开看。

十、成本与风险提示

把这一期的坑集中列一下:

十一、评分标准验收清单

总结

LLM-as-a-Judge 的价值在于把评测从人肉劳动变成可重复的调用,但这一切的前提是评分标准经得起推敲:问题原子化保证每个维度独立可统计,MUST 表述保证判定可验证,只评 prompt 明确要求的内容杜绝脑补,golden set 校准保证标准与人类评分对齐。四步走完,评测脚本才敢接进上线门禁。我在 4sapi(https://4sapi.com)的评测流水线上,现在每个评分标准上线前都会先过一遍验收清单——评测的一致性和 token 账单,会随标准质量一起变好。欢迎在评论区发表想法,一起聊聊 LLM-as-a-Judge 的评分标准设计。