はじめに
LLMを使った処理は、最終的な出力だけを見ても「どの工程で時間がかかったのか」「どの候補を、なぜ採用しなかったのか」が分かりにくいことがあります。
今回作成した Prompt Learning Lab では、プロンプト改善を次の流れで実行しています。
- Trainデータから改善候補を作る
- Validationデータで候補を評価する
- Guardrailを満たした候補だけを採用する
- 選定後のプロンプトをTestデータで1回だけ評価する
各工程の実行順序と判断結果を確認するため、オープンソースのAI Observabilityツールである Arize Phoenix を使用して見ました。
- PythonアプリケーションからTraceを送信する
- Phoenix UIでTraceとSpanを読む
-
selectionSpanで候補の採否を確認する - Final Testの指標をローカル成果物と照合する
サンプルには感情分類を使います。
Phoenixとは
Phoenixは、LLMアプリケーションの実行経路を可視化し、評価や問題調査に利用できるオープンソースのObservabilityツールです。OpenTelemetry / OpenInferenceを利用して、アプリケーションから送信されたTraceを収集します。
最初に、TraceとSpanだけ押さえておきます。
- Trace: 1回の処理全体を表す記録
- Span: Traceを構成する個々の処理単位
- Attribute: Spanに付けるモデル名、iteration、評価値、採否などの情報
今回の実験では、1回の実験を experiment_run Traceとして記録し、その中にデータ読み込み、候補生成、評価、選定、最終テストのSpanを配置します。
experiment_run
├── dataset_load
├── baseline_validation
├── iteration_1
│ ├── train_inference
│ ├── feedback_generation
│ ├── prompt_optimization
│ ├── candidate_validation
│ └── selection
├── iteration_2
├── iteration_3
└── final_test
Phoenixの公式ドキュメントでも、Traceは複数のSpanから構成され、リクエストがどの処理を通ったかを確認するためのものと説明されています。
今回の構成
PhoenixはDockerコンテナとして起動し、PythonアプリケーションはOTLP over HTTPでSpanを送信します。
Python CLI
│
│ OTLP/HTTP: http://localhost:6006/v1/traces
▼
Phoenix container
├── Trace collector
├── Web UI: http://localhost:6006
└── Docker volume: phoenix-data
Phoenixが停止している場合でも、実験自体を失敗させない構成にしました。Phoenixへの送信で問題が起きたときは警告だけを表示し、runs/<task>/<run-id>/ に保存するローカル成果物を正とします。
また、Phoenixには入力原文や参照要約を送らず、次のような安全な情報だけを記録します。
- dataset名、revision、seed、分割比率
- モデル名
- promptや入力のSHA-256 hash
- iteration、評価指標、失敗件数
-
acceptedなどの判断結果
DockerでPhoenixを起動する
このプロジェクトでは、次のCompose設定を使用しています。
services:
phoenix:
image: ${PHOENIX_IMAGE:-arizephoenix/phoenix:latest}
ports:
- "127.0.0.1:${PHOENIX_HTTP_PORT:-6006}:6006"
- "127.0.0.1:${PHOENIX_GRPC_PORT:-4317}:4317"
environment:
PHOENIX_WORKING_DIR: /data
volumes:
- phoenix-data:/data
volumes:
phoenix-data:
まず、Compose設定が解決できることを確認してから起動します。
docker compose config
docker compose up -d
docker compose ps
ブラウザで次のURLを開き、Phoenixの画面が表示されれば起動完了です。
http://localhost:6006
ローカルでself-hostする場合の標準的なApplication Endpointも http://localhost:6006 です。Dockerを含むself-host方法は公式ドキュメントでも案内されています。
このCompose設定は学習・ローカル検証向けです。latest は後から内容が変わるため、再現性が必要な環境では検証済みのtagまたはdigestを PHOENIX_IMAGE に指定します。
PythonアプリケーションからTraceを送る
Phoenixを有効化し、送信先を設定します。
export PHOENIX_ENABLED=true
export PHOENIX_COLLECTOR_ENDPOINT=http://localhost:6006
このプロジェクトでは、PHOENIX_COLLECTOR_ENDPOINT に /v1/traces を追加したURLをOTLP exporterへ渡しています。
transport = OTLPSpanExporter(
endpoint=os.environ.get(
"PHOENIX_OTLP_ENDPOINT",
"http://localhost:6006/v1/traces",
),
timeout=5,
)
provider = TracerProvider(
resource=Resource({
"service.name": "prompt-learning",
"openinference.project.name": "prompt-learning",
}),
)
openinference.project.name を設定しているため、送信したTraceはPhoenix UIの prompt-learning Projectにまとまります。
感情分類を実行する
最初に感情分類を実行します。
python -m prompt_learning.cli run --config configs/emotion.yaml
使用した主な条件は次のとおりです。
| 項目 | 値 |
|---|---|
| dataset | dair-ai/emotion |
| seed | 42 |
| label | sadness / joy / love / anger / fear / surprise |
| split | Train 60% / Validation 20% / Test 20% |
| inference model | gpt-4o-mini |
| 最大改善回数 | 3回 |
この設定では6 labelから各100件を取得し、Train 360件、Validation 120件、Test 120件に分割します。モデルAPIを複数回呼ぶため、実行時間と料金に注意してください。
実行が完了すると、ターミナルにRun ID、選択されたprompt、Validationでの採否、Test reportの場所が表示されます。ローカル成果物は次のディレクトリに保存されます。
runs/emotion/<run-id>/
今回確認したRun IDは次の値です。
fa23ed15622e48569d3311ac840fb99d
PhoenixでTraceを確認する
Phoenix UIで Projects → prompt-learning を開き、experiment_run を選択します。
中央のツリーから、次の順番で処理されたことを確認できます。
-
dataset_loadでデータセットを読み込む -
baseline_validationで現在のpromptを評価する -
iteration_1からiteration_3で改善候補を生成・評価する - 候補ごとに
selectionで採否を決める - 最後に
final_testを実行する
Traceをログの代替として眺めるだけでなく、「設計した実行順序になっているか」を確認する点が重要です。今回であれば、Testが候補選定ループの外にあり、選定後にだけ実行されていることを確認できます。
親の experiment_run Spanを選択すると、右側のAttributesにdataset、split比率、モデル、seed、run IDなどが表示されます。別の実行と比較するときは、まずこの条件が同じかを確認します。
selection Spanで採否を見る
iteration_3 の下にある selection Spanを選択すると、候補の採否をAttributesで確認できます。
今回の表示は次のとおりです。
iteration: 3
accepted: false
accepted=false は処理の失敗ではありません。候補promptは生成できたものの、ValidationのGuardrailを満たさなかったため、現在のpromptを維持したという判断です。
感情分類では、次の条件をすべて確認しています。
- macro F1を下げない
- accuracyの低下を1 percentage point以内に抑える
- invalid output rateを増やさない
- 各labelのrecallを5 points以上下げない
実際の3候補はすべて却下されました。
| iteration | accepted | 主な理由 |
|---|---|---|
| 1 | false | sadnessのrecall低下 |
| 2 | false | invalid率の増加、複数labelのrecall低下 |
| 3 | false | invalid率の増加、sadnessのrecall低下 |
平均指標が一部改善していても、特定labelが悪化した候補を採用しないことが、この実験の狙いです。
Final Testとローカル成果物を照合する
候補選定後は、維持されたbaseline promptをTestデータで1回だけ評価します。
感情分類の最終結果は次のとおりでした。
| 指標 | 値 |
|---|---|
| accuracy | 0.4750 |
| macro F1 | 0.4766 |
| invalid output rate | 0.0167 |
| prompt version | 0 |
Phoenixは実行経路を確認する場所として便利ですが、再現の根拠はローカル成果物にも残します。
runs/emotion/fa23ed15622e48569d3311ac840fb99d/
├── manifest.json
├── iteration_history.csv
├── selected_prompt.txt
├── test_metrics.json
├── failure_cases.csv
├── confusion_matrix.png
└── report.md
確認時は、PhoenixのAttributesと report.md、iteration_history.csv を照合します。UIだけに依存しないことで、Phoenixへの接続やclient互換性に問題があっても実験結果を追跡できます。
Traceから分かったこと
今回、Phoenixを導入して特に確認しやすくなったのは次の2点です。
-
処理の順序
Train、Validation、Selection、Final Testが意図した順番で実行されている。 -
採用しなかった判断
accepted=falseをSpanに残すことで、「候補を作れなかった」のではなく「評価したうえで却下した」と区別できる。
最終スコアだけでは、候補が何回作られ、どの段階で採用されなかったかは分かりません。Traceとローカル成果物を組み合わせることで、プロンプト改善の結果だけでなく、判断に至る過程も説明できるようになりました。


