Ga naar de inhoud

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 doetJe applicatie doet
de gescripte gesprekken als dataset opslaan, identiek voor elk systeemhet 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 beslissenhet 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
BestandWat het is
assistant.pyde geteste applicatie: een abonnementsassistent met sessietoestand
systems.pyde adapter: speelt een script af, legt conversation/v1 vast
judge_offline.pyde gescripte vervanger voor het judgemodel
evaluate.pydraait de evaluatie, de vergelijking en het beurtenalternatief
release.yamlhet beleid voor één run
comparison.yamlhet beleid voor de kandidaat tegen de baseline
turns_release.yamlhet beleid voor het beurtenalternatief
data/conversations.jsonl40 gescripte gesprekken
data/turns.jsonldezelfde 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, ...}]}
VeldBetekenis
turns[].indexwelke gescripte beurt dit beantwoordt; aaneengesloten vanaf 1
turns[].answerwat de assistent teruggaf, elke JSON
turns[].askedoptioneel, alleen om te lezen; de engine matcht op index
turns[].retrievaloptioneel, wat die beurt ophaalde, in de vorm van retrieval/v1
declared_turnshoeveel beurten het script declareerde
truncatedde 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.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'}

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_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

De 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 gesprekEén case per beurt, geclusterd
Eenheid van het percentagegesprekken die goed gingencorrect beantwoorde beurten
Effectieve steekproefgroottehet aantal gesprekkennog steeds het aantal gesprekken, niet beurten
Welke beurt faaldelees het transcriptelke beurt heeft een eigen oordeel
Geschiedenis die elke beurt zietde eigen eerdere antwoorden van de assistentde eerdere gebruikersbeurten van het script, afgespeeld
Vangt afdrijven door de eigen eerdere antwoordenjanee, elke beurt begint vanuit een gescripte geschiedenis
EvaluatorsConversationCompleted, 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 --live

De 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

SymptoomOorzaak en oplossing
this evaluator needs exactly one conversation/v1 artifact; the case recorded 0De 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 falseDe 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 1Nummer 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 themVoeg records=(CONVERSATION,) toe aan de @system-decorator.
Input tag 'conversation_completed' found using 'type' does not match any of the expected tags uit oloproof runGespreksevaluators zijn alleen SDK. Gebruik een script zoals hier.
Antwoorden lekken tussen gesprekkenEen sessie wordt tussen cases gedeeld. Maak er een per case.
Coherentieregels beslissen nooitDe 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.