본문으로 건너뛰기

가이드

튜토리얼: 여러 턴의 대화

Python SDK로 대화형 어시스턴트를 평가하는 실행 가능한 안내입니다. 애플리케이션이 스크립트로 정해진 대화를 새 세션에 대해 재생하고, 답한 내용을 conversation/v1 아티팩트로 기록하며, 두 평가기가 각 대화 전체를 심사합니다. 심사 모델 대신 스크립트로 된 대역을 써서 오프라인으로 실행하고, 이어서 후보 수정안을 비교하며, 마지막으로 턴을 클러스터 케이스로 평가하는 대안을 다룹니다.

구간, 결정 상태, 릴리스 조치 같은 용어는 개념에 정의되어 있고, 아티팩트는 시스템이 한 일 기록하기에서 다룹니다.

여기서 Oloproof가 하는 일과 하지 않는 일

Oloproof가 하는 일애플리케이션이 하는 일
스크립트로 된 대화를 데이터셋으로 저장하며, 모든 시스템에 동일함대화를 진행함: 스크립트의 각 턴을 순서대로 물음
스크립트의 모든 턴에 답했는지 확인함(ConversationCompleted)세션 상태를 소유하고, 케이스마다 새 세션을 시작하며, 초기화함
모델로 대화 기록 전체를 심사함(ConversationJudge)계속할 수 없을 때 무슨 일이 일어날지 정하고, 멈췄다는 사실을 기록함
구간을 계산하고, 두 시스템을 비교하며, 정책에 따라 결정함conversation/v1 아티팩트를 기록함

Oloproof에는 사용자 시뮬레이터가 없습니다. 사용자 턴을 절대 쓰지 않으므로, 사용자 쪽은 데이터셋의 스크립트가 정한 그대로입니다. 기록된 대화 안에서 턴 단위 지표도 없으며, 기록된 대화를 새 시스템에 대해 재생할 수도 없습니다. 대화 평가기는 Python SDK에만 있습니다. ConversationCompleted와 ConversationJudge는 oloproof.yaml의 평가기 유형이 아니므로, 이 튜토리얼은 oloproof run 대신 스크립트를 씁니다.

사전 준비

  • 빠른 시작에서처럼 Python 3.11 이상과 pip install oloproof.
  • 패키지와 함께 제공되는 예제 파일. 실행의 저장소가 그곳에 생기도록 새 디렉터리에 복사하세요.
oloproof init --example conversation ~/oloproof-conversation
cd ~/oloproof-conversation
파일의미
assistant.py테스트 대상 애플리케이션: 세션 상태를 가진 요금제 어시스턴트
systems.py어댑터: 스크립트를 재생하고 conversation/v1을 기록함
judge_offline.py심사 모델을 대신하는 스크립트 대역
evaluate.py평가, 비교, 턴 대안을 실행함
release.yaml실행 하나에 대한 정책
comparison.yaml후보를 기준선과 비교하는 정책
turns_release.yaml턴 대안에 대한 정책
data/conversations.jsonl스크립트로 된 대화 40개
data/turns.jsonl같은 대화를 턴마다 케이스 하나로 나눈 것

마지막의 선택적인 라이브 단계 전까지는 키도, 네트워크도, 제공자 비용도 필요 없습니다.

애플리케이션

assistant.py는 세 가지 요금제에 관한 질문에 답합니다. 상태는 하나, 대화가 다루는 요금제만 유지하므로, "그거 SSO도 포함되나요?" 같은 후속 질문에서 "그거"를 해석할 수 있습니다. 기준선에는 의도적인 결함이 있습니다. 요금제를 기억하지 못해서, 후속 질문에 기본 요금제를 기준으로 답합니다. 사람을 요청하는 사용자는 HandoffRequested로 대화를 끝냅니다.

class PlanAssistant:
    def __init__(self, *, remembers_plan):
        self.remembers_plan = remembers_plan
        self.reset()

    def reset(self):
        """Forget everything, so one conversation never leaks into the next."""
        self.current_plan = None

    def ask(self, question): ...

이 부분을 여러분의 애플리케이션으로 바꾸면 됩니다. 챗봇 클라이언트, 에이전트 세션, 서비스에 대한 HTTP 세션 등입니다. 무엇이든 자신의 상태와 초기화를 소유하며, Oloproof는 어댑터가 기록한 것만 봅니다.

데이터셋: 스크립트가 입력이다

data/conversations.jsonl의 한 줄이 대화 하나입니다.

{"expected": {"plan": "enterprise"}, "id": "conv_00", "input": {"turns": ["What does the enterprise plan cost?", "Does that include SSO?"]}, "metadata": {"pattern": "pronoun_followup"}}

사용자의 턴은 데이터셋 내용이며, 스위트의 다이제스트에 포함되고, 그것으로 측정하는 모든 시스템에 동일합니다. 이것이 두 시스템을 비교할 수 있게 해 줍니다. expected는 심사 모델에게 보여 주는 참조입니다. 40개 대화는 요금제를 밝히지 않는 후속 질문이 있는 24개, 매 턴 요금제를 밝히는 12개, 세 턴 중 두 번째 턴에서 사람을 요청하는 4개로 이루어집니다.

어댑터

systems.py는 케이스마다 새 세션을 시작하고, 스크립트의 각 턴을 순서대로 묻고, 돌아온 것을 기록합니다.

from oloproof import CONVERSATION, current_case, system


def replay(case, *, remembers_plan):
    script = [str(turn) for turn in case["turns"]]
    session = PlanAssistant(remembers_plan=remembers_plan)  # a new session per case
    turns = []
    truncated = False
    for index, question in enumerate(script, start=1):
        try:
            reply = session.ask(question)
        except HandoffRequested:
            truncated = True  # the recording stops here and says so
            break
        turns.append({"index": index, "asked": question, "answer": reply["answer"]})
    current_case().artifact(
        CONVERSATION,
        {"turns": turns, "declared_turns": len(script), "truncated": truncated},
    )
    last = turns[-1]["answer"] if turns else None
    return {"answer": last, "turns_answered": len(turns)}


@system(name="plan-assistant", version="baseline", records=(CONVERSATION,))
def baseline(case):
    return replay(case, remembers_plan=False)


@system(name="plan-assistant", version="candidate-remembers-plan", records=(CONVERSATION,))
def candidate(case):
    return replay(case, remembers_plan=True)

케이스마다 새 세션을 쓰는 것이 중요합니다. Oloproof는 케이스를 동시에, 정해지지 않은 순서로 실행하므로, 케이스 사이에 공유된 세션은 한 대화의 상태를 다른 대화로 새게 합니다. records=는 시스템이 그 아티팩트를 기록한다고 선언합니다. 없으면 대화 평가기는 모든 케이스를 누락으로 세는 대신 무엇이든 실행되기 전에 거부됩니다.

conversation/v1 아티팩트

기준선이 conv_00에 대해 기록한 것으로, oloproof export RUN_ID(번들의 cases.jsonl)에서 가져왔습니다.

{"declared_turns": 2, "truncated": false, "turns": [{"answer": "The enterprise plan costs a price agreed per contract.", "asked": "What does the enterprise plan cost?", "index": 1, ...}, {"answer": "The starter plan does not include SSO.", "asked": "Does that include SSO?", "index": 2, ...}]}

그리고 사람을 요청한 대화의 경우입니다.

{"declared_turns": 3, "truncated": true, "turns": [{"answer": "The team plan costs $20 a month.", "asked": "What does the team plan cost?", "index": 1, ...}]}
필드의미
turns[].index이것이 답하는 스크립트 턴, 1부터 연속
turns[].answer어시스턴트가 반환한 것, 임의의 JSON
turns[].asked선택 사항, 읽기 전용이며 엔진은 index로 맞춤
turns[].retrieval선택 사항, 그 턴이 검색한 것, retrieval/v1 형태
declared_turns스크립트가 선언한 턴 수
truncated무엇이 끊었든, 기록이 스크립트보다 먼저 멈춤

아티팩트는 기록될 때 검사됩니다. 선언보다 턴이 적은 기록은 truncated: true를 밝혀야 하고, 인덱스는 연속이어야 하며, 기록은 질문받은 것보다 많은 턴에 답할 수 없습니다. 형식이 잘못된 아티팩트는 SystemContractError로 실행을 멈춥니다.

두 평가기, 그리고 둘 다 필요한 이유

  • ConversationCompleted는 결정론적입니다. 어시스턴트가 스크립트의 모든 턴에 답했는가? 이것이 먼저 실행되는 이유는, 세 턴 중 첫 턴에서 멈춘 대화에 대한 다른 모든 주장은 다른 대화에 대한 주장이기 때문입니다. 잘린 대화는 이 평가기에서 실패합니다. 그것은 결과이지 누락 케이스가 아닙니다.
  • ConversationJudge는 대화 기록 전체, 즉 모든 사용자 턴과 어시스턴트 턴에 대한 모델 심사 모델입니다. 대화형 제품이 비난받는 실패는 관계적이기 때문입니다. 앞 턴의 답과 모순되는 답은 그 옆에 놓였을 때만 틀린 것입니다. 잘린 대화는 기록된 것으로 심사되며, 대화 기록이 심사 모델에게 어디서 멈췄는지 알려 줍니다.
evaluators = [
    ConversationCompleted(),
    ConversationJudge(criterion="plan_coherent", provider=..., model=..., rubric_text=RUBRIC),
]

오프라인 심사

심사 모델에는 모델이 필요합니다. 네트워크 없이 실행하기 위해 judge_offline.py는 스크립트로 된 제공자를 넘기는데, 이는 Oloproof 자체 테스트가 쓰는 것과 같은 도우미입니다(엔진 내부의 FakeProvider이며 공개 API가 아닙니다). 모든 심사 프롬프트에 고정된 규칙 하나로 답합니다. 모든 어시스턴트 턴이 참조가 가리키는 요금제를 언급하면 통과입니다. 이렇게 하면 판정이 결정론적이 되고 튜토리얼을 재현할 수 있습니다. 실제 심사 모델이 어떻게 동작하는지는 전혀 측정하지 않습니다.

정책

release.yaml:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - {id: completion-floor, metric: conversation_completed, min: 0.75}
  - {id: coherence-floor, metric: plan_coherent, min: 0.80}

실행하기

python evaluate.py
baseline run run_...
  conversation_completed: 0.900 [0.763, 0.972] over 40 conversations
  plan_coherent: 0.400 [0.249, 0.567] over 40 conversations
  gate BLOCK (exit 3)
  completion-floor: PASS (lower_bound_meets_minimum)
  coherence-floor: INSUFFICIENT_EVIDENCE (evaluator_not_validated)
  first failing conversation:
    conversation_completed: passed=True {'turns_recorded': 2, 'turns_declared': 2, 'truncated': False}
    plan_coherent: passed=False {'provider_model': 'offline-rule'}

읽는 방법은 다음과 같습니다.

  • 완료는 40개 중 36개이며, 네 개는 사람에게 넘긴 대화입니다. 하한이 0.75를 넘으므로 그 규칙은 PASS입니다.
  • 일관성은 40%이며, 그 규칙은 FAIL이 아니라 evaluator_not_validated와 함께 INSUFFICIENT_EVIDENCE를 읽습니다. 심사 모델의 규칙은 심사 모델이 사람 레이블에 대해 측정될 때까지 결정하지 않습니다(require_validated_evaluators는 기본적으로 켜져 있으며, 심사 모델에서 설명합니다). 추정치는 여전히 표시되고 여전히 증거입니다. 다만 그것만으로 릴리스하거나 차단할 수 없을 뿐입니다.
  • gate BLOCK (exit 3): 정책이 INSUFFICIENT_EVIDENCE에서 차단합니다. 종료 코드 3은 그 상태이지 실패가 아닙니다.

오프라인 대역은 이 예제를 위해 쓴 규칙이므로, 그것을 검증하는 것은 의미가 없습니다. 실제 심사 모델이라면 oloproof review RUN_ID --criterion plan_coherent --by YOU --sample 20으로 실행의 표본에 레이블을 붙인 다음 oloproof evaluators validate EVALUATOR_ID --by YOU를 실행하세요.

실패한 대화 살펴보기

SDK는 CLI가 읽는 것과 같은 저장소, 즉 실행한 디렉터리의 .oloproof/에 씁니다.

oloproof inspect RUN_ID --case conv_00
output: {
  "answer": "The starter plan does not include SSO.",
  "turns_answered": 2
}
judgments:
  conversation_completed: passed
  plan_coherent: failed
    judge text, not verified:
      every answer is about enterprise: False

사용자는 엔터프라이즈 요금제에 대해 물었는데 후속 질문에는 스타터 요금제를 기준으로 답했습니다. oloproof inspect RUN_ID --failures는 실패한 모든 대화를 나열하며, 요금제 이름이 없는 후속 질문 24개 모두 같은 방식으로 실패합니다. 다음 조치는 애플리케이션에 있습니다. 요금제를 세션 상태에 유지하세요.

후보 변경과 비교

systems.py의 candidate는 remembers_plan=True를 설정합니다. evaluate.py는 두 시스템을 같은 스크립트로 실행하고 comparison.yaml에 따라 케이스별로 비교합니다.

rules:
  - {id: coherence-better, kind: superiority, metric: plan_coherent}
  - {id: completion-no-worse, kind: non_inferiority, metric: conversation_completed, margin: 0.05}
comparison, candidate minus baseline
  conversation_completed: +0.000 [-0.127, +0.127] over 40 pairs
  plan_coherent: +0.600 [+0.337, +0.817] over 40 pairs
  gate BLOCK (exit 3)
  coherence-better: INSUFFICIENT_EVIDENCE (evaluator_not_validated)
  completion-no-worse: INSUFFICIENT_EVIDENCE (interval_overlaps_margin)

일관성 차이는 크고 그 구간은 0을 배제하지만, 심사 모델이 검증되지 않았으므로 그 규칙은 여전히 결정하지 않습니다. 완료는 변하지 않았으며, 40쌍으로는 그것이 5포인트 이내임을 보일 수 없습니다. 구간이 양쪽으로 12.7포인트까지 이릅니다. 둘 다 같은 다음 단계를 가리킵니다. 심사 모델을 검증하고 대화를 추가하세요.

대안: 턴을 케이스로, 클러스터로 분석하기

대화 단위의 판정은 대화가 얼마나 자주 잘 진행되었는지를 말해 줄 뿐, 어느 턴이 잘못되었는지는 말해 주지 않습니다. Oloproof에는 기록된 대화 안의 턴 단위 지표가 없으므로, 다른 방법은 각 턴을 독립된 케이스로 만들고 group_id로 한 대화의 턴들을 묶는 것입니다.

{"expected": {"plan": "enterprise"}, "group_id": "conv_00", "id": "conv_00_t2", "input": {"history": ["What does the enterprise plan cost?"], "question": "Does that include SSO?"}}

시스템은 스크립트의 이전 기록을 새 세션에 재생한 다음, 그 턴에 답합니다.

@system(name="plan-assistant-turns", version="baseline")
def turn_baseline(case):
    session = PlanAssistant(remembers_plan=False)
    for earlier in case["history"]:
        session.ask(str(earlier))
    return session.ask(str(case["question"]))

한 대화의 턴들은 독립적이지 않으므로, 어떤 케이스든 group_id를 가지면 스위트는 정책이 받아들여야 하는 근사 방법으로 클러스터 단위로 분석됩니다(turns_release.yaml은 allow_approximate_methods: true를 설정하며, 클러스터 케이스에서 설명합니다).

turns as cases: turn_plan 0.667 [0.588, 0.749] over 72 turns
  gate BLOCK (exit 1)
  turn-plan-floor: FAIL (upper_bound_below_minimum)

장단점은 다음과 같습니다.

대화당 케이스 하나턴당 케이스 하나, 클러스터
비율의 단위잘 진행된 대화올바르게 답한 턴
유효 표본 크기대화의 수턴이 아니라 여전히 대화의 수
어느 턴이 실패했는가대화 기록을 읽음각 턴에 자신의 판정이 있음
각 턴이 보는 이전 기록어시스턴트 자신의 이전 답스크립트의 이전 사용자 턴을 재생한 것
자신의 이전 답이 일으킨 표류를 잡아내는가예아니요, 각 턴은 스크립트의 이전 기록에서 시작함
평가기ConversationCompleted, ConversationJudge(SDK 전용)YAML이나 SDK의 어떤 평가기든

턴 대안은 사람에게 넘긴 네 대화를 제외하므로, 72개의 턴은 36개 대화에서 나옵니다. 여기서는 심사 모델이 결정할 수 없던 곳에서 결정할 수 있는데, ExactMatch는 결정론적이라 검증이 필요 없기 때문입니다.

선택 사항: 라이브 심사 모델

이 단계에는 여러분의 컴퓨터에서 서빙되는 모델이 필요합니다. 오프라인 튜토리얼이나 그 테스트는 이 단계를 실행하지 않습니다. Ollama가 실행 중이고 llama3.1을 받아 두었다면 다음과 같습니다.

python evaluate.py --live

그러면 심사 모델은 provider="openai_compatible"로 http://localhost:11434/v1을 호출합니다. 루프백 서버는 키가 필요 없고 컴퓨터 밖으로 아무것도 보내지 않습니다. 클라우드 제공자는 환경에 키가 필요하고, 각 대화 기록을 그 제공자에게 보내며, 판정마다 비용이 듭니다. 실제 모델의 판정은 대역의 판정과 다르므로 위의 숫자는 바뀌며, 검증하기 전까지 그 규칙은 여전히 evaluator_not_validated를 읽습니다.

문제 해결

증상원인과 해결
this evaluator needs exactly one conversation/v1 artifact; the case recorded 0어댑터가 current_case().artifact(CONVERSATION, ...)를 호출하지 않았거나, 그 전에 예외를 일으켰습니다. 대화가 일찍 멈추더라도 기록하세요.
malformed conversation/v1 artifact: ... 0 of 2 turns recorded and truncated is false실행이 SystemContractError로 멈춥니다. declared_turns보다 턴이 적은 기록은 truncated: true를 설정해야 합니다.
conversation turn indexes must be contiguous starting at 1턴 번호를 그것이 답하는 스크립트 턴에 따라 1, 2, 3으로 매기세요.
evaluator 'conversation_completed' needs conversation/v1 artifacts, but system ... does not declare that it records them@system 데코레이터에 records=(CONVERSATION,)를 추가하세요.
oloproof run에서 Input tag 'conversation_completed' found using 'type' does not match any of the expected tags대화 평가기는 SDK 전용입니다. 여기서처럼 스크립트를 쓰세요.
대화 사이에 답이 샘케이스 사이에 세션이 공유되고 있습니다. 케이스마다 하나씩 만드세요.
일관성 규칙이 결정하지 않음심사 모델이 검증되지 않았습니다. 검증하거나, 알고서 require_validated_evaluators: false를 설정하세요.

제한 사항

  • 사용자 시뮬레이터 없음: 모든 사용자 턴은 데이터셋 스크립트에서 오므로, 대화가 어시스턴트의 말에 따라 갈라질 수 없습니다.
  • 기록된 대화 안의 턴 단위 지표 없음: 대신 위의 장단점을 감수하고 턴을 클러스터 케이스로 쓰세요.
  • 대화 재생 없음: 기록된 대화를 다른 시스템에 대해 다시 실행할 수 없습니다. 두 시스템을 비교하려면 각 시스템이 같은 스크립트를 재생해야 합니다.
  • ConversationCompleted와 ConversationJudge는 Python SDK 전용입니다.
  • 텍스트만: 심사 모델에게는 JSON 텍스트만 보이며, 이미지나 오디오는 보이지 않습니다.
  • 오프라인 심사 모델은 스크립트로 된 규칙입니다. 그 판정은 작동 방식을 보여 줄 뿐, 실제 심사 모델의 정확도를 보여 주지 않습니다.

다음 단계

  • 심사 모델은 제공자, 검증, 재보정을 다룹니다.
  • 클러스터 케이스는 group_id와 근사 방법 사용 동의를 다룹니다.
  • Python API는 evaluate와 evaluate_comparison을 다룹니다.
  • 에이전트는 에이전트의 도구 루프에 대한 같은 경계를 다룹니다.