オブザーバビリティなしでLLMアプリのデバッグが困難な理由
LLMを活用したアプリをデプロイすると、最初はすべて順調に見えます。リクエストが来て、レスポンスが返って、ログには200 OKが表示される。そのうちユーザーから「おかしな回答が来た」と報告が届き、テキストの出力を眺めながらどこで問題が起きたのかまったく見当がつかない状況に陥ります。
従来のロギングでは対応しきれません。実際に送信されたプロンプトは何か、検索ステップが返した結果は何か、LLMが幻覚を起こしたのか、それともRAGパイプラインから不正なコンテキストを受け取ったのか、各ステップにかかった時間はどれくらいか——こうしたことを把握する必要があります。
去年リリースしたRAGチャットボットで、この問題を痛感しました。ユーザーが古い回答を受け取り続けていましたが、アプリログには成功した処理の記録しかありませんでした。print文でのデバッグを何時間も続けた末に判明した原因は、3ヶ月間更新されていないインデックスから古いembeddingを引っ張るベクター検索でした。外からは完全に見えない問題です。このインシデントを機に、LLMオブザーバビリティはすべてのAIプロジェクトにおける必須要件となりました。
Arize Phoenixは、まさにこの問題のために作られたオープンソースのオブザーバビリティプラットフォームです。分散トレーシングにおけるJaegerやZipkinのようなもの——ただしLLM、RAGパイプライン、AIエージェント向けに設計されています。1つのUIで完全なトレース可視化を提供します:プロンプト、補完結果、取得ドキュメント、ステップごとのレイテンシ、トークン数、評価スコアが一目で確認できます。
インストール
PhoenixはシングルDockerイメージとして提供されているため、既存のスタックにスムーズに組み込めます。docker-compose.ymlを作成します:
version: "3.8"
services:
phoenix:
image: arizephoenix/phoenix:latest
container_name: arize-phoenix
ports:
- "6006:6006" # Phoenix Web UI
- "4317:4317" # OTLP gRPC エンドポイント
- "4318:4318" # OTLP HTTP エンドポイント
volumes:
- phoenix_data:/root/.phoenix
restart: unless-stopped
environment:
- PHOENIX_WORKING_DIR=/root/.phoenix
volumes:
phoenix_data:
コンテナを起動します:
docker compose up -d
正常に起動したか確認します:
docker compose ps
docker logs arize-phoenix
ブラウザでhttp://localhost:6006を開きます。まだプロジェクトがない状態でPhoenixダッシュボードが表示されるはずです。リモートサーバーで動かしている場合は、localhostをサーバーのIPアドレスに置き換え、ファイアウォールまたはセキュリティグループでポート6006が開放されていることを確認してください。
phoenix_dataという名前付きボリュームがデータの永続化を自動的に処理するため、追加設定なしでコンテナを再起動してもトレースデータが保持されます。
設定
アプリをPhoenixに接続するには3つのステップが必要です:SDKのインストール、トレーサープロバイダーの登録、フレームワークのインストルメント化です。
パッケージのインストール
# コアの OTEL + Phoenix
pip install arize-phoenix-otel opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc
# フレームワークに合わせたインストルメンテーションパッケージを選択
pip install openinference-instrumentation-openai # OpenAI
pip install openinference-instrumentation-langchain # LangChain
pip install openinference-instrumentation-llama-index # LlamaIndex
トレーサーの登録とOpenAIのインストルメント化
from phoenix.otel import register
from openinference.instrumentation.openai import OpenAIInstrumentor
# Phoenixをトレースコレクターとして登録
tracer_provider = register(
project_name="my-llm-app",
endpoint="http://localhost:4317", # OTLP gRPC
)
# すべてのOpenAI呼び出しを自動インストルメント化 — 他のコード変更は不要
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)
LangChainも同じ方法で動作します——インストルメンターを入れ替えるだけです:
from phoenix.otel import register
from openinference.instrumentation.langchain import LangChainInstrumentor
tracer_provider = register(
project_name="rag-pipeline",
endpoint="http://localhost:4317",
)
LangChainInstrumentor().instrument(tracer_provider=tracer_provider)
以上です。あとはアプリを通常通り実行するだけで、LLMの呼び出し、検索ステップ、チェーンの実行がすべて自動的にトレースされます。
別のDockerコンテナからの接続
アプリもDockerで動いている場合は、localhostの代わりにPhoenixコンテナのサービス名を使います:
tracer_provider = register(
project_name="my-llm-app",
endpoint="http://arize-phoenix:4317", # Dockerサービス名
)
gRPCよりHTTPを好む場合も問題ありません——gRPCをクリーンに通さないプロキシの背後では特に便利です:
tracer_provider = register(
project_name="my-llm-app",
endpoint="http://arize-phoenix:4318/v1/traces",
protocol="http/protobuf",
)
動作確認とモニタリング
アプリにいくつかテストクエリを送り、http://localhost:6006を開きます。最初のトレースとともにプロジェクトのエントリが表示されるはずです。
UIでのトレースの確認
Tracesタブをクリックして任意のトレースを選択すると、スパンツリーに展開されます。典型的なRAGパイプラインはこのような構造になります:
- 最上位スパン:受信したユーザークエリ
- 子スパン:クエリのembedding生成
- 子スパン:取得したドキュメントチャンクを使ったベクター検索
- 子スパン:実際のLLM呼び出し——注入されたコンテキストを含む完全なプロンプトがここで確認できます
- 各スパンのメタデータ:レイテンシ、トークン数、モデル名、終了理由
まさにこれが、古いembeddingの問題をデバッグしていたときに欠けていたものです。Phoenixを動かしていれば、取得されたチャンクのタイムスタンプが何ヶ月も前の日付であることがトレース上に直接表示されていたはずです。以前は何時間もの推測作業が必要だったことが、5分以内で特定できます。
RAG品質評価の実行
Phoenixには幻覚検出と検索関連性のための組み込みエバリュエーターが含まれています。トレースされたスパンをデータフレームとして取得し、評価を実行します:
from phoenix.client import Client
from phoenix.evals import (
HallucinationEvaluator,
RelevanceEvaluator,
run_evals,
)
from phoenix.evals import OpenAIModel
# トレースされたスパンを取得
client = Client(endpoint="http://localhost:6006")
spans_df = client.get_spans_dataframe(project_name="rag-pipeline")
# 評価コストを抑えるために小さいモデルを使用
eval_model = OpenAIModel(model="gpt-4o-mini")
results = run_evals(
dataframe=spans_df,
evaluators=[
HallucinationEvaluator(eval_model),
RelevanceEvaluator(eval_model),
],
provide_explanation=True,
)
print(results[0].head())
スコアはPhoenixに書き戻され、各スパン上にインラインで表示されます。トレースリストを幻覚スコアでフィルタリングし、関連性の低い順にソートして、問題を引き起こした正確なプロンプトとコンテキストのペアに直接アクセスできます。
AIエージェントのモニタリング
エージェント型ワークフローも同様に扱われます。Phoenixはすべてのツール呼び出しを子スパンとしてトレースします。4回のWeb検索を実行し、1つのURLを取得し、最終的な回答を書くエージェントは、ツリー上に6つのスパンとして表示され、それぞれの入力・出力・タイミングが確認できます。この可視性だけで、エージェントが何をしているかを把握するためにあちこちに散りばめていたprint文によるスキャフォールディングの大部分を置き換えられます。
定期的に確認すべきメトリクス
1週間分のデータが蓄積されたら、継続的に確認する価値のある3つのシグナルがあります:
- プロジェクトごとのP95レイテンシ——急激なスパイクは通常、検索のボトルネックか上流のモデルタイムアウトを示します
- トークン使用量のトレンド——プロンプトの肥大化が請求ダッシュボードに現れる前に把握できます
- 時間経過に伴う幻覚率——上昇傾向にある場合は、ほぼ間違いなく検索パイプラインかプロンプトテンプレートに変更があったことを意味します
カスタムメタデータの付与
標準のOTEL属性はPhoenixですぐに動作します。スパンにユーザーIDとセッションIDをタグ付けすることで、インシデントのトリアージ時間を大幅に短縮できます:
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("user-query") as span:
span.set_attribute("user.id", user_id)
span.set_attribute("session.id", session_id)
span.set_attribute("query.type", "rag")
# ... ここにLLMの呼び出しを記述
コンテナ起動から最初のトレース確認まで、セットアップ全体で約20分かかります。その後は、LLMの問題のデバッグが何時間もの推測作業からトレースをクリックするだけの数分作業に変わります。オブザーバビリティなしでAIを本番環境で動かすのは、目をつぶって飛行しているようなものです。次の機能をリリースする前にPhoenixを導入しましょう。

