Przejdź do treści

Przewodniki

Samouczek: rozmowa wieloturowa

Praktyczny przewodnik do uruchomienia dla ewaluacji asystenta konwersacyjnego za pomocą Python SDK: Twoja aplikacja odtwarza oskryptowaną rozmowę w świeżej sesji, zapisuje swoje odpowiedzi jako artefakt conversation/v1, a dwa ewaluatory oceniają każdą rozmowę w całości. Działa offline z oskryptowanym zastępstwem modelu sędziego, potem porównuje poprawkę kandydującą, a kończy alternatywą: ewaluacją tur jako przypadków klastrowanych.

Terminy takie jak przedział, stan decyzji i działanie dotyczące wydania są zdefiniowane w Pojęciach; Zapisywanie tego, co zrobił system omawia artefakty.

Co Oloproof tutaj robi, a czego nie

OloproofTwoja aplikacja
przechowuje oskryptowane rozmowy jako zbiór danych, identyczny dla każdego systemuprowadzi rozmowę: zadaje każdą oskryptowaną turę po kolei
sprawdza, czy na każdą oskryptowaną turę udzielono odpowiedzi (ConversationCompleted)zarządza stanem sesji, zaczyna świeżą sesję dla każdego przypadku i ją resetuje
ocenia cały zapis rozmowy modelem (ConversationJudge)decyduje, co się dzieje, gdy nie może kontynuować, i zapisuje, że się zatrzymała
liczy przedziały, porównuje dwa systemy i rozstrzyga względem politykizapisuje artefakt conversation/v1

Oloproof nie ma symulatora użytkownika: nigdy nie pisze tury użytkownika, więc strona użytkownika to to, co skryptuje zbiór danych. Nie ma metryki na poziomie tury w obrębie zapisanej rozmowy i nie potrafi odtworzyć zapisanej rozmowy względem nowego systemu. Ewaluatory rozmów istnieją tylko w Python SDK: ConversationCompleted i ConversationJudge nie są typami ewaluatorów w oloproof.yaml, więc ten samouczek używa skryptu zamiast oloproof run.

Wymagania wstępne

  • Python 3.11 lub nowszy oraz pip install oloproof, jak w szybkim starcie.
  • Przykładowe pliki, dostarczane z pakietem. Skopiuj je do nowego katalogu, aby magazyn przebiegu trafił tam:
oloproof init --example conversation ~/oloproof-conversation
cd ~/oloproof-conversation
PlikCzym jest
assistant.pytestowana aplikacja: asystent planów ze stanem sesji
systems.pyadapter: odtwarza skrypt, zapisuje conversation/v1
judge_offline.pyoskryptowane zastępstwo modelu sędziego
evaluate.pyuruchamia ewaluację, porównanie i alternatywę z turami
release.yamlpolityka dla jednego przebiegu
comparison.yamlpolityka dla kandydata względem punktu odniesienia
turns_release.yamlpolityka dla alternatywy z turami
data/conversations.jsonl40 oskryptowanych rozmów
data/turns.jsonlte same rozmowy, jeden przypadek na turę

Bez klucza, bez sieci i bez kosztów dostawcy, aż do opcjonalnego kroku na żywo na końcu.

Aplikacja

assistant.py odpowiada na pytania o trzy plany cenowe. Przechowuje jeden element stanu, plan, którego dotyczy rozmowa, aby pytanie uzupełniające takie jak "Does that include SSO?" mogło rozwiązać "that". Punkt odniesienia ma celową wadę: nie pamięta planu, więc na pytanie uzupełniające odpowiada o planie domyślnym. Użytkownik proszący o człowieka kończy rozmowę z 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): ...

To jest część, którą zastępujesz własną aplikacją: klientem chatbota, sesją agenta, sesją HTTP do Twojej usługi. Czymkolwiek jest, zarządza własnym stanem i jego resetem; Oloproof widzi tylko to, co zapisze adapter.

Zbiór danych: skrypt jest wejściem

Jeden wiersz data/conversations.jsonl to jedna rozmowa:

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

Tury użytkownika są treścią zbioru danych, objętą skrótem zestawu i identyczną dla każdego systemu mierzonego względem nich; to właśnie czyni dwa systemy porównywalnymi. expected to wzorzec pokazywany sędziemu. Spośród 40 rozmów 24 mają pytanie uzupełniające, które nie nazywa planu, 12 nazywa plan w każdej turze, a 4 proszą o człowieka w drugiej z trzech tur.

Adapter

systems.py zaczyna świeżą sesję dla każdego przypadku, zadaje każdą oskryptowaną turę po kolei i zapisuje, co wróciło:

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)

Świeża sesja dla każdego przypadku ma znaczenie: Oloproof uruchamia przypadki równolegle i bez ustalonej kolejności, a sesja współdzielona między przypadkami pozwoliłaby stanowi jednej rozmowy przeciec do innej. records= deklaruje, że system zapisuje artefakt; bez tego ewaluatory rozmów są odrzucane, zanim cokolwiek się uruchomi, zamiast liczyć każdy przypadek jako brakujący.

Artefakt conversation/v1

Co punkt odniesienia zapisał dla conv_00, z oloproof export RUN_ID (plik cases.jsonl pakietu):

{"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, ...}]}

A dla rozmowy, która poprosiła o człowieka:

{"declared_turns": 3, "truncated": true, "turns": [{"answer": "The team plan costs $20 a month.", "asked": "What does the team plan cost?", "index": 1, ...}]}
PoleZnaczenie
turns[].indexna którą oskryptowaną turę to odpowiada; ciągłe od 1
turns[].answerco zwrócił asystent, dowolny JSON
turns[].askedopcjonalne, tylko do czytania; silnik dopasowuje po index
turns[].retrievalopcjonalne, co ta tura wyszukała, w kształcie retrieval/v1
declared_turnsile tur zadeklarował skrypt
truncatedzapis urywa się przed końcem skryptu, bez względu na to, co go przerwało

Artefakt jest sprawdzany w chwili zapisu: zapis z mniejszą liczbą tur niż zadeklarowana musi mówić truncated: true, indeksy muszą być ciągłe, a zapis nie może odpowiadać na więcej tur, niż zadano. Błędnie zbudowany artefakt zatrzymuje przebieg z SystemContractError.

Dwa ewaluatory i dlaczego oba

  • ConversationCompleted jest deterministyczny: czy asystent odpowiedział na każdą oskryptowaną turę? Działa pierwszy, ponieważ każde inne twierdzenie o rozmowie, która zatrzymała się na pierwszej z trzech tur, jest twierdzeniem o innej rozmowie. Ucięta rozmowa go oblewa; to wynik, a nie brakujący przypadek.
  • ConversationJudge to sędzia-model oceniający cały zapis rozmowy, każdą turę użytkownika i asystenta, ponieważ niepowodzenia, za które obwinia się produkt konwersacyjny, są relacyjne: odpowiedź sprzeczna z odpowiedzią z poprzedniej tury jest błędna tylko obok niej. Ucięta rozmowa jest oceniana na podstawie tego, co zapisano, a zapis mówi sędziemu, gdzie się zatrzymała.
evaluators = [
    ConversationCompleted(),
    ConversationJudge(criterion="plan_coherent", provider=..., model=..., rubric_text=RUBRIC),
]

Ocenianie offline

Sędzia potrzebuje modelu. Aby działać bez sieci, judge_offline.py przekazuje oskryptowanego dostawcę, tego samego pomocnika, którego używają własne testy Oloproof (FakeProvider z wnętrza silnika, nie publiczne API). Odpowiada na każdy prompt sędziego według jednej stałej reguły: pass, gdy każda tura asystenta nazywa plan, który nazywa wzorzec. To czyni oceny deterministycznymi, a samouczek odtwarzalnym. Nie mierzy niczego z tego, jak zachowuje się prawdziwy model sędziego.

Polityka

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}

Uruchom

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'}

Jak go czytać:

  • Ukończenie to 36 z 40, czyli cztery przekazania. Jego dolna granica przekracza 0.75, więc ta reguła ma wynik PASS.
  • Spójność wynosi 40%, a jej reguła pokazuje INSUFFICIENT_EVIDENCE z evaluator_not_validated, a nie FAIL. Reguły sędziego nie rozstrzygają, dopóki sędzia nie zostanie zmierzony względem ludzkich etykiet (require_validated_evaluators jest domyślnie włączone; wyjaśniają to Sędziowie). Estymata nadal jest pokazywana i nadal jest dowodem: po prostu nie może sama przepuścić ani zablokować wydania.
  • gate BLOCK (exit 3): polityka blokuje na INSUFFICIENT_EVIDENCE. Kod 3 to ten stan, a nie niepowodzenie.

Walidowanie zastępstwa offline nie miałoby sensu, bo to reguła napisana dla tego przykładu. Z prawdziwym sędzią oetykietuj próbę przebiegu przez oloproof review RUN_ID --criterion plan_coherent --by YOU --sample 20, a potem uruchom oloproof evaluators validate EVALUATOR_ID --by YOU.

Przejrzyj nieudaną rozmowę

SDK zapisuje do tego samego magazynu, który czyta CLI, .oloproof/ w katalogu, z którego uruchamiałeś:

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

Użytkownik zapytał o plan enterprise, a na pytanie uzupełniające odpowiedziano o planie starter. oloproof inspect RUN_ID --failures wymienia każdą nieudaną rozmowę; wszystkie 24 pytania uzupełniające bez nazwy planu zawodzą w ten sam sposób. Następne działanie leży w aplikacji: przechowuj plan w stanie sesji.

Zmiana kandydująca i porównanie

candidate w systems.py ustawia remembers_plan=True. evaluate.py uruchamia oba systemy na tych samych skryptach i porównuje je przypadek po przypadku zgodnie z 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)

Różnica spójności jest duża, a jej przedział wyklucza zero, ale sędzia jest niezwalidowany, więc jego reguła nadal nie rozstrzyga. Ukończenie się nie zmieniło, a 40 par nie może wykazać, że mieści się w pięciu punktach: przedział sięga 12,7 punktu w każdą stronę. Oba wskazują ten sam następny krok: zwaliduj sędziego i dodaj rozmowy.

Alternatywa: tury jako przypadki, analizowane jako klastry

Werdykt na poziomie rozmowy mówi, jak często rozmowa poszła dobrze, a nie która tura poszła źle. Ponieważ Oloproof nie ma metryki na poziomie tury w obrębie zapisanej rozmowy, drugą drogą jest uczynienie każdej tury osobnym przypadkiem i powiązanie tur jednej rozmowy przez 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 odtwarza oskryptowaną historię w świeżej sesji, a potem odpowiada na turę:

@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"]))

Tury jednej rozmowy nie są niezależne, więc gdy tylko którykolwiek przypadek ma group_id, zestaw jest analizowany według klastrów metodą przybliżoną, którą polityka musi zaakceptować (turns_release.yaml ustawia allow_approximate_methods: true; wyjaśniają to Przypadki klastrowane):

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)

Kompromis:

Jeden przypadek na rozmowęJeden przypadek na turę, klastrowany
Jednostka odsetkarozmowy, które poszły dobrzetury z poprawną odpowiedzią
Efektywna liczebność próbyliczba rozmównadal liczba rozmów, a nie tur
Która tura zawiodłaprzeczytaj zapis rozmowykażda tura ma własny werdykt
Historia, którą widzi każda turawłasne wcześniejsze odpowiedzi asystentawcześniejsze tury użytkownika ze skryptu, odtworzone
Wykrywa dryf spowodowany własnymi wcześniejszymi odpowiedziamitaknie, każda tura zaczyna od oskryptowanej historii
EwaluatoryConversationCompleted, ConversationJudge (tylko SDK)dowolny ewaluator, w YAML lub SDK

Alternatywa z turami wyklucza cztery rozmowy z przekazaniem, więc jej 72 tury pochodzą z 36 rozmów. Tutaj może rozstrzygnąć tam, gdzie sędzia nie mógł, ponieważ ExactMatch jest deterministyczny i nie wymaga walidacji.

Opcjonalnie: model sędziego na żywo

Ten krok wymaga modelu serwowanego na Twojej maszynie. Nie uruchamia go samouczek offline ani jego test. Przy działającej Ollama i pobranym llama3.1:

python evaluate.py --live

Sędzia wywołuje wtedy http://localhost:11434/v1 z provider="openai_compatible". Serwer na pętli zwrotnej nie potrzebuje klucza i niczego nie wysyła poza maszynę. Dostawca chmurowy wymaga swojego klucza w środowisku, wysyła mu każdy zapis rozmowy i kosztuje pieniądze za każdą ocenę. Werdykty prawdziwego modelu różnią się od werdyktów zastępstwa, więc liczby powyżej się zmienią, a jego reguły nadal pokazują evaluator_not_validated, dopóki go nie zwalidujesz.

Rozwiązywanie problemów

ObjawPrzyczyna i rozwiązanie
this evaluator needs exactly one conversation/v1 artifact; the case recorded 0Adapter nie wywołał current_case().artifact(CONVERSATION, ...) albo zgłosił wyjątek wcześniej. Zapisuj nawet wtedy, gdy rozmowa kończy się wcześnie.
malformed conversation/v1 artifact: ... 0 of 2 turns recorded and truncated is falsePrzebieg zatrzymuje się z SystemContractError. Zapis z mniejszą liczbą tur niż declared_turns musi ustawić truncated: true.
conversation turn indexes must be contiguous starting at 1Numeruj tury 1, 2, 3 według oskryptowanej tury, na którą odpowiadają.
evaluator 'conversation_completed' needs conversation/v1 artifacts, but system ... does not declare that it records themDodaj records=(CONVERSATION,) do dekoratora @system.
Input tag 'conversation_completed' found using 'type' does not match any of the expected tags z oloproof runEwaluatory rozmów są tylko w SDK. Użyj skryptu, jak tutaj.
Odpowiedzi przeciekają między rozmowamiSesja jest współdzielona między przypadkami. Twórz jedną na przypadek.
Reguły spójności nigdy nie rozstrzygająSędzia jest niezwalidowany. Zwaliduj go albo świadomie ustaw require_validated_evaluators: false.

Ograniczenia

  • Brak symulatora użytkownika: każda tura użytkownika pochodzi ze skryptu zbioru danych, więc rozmowa nie może się rozgałęziać zależnie od tego, co powiedział asystent.
  • Brak metryki na poziomie tury w obrębie zapisanej rozmowy; zamiast tego użyj tur jako przypadków klastrowanych, z kompromisem opisanym wyżej.
  • Brak odtwarzania rozmów: zapisanej rozmowy nie da się uruchomić ponownie względem innego systemu. Porównanie dwóch systemów oznacza, że każdy odtwarza ten sam skrypt.
  • ConversationCompleted i ConversationJudge są dostępne tylko w Python SDK.
  • Tylko tekst: sędziemu pokazuje się tekst JSON, nigdy obrazy ani dźwięk.
  • Sędzia offline to oskryptowana reguła. Jego werdykty pokazują mechanikę, a nie dokładność prawdziwego sędziego.

Dokąd dalej

  • Sędziowie omawiają dostawców, walidację i rekalibrację.
  • Przypadki klastrowane omawiają group_id i zgodę na metody przybliżone.
  • Python API omawia evaluate i evaluate_comparison.
  • Agenci omawiają tę samą granicę dla pętli narzędzi agenta.