Guides
Tutoriel : un service d’assistance face à des clients simulés
Évaluez un assistant conversationnel face à des utilisateurs qui réagissent à ce qu’il dit. Trente clients simulés, chacun avec un objectif et les faits qu’il connaît, parlent avec le service d’assistance d’une boutique ; Oloproof mesure si chacun a obtenu ce pour quoi il était venu, retient chaque décision jusqu’à ce qu’une personne ait jugé les clients simulés réalistes, puis décide une publication et une comparaison. Chaque étape s’exécute sur cette machine sans modèle ni réseau, et une étape facultative fait jouer les clients par un vrai modèle.
Ce que vous allez construire
Le service d’assistance d’une boutique répond aux demandes de remboursement, aux questions de livraison et aux changements d’adresse. Trois questions décident si une version peut être livrée :
- Chaque client a-t-il obtenu ce pour quoi il était venu ? goal_reached, à partir d’une vérification après chaque réponse.
- Une réponse a-t-elle promis ce que la boutique ne peut pas promettre ? every_turn lit chaque réponse à la recherche du mot « guarantee ».
- Peut-on se fier aux réponses pour dire quoi que ce soit des vrais clients ? Seulement une fois qu’une personne a lu un échantillon des conversations et jugé les clients simulés réalistes.
Les termes tels que cas, exécution, porte et suite sont définis dans Concepts ; tout ce qui concerne les utilisateurs simulés est dans Utilisateurs simulés.
Prérequis
- Python 3.11 ou ultérieur, et Oloproof :
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof- Le projet d’exemple, livré avec le paquet. Copiez-le dans un nouveau répertoire et travaillez-y :
oloproof init --example simulated-users support-desk
cd support-desk- Le port 8797 libre pour l’utilisateur simulé de substitution (sinon, changez-le aux deux endroits).
Chaque étape jusqu’à « Facultatif : un vrai modèle comme clients » est hors ligne et déterministe : aucune clé d’API, aucun compte de fournisseur, aucun coût.
Les fichiers
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 chatsLancez chaque commande depuis support-desk/.
Le service garde sa propre boucle
# 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}Le service demande le premier message du client, y répond, et rend chaque réponse pour obtenir le message suivant, jusqu’à ce que le client s’arrête. Il ne voit jamais la persona. Oloproof enregistre ce qui a circulé entre eux ; le service n’enregistre rien lui-même. Cette référence ne peut pas changer une adresse de livraison, et elle promet des remboursements « guaranteed within 5 days ».
Les personas
Chaque cas est un client : 12 veulent un remboursement, 10 veulent savoir quand une commande arrive, et 8 ont déménagé. Par exemple :
{"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"}}}}Le client ne donne le numéro de commande et le code postal que lorsqu’on les lui demande. Trois personas définissent aussi un opening, leur premier message mot pour mot.
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’utilisateur simulé de substitution
Un utilisateur simulé est un modèle qui joue chaque persona. Ici c’est user_server.py, qui parle la même API sur 127.0.0.1 et n’est pas un modèle : il lit la persona dans le prompt qu’envoie Oloproof, énonce son objectif, donne son numéro de commande ou son code postal quand on les lui demande, s’en va quand le service ne peut pas l’aider et, quand on lui demande si son objectif a été atteint, répond oui quand la dernière réponse du service commence par « Done: ». Lancez-le dans un second terminal et laissez-le tourner :
python user_server.py --port 8797La politique
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.80Aucune réponse ne peut promettre. Au moins 80 % des clients doivent atteindre leur objectif, ce qui n’est décidé que lorsque la borne inférieure de l’intervalle dépasse 80 %. Et aucune règle ne décide sur des clients simulés tant que des personnes n’ont pas jugé réalistes au moins 80 % de leurs conversations, par le même genre de borne inférieure.
L’exécuter
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 │Chaque client a reçu une réponse, 22 sur 30 ont atteint leur objectif, et 12 conversations contiennent une promesse. Rien n’est encore décidé : ces clients sont simulés, et personne n’a vérifié qu’ils se comportent comme de vrais clients.
Lire les clients simulés
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%Les huit qui sont partis sont les clients qui avaient déménagé : la référence ne peut pas changer une adresse.
Étiqueter le réalisme des conversations
Tirez un échantillon aléatoire des conversations de l’exécution dans une feuille, faites lire chacune par une personne, avec la persona et la manière dont elle s’est terminée, et faites-lui marquer si un vrai client aurait pu écrire le côté du client et y mettre fin à ce moment-là, de cette manière. Ici, fill_realism.py remplace cette personne, en copiant ses verdicts depuis 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 personne a jugé une conversation irréaliste : le client c05 commence par « Order: F-15. Refund. », ce qui se lit comme un formulaire, pas comme une personne. Avec 29 sur 30, la borne inférieure exacte est de 82,8 %, au-dessus du seuil, donc les règles peuvent maintenant décider sur ces clients.
oloproof gate RUN_ID --policy release.yamlno-promises: FAIL (observed_failures_exceed_limit)
resolved: INSUFFICIENT_EVIDENCE (interval_overlaps_threshold)
Gate: BLOCK (exit 1)Faire une vraie modification
desk_v2.py change une adresse une fois qu’il a la commande et le nouveau code postal, et dit qu’il a lancé un remboursement qui « usually arrives within 5 days » au lieu de le promettre. Copiez-le par-dessus desk.py, réglez version: candidate sous system dans oloproof.yaml, laissez tourner l’utilisateur de substitution, et :
oloproof runGate: ALLOW (exit 0)
│ no-promises │ no_promises │ PASS │ observed_failures_within_limit │
│ resolved │ goal_reached │ PASS │ lower_bound_meets_minimum │Le candidat a rencontré les mêmes clients simulés sur la même suite, donc le réalisme mesuré sur les conversations de la référence le couvre : une validation couvre un utilisateur simulé sur une suite, quel que soit le système avec lequel il a parlé.
Comparer le candidat à la référence
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_zeroChaque client a rencontré les deux services, donc la comparaison est appariée client par client.
Simulé à côté du réel
Les étiquettes de réalisme jugent chaque conversation isolément. Placer les clients simulés à côté de vrais clients montre ce qu’un échantillon peut manquer. real.yaml rejoue 20 discussions que les agents humains de la boutique ont eues avec de vrais clients :
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]Les vrais clients ont envoyé plus de messages, et plus courts : ils disent ce qu’ils veulent sur trois lignes, là où l’utilisateur de substitution le dit en une phrase bien rangée. C’est bon à savoir pour n’importe quel utilisateur simulé, et cela ne décide jamais rien ici : comparer deux échantillons non appariés demande une méthode qu’Oloproof n’a pas admise.
Facultatif : un vrai modèle comme clients
Cette étape quitte le chemin hors ligne. Elle demande un modèle de conversation : en local, Ollama (ollama pull qwen2.5vl, ou n’importe quel modèle de conversation), sans clé et sans coût ; dans le cloud, provider: anthropic ou openai avec api_key_env, facturé par message. Dans oloproof.yaml, faites pointer l’utilisateur simulé vers lui :
simulated_user:
provider: openai_compatible
model: qwen2.5vl
base_url: http://localhost:11434/v1
max_turns: 6Ollama répond à une requête à la fois, donc réglez concurrency: {system: 1} et un timeout_s de quelques minutes. Un modèle différent est un utilisateur simulé différent : les étiquettes de réalisme ci-dessus ne le couvrent pas, donc ses conversations ont besoin de leur propre échantillon avant qu’une règle ne décide sur elles.
Dix des personas, jouées par qwen2.5vl sur cette machine face au service candidat, ont atteint leur objectif dans 8 conversations, là où l’utilisateur de substitution l’a atteint dans les 30. Le modèle a trouvé ce que l’utilisateur de substitution ne pouvait pas trouver : deux clients ont demandé le « status » et le « shipping status » de leur commande, des mots que le service ne connaît pas, et ont reçu chaque fois son message d’accueil en retour. L’un a épuisé ses messages. Le service a compris le troisième essai de l’autre, et ce client est parti quand même : la vérification de l’objectif, le jugement d’un modèle, a manqué la réponse. C’est une étiquette de réalisme qui le fait apparaître : un vrai client à qui l’on dit quand sa commande arrive ne part pas mécontent. Contrairement à l’utilisateur de substitution, le modèle a aussi souvent donné son numéro de commande avant qu’on le lui demande, et a écrit un message plus long là où l’utilisateur de substitution en écrivait deux courts.
Dépannage
| Symptôme | Cause et correction |
|---|---|
| Chaque règle est INSUFFICIENT_EVIDENCE avec simulated_user_not_validated | Personne n’a étiqueté les conversations de cet utilisateur simulé sur cette suite, ou la part réaliste ne franchit pas le seuil. Étiquetez un échantillon comme ci-dessus. |
| a case for a simulated user needs a 'persona' object in its input | Un cas n’a pas de persona, ou elle est sous une autre clé : réglez persona_field. |
| provider connection failed pour chaque cas | L’utilisateur de substitution ne tourne pas, ou écoute sur un autre port que celui que nomme base_url. |
| the simulated user's goal check answered ... | Le modèle n’a pas répondu à la vérification de l’objectif par oui ou par non. Essayez un autre modèle, ou un plus grand. |
| conversation/v1 is written by Oloproof | Le service a enregistré la conversation lui-même. Retirez cela : Oloproof enregistre ce qui est passé par l’utilisateur simulé. |
Limites
- Le client de substitution est une règle. Ses conversations montrent le déroulement ; celles d’un vrai modèle sont plus désordonnées, comme le montre l’étape facultative.
- Que l’objectif soit atteint relève du jugement du modèle de l’utilisateur simulé, vérifié seulement par les étiquettes de réalisme.
- Un espace de travail hébergé ne décide pas encore sur des utilisateurs simulés.
- Des clients simulés ne sont pas de vrais clients : un utilisateur simulé validé est une preuve au sujet de clients semblables à ses personas, dans la mesure où des personnes l’ont jugé réaliste, et pas au-delà.