Aller au contenu

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 chats

Lancez 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: true

L’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 8797

La 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.80

Aucune 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 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 │

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.yaml
no-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 run
Gate: 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.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

Chaque 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: 6

Ollama 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ômeCause et correction
Chaque règle est INSUFFICIENT_EVIDENCE avec simulated_user_not_validatedPersonne 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 inputUn cas n’a pas de persona, ou elle est sous une autre clé : réglez persona_field.
provider connection failed pour chaque casL’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 OloproofLe 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à.