Handleidingen
Tutorial: een gesprek met meerdere beurten
Een uitvoerbare doorloop voor het evalueren van een conversationele assistent met de Python-SDK: je applicatie speelt een gescript gesprek af tegen een verse sessie, legt vast wat ze antwoordde als een conversation/v1-artefact, en twee evaluators beoordelen elk volledig gesprek. Hij draait offline met een gescripte vervanger voor het judgemodel, vergelijkt daarna een kandidaatfix, en eindigt met het alternatief om beurten als geclusterde cases te evalueren.
Termen zoals interval, beslissingstoestand en releaseactie worden gedefinieerd in Kernbegrippen; Vastleggen wat een systeem deed behandelt artefacten.
Wat Oloproof hier wel en niet doet
| Oloproof doet | Je applicatie doet |
|---|---|
| de gescripte gesprekken als dataset opslaan, identiek voor elk systeem | het gesprek aansturen: elke gescripte beurt op volgorde stellen |
| controleren dat elke gescripte beurt beantwoord is (ConversationCompleted) | de sessietoestand beheren, per case een verse sessie starten en die terugzetten |
| het hele transcript met een model beoordelen (ConversationJudge) | beslissen wat er gebeurt als ze niet verder kan, en vastleggen dat ze stopte |
| intervallen berekenen, twee systemen vergelijken en tegen een beleid beslissen | het conversation/v1-artefact vastleggen |
Oloproof heeft geen gebruikerssimulator: het schrijft nooit een gebruikersbeurt, dus de kant van de gebruiker is wat de dataset scripte. Het heeft geen metriek per beurt binnen een vastgelegd gesprek, en het kan een vastgelegd gesprek niet opnieuw afspelen tegen een nieuw systeem. De gespreksevaluators bestaan alleen in de Python-SDK: ConversationCompleted en ConversationJudge zijn geen evaluatortypen in oloproof.yaml, dus deze tutorial gebruikt een script in plaats van oloproof run.
Vereisten
- Python 3.11 of later en pip install oloproof, zoals in de quickstart.
- De voorbeeldbestanden, die met het pakket worden meegeleverd. Kopieer ze naar een nieuwe map zodat de store van de run daar terechtkomt:
oloproof init --example conversation ~/oloproof-conversation
cd ~/oloproof-conversation| Bestand | Wat het is |
|---|---|
| assistant.py | de geteste applicatie: een abonnementsassistent met sessietoestand |
| systems.py | de adapter: speelt een script af, legt conversation/v1 vast |
| judge_offline.py | de gescripte vervanger voor het judgemodel |
| evaluate.py | draait de evaluatie, de vergelijking en het beurtenalternatief |
| release.yaml | het beleid voor één run |
| comparison.yaml | het beleid voor de kandidaat tegen de baseline |
| turns_release.yaml | het beleid voor het beurtenalternatief |
| data/conversations.jsonl | 40 gescripte gesprekken |
| data/turns.jsonl | dezelfde gesprekken, één case per beurt |
Geen sleutel, geen netwerk en geen providerkosten, tot de optionele live stap aan het eind.
De applicatie
assistant.py beantwoordt vragen over drie prijsabonnementen. Ze houdt één stuk toestand bij, het abonnement waar het gesprek over gaat, zodat een vervolgvraag zoals "Does that include SSO?" kan oplossen waar "that" naar verwijst. De baseline heeft een opzettelijke fout: hij onthoudt het abonnement niet, dus een vervolgvraag wordt beantwoord over het standaardabonnement. Een gebruiker die om een mens vraagt beëindigt het gesprek met 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): ...Dit is het deel dat je vervangt door je eigen applicatie: een chatbotclient, een agentsessie, een HTTP-sessie naar je dienst. Wat het ook is, het beheert zijn eigen toestand en het terugzetten ervan; Oloproof ziet alleen wat de adapter vastlegt.
De dataset: het script is de input
Eén regel van data/conversations.jsonl is één gesprek:
{"expected": {"plan": "enterprise"}, "id": "conv_00", "input": {"turns": ["What does the enterprise plan cost?", "Does that include SSO?"]}, "metadata": {"pattern": "pronoun_followup"}}De beurten van de gebruiker zijn datasetinhoud, gedekt door de digest van de suite en identiek voor elk systeem dat ertegen wordt gemeten; dat maakt twee systemen vergelijkbaar. expected is de referentie die de judge te zien krijgt. Van de 40 gesprekken hebben er 24 een vervolgvraag die geen abonnement noemt, noemen er 12 het abonnement bij elke beurt, en vragen er 4 bij beurt twee van drie om een mens.
De adapter
systems.py start per case een verse sessie, stelt elke gescripte beurt op volgorde, en legt vast wat er terugkwam:
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)Een verse sessie per case doet ertoe: Oloproof draait cases gelijktijdig en in geen vaste volgorde, en een sessie die tussen cases gedeeld wordt zou de toestand van het ene gesprek in het andere laten lekken. records= declareert dat het systeem het artefact vastlegt; zonder dat worden de gespreksevaluators geweigerd voordat er iets draait, in plaats van elke case als ontbrekend te tellen.
Het conversation/v1-artefact
Wat de baseline voor conv_00 vastlegde, uit oloproof export RUN_ID (de cases.jsonl van de bundel):
{"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, ...}]}En voor een gesprek dat om een mens vroeg:
{"declared_turns": 3, "truncated": true, "turns": [{"answer": "The team plan costs $20 a month.", "asked": "What does the team plan cost?", "index": 1, ...}]}| Veld | Betekenis |
|---|---|
| turns[].index | welke gescripte beurt dit beantwoordt; aaneengesloten vanaf 1 |
| turns[].answer | wat de assistent teruggaf, elke JSON |
| turns[].asked | optioneel, alleen om te lezen; de engine matcht op index |
| turns[].retrieval | optioneel, wat die beurt ophaalde, in de vorm van retrieval/v1 |
| declared_turns | hoeveel beurten het script declareerde |
| truncated | de opname stopt voor het einde van het script, wat haar ook afkapte |
Het artefact wordt gecontroleerd als het wordt vastgelegd: een opname met minder beurten dan gedeclareerd moet truncated: true zeggen, indexen moeten aaneengesloten zijn, en een opname kan niet meer beurten beantwoorden dan er gesteld werden. Een misvormd artefact stopt de run met een SystemContractError.
De twee evaluators, en waarom allebei
- ConversationCompleted is deterministisch: beantwoordde de assistent elke gescripte beurt? Hij draait eerst omdat elke andere bewering over een gesprek dat bij beurt één van drie stopte een bewering over een ander gesprek is. Een afgekapt gesprek faalt erop; dat is een resultaat, geen ontbrekende case.
- ConversationJudge is een modeljudge over het hele transcript, elke beurt van gebruiker en assistent, omdat de mislukkingen die een conversationeel product verweten worden relationeel zijn: een antwoord dat een antwoord uit de vorige beurt tegenspreekt is pas fout naast dat antwoord. Een afgekapt gesprek wordt beoordeeld op wat er werd vastgelegd, en het transcript vertelt de judge waar het stopte.
evaluators = [
ConversationCompleted(),
ConversationJudge(criterion="plan_coherent", provider=..., model=..., rubric_text=RUBRIC),
]Offline beoordelen
Een judge heeft een model nodig. Om zonder netwerk te draaien geeft judge_offline.py een gescripte provider mee, dezelfde helper die Oloproofs eigen tests gebruiken (FakeProvider uit de interne engine, geen publieke API). Die beantwoordt elke judgeprompt volgens één vaste regel: geslaagd als elke beurt van de assistent het abonnement noemt dat de referentie noemt. Dat maakt de oordelen deterministisch en de tutorial reproduceerbaar. Hij meet niets over hoe een echt judgemodel zich gedraagt.
Het beleid
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}Draai het
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'}Zo lees je het:
- Voltooiing is 36 van 40, de vier overdrachten. De ondergrens ligt boven 0,75, dus die regel is PASS.
- Coherentie is 40%, en de regel leest INSUFFICIENT_EVIDENCE met evaluator_not_validated, niet FAIL. De regels van een judge beslissen pas als de judge is gemeten tegen menselijke labels (require_validated_evaluators staat standaard aan; Judges legt het uit). De schatting wordt nog steeds getoond, en is nog steeds bewijs: ze kan alleen niet op zichzelf vrijgeven of blokkeren.
- gate BLOCK (exit 3): het beleid blokkeert op INSUFFICIENT_EVIDENCE. Exit 3 is die toestand, geen mislukking.
De offline vervanger valideren zou zinloos zijn, omdat het een regel is die voor dit voorbeeld is geschreven. Label met een echte judge een steekproef van de run met oloproof review RUN_ID --criterion plan_coherent --by YOU --sample 20 en draai daarna oloproof evaluators validate EVALUATOR_ID --by YOU.
Een mislukt gesprek bekijken
De SDK schrijft naar dezelfde store die de CLI leest, .oloproof/ in de map van waaruit je draaide:
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: FalseDe gebruiker vroeg naar het enterprise-abonnement en de vervolgvraag werd beantwoord over het starter-abonnement. oloproof inspect RUN_ID --failures toont elk mislukt gesprek; alle 24 vervolgvragen zonder abonnementsnaam falen op dezelfde manier. De volgende stap ligt in de applicatie: houd het abonnement in de sessietoestand.
Een kandidaatwijziging, en de vergelijking
candidate in systems.py zet remembers_plan=True. evaluate.py draait beide systemen over dezelfde scripts en vergelijkt ze case voor case onder 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)Het coherentieverschil is groot en het interval sluit nul uit, maar de judge is niet gevalideerd, dus de regel beslist nog steeds niet. Voltooiing is onveranderd, en 40 paren kunnen niet laten zien dat het binnen vijf punten blijft: het interval reikt 12,7 punten naar beide kanten. Beide wijzen naar dezelfde volgende stap: valideer de judge en voeg gesprekken toe.
Het alternatief: beurten als cases, geanalyseerd als clusters
Een oordeel op gespreksniveau zegt hoe vaak een gesprek goed ging, niet welke beurt misging. Omdat Oloproof geen metriek per beurt binnen een vastgelegd gesprek heeft, is de andere route om van elke beurt een eigen case te maken en de beurten van een gesprek te verbinden met 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?"}}Het systeem speelt de gescripte geschiedenis af in een verse sessie, en beantwoordt dan de beurt:
@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"]))Beurten van één gesprek zijn niet onafhankelijk, dus zodra een case een group_id heeft wordt de suite per cluster geanalyseerd met een benaderende methode die een beleid moet accepteren (turns_release.yaml zet allow_approximate_methods: true; Geclusterde cases legt het uit):
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)De afweging:
| Eén case per gesprek | Eén case per beurt, geclusterd | |
|---|---|---|
| Eenheid van het percentage | gesprekken die goed gingen | correct beantwoorde beurten |
| Effectieve steekproefgrootte | het aantal gesprekken | nog steeds het aantal gesprekken, niet beurten |
| Welke beurt faalde | lees het transcript | elke beurt heeft een eigen oordeel |
| Geschiedenis die elke beurt ziet | de eigen eerdere antwoorden van de assistent | de eerdere gebruikersbeurten van het script, afgespeeld |
| Vangt afdrijven door de eigen eerdere antwoorden | ja | nee, elke beurt begint vanuit een gescripte geschiedenis |
| Evaluators | ConversationCompleted, ConversationJudge (alleen SDK) | elke evaluator, in YAML of de SDK |
Het beurtenalternatief sluit de vier overdrachtsgesprekken uit, dus de 72 beurten komen uit 36 gesprekken. Hier kan het beslissen waar de judge dat niet kon, omdat ExactMatch deterministisch is en geen validatie nodig heeft.
Optioneel: een live judgemodel
Deze stap heeft een model nodig dat op je machine wordt geserveerd. De offline tutorial en zijn test draaien hem niet. Met Ollama actief en llama3.1 binnengehaald:
python evaluate.py --liveDe judge roept dan http://localhost:11434/v1 aan met provider="openai_compatible". Een loopbackserver heeft geen sleutel nodig en stuurt niets van de machine af. Een cloudprovider heeft zijn sleutel in de omgeving nodig, stuurt elk transcript naar die provider en kost geld per oordeel. De oordelen van een echt model verschillen van die van de vervanger, dus de getallen hierboven veranderen, en zijn regels lezen nog steeds evaluator_not_validated totdat je hem valideert.
Problemen oplossen
| Symptoom | Oorzaak en oplossing |
|---|---|
| this evaluator needs exactly one conversation/v1 artifact; the case recorded 0 | De adapter riep current_case().artifact(CONVERSATION, ...) niet aan, of wierp daarvoor een fout op. Leg ook vast als het gesprek vroeg stopt. |
| malformed conversation/v1 artifact: ... 0 of 2 turns recorded and truncated is false | De run stopt met een SystemContractError. Een opname met minder beurten dan declared_turns moet truncated: true zetten. |
| conversation turn indexes must be contiguous starting at 1 | Nummer beurten 1, 2, 3 naar de gescripte beurt die ze beantwoorden. |
| evaluator 'conversation_completed' needs conversation/v1 artifacts, but system ... does not declare that it records them | Voeg records=(CONVERSATION,) toe aan de @system-decorator. |
| Input tag 'conversation_completed' found using 'type' does not match any of the expected tags uit oloproof run | Gespreksevaluators zijn alleen SDK. Gebruik een script zoals hier. |
| Antwoorden lekken tussen gesprekken | Een sessie wordt tussen cases gedeeld. Maak er een per case. |
| Coherentieregels beslissen nooit | De judge is niet gevalideerd. Valideer hem, of zet bewust require_validated_evaluators: false. |
Beperkingen
- Geen gebruikerssimulator: elke gebruikersbeurt komt uit het datasetscript, dus het gesprek kan niet vertakken op wat de assistent zei.
- Geen metriek per beurt binnen een vastgelegd gesprek; gebruik in plaats daarvan beurten als geclusterde cases, met de afweging hierboven.
- Geen gespreksreplay: een vastgelegd gesprek kan niet opnieuw tegen een ander systeem worden gedraaid. Twee systemen vergelijken betekent dat elk hetzelfde script afspeelt.
- ConversationCompleted en ConversationJudge bestaan alleen in de Python-SDK.
- Alleen tekst: een judge krijgt JSON-tekst te zien, nooit beelden of audio.
- De offline judge is een gescripte regel. Zijn oordelen tonen de mechaniek, niet de nauwkeurigheid van een echte judge.
Waar verder
- Judges behandelt providers, validatie en herkalibratie.
- Geclusterde cases behandelt group_id en het toestaan van benaderende methoden.
- De Python-API behandelt evaluate en evaluate_comparison.
- Agents behandelt dezelfde grens voor de toollus van een agent.