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 chatsEsegui 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: trueL'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 8797La 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.80Nessuna 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 runGate: 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.yamlno-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 runGate: 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.yamlgoal_reached: +26.7 points [+1.2, +54.1] · 30 paired · 0 missing · 0 excluded
resolves-more goal_reached superiority PASS difference_above_zeroOgni 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: 6Ollama 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
| Sintomo | Causa e rimedio |
|---|---|
| Ogni regola è INSUFFICIENT_EVIDENCE con simulated_user_not_validated | Nessuno 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 input | Un caso non ha un personaggio, oppure si trova sotto un'altra chiave: imposta persona_field. |
| provider connection failed da ogni caso | Il 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 Oloproof | Il 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.