Vai al contenuto

Guide

Tutorial: un servizio di assistenza alla prova con clienti simulati

Valuta un assistente conversazionale con utenti che reagiscono a ciò che dice. Trenta clienti simulati, ciascuno con un obiettivo e i fatti che conosce, parlano con il servizio di assistenza di un negozio; Oloproof misura se ciascuno ha ottenuto ciò per cui era venuto, trattiene ogni decisione finché una persona non ha trovato realistici i clienti simulati, e poi decide un rilascio e un confronto. Ogni passo gira su questa macchina senza modello e senza rete, e un passo facoltativo fa interpretare i clienti a un vero modello.

Che cosa costruirai

Il servizio di assistenza di un negozio risponde a richieste di rimborso, domande sulle consegne e cambi di indirizzo. Tre domande decidono se una versione può essere rilasciata:

  • Ogni cliente ha ottenuto ciò per cui era venuto? goal_reached, da un controllo dopo ogni risposta.
  • Qualche risposta ha promesso ciò che il negozio non può promettere? every_turn legge ogni risposta cercando la parola "guarantee".
  • Ci si può fidare delle risposte per dire qualcosa sui clienti reali? Solo dopo che una persona ha letto un campione delle conversazioni e ha trovato realistici i clienti simulati.

Termini come caso, esecuzione, gate e suite sono definiti in Concetti; tutto ciò che riguarda gli utenti simulati è in Utenti simulati.

Prerequisiti

  • Python 3.11 o successivo, e Oloproof:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • Il progetto di esempio, incluso nel pacchetto. Copialo in una nuova directory e lavora lì:
oloproof init --example simulated-users support-desk
cd support-desk
  • La porta 8797 libera per l'utente simulato sostitutivo (altrimenti cambiala in entrambi i punti).

Ogni passo fino a "Facoltativo: un vero modello come clienti" è offline e deterministico: nessuna chiave API, nessun account di provider, nessun costo.

I file

support-desk/
  desk.py                      the support desk under test (baseline)
  desk_v2.py                   the candidate change
  user_server.py               a stand-in simulated user speaking the OpenAI API on 127.0.0.1
  oloproof.yaml                the suite: the simulated user and four evaluators
  release.yaml                 rules for a run, with the realism bar
  compare.yaml                 a rule for a comparison
  data/customers.jsonl         30 personas
  labels/realism_verdicts.csv  one person's realism verdicts on the baseline's conversations
  fill_realism.py              copies those verdicts into a labelling sheet
  logged.py                    replays logged chats with human agents
  real.yaml                    a suite of 20 of those chats
  logged_release.yaml          a rule for the logged chats
  data/logged_chats.jsonl      the 20 chats

Esegui ogni comando da support-desk/.

Il servizio mantiene il proprio ciclo

# desk.py (excerpt)
def talk(case: dict[str, Any]) -> dict[str, Any]:
    user = current_case().simulated_user()
    desk = Desk()
    message = user.opening()
    while message is not None:
        message = user.reply(desk.answer(message))
    return {"need": desk.need, "order": desk.order}

Il servizio chiede il primo messaggio del cliente, gli risponde, e restituisce ogni risposta per ottenere il messaggio successivo, finché il cliente non si ferma. Non vede mai il personaggio. Oloproof registra ciò che è passato tra i due; il servizio non registra nulla da sé. Questa baseline non può cambiare un indirizzo di consegna, e promette rimborsi "guaranteed within 5 days".

I personaggi

Ogni caso è un cliente: 12 vogliono un rimborso, 10 vogliono sapere quando arriva un ordine, e 8 hanno traslocato. Per esempio:

{"id": "c25", "input": {"persona": {"description": "A retired teacher who ordered a cookbook last week. Writes short, polite messages.", "goal": "Have the order of a cookbook delivered to my new address.", "facts": {"order": "B-35", "postcode": "10275"}}}}

Il cliente dà il numero d'ordine e il codice postale solo quando gli vengono chiesti. Tre personaggi impostano anche un opening, il loro primo messaggio parola per parola.

La suite

version: 1
project: support-desk
dataset: data/customers.jsonl
system:
  name: support-desk
  version: baseline
  callable: desk:talk
  # A model plays each case's persona; here the stand-in in user_server.py.
  simulated_user:
    provider: openai_compatible
    model: stand-in-user
    base_url: http://127.0.0.1:8797/v1
    max_turns: 6
evaluators:
  # The desk answered every message the customer sent.
  - type: conversation_completed
  # The customer said they got what they came for.
  - type: goal_reached
  # ... within three of their own messages.
  - type: turns_to_goal
    max_turns: 3
  # No answer promises what the shop cannot promise.
  - type: every_turn
    criterion: no_promises
    not_contains: [guarantee]
    casefold: true

L'utente simulato sostitutivo

Un utente simulato è un modello che interpreta ogni personaggio. Qui è user_server.py, che parla la stessa API su 127.0.0.1 e non è un modello: legge il personaggio dal prompt che Oloproof invia, dichiara il suo obiettivo, dà il numero d'ordine o il codice postale quando gli vengono chiesti, se ne va quando il servizio non può aiutarlo e, quando gli si chiede se il suo obiettivo è stato raggiunto, risponde sì quando l'ultima risposta del servizio inizia con "Done:". Avvialo in un secondo terminale e lascialo in esecuzione:

python user_server.py --port 8797

La policy

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
require_validated_evaluators: true
minimum_simulated_user_realism: 0.80
rules:
  - id: no-promises
    metric: no_promises
    kind: observed_count
    max_failures: 0
  - id: resolved
    metric: goal_reached
    min: 0.80

Nessuna risposta può promettere. Almeno l'80% dei clienti deve raggiungere il proprio obiettivo, deciso solo quando il limite inferiore dell'intervallo supera l'80%. E nessuna regola decide su clienti simulati finché delle persone non hanno trovato realistiche almeno l'80% delle loro conversazioni, con lo stesso tipo di limite inferiore.

Eseguilo

oloproof run
Gate: BLOCK (exit 3)
│ no-promises │ no_promises  │ INSUFFICIENT_EVIDENCE │ simulated_user_not_validated │
│ resolved    │ goal_reached │ INSUFFICIENT_EVIDENCE │ simulated_user_not_validated │
│ conversation_completed │ 100.0%   │ [88.4%, 100.0%] │ 30 / 30 observed · 0 missing · 0 excluded │
│ goal_reached           │ 73.3%    │ [54.1%, 87.8%]  │ 22 / 30 observed · 0 missing · 0 excluded │
│ goal_within_3_turns    │ 73.3%    │ [54.1%, 87.8%]  │ 22 / 30 observed · 0 missing · 0 excluded │
│ no_promises            │ 60.0%    │ [40.6%, 77.4%]  │ 18 / 30 observed · 0 missing · 0 excluded │

Ogni cliente ha ricevuto risposta, 22 su 30 hanno raggiunto il proprio obiettivo, e 12 conversazioni contengono una promessa. Nulla è ancora deciso: questi clienti sono simulati, e nessuno ha verificato che si comportino come quelli reali.

Leggi i clienti simulati

oloproof simulated-users RUN_ID
  Realism: no conversation has been drawn and labelled yet.
  Not validated: nothing labelled yet, against a bar of 80.0%.
Features (median [quartiles]; descriptive, never a gate):
  user messages           2 [1, 2]
  user message length     49 [10, 67]
  How the simulated conversations ended: goal_reached 73%, user_left 27%

Gli otto che se ne sono andati sono i clienti che avevano traslocato: la baseline non può cambiare un indirizzo.

Etichetta il realismo delle conversazioni

Estrai un campione casuale delle conversazioni dell'esecuzione in un foglio, fai leggere ciascuna a una persona, con il personaggio e come è terminata, e fai segnare se un cliente reale avrebbe potuto scrivere il lato del cliente e chiuderlo a quel punto, in quel modo. Qui fill_realism.py sostituisce quella persona, copiando i suoi verdetti da labels/realism_verdicts.csv:

oloproof labels export RUN_ID --criterion simulated_user_realistic --sample 30 --local --out realism.csv
python fill_realism.py realism.csv
oloproof labels import realism.csv
oloproof simulated-users RUN_ID
  Realism: 29 of 30 drawn conversations labelled realistic (30 labelled), [82.8%, 99.9%] at 95%
  Validated: the lower bound clears the bar of 80.0%.

La persona ha trovato una conversazione non realistica: il cliente c05 apre con "Order: F-15. Refund.", che si legge come un modulo, non come una persona. Con 29 su 30, il limite inferiore esatto è 82,8%, sopra la soglia, quindi ora le regole possono decidere su questi clienti.

oloproof gate RUN_ID --policy release.yaml
no-promises: FAIL (observed_failures_exceed_limit)
resolved: INSUFFICIENT_EVIDENCE (interval_overlaps_threshold)
Gate: BLOCK (exit 1)

Fai una modifica reale

desk_v2.py cambia un indirizzo una volta che ha l'ordine e il nuovo codice postale, e dice di aver avviato un rimborso che "usually arrives within 5 days" invece di prometterlo. Copialo sopra desk.py, imposta version: candidate sotto system in oloproof.yaml, lascia il sostituto in esecuzione, e:

oloproof run
Gate: ALLOW (exit 0)
│ no-promises │ no_promises  │ PASS  │ observed_failures_within_limit │
│ resolved    │ goal_reached │ PASS  │ lower_bound_meets_minimum      │

Il candidato ha incontrato gli stessi clienti simulati sulla stessa suite, quindi il realismo misurato sulle conversazioni della baseline lo copre: una validazione copre un utente simulato su una suite, con qualunque sistema abbia parlato.

Confronta il candidato con la baseline

oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml
goal_reached: +26.7 points [+1.2, +54.1] · 30 paired · 0 missing · 0 excluded
  resolves-more  goal_reached  superiority  PASS  difference_above_zero

Ogni cliente ha incontrato entrambi i servizi, quindi il confronto è appaiato cliente per cliente.

Simulato accanto al reale

Le etichette di realismo giudicano ogni conversazione da sola. Mettere i clienti simulati accanto a quelli reali mostra ciò che un campione può non cogliere. real.yaml rigioca 20 chat che gli operatori umani del negozio hanno avuto con clienti reali:

oloproof run --config real.yaml --policy logged_release.yaml
oloproof simulated-users BASELINE_RUN_ID --real REAL_RUN_ID
                          simulated (run_01M52Q1C)    real (run_01M52Q25)
  user messages           2 [1, 2]                    3 [3, 4]
  user message length     49 [10, 67]                 13 [4, 32]

I clienti reali hanno inviato più messaggi e più brevi: dicono ciò che vogliono in tre righe, dove il sostituto lo dice in una frase ordinata. Vale la pena saperlo di qualsiasi utente simulato, e qui non decide mai nulla: confrontare due campioni non appaiati richiede un metodo che Oloproof non ha ammesso.

Facoltativo: un vero modello come clienti

Questo passo lascia il percorso offline. Richiede un modello di chat: in locale, Ollama (ollama pull qwen2.5vl, o qualsiasi modello di chat), senza chiave e senza costi; nel cloud, provider: anthropic o openai con api_key_env, fatturato per messaggio. In oloproof.yaml, punta l'utente simulato su di esso:

  simulated_user:
    provider: openai_compatible
    model: qwen2.5vl
    base_url: http://localhost:11434/v1
    max_turns: 6

Ollama risponde a una richiesta alla volta, quindi imposta concurrency: {system: 1} e un timeout_s di qualche minuto. Un modello diverso è un utente simulato diverso: le etichette di realismo qui sopra non lo coprono, quindi le sue conversazioni hanno bisogno di un proprio campione prima che una regola decida su di esse.

Dieci dei personaggi, interpretati da qwen2.5vl su questa macchina con il servizio candidato, hanno raggiunto il proprio obiettivo in 8 conversazioni, dove il sostituto lo ha raggiunto in tutte e 30. Il modello ha trovato ciò che il sostituto non poteva trovare: due clienti hanno chiesto lo "status" e lo "shipping status" del loro ordine, parole che il servizio non conosce, e ogni volta hanno ricevuto in risposta il suo saluto. Uno ha esaurito i messaggi. Il servizio ha capito il terzo tentativo dell'altro, e quel cliente se n'è andato comunque: il controllo dell'obiettivo, il giudizio di un modello, ha mancato la risposta. È un'etichetta di realismo a farlo emergere: un cliente reale a cui si dice quando arriva il suo ordine non se ne va scontento. A differenza del sostituto, il modello ha anche dato spesso il numero d'ordine prima che gli venisse chiesto, e ha scritto un messaggio più lungo dove il sostituto ne scriveva due brevi.

Risoluzione dei problemi

SintomoCausa e rimedio
Ogni regola è INSUFFICIENT_EVIDENCE con simulated_user_not_validatedNessuno ha etichettato le conversazioni di questo utente simulato su questa suite, oppure la quota realistica non supera la soglia. Etichetta un campione come sopra.
a case for a simulated user needs a 'persona' object in its inputUn caso non ha un personaggio, oppure si trova sotto un'altra chiave: imposta persona_field.
provider connection failed da ogni casoIl sostituto non è in esecuzione, oppure ascolta su una porta diversa da quella che nomina base_url.
the simulated user's goal check answered ...Il modello non ha risposto al controllo dell'obiettivo con sì o no. Prova un altro modello, o uno più grande.
conversation/v1 is written by OloproofIl servizio ha registrato la conversazione da sé. Togli questa parte: Oloproof registra ciò che è passato attraverso l'utente simulato.

Limitazioni

  • Il cliente sostitutivo è una regola. Le sue conversazioni mostrano il meccanismo; quelle di un vero modello sono più disordinate, come mostra il passo facoltativo.
  • Che un obiettivo sia stato raggiunto è il giudizio del modello dell'utente simulato, verificato solo attraverso le etichette di realismo.
  • Uno spazio di lavoro ospitato non decide ancora su utenti simulati.
  • I clienti simulati non sono clienti reali: un utente simulato validato è un'evidenza sui clienti simili ai suoi personaggi, nella misura in cui le persone lo hanno trovato realistico, e non oltre.