Ga naar de inhoud

Handleidingen

Tutorial: een supportdesk tegen gesimuleerde klanten

Evalueer een conversationele assistent tegen gebruikers die reageren op wat hij zegt. Dertig gesimuleerde klanten, elk met een doel en de feiten die ze kennen, praten met de supportdesk van een winkel; Oloproof meet of elk van hen kreeg waarvoor ze kwamen, houdt elke beslissing achter totdat een mens de gesimuleerde klanten realistisch heeft bevonden, en beslist daarna over een release en een vergelijking. Elke stap draait op deze machine zonder model en zonder netwerk, en een optionele stap zet een echt model in als de klanten.

Wat je gaat bouwen

De supportdesk van een winkel beantwoordt vragen over terugbetalingen, levering en adreswijzigingen. Drie vragen beslissen of een versie mag worden uitgebracht:

  • Kreeg elke klant waarvoor hij kwam? goal_reached, uit een controle na elk antwoord.
  • Beloofde een antwoord iets wat de winkel niet kan beloven? every_turn leest elk antwoord op het woord "guarantee".
  • Mag je erop vertrouwen dat de antwoorden iets zeggen over echte klanten? Pas zodra een mens een steekproef van de gesprekken heeft gelezen en de gesimuleerde klanten realistisch heeft bevonden.

Termen als case, run, gate en suite worden gedefinieerd in Kernbegrippen; alles over gesimuleerde gebruikers staat in Gesimuleerde gebruikers.

Vereisten

  • Python 3.11 of nieuwer, en Oloproof:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • Het voorbeeldproject, dat met het pakket meekomt. Kopieer het naar een nieuwe map en werk daar:
oloproof init --example simulated-users support-desk
cd support-desk
  • Poort 8797 vrij voor de vervangende gesimuleerde gebruiker (pas hem anders op beide plaatsen aan).

Elke stap tot "Optioneel: een echt model als de klanten" is offline en deterministisch: geen API-sleutel, geen provideraccount, geen kosten.

De bestanden

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

Voer elk commando uit vanuit support-desk/.

De desk houdt zijn eigen lus

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

De desk vraagt om het eerste bericht van de klant, beantwoordt het, en geeft elk antwoord terug voor het volgende bericht, totdat de klant stopt. Hij ziet de persona nooit. Oloproof legt vast wat er tussen hen werd uitgewisseld; de desk legt zelf niets vast. Deze baseline kan geen leveringsadres wijzigen, en hij belooft terugbetalingen "guaranteed within 5 days".

De persona's

Elke case is één klant: 12 willen een terugbetaling, 10 willen weten wanneer een bestelling aankomt, en 8 zijn verhuisd. Bijvoorbeeld:

{"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"}}}}

De klant geeft het ordernummer en de postcode alleen wanneer erom gevraagd wordt. Drie persona's stellen ook een opening in, hun eerste bericht woord voor woord.

De 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

De vervangende gesimuleerde gebruiker

Een gesimuleerde gebruiker is een model dat elke persona speelt. Hier is het user_server.py, dat dezelfde API spreekt op 127.0.0.1 en geen model is: hij leest de persona uit de prompt die Oloproof stuurt, noemt zijn doel, geeft zijn ordernummer of postcode wanneer erom gevraagd wordt, vertrekt wanneer de desk niet kan helpen, en zegt, gevraagd of zijn doel bereikt is, ja wanneer het laatste antwoord van de desk begint met "Done:". Start hem in een tweede terminal en laat hem draaien:

python user_server.py --port 8797

De 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

Geen enkel antwoord mag iets beloven. Minstens 80% van de klanten moet zijn doel bereiken, alleen beslist wanneer de ondergrens van het interval boven 80% uitkomt. En geen enkele regel beslist over gesimuleerde klanten totdat mensen minstens 80% van hun gesprekken realistisch hebben bevonden, gemeten aan dezelfde soort ondergrens.

Draaien

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 │

Elke klant kreeg antwoord, 22 van de 30 bereikten hun doel, en 12 gesprekken bevatten een belofte. Er is nog niets beslist: deze klanten zijn gesimuleerd, en niemand heeft gecontroleerd of ze zich gedragen als echte.

De gesimuleerde klanten lezen

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%

De acht die vertrokken, zijn de klanten die verhuisd waren: de baseline kan geen adres wijzigen.

De gesprekken labelen op realisme

Trek een willekeurige steekproef van de gesprekken van de run in een sheet, laat een mens elk ervan lezen, met de persona en hoe het eindigde, en laat hem aangeven of een echte klant de kant van de klant had kunnen schrijven en het daar, op die manier, had kunnen beëindigen. Hier speelt fill_realism.py die persoon en kopieert zijn oordelen uit 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%.

De persoon vond één gesprek onrealistisch: klant c05 opent met "Order: F-15. Refund.", wat leest als een formulier, niet als een mens. Met 29 van de 30 is de exacte ondergrens 82.8%, boven de lat, dus regels mogen nu over deze klanten beslissen.

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

Een echte wijziging maken

desk_v2.py wijzigt een adres zodra hij de bestelling en de nieuwe postcode heeft, en zegt dat hij een terugbetaling heeft gestart die "usually arrives within 5 days" in plaats van die te beloven. Kopieer het over desk.py, zet version: candidate onder system in oloproof.yaml, laat de vervanger draaien, en:

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

De kandidaat ontmoette dezelfde gesimuleerde klanten op dezelfde suite, dus het realisme dat op de gesprekken van de baseline werd gemeten, dekt hem: één validatie dekt één gesimuleerde gebruiker op één suite, met welk systeem hij ook sprak.

De kandidaat met de baseline vergelijken

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

Elke klant ontmoette beide desks, dus de vergelijking is gepaard, klant per klant.

Gesimuleerd naast echt

Realismelabels beoordelen elk gesprek op zichzelf. De gesimuleerde klanten naast echte zetten laat zien wat een steekproef kan missen. real.yaml speelt 20 chats opnieuw af die de menselijke medewerkers van de winkel met echte klanten voerden:

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]

Echte klanten stuurden meer en kortere berichten: ze zeggen wat ze willen verspreid over drie regels, waar de vervanger het in één nette zin zegt. Dat is het weten waard over elke gesimuleerde gebruiker, en het beslist hier nooit iets: twee ongepaarde steekproeven vergelijken vraagt een methode die Oloproof niet heeft toegelaten.

Optioneel: een echt model als de klanten

Deze stap verlaat het offline pad. Hij heeft een chatmodel nodig: lokaal Ollama (ollama pull qwen2.5vl, of elk chatmodel), zonder sleutel en zonder kosten; in de cloud provider: anthropic of openai met api_key_env, afgerekend per bericht. Laat de gesimuleerde gebruiker in oloproof.yaml ernaar wijzen:

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

Ollama beantwoordt één verzoek tegelijk, dus zet concurrency: {system: 1} en een timeout_s van een paar minuten. Een ander model is een andere gesimuleerde gebruiker: de realismelabels hierboven dekken hem niet, dus zijn gesprekken hebben een eigen steekproef nodig voordat een regel erover beslist.

Tien van de persona's, gespeeld door qwen2.5vl op deze machine tegen de kandidaat-desk, bereikten hun doel in 8 gesprekken, waar de vervanger het in alle 30 bereikte. Het model vond wat de vervanger niet kon vinden: twee klanten vroegen naar de "status" en de "shipping status" van hun bestelling, woorden die de desk niet kent, en kregen elke keer zijn begroeting terug. De een raakte door zijn berichten heen. De desk begreep de derde poging van de ander, en die klant vertrok toch: de doelcontrole, het oordeel van een model, miste het antwoord. Een realismelabel is waar dat zichtbaar wordt: een echte klant die hoort wanneer zijn bestelling aankomt, vertrekt niet ontevreden. Anders dan de vervanger gaf het model ook vaak zijn ordernummer voordat erom gevraagd werd, en schreef het één langer bericht waar de vervanger er twee korte schreef.

Problemen oplossen

SymptoomOorzaak en oplossing
Elke regel is INSUFFICIENT_EVIDENCE met simulated_user_not_validatedNiemand heeft de gesprekken van deze gesimuleerde gebruiker op deze suite gelabeld, of het realistische aandeel haalt de lat niet. Label een steekproef zoals hierboven.
a case for a simulated user needs a 'persona' object in its inputEen case heeft geen persona, of die staat onder een andere sleutel: zet persona_field.
provider connection failed bij elke caseDe vervanger draait niet, of luistert op een andere poort dan base_url noemt.
the simulated user's goal check answered ...Het model beantwoordde de doelcontrole niet met ja of nee. Probeer een ander model, of een groter.
conversation/v1 is written by OloproofDe desk legde het gesprek zelf vast. Haal dat weg: Oloproof legt vast wat er door de gesimuleerde gebruiker ging.

Beperkingen

  • De vervangende klant is een regel. Zijn gesprekken tonen het mechanisme; die van een echt model zijn rommeliger, zoals de optionele stap laat zien.
  • Of een doel bereikt werd, is het oordeel van het model van de gesimuleerde gebruiker, alleen gecontroleerd via de realismelabels.
  • Een gehoste werkruimte beslist nog niet over gesimuleerde gebruikers.
  • Gesimuleerde klanten zijn geen echte: een gevalideerde gesimuleerde gebruiker is bewijs over klanten zoals zijn persona's, voor zover mensen hem realistisch vonden, en niet verder.