本文へスキップ

ガイド

チュートリアル: 模擬された顧客を相手にするサポートデスク

会話型アシスタントを、その発言に反応するユーザーに対して評価します。それぞれ目標と知っている事実を持つ 30 人の模擬された顧客が、ある店のサポートデスクと話します。Oloproof は、各顧客が求めていたものを得られたかを測定し、模擬された顧客が現実的であると人が認めるまですべての判断を保留し、そのうえでリリースと比較を判断します。すべてのステップはこのマシン上で、モデルもネットワークも使わずに実行でき、任意のステップで実際のモデルを顧客に差し替えます。

作るもの

ある店のサポートデスクは、返金、配送に関する質問、住所の変更に答えます。バージョンをリリースしてよいかは、3 つの問いで決まります。

  • 各顧客は求めていたものを得られたか。goal_reached が、各回答の後のチェックから判断します。
  • 店が約束できないことを約束した回答はなかったか。every_turn がすべての回答を読み、「guarantee」という語を探します。
  • その回答は、実際の顧客について何かを語るものとして信頼できるか。人が会話のサンプルを読み、模擬された顧客を現実的だと認めてからはじめて、そう言えます。

ケース、実行、ゲート、スイートなどの用語は基本概念で定義しています。模擬ユーザーについてのすべては模擬ユーザーにあります。

前提条件

  • Python 3.11 以降と Oloproof:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • パッケージに同梱されているサンプルプロジェクト。新しいディレクトリにコピーし、そこで作業します:
oloproof init --example simulated-users support-desk
cd support-desk
  • 代役の模擬ユーザーのためにポート 8797 が空いていること(空いていなければ、両方の箇所で変更してください)。

「任意: 実際のモデルを顧客にする」までのすべてのステップはオフラインかつ決定的です。API キーも、プロバイダーのアカウントも、費用も不要です。

ファイル

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

すべてのコマンドは support-desk/ から実行してください。

デスクは自分のループを保つ

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

デスクは顧客の最初のメッセージを求め、それに答え、顧客が止まるまで、次のメッセージを得るために各回答を返します。デスクがペルソナを見ることはありません。Oloproof が両者の間でやり取りされたものを記録し、デスク自身は何も記録しません。このベースラインは配送先の住所を変更できず、返金が「guaranteed within 5 days」だと約束します。

ペルソナ

各ケースは 1 人の顧客です。12 人は返金を望み、10 人は注文がいつ届くかを知りたがり、8 人は引っ越しました。たとえば次のとおりです:

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

顧客は、尋ねられたときにだけ注文番号と郵便番号を伝えます。3 つのペルソナは opening、つまり最初のメッセージを一字一句そのまま設定しています。

スイート

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

代役の模擬ユーザー

模擬ユーザーは、各ペルソナを演じるモデルです。ここでは user_server.py で、これは 127.0.0.1 上で同じ API を話しますが、モデルではありません。Oloproof が送るプロンプトからペルソナを読み取り、目標を述べ、尋ねられたら注文番号や郵便番号を伝え、デスクが助けられないときは去ります。そして、目標が達成されたかを尋ねられると、デスクの最後の回答が「Done:」で始まるときに yes と答えます。2 つ目のターミナルで起動し、動かしたままにしてください:

python user_server.py --port 8797

ポリシー

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

約束する回答は 1 つもあってはなりません。顧客の少なくとも 80% が目標を達成しなければならず、これは区間の下限が 80% を上回ったときにだけ判断されます。そして、人が模擬された顧客の会話の少なくとも 80% を、同じ種類の下限で現実的だと認めるまで、模擬された顧客にもとづいて判断するルールはありません。

実行する

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 │

すべての顧客に回答があり、30 人中 22 人が目標を達成し、12 の会話に約束が含まれています。まだ何も判断されていません。これらの顧客は模擬されたもので、実際の顧客のようにふるまうかを誰も確認していないからです。

模擬された顧客を読む

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%

去った 8 人は、引っ越した顧客です。ベースラインは住所を変更できません。

会話に現実性のラベルを付ける

実行の会話から無作為なサンプルをシートに抜き出し、人にそれぞれをペルソナと会話の終わり方とともに読んでもらい、実際の顧客が顧客側を書き、そこで、そのように会話を終えることがありえたかを記してもらいます。ここでは fill_realism.py がその人の代わりとなり、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%.

その人は 1 つの会話を非現実的だと判断しました。顧客 c05 は「Order: F-15. Refund.」で始めますが、これは人ではなく書式のように読めます。30 件中 29 件で、正確な下限は 82.8% となり基準を上回るので、ルールはこれらの顧客にもとづいて判断できるようになりました。

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

実際に変更する

desk_v2.py は、注文と新しい郵便番号がわかれば住所を変更し、返金を約束する代わりに、「usually arrives within 5 days」とする返金を開始したと伝えます。それを desk.py に上書きコピーし、oloproof.yaml の system の下で version: candidate を設定し、代役を動かしたまま、次を実行します:

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

候補は同じスイートで同じ模擬された顧客に出会ったので、ベースラインの会話で測定された現実性がそれを対象とします。1 つの検証は、1 つのスイートにおける 1 つの模擬ユーザーを、それがどのシステムと話したかにかかわらず対象とします。

候補をベースラインと比較する

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

各顧客は両方のデスクに出会ったので、比較は顧客ごとに対応づけられます。

模擬と実際を並べる

現実性のラベルは各会話を単独で判断します。模擬された顧客を実際の顧客と並べると、サンプルが見落としうるものがわかります。real.yaml は、店の人間の担当者が実際の顧客と交わした 20 のチャットを再生します:

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]

実際の顧客は、より多くの、より短いメッセージを送りました。代役がきれいな 1 文で言うことを、実際の顧客は 3 行に分けて言います。これはどの模擬ユーザーについても知っておく価値があり、ここで何かを判断することは決してありません。対応のない 2 つのサンプルを比較するには、Oloproof がまだ認めていない手法が必要です。

任意: 実際のモデルを顧客にする

このステップはオフラインの道筋から外れます。チャットモデルが必要です。ローカルでは Ollama(ollama pull qwen2.5vl、または任意のチャットモデル)で、キーも費用も不要です。クラウドでは、provider: anthropic または openai と api_key_env を使い、メッセージごとに課金されます。oloproof.yaml で、模擬ユーザーをそのモデルに向けてください:

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

Ollama は一度に 1 つのリクエストにしか答えないので、concurrency: {system: 1} と、数分の timeout_s を設定してください。別のモデルは別の模擬ユーザーです。上の現実性のラベルはそれを対象としないので、どのルールもその会話にもとづいて判断する前に、その会話の独自のサンプルが必要です。

このマシン上で qwen2.5vl が演じたペルソナ 10 人は、候補のデスクに対して 8 つの会話で目標を達成しました。代役は 30 人すべてで達成していました。モデルは代役が見つけられなかったものを見つけました。2 人の顧客が注文の「status」と「shipping status」を尋ねましたが、これはデスクが知らない語で、どちらも毎回デスクの挨拶が返ってきただけでした。1 人はメッセージを使い果たしました。もう 1 人の 3 度目の試みはデスクに理解されましたが、その顧客はそれでも去りました。モデルの判断である目標チェックが、その回答を見逃したのです。それが表れるのが現実性のラベルです。注文がいつ届くかを告げられた実際の顧客は、不満を抱えて去ったりはしません。また、モデルは代役と違って、尋ねられる前に注文番号を伝えることが多く、代役が短いメッセージを 2 つ書くところを、長いメッセージ 1 つで書きました。

トラブルシューティング

症状原因と対処
すべてのルールが simulated_user_not_validated とともに INSUFFICIENT_EVIDENCE になるこのスイートで、この模擬ユーザーの会話に誰もラベルを付けていないか、現実的な割合が基準を上回っていません。上のようにサンプルにラベルを付けてください。
a case for a simulated user needs a 'persona' object in its inputあるケースにペルソナがないか、別のキーの下にあります。persona_field を設定してください。
すべてのケースで provider connection failed代役が動いていないか、base_url が示すのとは別のポートで待ち受けています。
the simulated user's goal check answered ...モデルが目標チェックに yes か no で答えませんでした。別のモデルか、より大きなモデルを試してください。
conversation/v1 is written by Oloproofデスクが会話を自分で記録しました。それを取り除いてください。Oloproof は模擬ユーザーを通過したものを記録します。

制限事項

  • 代役の顧客はルールです。その会話は仕組みを示すもので、任意のステップが示すとおり、実際のモデルの会話はもっと乱雑です。
  • 目標が達成されたかどうかは模擬ユーザーのモデルの判断であり、現実性のラベルを通じてしか確認されません。
  • ホストされたワークスペースは、まだ模擬ユーザーにもとづいて判断しません。
  • 模擬された顧客は実際の顧客ではありません。検証された模擬ユーザーは、人がそれを現実的だと認めた範囲で、そのペルソナのような顧客についての証拠であり、それ以上のものではありません。