Vai al contenuto

Guide

Tutorial: una conversazione a più turni

Un percorso eseguibile per valutare un assistente conversazionale con l'SDK Python: la tua applicazione rigioca una conversazione scriptata contro una sessione nuova, registra ciò che ha risposto come artefatto conversation/v1, e due valutatori giudicano ogni conversazione intera. Gira offline con un sostituto scriptato del modello giudice, poi confronta una correzione candidata, e termina con l'alternativa di valutare i turni come casi raggruppati in cluster.

Termini come intervallo, stato di decisione e azione di rilascio sono definiti in Concetti; Registrare ciò che un sistema ha fatto tratta gli artefatti.

Che cosa fa e che cosa non fa Oloproof qui

Oloproof faLa tua applicazione fa
memorizza le conversazioni scriptate come dataset, identiche per ogni sistemapilota la conversazione: pone ogni turno scriptato in ordine
verifica che ogni turno scriptato abbia ricevuto risposta (ConversationCompleted)possiede lo stato della sessione, avvia una sessione nuova per ogni caso e la reimposta
giudica l'intera trascrizione con un modello (ConversationJudge)decide che cosa succede quando non può continuare, e registra che si è fermata
calcola gli intervalli, confronta due sistemi e decide rispetto a una policyregistra l'artefatto conversation/v1

Oloproof non ha alcun simulatore di utente: non scrive mai un turno dell'utente, quindi il lato dell'utente è ciò che il dataset prevede nello script. Non ha alcuna metrica a livello di turno all'interno di una conversazione registrata, e non può rigiocare una conversazione registrata contro un nuovo sistema. I valutatori di conversazione esistono solo nell'SDK Python: ConversationCompleted e ConversationJudge non sono tipi di valutatore in oloproof.yaml, quindi questo tutorial usa uno script invece di oloproof run.

Prerequisiti

  • Python 3.11 o successivo e pip install oloproof, come nel quickstart.
  • I file di esempio, distribuiti con il pacchetto. Copiali in una nuova directory così che l'archivio dell'esecuzione finisca lì:
oloproof init --example conversation ~/oloproof-conversation
cd ~/oloproof-conversation
FileChe cos'è
assistant.pyl'applicazione sotto test: un assistente sui piani con stato di sessione
systems.pyl'adattatore: rigioca uno script, registra conversation/v1
judge_offline.pyil sostituto scriptato del modello giudice
evaluate.pyesegue la valutazione, il confronto e l'alternativa per turni
release.yamlla policy per un'esecuzione
comparison.yamlla policy per il candidato rispetto alla baseline
turns_release.yamlla policy per l'alternativa per turni
data/conversations.jsonl40 conversazioni scriptate
data/turns.jsonlle stesse conversazioni, un caso per turno

Nessuna chiave, nessuna rete e nessun costo di provider, fino al passo facoltativo dal vivo alla fine.

L'applicazione

assistant.py risponde a domande su tre piani tariffari. Conserva un solo elemento di stato, il piano di cui parla la conversazione, così che una domanda successiva come "Does that include SSO?" possa risolvere "that". La baseline ha un difetto deliberato: non ricorda il piano, quindi a una domanda successiva risponde riguardo al piano predefinito. Un utente che chiede di parlare con una persona termina la conversazione con 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): ...

Questa è la parte che sostituisci con la tua applicazione: un client di chatbot, una sessione di agente, una sessione HTTP verso il tuo servizio. Qualunque cosa sia, possiede il proprio stato e la propria reimpostazione; Oloproof vede solo ciò che l'adattatore registra.

Il dataset: lo script è l'input

Una riga di data/conversations.jsonl è una conversazione:

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

I turni dell'utente sono contenuto del dataset, coperti dal digest della suite e identici per ogni sistema misurato su di essi; è questo che rende confrontabili due sistemi. expected è il riferimento mostrato al giudice. Le 40 conversazioni sono 24 con una domanda successiva che non nomina alcun piano, 12 che nominano il piano a ogni turno, e 4 che chiedono una persona al secondo turno su tre.

L'adattatore

systems.py avvia una sessione nuova per ogni caso, pone ogni turno scriptato in ordine e registra ciò che è tornato:

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)

Una sessione nuova per ogni caso è importante: Oloproof esegue i casi in modo concorrente e senza un ordine fisso, e una sessione condivisa tra casi lascerebbe trapelare lo stato di una conversazione in un'altra. records= dichiara che il sistema registra l'artefatto; senza di esso i valutatori di conversazione vengono rifiutati prima che giri qualsiasi cosa, invece di contare ogni caso come mancante.

L'artefatto conversation/v1

Ciò che la baseline ha registrato per conv_00, da oloproof export RUN_ID (il cases.jsonl del bundle):

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

E per una conversazione che ha chiesto una persona:

{"declared_turns": 3, "truncated": true, "turns": [{"answer": "The team plan costs $20 a month.", "asked": "What does the team plan cost?", "index": 1, ...}]}
CampoSignificato
turns[].indexa quale turno scriptato risponde; contiguo a partire da 1
turns[].answerciò che l'assistente ha restituito, qualsiasi JSON
turns[].askedfacoltativo, solo per la lettura; il motore fa corrispondere su index
turns[].retrievalfacoltativo, ciò che quel turno ha recuperato, nella forma retrieval/v1
declared_turnsquanti turni lo script ha dichiarato
truncatedla registrazione si ferma prima della fine dello script, qualunque cosa l'abbia interrotta

L'artefatto viene verificato quando viene registrato: una registrazione con meno turni di quelli dichiarati deve indicare truncated: true, gli indici devono essere contigui, e una registrazione non può rispondere a più turni di quanti ne siano stati posti. Un artefatto malformato ferma l'esecuzione con un SystemContractError.

I due valutatori, e perché entrambi

  • ConversationCompleted è deterministico: l'assistente ha risposto a ogni turno scriptato? Gira per primo perché qualsiasi altra affermazione su una conversazione fermatasi al primo turno su tre è un'affermazione su una conversazione diversa. Una conversazione troncata lo fallisce; è un risultato, non un caso mancante.
  • ConversationJudge è un giudice basato su modello sull'intera trascrizione, ogni turno di utente e di assistente, perché i fallimenti di cui si incolpa un prodotto conversazionale sono relazionali: una risposta che contraddice quella del turno precedente è sbagliata solo accanto a essa. Una conversazione troncata viene giudicata su ciò che è stato registrato, e la trascrizione dice al giudice dove si è fermata.
evaluators = [
    ConversationCompleted(),
    ConversationJudge(criterion="plan_coherent", provider=..., model=..., rubric_text=RUBRIC),
]

Giudicare offline

Un giudice ha bisogno di un modello. Per girare senza rete, judge_offline.py passa un provider scriptato, lo stesso helper che usano i test di Oloproof (FakeProvider dagli interni del motore, non API pubblica). Risponde a ogni prompt del giudice con un'unica regola fissa: successo quando ogni turno dell'assistente nomina il piano nominato dal riferimento. Questo rende deterministici i giudizi e riproducibile il tutorial. Non misura nulla di come si comporta un vero modello giudice.

La policy

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}

Eseguila

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

Come leggerlo:

  • Il completamento è 36 su 40, i quattro passaggi a una persona. Il suo limite inferiore supera 0.75, quindi quella regola è PASS.
  • La coerenza è 40%, e la sua regola riporta INSUFFICIENT_EVIDENCE con evaluator_not_validated, non FAIL. Le regole di un giudice non decidono finché il giudice non è stato misurato rispetto a etichette umane (require_validated_evaluators è attivo per impostazione predefinita; Giudici lo spiega). La stima viene comunque mostrata, ed è comunque evidenza: solo non può da sola consentire un rilascio o bloccarlo.
  • gate BLOCK (exit 3): la policy blocca su INSUFFICIENT_EVIDENCE. L'uscita 3 è quello stato, non un fallimento.

Validare il sostituto offline non avrebbe senso, poiché è una regola scritta per questo esempio. Con un giudice reale, etichetta un campione dell'esecuzione con oloproof review RUN_ID --criterion plan_coherent --by YOU --sample 20 e poi esegui oloproof evaluators validate EVALUATOR_ID --by YOU.

Esamina una conversazione fallita

L'SDK scrive nello stesso archivio che la CLI legge, .oloproof/ nella directory da cui hai eseguito:

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

L'utente ha chiesto del piano enterprise e alla domanda successiva è stata data una risposta sul piano starter. oloproof inspect RUN_ID --failures elenca ogni conversazione fallita; tutte le 24 domande successive senza nome del piano falliscono allo stesso modo. La prossima azione è nell'applicazione: conservare il piano nello stato della sessione.

Una modifica candidata, e il confronto

candidate in systems.py imposta remembers_plan=True. evaluate.py esegue entrambi i sistemi sugli stessi script e li confronta caso per caso secondo 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)

La differenza di coerenza è grande e il suo intervallo esclude lo zero, ma il giudice non è validato, quindi la sua regola ancora non decide. Il completamento è invariato, e 40 coppie non possono mostrare che sia entro cinque punti: l'intervallo arriva a 12.7 punti in entrambe le direzioni. Entrambi indicano lo stesso passo successivo: validare il giudice e aggiungere conversazioni.

L'alternativa: turni come casi, analizzati come cluster

Un verdetto a livello di conversazione dice quanto spesso una conversazione è andata bene, non quale turno è andato storto. Poiché Oloproof non ha alcuna metrica a livello di turno all'interno di una conversazione registrata, l'altra strada è fare di ogni turno un caso a sé e legare insieme i turni di una conversazione con 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?"}}

Il sistema rigioca la cronologia scriptata in una sessione nuova, poi risponde al turno:

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

I turni di una conversazione non sono indipendenti, quindi non appena un caso ha un group_id la suite viene analizzata per cluster con un metodo approssimato che una policy deve accettare (turns_release.yaml imposta allow_approximate_methods: true; Casi raggruppati lo spiega):

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)

Il compromesso:

Un caso per conversazioneUn caso per turno, in cluster
Unità del tassoconversazioni andate beneturni a cui si è risposto correttamente
Dimensione effettiva del campioneil numero di conversazionisempre il numero di conversazioni, non di turni
Quale turno è fallitoleggi la trascrizioneogni turno ha il proprio verdetto
Cronologia che vede ogni turnole risposte precedenti dell'assistente stessoi turni utente precedenti dello script, rigiocati
Rileva la deriva causata dalle proprie risposte precedentisìno, ogni turno parte da una cronologia scriptata
ValutatoriConversationCompleted, ConversationJudge (solo SDK)qualsiasi valutatore, in YAML o nell'SDK

L'alternativa per turni esclude le quattro conversazioni con passaggio a una persona, quindi i suoi 72 turni provengono da 36 conversazioni. Qui può decidere dove il giudice non poteva, perché ExactMatch è deterministico e non richiede validazione.

Facoltativo: un modello giudice dal vivo

Questo passo richiede un modello servito sulla tua macchina. Non viene eseguito dal tutorial offline né dal suo test. Con Ollama in esecuzione e llama3.1 scaricato:

python evaluate.py --live

Il giudice chiama allora http://localhost:11434/v1 con provider="openai_compatible". Un server di loopback non richiede alcuna chiave e non invia nulla fuori dalla macchina. Un provider cloud richiede la sua chiave nell'ambiente, invia ogni trascrizione a quel provider e costa denaro per giudizio. I verdetti di un modello reale differiscono da quelli del sostituto, quindi i numeri qui sopra cambieranno, e le sue regole riportano ancora evaluator_not_validated finché non lo validi.

Risoluzione dei problemi

SintomoCausa e soluzione
this evaluator needs exactly one conversation/v1 artifact; the case recorded 0L'adattatore non ha chiamato current_case().artifact(CONVERSATION, ...), o ha sollevato un'eccezione prima. Registra anche quando la conversazione si ferma in anticipo.
malformed conversation/v1 artifact: ... 0 of 2 turns recorded and truncated is falseL'esecuzione si ferma con un SystemContractError. Una registrazione con meno turni di declared_turns deve impostare truncated: true.
conversation turn indexes must be contiguous starting at 1Numera i turni 1, 2, 3 secondo il turno scriptato a cui rispondono.
evaluator 'conversation_completed' needs conversation/v1 artifacts, but system ... does not declare that it records themAggiungi records=(CONVERSATION,) al decoratore @system.
Input tag 'conversation_completed' found using 'type' does not match any of the expected tags da oloproof runI valutatori di conversazione sono solo SDK. Usa uno script come qui.
Le risposte trapelano tra conversazioniUna sessione è condivisa tra casi. Creane una per caso.
Le regole di coerenza non decidono maiIl giudice non è validato. Validalo, o imposta consapevolmente require_validated_evaluators: false.

Limitazioni

  • Nessun simulatore di utente: ogni turno dell'utente proviene dallo script del dataset, quindi la conversazione non può ramificarsi in base a ciò che l'assistente ha detto.
  • Nessuna metrica a livello di turno all'interno di una conversazione registrata; usa invece i turni come casi in cluster, con il compromesso descritto sopra.
  • Nessuna riesecuzione di conversazioni: una conversazione registrata non può essere rieseguita contro un altro sistema. Confrontare due sistemi significa che ciascuno rigioca lo stesso script.
  • ConversationCompleted e ConversationJudge sono solo per l'SDK Python.
  • Solo testo: a un giudice viene mostrato testo JSON, mai immagini o audio.
  • Il giudice offline è una regola scriptata. I suoi verdetti mostrano il meccanismo, non l'accuratezza di un giudice reale.

Dove andare ora

  • Giudici tratta provider, validazione e ricalibrazione.
  • Casi raggruppati tratta group_id e l'adesione ai metodi approssimati.
  • L'API Python tratta evaluate e evaluate_comparison.
  • Agenti tratta lo stesso confine per il ciclo di strumenti di un agente.