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
| Oloproof | Twoja aplikacja |
|---|---|
| przechowuje oskryptowane rozmowy jako zbiór danych, identyczny dla każdego systemu | prowadzi 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 polityki | zapisuje 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| Plik | Czym jest |
|---|---|
| assistant.py | testowana aplikacja: asystent planów ze stanem sesji |
| systems.py | adapter: odtwarza skrypt, zapisuje conversation/v1 |
| judge_offline.py | oskryptowane zastępstwo modelu sędziego |
| evaluate.py | uruchamia ewaluację, porównanie i alternatywę z turami |
| release.yaml | polityka dla jednego przebiegu |
| comparison.yaml | polityka dla kandydata względem punktu odniesienia |
| turns_release.yaml | polityka dla alternatywy z turami |
| data/conversations.jsonl | 40 oskryptowanych rozmów |
| data/turns.jsonl | te 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, ...}]}| Pole | Znaczenie |
|---|---|
| turns[].index | na którą oskryptowaną turę to odpowiada; ciągłe od 1 |
| turns[].answer | co zwrócił asystent, dowolny JSON |
| turns[].asked | opcjonalne, tylko do czytania; silnik dopasowuje po index |
| turns[].retrieval | opcjonalne, co ta tura wyszukała, w kształcie retrieval/v1 |
| declared_turns | ile tur zadeklarował skrypt |
| truncated | zapis 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.pybaseline 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_00output: {
"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: FalseUż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 odsetka | rozmowy, które poszły dobrze | tury z poprawną odpowiedzią |
| Efektywna liczebność próby | liczba rozmów | nadal liczba rozmów, a nie tur |
| Która tura zawiodła | przeczytaj zapis rozmowy | każda tura ma własny werdykt |
| Historia, którą widzi każda tura | własne wcześniejsze odpowiedzi asystenta | wcześniejsze tury użytkownika ze skryptu, odtworzone |
| Wykrywa dryf spowodowany własnymi wcześniejszymi odpowiedziami | tak | nie, każda tura zaczyna od oskryptowanej historii |
| Ewaluatory | ConversationCompleted, 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 --liveSę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
| Objaw | Przyczyna i rozwiązanie |
|---|---|
| this evaluator needs exactly one conversation/v1 artifact; the case recorded 0 | Adapter 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 false | Przebieg zatrzymuje się z SystemContractError. Zapis z mniejszą liczbą tur niż declared_turns musi ustawić truncated: true. |
| conversation turn indexes must be contiguous starting at 1 | Numeruj 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 them | Dodaj records=(CONVERSATION,) do dekoratora @system. |
| Input tag 'conversation_completed' found using 'type' does not match any of the expected tags z oloproof run | Ewaluatory rozmów są tylko w SDK. Użyj skryptu, jak tutaj. |
| Odpowiedzi przeciekają między rozmowami | Sesja 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.