ガイド
チュートリアル: 複数ターンの会話
Python SDK で対話型アシスタントを評価する手順を、実際に動かしながら進みます。あなたのアプリケーションは台本どおりの会話を新しいセッションに対して再生し、答えたことを conversation/v1 アーティファクトとして記録し、2 つの評価器が会話全体をそれぞれ判定します。ジャッジモデルの代わりに台本どおりの代役を使ってオフラインで実行し、続いて候補の修正を比較し、最後にターンをクラスタ化されたケースとして評価するという代替手段を示します。
区間、判断状態、リリースアクションといった用語は基本概念で定義しています。アーティファクトについてはシステムが行ったことを記録するを参照してください。
ここで Oloproof が行うこと、行わないこと
| Oloproof が行うこと | あなたのアプリケーションが行うこと |
|---|---|
| 台本どおりの会話をデータセットとして保存する。どのシステムにも同一 | 会話を駆動する: 台本の各ターンを順に尋ねる |
| 台本のすべてのターンに答えたかを確認する(ConversationCompleted) | セッションの状態を持ち、ケースごとに新しいセッションを始め、それをリセットする |
| トランスクリプト全体をモデルで判定する(ConversationJudge) | 続けられないときに何が起きるかを決め、止まったことを記録する |
| 区間を計算し、2 つのシステムを比較し、ポリシーに照らして判断する | conversation/v1 アーティファクトを記録する |
Oloproof にはユーザーシミュレーターがありません。ユーザーのターンを書くことはなく、ユーザー側はデータセットの台本そのものです。記録された会話の中のターン単位のメトリクスはなく、記録された会話を新しいシステムに対して再生することもできません。会話の評価器は Python SDK にのみ存在します。ConversationCompleted と ConversationJudge は oloproof.yaml の評価器の型ではないため、このチュートリアルでは oloproof run ではなくスクリプトを使います。
前提条件
- クイックスタートと同様に、Python 3.11 以降と pip install oloproof。
- パッケージに同梱されているサンプルファイル。実行のストアがそこに置かれるよう、新しいディレクトリにコピーしてください。
oloproof init --example conversation ~/oloproof-conversation
cd ~/oloproof-conversation| ファイル | 内容 |
|---|---|
| assistant.py | テスト対象のアプリケーション: セッション状態を持つプランのアシスタント |
| systems.py | アダプター: 台本を再生し、conversation/v1 を記録する |
| judge_offline.py | ジャッジモデルの代わりとなる台本どおりの代役 |
| evaluate.py | 評価、比較、ターンによる代替手段を実行する |
| release.yaml | 1 つの実行のためのポリシー |
| comparison.yaml | 候補をベースラインと比べるためのポリシー |
| turns_release.yaml | ターンによる代替手段のためのポリシー |
| data/conversations.jsonl | 40 の台本どおりの会話 |
| data/turns.jsonl | 同じ会話を、ターンごとに 1 ケースにしたもの |
最後の任意のステップまでは、キーもネットワークもプロバイダーの費用も必要ありません。
アプリケーション
assistant.py は 3 つの料金プランについての質問に答えます。会話が対象としているプランという 1 つの状態を保持し、「それには SSO が含まれますか?」のような追加の質問が「それ」を解決できるようにしています。ベースラインには意図的な欠陥があります。プランを覚えていないため、追加の質問にはデフォルトのプランについて答えてしまいます。人との対話を求めるユーザーは、HandoffRequested で会話を終えます。
class PlanAssistant:
def __init__(self, *, remembers_plan):
self.remembers_plan = remembers_plan
self.reset()
def reset(self):
"""Forget everything, so one conversation never leaks into the next."""
self.current_plan = None
def ask(self, question): ...ここがあなた自身のアプリケーションに置き換える部分です。チャットボットのクライアント、エージェントのセッション、あなたのサービスへの HTTP セッションなどです。それが何であれ、状態とそのリセットはそれ自身が持ちます。Oloproof が見るのはアダプターが記録するものだけです。
データセット: 台本が入力である
data/conversations.jsonl の 1 行が 1 つの会話です。
{"expected": {"plan": "enterprise"}, "id": "conv_00", "input": {"turns": ["What does the enterprise plan cost?", "Does that include SSO?"]}, "metadata": {"pattern": "pronoun_followup"}}ユーザーのターンはデータセットの内容であり、スイートのダイジェストに含まれ、それに対して測定されるどのシステムにも同一です。それが 2 つのシステムを比較可能にしています。expected はジャッジに示される参照です。40 の会話のうち、24 はプランを名指ししない追加の質問を含み、12 は毎ターンでプランを名指しし、4 は 3 ターンのうち 2 ターン目で人との対話を求めます。
アダプター
systems.py はケースごとに新しいセッションを始め、台本の各ターンを順に尋ね、返ってきたものを記録します。
from oloproof import CONVERSATION, current_case, system
def replay(case, *, remembers_plan):
script = [str(turn) for turn in case["turns"]]
session = PlanAssistant(remembers_plan=remembers_plan) # a new session per case
turns = []
truncated = False
for index, question in enumerate(script, start=1):
try:
reply = session.ask(question)
except HandoffRequested:
truncated = True # the recording stops here and says so
break
turns.append({"index": index, "asked": question, "answer": reply["answer"]})
current_case().artifact(
CONVERSATION,
{"turns": turns, "declared_turns": len(script), "truncated": truncated},
)
last = turns[-1]["answer"] if turns else None
return {"answer": last, "turns_answered": len(turns)}
@system(name="plan-assistant", version="baseline", records=(CONVERSATION,))
def baseline(case):
return replay(case, remembers_plan=False)
@system(name="plan-assistant", version="candidate-remembers-plan", records=(CONVERSATION,))
def candidate(case):
return replay(case, remembers_plan=True)ケースごとに新しいセッションを使うことが重要です。Oloproof はケースを並行して、決まった順序なしに実行するため、ケース間で共有されたセッションは、ある会話の状態を別の会話に漏らしてしまいます。records= はシステムがそのアーティファクトを記録することを宣言します。これがなければ、会話の評価器はすべてのケースを欠測として数えるのではなく、何かが実行される前に拒否されます。
conversation/v1 アーティファクト
oloproof export RUN_ID(バンドルの cases.jsonl)から得た、ベースラインが conv_00 について記録したものです。
{"declared_turns": 2, "truncated": false, "turns": [{"answer": "The enterprise plan costs a price agreed per contract.", "asked": "What does the enterprise plan cost?", "index": 1, ...}, {"answer": "The starter plan does not include SSO.", "asked": "Does that include SSO?", "index": 2, ...}]}人との対話を求めた会話については次のとおりです。
{"declared_turns": 3, "truncated": true, "turns": [{"answer": "The team plan costs $20 a month.", "asked": "What does the team plan cost?", "index": 1, ...}]}| フィールド | 意味 |
|---|---|
| turns[].index | これが答える台本のターン。1 から連続 |
| turns[].answer | アシスタントが返したもの。任意の JSON |
| turns[].asked | 任意。読むためだけのもので、エンジンは index で照合する |
| turns[].retrieval | 任意。そのターンが検索したもの。retrieval/v1 の形 |
| declared_turns | 台本が宣言したターン数 |
| truncated | 何が原因であれ、記録が台本の途中で終わっている |
アーティファクトは記録時に確認されます。宣言より少ないターンの記録は truncated: true を示さなければならず、インデックスは連続していなければならず、記録は尋ねられた以上のターンに答えることはできません。不正な形式のアーティファクトは SystemContractError で実行を止めます。
2 つの評価器と、両方を使う理由
- ConversationCompleted は決定的です。アシスタントは台本のすべてのターンに答えたか?これが最初に実行されるのは、3 ターンのうち 1 ターン目で止まった会話について他の何かを主張することは、別の会話についての主張になるからです。途中で切れた会話はこれに不合格になります。それは結果であり、欠測ケースではありません。
- ConversationJudge は、すべてのユーザーとアシスタントのターンを含むトランスクリプト全体に対するモデルジャッジです。対話型の製品が責められる失敗は関係的なものだからです。前のターンの答えと矛盾する答えは、それと並べたときにだけ誤りになります。途中で切れた会話は記録されたものに基づいて判定され、トランスクリプトはどこで止まったかをジャッジに伝えます。
evaluators = [
ConversationCompleted(),
ConversationJudge(criterion="plan_coherent", provider=..., model=..., rubric_text=RUBRIC),
]オフラインでの判定
ジャッジにはモデルが必要です。ネットワークなしで実行するために、judge_offline.py は台本どおりのプロバイダーを渡します。これは Oloproof 自身のテストが使うのと同じヘルパーです(エンジン内部の FakeProvider で、公開 API ではありません)。すべてのジャッジのプロンプトに、1 つの固定ルールで答えます。アシスタントのすべてのターンが参照の名指しするプランを名指ししていれば合格です。これにより判定は決定的になり、チュートリアルは再現可能になります。実際のジャッジモデルがどう振る舞うかについては何も測定しません。
ポリシー
release.yaml:
version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
- {id: completion-floor, metric: conversation_completed, min: 0.75}
- {id: coherence-floor, metric: plan_coherent, min: 0.80}実行する
python evaluate.pybaseline run run_...
conversation_completed: 0.900 [0.763, 0.972] over 40 conversations
plan_coherent: 0.400 [0.249, 0.567] over 40 conversations
gate BLOCK (exit 3)
completion-floor: PASS (lower_bound_meets_minimum)
coherence-floor: INSUFFICIENT_EVIDENCE (evaluator_not_validated)
first failing conversation:
conversation_completed: passed=True {'turns_recorded': 2, 'turns_declared': 2, 'truncated': False}
plan_coherent: passed=False {'provider_model': 'offline-rule'}読み方は次のとおりです。
- 完了は 40 のうち 36 で、4 つのハンドオフが残りです。その下限は 0.75 を超えるので、そのルールは PASS です。
- 一貫性は 40% で、そのルールは FAIL ではなく evaluator_not_validated で INSUFFICIENT_EVIDENCE になります。ジャッジのルールは、ジャッジが人間のラベルに対して測定されるまで判断しません(require_validated_evaluators はデフォルトで有効です。ジャッジで説明しています)。推定値はそれでも示され、それでもエビデンスです。ただ、それ単独ではリリースもブロックもできません。
- gate BLOCK (exit 3): ポリシーは INSUFFICIENT_EVIDENCE でブロックします。終了コード 3 はその状態であり、失敗ではありません。
オフラインの代役を検証しても意味がありません。この例のために書かれたルールだからです。実際のジャッジでは、oloproof review RUN_ID --criterion plan_coherent --by YOU --sample 20 で実行のサンプルにラベルを付け、その後 oloproof evaluators validate EVALUATOR_ID --by YOU を実行してください。
失敗した会話を調べる
SDK は CLI が読むのと同じストア、つまり実行したディレクトリの .oloproof/ に書き込みます。
oloproof inspect RUN_ID --case conv_00output: {
"answer": "The starter plan does not include SSO.",
"turns_answered": 2
}
judgments:
conversation_completed: passed
plan_coherent: failed
judge text, not verified:
every answer is about enterprise: Falseユーザーはエンタープライズプランについて尋ねましたが、追加の質問にはスタータープランについて答えています。oloproof inspect RUN_ID --failures は失敗したすべての会話を列挙します。プラン名のない 24 の追加の質問はすべて同じように失敗しています。次の行動はアプリケーションの中にあります。プランをセッション状態に保持することです。
候補の変更と比較
systems.py の candidate は remembers_plan=True を設定します。evaluate.py は両方のシステムを同じ台本で実行し、comparison.yaml の下でケースごとに比較します。
rules:
- {id: coherence-better, kind: superiority, metric: plan_coherent}
- {id: completion-no-worse, kind: non_inferiority, metric: conversation_completed, margin: 0.05}comparison, candidate minus baseline
conversation_completed: +0.000 [-0.127, +0.127] over 40 pairs
plan_coherent: +0.600 [+0.337, +0.817] over 40 pairs
gate BLOCK (exit 3)
coherence-better: INSUFFICIENT_EVIDENCE (evaluator_not_validated)
completion-no-worse: INSUFFICIENT_EVIDENCE (interval_overlaps_margin)一貫性の差は大きく、その区間はゼロを含みませんが、ジャッジは検証されていないため、そのルールはやはり判断しません。完了は変わらず、40 組では 5 ポイント以内であることを示せません。区間はどちらの方向にも 12.7 ポイントに達します。どちらも同じ次のステップを指しています。ジャッジを検証し、会話を増やすことです。
代替手段: ターンをケースとし、クラスターとして分析する
会話単位の判定は、会話がどれだけの頻度でうまくいったかを示しますが、どのターンでうまくいかなかったかは示しません。Oloproof には記録された会話の中のターン単位のメトリクスがないため、もう 1 つの道は、各ターンをそれぞれのケースにし、会話のターンを group_id で結びつけることです。
{"expected": {"plan": "enterprise"}, "group_id": "conv_00", "id": "conv_00_t2", "input": {"history": ["What does the enterprise plan cost?"], "question": "Does that include SSO?"}}システムは台本の履歴を新しいセッションに再生し、そのターンに答えます。
@system(name="plan-assistant-turns", version="baseline")
def turn_baseline(case):
session = PlanAssistant(remembers_plan=False)
for earlier in case["history"]:
session.ask(str(earlier))
return session.ask(str(case["question"]))1 つの会話のターンは独立していないため、いずれかのケースが group_id を持つと、スイートはポリシーが受け入れなければならない近似手法でクラスターごとに分析されます(turns_release.yaml は allow_approximate_methods: true を設定しています。クラスタ化されたケースで説明しています)。
turns as cases: turn_plan 0.667 [0.588, 0.749] over 72 turns
gate BLOCK (exit 1)
turn-plan-floor: FAIL (upper_bound_below_minimum)トレードオフは次のとおりです。
| 会話ごとに 1 ケース | ターンごとに 1 ケース、クラスタ化 | |
|---|---|---|
| 率の単位 | うまくいった会話 | 正しく答えられたターン |
| 実効サンプルサイズ | 会話の数 | やはりターンの数ではなく会話の数 |
| どのターンで失敗したか | トランスクリプトを読む | 各ターンがそれぞれの判定を持つ |
| 各ターンが見る履歴 | アシスタント自身の以前の答え | 台本の以前のユーザーのターンを再生したもの |
| 自身の以前の答えによるずれを捉えるか | はい | いいえ、各ターンは台本の履歴から始まる |
| 評価器 | ConversationCompleted、ConversationJudge(SDK のみ) | YAML または SDK の任意の評価器 |
ターンによる代替手段は 4 つのハンドオフの会話を除外するため、その 72 ターンは 36 の会話から来ています。ここでは、ExactMatch が決定的で検証を必要としないため、ジャッジが判断できなかったところで判断できます。
任意: 実際のジャッジモデル
このステップには、あなたのマシンで提供されるモデルが必要です。オフラインのチュートリアルやそのテストでは実行されません。Ollama を起動し、llama3.1 を取得した状態で次を実行します。
python evaluate.py --liveするとジャッジは provider="openai_compatible" で http://localhost:11434/v1 を呼び出します。ループバックのサーバーにはキーが不要で、マシンの外に何も送りません。クラウドのプロバイダーは環境にキーが必要で、各トランスクリプトをそのプロバイダーに送り、判定ごとに費用がかかります。実際のモデルの判定は代役のものとは異なるため、上の数値は変わります。そして検証するまで、そのルールはやはり evaluator_not_validated になります。
トラブルシューティング
| 症状 | 原因と対処 |
|---|---|
| this evaluator needs exactly one conversation/v1 artifact; the case recorded 0 | アダプターが current_case().artifact(CONVERSATION, ...) を呼ばなかったか、その前に例外を出しました。会話が早く止まった場合でも記録してください。 |
| malformed conversation/v1 artifact: ... 0 of 2 turns recorded and truncated is false | 実行は SystemContractError で止まります。declared_turns より少ないターンの記録は truncated: true を設定しなければなりません。 |
| conversation turn indexes must be contiguous starting at 1 | ターンには、それが答える台本のターンに従って 1、2、3 と番号を付けてください。 |
| evaluator 'conversation_completed' needs conversation/v1 artifacts, but system ... does not declare that it records them | @system デコレーターに records=(CONVERSATION,) を追加してください。 |
| oloproof run から Input tag 'conversation_completed' found using 'type' does not match any of the expected tags | 会話の評価器は SDK のみです。ここと同様にスクリプトを使ってください。 |
| 会話の間で答えが漏れる | セッションがケース間で共有されています。ケースごとに 1 つ作成してください。 |
| 一貫性のルールが決して判断しない | ジャッジが検証されていません。検証するか、承知のうえで require_validated_evaluators: false を設定してください。 |
制限事項
- ユーザーシミュレーターはありません。ユーザーのターンはすべてデータセットの台本から来るため、会話はアシスタントの発言によって分岐できません。
- 記録された会話の中のターン単位のメトリクスはありません。代わりに、上のトレードオフを踏まえて、ターンをクラスタ化されたケースとして使ってください。
- 会話のリプレイはありません。記録された会話を別のシステムに対して再実行することはできません。2 つのシステムを比較するとは、それぞれが同じ台本を再生することです。
- ConversationCompleted と ConversationJudge は Python SDK のみです。
- テキストのみです。ジャッジに示されるのは JSON テキストであり、画像や音声ではありません。
- オフラインのジャッジは台本どおりのルールです。その判定は仕組みを示すものであり、実際のジャッジの精度を示すものではありません。
次に読むもの
- ジャッジは、プロバイダー、検証、再キャリブレーションを扱います。
- クラスタ化されたケースは、group_id と近似手法のオプトインを扱います。
- Python API は evaluate と evaluate_comparison を扱います。
- エージェントは、エージェントのツールループについての同じ境界を扱います。