跳到主要内容

指南

教程:使用评分细则评判模型的文本生成

评估一个编写自由文本的函数,这里是一个工单摘要器,使用格式检查和一个评分细则评判模型;在评判模型获准决定任何事情之前,先用一个人的标签来度量它;然后比较一次真正的修改。评判模型在本机上运行,无需模型,也无需联网,另有一个可选步骤会换用真实模型。

你将构建什么

一个把客服工单转写成一两句话的摘要器。“好”是一个判断,而不是字符串匹配,所以任务成功由一个带评分细则的 LLM 评判模型来决定:摘要是否说明了客服人员需要的事实?两个确定性评估器检查格式,这不需要参考。用例、运行、指标、评判模型和门禁等术语在核心概念中有定义。

同样的结构也适用于信息抽取或任何其他生成:一个函数在字典中返回文本,参考说明好的回答必须包含什么,评分细则说明如何判定。

前提条件

  • Python 3.11 或更高版本,以及安装在虚拟环境中的 Oloproof:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • 示例项目,它随软件包一起提供。把它复制到一个新目录并在那里工作:
oloproof init --example generation ticket-summaries
cd ticket-summaries
  • 为替身评判模型空出 8799 端口(如果不行,就在两处同时修改)。

在“可选:用真实模型作为评判模型”之前的每一步都是离线且确定性的:没有 API 密钥,没有提供方账户,没有费用。

文件

ticket-summaries/
  app.py                        the summariser under test (baseline)
  app_v2.py                     the candidate change
  judge_server.py               a stand-in judge speaking the OpenAI API on 127.0.0.1
  rubrics/covers_facts.md       the judge's rubric
  oloproof.yaml                 the suite
  release.yaml                  rules for a run
  compare.yaml                  a rule for a comparison
  data/tickets.jsonl            20 cases
  labels/reviewer_verdicts.csv  one person's verdicts on the baseline's summaries
  fill_labels.py                copies those verdicts into a labelling sheet

在 ticket-summaries/ 中运行每条命令。

替身评判模型,以及它不是什么

评分细则评判模型是一种评估器,它把一个提示(评分细则、用例的输入、它的 expected 和输出)发送给一个模型,并读回 {"pass": true|false, "rationale": "..."}。Oloproof 可以与任何使用 OpenAI chat API 的服务器通信,而 localhost 上的服务器不需要密钥。

judge_server.py 就是这样一个服务器,但它不是模型。只有当摘要包含用例 expected 中 must_mention 下的每一个短语(忽略大小写)时,它才让摘要通过。这是一条固定规则,所以本教程在每台机器上都给出相同的数字。它无法察觉编造的事实,而真实的模型评判模型会被要求这样做。在第二个终端中启动它并让它保持运行:

python judge_server.py --port 8799
stand-in judge on http://127.0.0.1:8799/v1

应用及其适配器

# app.py
@system(name="ticket-summariser", version="first-sentence")
def summarise(case: dict[str, Any]) -> dict[str, str]:
    return {"summary": sentences(str(case["ticket"]))[0]}

Python 应用的适配器就是这个函数:它接收用例的 input 并返回一个字典。对于你自己的生成器,在函数内部调用你的模型或链,并把文本放在某个键下返回。Oloproof 对每个用例调用它一次,并按函数的源代码和声明的 version 缓存输出;它不管理你的模型客户端、提示词或状态。把函数读取的文件(例如提示模板)列在 system.code_paths 下。

数据集

{"id":"t01","input":{"ticket":"Hello. Order 1042 arrived with a cracked screen. I would like a replacement, not a refund."},"expected":{"must_mention":["1042","cracked","replacement"]}}
{"id":"t06","input":{"ticket":"Please cancel my subscription at the end of this month. I am moving abroad."},"expected":{"must_mention":["cancel","end of this month"]}}

input 是函数接收到的内容。expected 是评判模型读取的参考:这里是摘要必须包含的事实列表,而不是一份完整的参考摘要,因为许多不同的摘要都是正确的。t01 的输出是 {"summary": "Hello."}。

选择评估器

version: 1
project: ticket-summaries
dataset: data/tickets.jsonl
system:
  name: ticket-summariser
  version: first-sentence
  callable: app:summarise
  timeout_s: 30
evaluators:
  - type: json_schema
    criterion: format_valid
    field: null
    schema:
      type: object
      required: [summary]
      properties:
        summary: {type: string, minLength: 1}
      additionalProperties: false
  - type: regex
    criterion: short_enough
    field: summary
    pattern: '^.{1,160}$'
    pass_if: match
  - type: rubric_judge
    criterion: covers_facts
    provider: openai_compatible
    model: stand-in-judge
    base_url: http://127.0.0.1:8799/v1
    rubric_file: rubrics/covers_facts.md
判据评估器需要 expected度量
format_validjson_schema否格式:一个非空字符串字段
short_enoughregex否格式:最多 160 个字符
covers_factsrubric_judge是任务成功,按评分细则的定义

Hello. 通过了两项格式检查。只有评判模型会说它是一个无用的摘要。评判模型也可以在没有参考的情况下运行:像“PASS if the summary contains no greeting”这样的评分细则只读取输入和输出,没有 expected 的用例仍然会被评判。这时它做不到的,是对照你信任的答案核对事实。

评分细则:

PASS when the summary states every fact listed under must_mention in the expected answer, in
words a support agent would recognise, and adds nothing the ticket does not say.
FAIL when any listed fact is missing, changed or contradicted.

策略

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
require_validated_evaluators: true
rules:
  - id: valid-format
    metric: format_valid
    kind: observed_count
    max_failures: 0
  - id: short-enough
    metric: short_enough
    kind: observed_count
    max_failures: 0
  - id: covers-facts-floor
    metric: covers_facts
    min: 0.60

require_validated_evaluators: true 是引擎的默认值,这里把它写出来,是因为它正是本教程的要点:一个没有人与人工比较过的评判模型,不能决定一条规则。

运行

oloproof run
Run run_01M4... [DECIDED/COMPLETE]
Gate: BLOCK (exit 3)
│ valid-format       │ format_valid │ PASS                  │ observed_failures_within_limit │
│ short-enough       │ short_enough │ PASS                  │ observed_failures_within_limit │
│ covers-facts-floor │ covers_facts │ INSUFFICIENT_EVIDENCE │ evaluator_not_validated        │
covers-facts-floor: the judge (or model or custom evaluator) behind this rule has not been measured against
people yet, so it may not decide.
  Label a sample:  oloproof review run_01M4... --criterion covers_facts --by YOU --sample 20
  Then measure it: oloproof evaluators validate EVALUATOR_ID --by YOU (ids: oloproof evaluators list)
│ format_valid │ 100.0%   │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │
│ short_enough │ 100.0%   │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │
│ covers_facts │ 45.0%    │ [23.0%, 68.5%]  │ 9 / 20 observed · 0 missing · 0 excluded  │
Cache: execution 0 hit/20 miss; judgment 0 hit/60 miss

格式规则通过了。评判模型让 20 个摘要中的 9 个通过,但规则是 INSUFFICIENT_EVIDENCE,原因为 evaluator_not_validated,门禁以退出码 3 阻止。规则并没有依据 45% 作出决策:评判模型的错误率在被度量之前是未知的,所以基于它的结论构建的区间会带有一个未说明的误差。引擎把这报告为 INSUFFICIENT_EVIDENCE,而不是 MANUAL_REVIEW 或 FAIL:缺少的是用来决策的证据,输出会打印出提供证据的两条命令。

查看失败项

oloproof inspect RUN_ID --failures
11 of 20 cases failed, errored or did not finish

t01
  output: {"summary": "Hello."}
  covers_facts: failed
    judge text, not verified: missing: 1042, cracked, replacement

t02
  output: {"summary": "I was charged twice for order 2210."}
  covers_facts: failed
    judge text, not verified: missing: 49
...

评判模型的理由显示为“judge text, not verified”:它是模型的解释,而不是证据。无论如何,规律很清楚:第一句往往是一句问候。

用一个人来度量评判模型

验证会把评判模型的结论与一个人对同一批回答的结论进行比较。从运行的用例中随机抽取一个样本放进一张表。评判模型的结论不会出现在表中,这样标注者就不会被它们锚定:

oloproof labels export RUN_ID --criterion covers_facts --sample 20 --local --out sample.csv
Wrote 20 cases to sample.csv, drawn at random with seed 2701013296, without the judge's verdict.
  This is a local sample, good-faith only, because it was drawn on this machine.
Fill in `passed` (pass or fail) and `labelled_by` on each row you judge, then run `oloproof labels import sample.csv`.

--local 在本机上抽样,而不询问托管工作区;种子由引擎选择。只有 20 个用例时,20 个的样本就是全部用例。实际操作中,一个人会读每一行的工单和摘要并填写 passed。在本教程中,labels/reviewer_verdicts.csv 保存了一位审核者对基线摘要给出的结论,fill_labels.py 把它们复制到表中:

python fill_labels.py sample.csv
oloproof labels import sample.csv
filled 20 rows of sample.csv
Recorded 20 labels from sample.csv (20 measurement).

审核者与评判模型有一次意见不同:在 t02(“I was charged twice for order 2210.”)上,他们认为缺少金额无关紧要,让它通过了。标签指明了它们所评判的确切回答,所以这些结论只适用于基线运行。

找到评判模型的版本 id 并验证它:

oloproof evaluators list
oloproof evaluators validate EVALUATOR_ID --by alice
covers_facts  LLM_JUDGE  UNVALIDATED  (declared)  sha256:a662...

covers_facts: sha256:a662... is now VALIDATED
  agreement 95.0% [75.1%, 99.9%] · 19 of 20 labelled cases agreed · 0 labelled but not judged · kappa 0.900
  bias -5.0 points [-32.4, +20.7] · the judge's pass rate minus the people's · 20 cases · 0 labelled but not judged
  passes what people pass 90.0% [55.4%, 99.8%] · the judge passed 9 of 10 cases people passed · 0 labelled but not judged
  fails what people fail 100.0% [69.1%, 100.0%] · the judge failed 10 of 10 cases people failed · 0 labelled but not judged

读区间,而不是 95%:20 个标签显示一致率至少为 75.1%。策略可以用 minimum_evaluator_agreement 提出更高的要求,它比较的正是这个下界,而 validate 会拒绝低于它的评判模型。评判器指南介绍了这个标准、偏差、探针,以及在终端中进行标注的 oloproof review。

现在不调用摘要器或评判模型,对已存储的运行重新做出决策:

oloproof gate RUN_ID --policy release.yaml
valid-format: PASS (observed_failures_within_limit)
short-enough: PASS (observed_failures_within_limit)
covers-facts-floor: INSUFFICIENT_EVIDENCE (interval_overlaps_threshold)
  no sample size would make this PASS: the observed rate (0.500) is itself below the threshold (0.600), so more cases would move it toward FAIL
Gate: BLOCK (exit 3)

现在评判模型可以决策了,而决策针对的是摘要器:它引用的比率 0.500 并不是评判模型的 45%。由于这次运行有一个盲测的随机度量标签样本,门禁读取的是经这些标签校正后的评判模型(见评判器指南中的“Judge-corrected gates”)。这种校正是 PPI,即预测驱动推断:它用已标注的样本度量评判模型的比率与人工的比率相差多远,并据此移动估计值、加宽区间。导出文件中关于 PPI 的说明指的也是这一点。无论哪种方式,基线都没有达到下限,增加用例也改变不了这一点。

做一次真正的修改

app_v2.py 会跳过简短的客套话,保留接下来的两句。把它复制到 app.py 上覆盖,在 oloproof.yaml 的 system 下设置 version: skip-pleasantries,保持评判模型运行,然后:

oloproof run
Gate: ALLOW (exit 0)
│ covers-facts-floor │ covers_facts │ PASS  │ lower_bound_meets_minimum      │
│ covers_facts │ 100.0%   │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │
Cache: execution 0 hit/20 miss; judgment 6 hit/54 miss

评判模型是同一个已验证的版本,所以它的规则直接作出决策。有六个评判结果来自缓存,针对的是两个版本写得完全相同的摘要。没有人标注过这些新摘要;是评判模型的验证让它的结论得以成立。

把候选与基线进行比较

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
require_validated_evaluators: true
rules:
  - id: covers-more-facts
    kind: superiority
    metric: covers_facts
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml
format_valid: +0.0 points [-23.6, +23.6] · 20 paired · 0 missing · 0 excluded
short_enough: +0.0 points [-23.6, +23.6] · 20 paired · 0 missing · 0 excluded
covers_facts: +55.0 points [+13.0, +84.4] · 20 paired · 0 missing · 0 excluded
Decisions
  covers-more-facts  covers_facts  superiority  PASS  difference_above_zero
Gate: ALLOW (exit 0)

比较不应用 PPI 校正:它比较的是评判模型自己在两次运行上的结论,这就是为什么提升是从评判模型的 45% 而不是上面校正后的 0.500 算起。十一个摘要变好了,没有一个变差;提升的区间完全在零以上,所以优效性规则通过,命令以 0 退出。格式由不允许任何失败的运行规则来守护,而不是由比较来守护:在 20 个用例上,比较两个完美的格式分数,只能说差异在 23.6 个百分点以内。

可选:用真实模型作为评判模型

这一步离开了离线路径。它需要一个模型服务器,如果使用云端提供方,还需要密钥和费用。

  • 本地,无需密钥也无需费用:localhost 上的 Ollama、LM Studio 或 llama.cpp。拉取一个聊天模型(对于 Ollama,ollama pull llama3.1)。
  • 云端:provider: anthropic 或 openai,并用 api_key_env 指明保存你密钥的变量;或者 openai_compatible,带上 base_url 和 api_key_env。每个用例是一次评判调用(第一次回复不是有效 JSON 时为两次),按你的提供方的费率计费,并且对于已经评判过的回答,Oloproof 绝不会再次调用评判模型。

把草拟的评判模型写在一个单独的文件中,就像它出现在 evaluators: 下那样:

# live_judge.yaml
type: rubric_judge
criterion: covers_facts
provider: openai_compatible
model: llama3.1
base_url: http://localhost:11434/v1
rubric_file: rubrics/covers_facts.md

并用审核者已经标注过的回答来试验它,而不验证也不采用它:

oloproof evaluators try live_judge.yaml

本地服务器默认一次只应答一个请求;在 oloproof.yaml 中加入 concurrency: {system: 2, judge: 2},这样排队的调用就不会超时。在一台笔记本电脑上用一个小型本地模型(qwen2.5vl)运行这一步,打印出:

covers_facts: draft sha256:b88a... on 20 labelled cases · 20 judged now, 0 from cache, 11 errored
  agreement 88.9% [19.1%, 99.9%] · 8 of 9 labelled cases agreed · 11 labelled but not judged · kappa 0.769

有十一次调用超时,一致性区间把每一次都按两种方向计入,所以它向下延伸到 19.1%:不应答的评判模型是无法被度量的。更大的模型、更长的超时或更少的并发调用可以解决这个问题。要采用这个模型,就把它放进 oloproof.yaml,替换掉替身。那是一个新的评估器版本:它的配置(模型、端点、评分细则)就是它的身份,所以替身的验证不会延续过来。用它重新运行基线,并像上面那样对照标签验证它。

故障排除

症状原因与修复
covers_facts 全部缺失,no_observations评判服务器没有运行,或不在 base_url 上。每次评判调用都出错了;oloproof inspect RUN_ID --failures 会显示原因。
验证之后仍出现 evaluator_not_validated你修改了评判模型(模型、端点、端口、评分细则),产生了一个新版本。验证那个版本。
labels import 拒绝文件并指出某一行该行指定的用例或执行不在这次运行中;从你要标注的运行重新导出。
labels export 说无法连接到工作区你已登录某个工作区,所以它请求由工作区来抽样。--local 改为在本机抽样。
云端评判模型在任何调用之前就失败它的密钥不在 api_key_env 指定的变量中。

局限

  • 替身评判模型只是短语匹配。它演示的是工作流程,而不是评判质量。
  • 没有 BLEU、ROUGE 或嵌入相似度评估器。在 SDK 中,可以用 @evaluator 写一个;oloproof.yaml 目前还无法指定自定义评估器。
  • 评判模型看到的是文本:输入、参考和输出的 JSON。它看不到图像或音频。
  • 二十个标签给出的一致性区间很宽。对于你所依赖的评判模型,请以随机且盲测的方式标注更多。
  • 本地样本只代表善意。对于其他人所依赖的评判模型,请推送运行,让托管工作区来抽取样本(评判器)。