본문으로 건너뛰기

가이드

튜토리얼: 루브릭 심사 모델을 이용한 텍스트 생성

자유 텍스트를 작성하는 함수(여기서는 티켓 요약기)를 형식 검사와 루브릭 심사 모델로 평가합니다. 심사 모델이 무엇이든 결정하기 전에 사람의 레이블과 대조해 측정하고, 실제 변경을 비교합니다. 심사 모델은 모델도 네트워크도 없이 이 머신에서 실행되며, 선택 단계에서 실제 모델로 바꿀 수 있습니다.

만들 것

지원 티켓을 한두 문장으로 요약하는 요약기입니다. "좋다"는 문자열 일치가 아니라 판단이므로, 작업 성공은 루브릭을 가진 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 채팅 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" 같은 루브릭은 입력과 출력만 읽으며, 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개를 통과시켰지만, 규칙은 사유 evaluator_not_validated와 함께 INSUFFICIENT_EVIDENCE이고 게이트는 종료 코드 3으로 차단합니다. 규칙은 45%로 결정하지 않았습니다. 심사 모델의 오류율은 측정되기 전까지 알 수 없으므로, 그 판정 위에 세운 구간은 드러나지 않은 오류를 안게 됩니다. 엔진은 이를 MANUAL_REVIEW나 FAIL이 아닌 INSUFFICIENT_EVIDENCE로 보고합니다. 결정할 근거가 부족한 것이며, 출력은 그 근거를 채울 두 명령을 보여 줍니다.

실패 살펴보기

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(prediction-powered inference)입니다. 레이블된 표본으로 심사 모델의 비율이 사람의 비율에서 얼마나 떨어져 있는지 측정하고, 그만큼 추정치를 옮기고 구간을 넓힙니다. 내보내기 출력의 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 보정을 적용하지 않습니다. 두 실행에 대한 심사 모델 자신의 판정을 비교하므로, 개선폭은 위의 보정된 0.500이 아니라 심사 모델의 45%에서 출발합니다. 요약 열한 개가 나아졌고 나빠진 것은 없습니다. 개선폭의 구간이 전부 0보다 위에 있으므로 우월성 규칙은 통과하고 명령은 0으로 종료합니다. 형식은 비교가 아니라 실패를 허용하지 않는 실행 규칙이 지킵니다. 20개 케이스에서 완벽한 형식 점수 두 개를 비교하면 차이가 23.6포인트 이내라는 말밖에 할 수 없습니다.

선택 사항: 실제 모델을 심사 모델로

이 단계는 오프라인 경로를 벗어납니다. 모델 서버가 필요하며, 클라우드 제공자라면 키와 비용이 듭니다.

  • 로컬, 키와 비용 없음: localhost의 Ollama, LM Studio 또는 llama.cpp. 채팅 모델을 받으십시오(Ollama라면 ollama pull llama3.1).
  • 클라우드: 키를 담은 변수를 api_key_env로 지정한 provider: anthropic 또는 openai, 또는 base_url과 api_key_env를 지정한 openai_compatible. 케이스마다 심사 호출이 한 번(첫 응답이 유효한 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을 봅니다. 이미지나 오디오는 보지 않습니다.
  • 레이블 20개로는 일치 구간이 넓습니다. 의존하는 심사 모델이라면 무작위로, 블라인드로 더 많이 레이블을 다십시오.
  • 로컬 표본은 선의에 기반할 뿐입니다. 다른 사람이 의존하는 심사 모델이라면 실행을 push하고 호스팅된 워크스페이스가 표본을 뽑게 하십시오(심사 모델).