본문으로 건너뛰기

가이드

튜토리얼: 분류기 또는 구조화된 출력

지원 문의에 레이블을 붙이는 Python 함수를 평가하고, 릴리스가 차단된 이유를 읽고, 놓친 케이스를 고치고, 그 수정을 원래 버전과 비교합니다. 모두 계정, 네트워크, 모델 없이 여러분의 머신에서 이루어집니다.

만들 것

answer와 label(refund, account 또는 other)을 담은 JSON 객체를 반환하는 지원 봇입니다. 이 봇에 세 가지 요구 사항을 적용합니다. 레이블이 충분히 자주 맞을 것, 출력이 항상 올바른 형태일 것, 그리고 어떤 답변도 미국 사회보장번호처럼 보이는 것을 노출하지 않을 것. 그중 두 가지는 참조 답이 필요 없는 형식 검사이고, 하나는 참조 레이블에 대한 작업 성공을 측정합니다. 이 차이는 중요하며, 이 페이지는 둘을 구분해 다룹니다.

아래에서 쓰는 용어(케이스, 실행, 지표, 구간, 규칙, 게이트)는 핵심 개념에 정의되어 있습니다.

사전 준비

  • Python 3.11 이상.
  • 가상 환경에 설치된 Oloproof:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • 패키지와 함께 제공되는 예제 프로젝트와 후보 변경. 둘 다 새 디렉터리로 복사하고 첫 번째 디렉터리에서 작업합니다. 모든 파일이 아래에도 나와 있으므로 직접 입력해도 됩니다:
oloproof init --example support_bot support-classifier
oloproof init --example classification support-change
cd support-classifier

이 페이지 어디에서도 API 키, 제공자 계정, 네트워크 접근을 사용하지 않습니다.

파일

support-classifier/
  app.py              the application under test (a Python callable)
  oloproof.yaml       the suite: dataset, system, evaluators
  release.yaml        the release policy: rules the run is decided against
  data/support.jsonl  18 cases, one JSON object per line
  rubrics/helpful.md  a judge rubric, unused here

모든 명령은 support-classifier/ 디렉터리에서 실행하십시오. Oloproof는 그곳의 .oloproof/에 저장소를 둡니다. 처음부터 다시 시작하려면 그 디렉터리를 삭제하십시오.

애플리케이션과 어댑터

애플리케이션은 어댑터를 통해 호출됩니다. Python 애플리케이션의 어댑터는 함수 그 자체입니다. Oloproof는 그 함수를 import하고, 케이스마다 케이스의 input으로 한 번씩 호출하며, 반환된 딕셔너리를 그 케이스의 출력으로 기록합니다.

# app.py
from typing import Any

from oloproof import system


@system(name="support-bot", version="slice-a-example")
def answer(case: dict[str, Any]) -> dict[str, str]:
    question = str(case["question"]).lower()
    if "refund" in question:
        return {"answer": "Refunds are available within 30 days when the order is eligible.",
                "label": "refund"}
    if "password" in question or "login" in question:
        return {"answer": "Use password reset, then contact support if the login still fails.",
                "label": "account"}
    return {"answer": "A support specialist will follow up with the next step.", "label": "other"}

여러분의 분류기를 평가하려면 코드는 그대로 두고, 이것처럼 그 분류기를 호출해 딕셔너리를 반환하는 얇은 함수를 작성하십시오. 함수는 async여도 됩니다. Oloproof는 이를 호출할 뿐 애플리케이션을 호스팅하거나 샌드박스에 넣거나 초기화하지 않으므로, 호출 사이에 애플리케이션이 유지하는 상태는 여러분이 관리해야 합니다.

oloproof.yaml은 그 함수와 평가기를 지정합니다:

version: 1
project: support-bot-example
dataset: data/support.jsonl
system:
  name: support-bot
  version: slice-a-example
  callable: app:answer
  timeout_s: 30
evaluators:
  - type: exact_match
    criterion: exact_label
    field: label
  - type: json_schema
    criterion: format_valid
    field: null
    schema:
      type: object
      required: [answer, label]
      properties:
        answer: {type: string}
        label: {type: string}
      additionalProperties: false
  - type: regex
    criterion: pii_free
    field: answer
    pattern: '\b\d{3}-\d{2}-\d{4}\b'
    pass_if: no_match

출력은 함수의 소스, 선언된 version, config를 기준으로 캐시됩니다. 함수가 다른 파일(프롬프트, 규칙 표)을 읽는다면 system.code_paths 아래에 나열하십시오. 그래야 그 파일을 수정했을 때 시스템이 다시 실행됩니다.

데이터셋

한 줄에 케이스 하나입니다. input은 함수가 case로 받는 것과 정확히 같고, expected는 exact_match 평가기가 비교하는 참조입니다:

{"id":"refund_00","input":{"question":"Can I get a refund for yesterday's order?"},"expected":{"label":"refund"}}
{"id":"account_04","input":{"question":"I can't sign in on my new phone."},"expected":{"label":"account"}}
{"id":"other_04","input":{"question":"I don't want a refund, I just need a copy of my receipt."},"expected":{"label":"other"}}

함수는 각 케이스에 대해 {"answer": "Use password reset, ...", "label": "account"} 같은 객체를 반환합니다.

평가기 고르기

기준평가기expected 필요측정하는 것
exact_labellabel에 대한 exact_match예작업 성공: 레이블이 올바름
format_valid출력 전체에 대한 json_schema아니요형식: 객체가 정확히 두 개의 문자열 필드를 가짐
pii_freeanswer에 대한 regex, pass_if: no_match아니요텍스트의 안전 속성

형식 검사는 형식은 올바르지만 틀린 답도 통과시키므로 결코 작업 성공을 대신할 수 없습니다. 작업 검사는 모든 케이스에 참조가 필요하며, 참조가 없는 케이스는 exact_match가 채점할 수 없습니다. 결정적 평가기는 사람과 대조한 검증이 필요 없습니다. 두 번 실행해도 같은 판정을 냅니다.

정책

release.yaml은 실행을 판정하는 기준입니다:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
warn_on: []
rules:
  - id: exact-label-floor
    metric: exact_label
    min: 0.70
  - id: valid-format
    metric: format_valid
    kind: observed_count
    max_failures: 0
  - id: pii-free
    metric: pii_free
    kind: observed_count
    max_failures: 0

exact-label-floor는 레이블이 최소 70%는 맞아야 한다는 뜻이며, 95% 구간 전체가 0.70 이상일 때만 통과합니다. 두 observed_count 규칙은 실행한 케이스에서 실패를 전혀 허용하지 않습니다. 이 규칙은 이 케이스들을 기술할 뿐, 사용자가 할 모든 질문을 기술하지는 않습니다.

실행하기

oloproof run

실제 출력(일부 생략):

Run run_01M4... [DECIDED/COMPLETE]
Gate: BLOCK (exit 3)
│ exact-label-floor │ exact_label  │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold    │
│ valid-format      │ format_valid │ PASS                  │ observed_failures_within_limit │
│ pii-free          │ pii_free     │ PASS                  │ observed_failures_within_limit │
│ exact_label  │ 72.2%    │ [46.5%, 90.4%]  │ 13 / 18 observed · 0 missing · 0 excluded │
│ format_valid │ 100.0%   │ [81.4%, 100.0%] │ 18 / 18 observed · 0 missing · 0 excluded │
│ pii_free     │ 100.0%   │ [81.4%, 100.0%] │ 18 / 18 observed · 0 missing · 0 excluded │
Cache: execution 0 hit/18 miss; judgment 0 hit/54 miss

읽는 법:

  • 18개 중 13개 레이블이 맞아 72.2%입니다. 0.70보다 높지만 구간은 46.5%까지 내려갑니다. 18개 케이스로는 실제 비율이 최소 0.70임을 보일 수 없습니다. 그래서 규칙은 PASS도 FAIL도 아닌 INSUFFICIENT_EVIDENCE입니다.
  • 모든 출력이 올바른 형태이고 SSN처럼 보이는 숫자를 포함한 출력이 없으므로 두 형식 규칙은 통과합니다.
  • block_on에 INSUFFICIENT_EVIDENCE가 있으므로 게이트는 차단하고 명령은 3으로 종료합니다. 종료 코드 0은 정책이 차단하는 것이 하나도 없다는 뜻이며, CI 게이트에 모든 코드가 나와 있습니다.

다시 실행하면 캐시 줄은 execution 18 hit/0 miss가 됩니다. 아무것도 바뀌지 않았으므로 함수는 호출되지 않습니다.

실패 살펴보기

oloproof inspect RUN_ID --failures
oloproof inspect RUN_ID --case refund_04

RUN_ID는 실행 출력의 첫 줄에 있는 id입니다.

5 of 18 cases failed, errored or did not finish

refund_04
  output: {"answer": "A support specialist will follow up with the next step.", "label": "other"}
  exact_label: failed
...
other_04
  output: {"answer": "Refunds are available within 30 days when the order is eligible.", "label": "refund"}
  exact_label: failed
case refund_04
input: {
  "question": "I was charged twice this month and want my money back."
}
expected: {
  "label": "refund"
}
execution: OK, 1 ms
output: {
  "answer": "A support specialist will follow up with the next step.",
  "label": "other"
}
judgments:
  exact_label: failed
  format_valid: passed
  pii_free: passed

입력을 읽어 보면 패턴이 분명합니다. "money back", "reverse the payment", "sign in", "two-factor"는 키워드 목록에 없고, other_04는 "I don't want a refund"라고 말하는데 "refund"라는 단어가 그래도 일치합니다. refund_04는 틀렸으면서도 두 형식 검사를 모두 통과한다는 점에 주목하십시오. 이것이 형식 확인과 성공 측정 사이의 차이입니다.

여기서 의미 있는 다음 행동은 두 가지입니다. 놓친 케이스를 고치거나(아래), 케이스를 추가하는 것입니다. 같은 정확도로 케이스가 늘면 구간이 좁아지며, oloproof plan RUN_ID --run이 몇 개가 필요한지 추정합니다.

실제로 변경하기

../support-change/app.py를 app.py 위에 복사하십시오. 놓친 표현들을 추가합니다:

REFUND_WORDS = ("refund", "money back", "reverse the payment")
ACCOUNT_WORDS = ("password", "login", "sign in", "two-factor")


@system(name="support-bot", version="keywords-v2")
def answer(case: dict[str, Any]) -> dict[str, str]:
    question = str(case["question"]).lower()
    if any(word in question for word in REFUND_WORDS):
        ...

그리고 oloproof.yaml의 system 아래에 version: keywords-v2를 설정해 실행이 새 버전으로 기록되게 하십시오. 그런 다음:

oloproof run
Gate: ALLOW (exit 0)
│ exact-label-floor │ exact_label  │ PASS  │ lower_bound_meets_minimum      │
│ exact_label  │ 94.4%    │ [72.7%, 99.9%]  │ 17 / 18 observed · 0 missing · 0 excluded │

18개 중 17개가 맞고 구간의 하한 72.7%가 0.70을 넘으므로 규칙은 통과하고 명령은 0으로 종료합니다. other_04는 여전히 실패합니다. 수정이 부정 표현은 다루지 않았기 때문입니다.

후보를 기준선과 비교하기

실행 규칙은 후보가 여러분의 하한을 충족하는지 묻습니다. 비교는 후보가 기준선과 케이스별로 어떻게 다른지 묻습니다. ../support-change/compare.yaml을 프로젝트로 복사하십시오. 비교 규칙 하나가 들어 있습니다:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - id: label-no-regression
    kind: non_inferiority
    metric: exact_label
    margin: 0.10
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml
Comparison sha256:de76... of run_01M4...TJAD against run_01M4...ECVEF · 18 paired cases
exact_label: +22.2 points [-12.9, +57.0] · 18 paired · 0 missing · 0 excluded
format_valid: +0.0 points [-25.8, +25.8] · 18 paired · 0 missing · 0 excluded
pii_free: +0.0 points [-25.8, +25.8] · 18 paired · 0 missing · 0 excluded
Decisions
  label-no-regression  exact_label  non-inferiority, margin 10.0 points  INSUFFICIENT_EVIDENCE  interval_overlaps_margin
    about 3 more paired cases would decide it, if the difference holds (21 in total at 22% discordance)
Gate: BLOCK (exit 3)

후보는 네 케이스를 고쳤고 하나도 망가뜨리지 않았으며, 추정 개선폭은 22포인트입니다. 하지만 바뀐 케이스는 네 개뿐이고, 18개의 짝지은 케이스로는 구간이 12.9포인트 악화부터 57포인트 개선까지 걸쳐 10포인트 마진을 가로지릅니다. 비교는 아직 후보가 허용 범위보다 더 나쁠 가능성을 배제할 수 없으므로 INSUFFICIENT_EVIDENCE이고 3으로 종료합니다. 그 아래 줄은 표본 크기 추정입니다. --policy 없이 compare는 차이를 출력하고, 프로젝트의 release.yaml에 비교 규칙이 선언되어 있지 않다고 알리며, 아무것도 결정되지 않았으므로 0으로 종료합니다.

후보를 기준선과 비교하기에서 마진과 다른 규칙 종류를 설명합니다.

문제 해결

증상원인과 해결
app에 대한 ModuleNotFoundErrorapp.py가 있는 디렉터리에서 실행하거나, 그곳에서 import할 수 있는 모듈 경로를 callable에 지정하십시오.
규칙이 어떤 평가기도 만들지 않는 지표를 지정함규칙의 metric은 평가기의 criterion과 같아야 합니다. 오류 메시지에 존재하는 지표가 나열됩니다.
분류기를 수정했는데 실행이 모든 출력을 재사용함캐시는 callable의 소스를 따릅니다. callable이 읽는 보조 파일은 system.code_paths 아래에 나열해야 합니다.
exact_label이 케이스를 누락으로 보고함해당 실행이 예외를 일으켰거나 시간 초과되었습니다. oloproof inspect RUN_ID --failures가 각 오류를 보여 줍니다.
추정치가 높은데 실행이 3으로 종료함추정치가 아니라 구간이 결정합니다. 케이스를 추가하거나, 실행 전에 정한 더 낮은 하한을 받아들이십시오.

한계

  • SDK와 YAML은 평가기별 통과율을 보고합니다. 이런 분류기에 대한 혼동 행렬이나 클래스별 정밀도와 재현율은 없습니다. 점수를 내는 모델에는 predictive: 블록이 이를 제공합니다(분류기와 회귀 모델).
  • observed_count 규칙은 실행한 케이스를 기술할 뿐, 보지 않은 입력에 대해서는 아무것도 주장하지 않습니다.
  • 18개 케이스에 대한 비교는 큰 차이만 가려냅니다. 50개 이상의 실제 케이스가 더 유용한 하한입니다.
  • oloproof.yaml은 사용자 정의 @evaluator를 지정할 수 없습니다. 그러려면 SDK가 필요합니다(Python API).