TL;DR
- LLMアプリのObservabilityは「ログ・メトリクス・トレース」の古典的3本柱では不十分。プロンプト・トークン・品質スコアの3軸が新たに必要
- OpenTelemetryのOTLP exporterでトレースを統一収集し、LangSmith/Langfuseへ送ることで「何を聞いてどう答えたか」を記録できる
- アラートは「レイテンシ」「トークンコスト」「品質スコア低下」の3軸で設定するのが2026年の実装標準
- 既存のAPMツール(Datadog/New Relicなど)はLLM特有の非構造化IOに対応できていない(公式値:2026年時点でOpenTelemetry gen-ai semconv v1.28が策定中)
- 本記事の設定例はPython 3.11 + opentelemetry-sdk 1.24 + langsmith 0.1.xで動作確認済み(確認日: 2026-05-01)
この記事の目的と成功基準
- 目的: LLMアプリのObservabilityをOpenTelemetryベースで再設計し、本番運用に必要なトレース・アラートを実装できるようにする
- 想定読者: LLMプロダクトを本番稼働させているAIエンジニア・SRE
- 成功基準: 記事を読み終えた後、OTELトレーサーの初期化とLangSmith/Langfuseへの送信設定を自分で書けること
はじめに:なぜ従来のAPMがLLMに効かないのか
2023〜2024年にかけてLLMアプリを本番に投入したチームの多くが、Datadogや New Relic などの既存APMをそのまま流用した。結果として「サービスは動いているのに何が起きているか分からない」という状態に陥ることが多かった。
問題は3つある。
1. 非構造化入出力: HTTPリクエストのボディが自然言語なので、従来の「エンドポイント×ステータスコード」の集計では品質が見えない
2. 確率的な出力: 同じリクエストでも毎回異なるレスポンスが返る。エラーレートの計算が成り立たない
3. トークンコストが見えない: API呼び出しの成功/失敗だけでなく、トークン消費量とコストを追跡しないと予算管理ができない
これらを解決するのが「Observability 2.0」の考え方だ。従来のメトリクス・ログ・トレースの三本柱に、プロンプト品質・トークンコスト・LLM品質スコアを加えた六軸での監視体制を構築する。
Observability 2.0の全体像
┌─────────────────────────────────────────────────────┐
│ LLMアプリ (Python) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ ユーザー │ │ LLM Chain│ │ ツール呼び出し│ │
│ │ リクエスト│───▶│ (LangChain│───▶│ (検索/DB等) │ │
│ └──────────┘ │ /LlamaI.) └──────────────┘ │
│ └────┬─────┘ │
│ │ OpenTelemetry Span │
└────────────────────────┼────────────────────────────┘
│ OTLP (gRPC/HTTP)
┌──────────────┼──────────────────┐
▼ ▼ ▼
LangSmith Langfuse Grafana/Tempo
(品質分析) (コスト追跡) (インフラ統合)
各ツールの使い分け方針は以下の通り。
| ツール | 主な用途 | 特徴 |
|---|---|---|
| LangSmith | プロンプト品質・デバッグ | LangChain公式、デバッグUI が優秀 |
| Langfuse | コスト追跡・A/Bテスト | OSS・セルフホスト可能、LLM非依存 |
| OpenTelemetry | インフラ層との統合 | ベンダー中立、既存APMへのブリッジ |
詳細な設計方針の検討プロセスはLLM本番運用チェックリストにまとめてある。
D-3:OpenTelemetryの設定(OTLP Exporter)
OpenTelemetry公式ドキュメントに従い、Python SDKでトレーサーを初期化する。
前提条件
Python 3.11+
opentelemetry-sdk >= 1.24.0
opentelemetry-exporter-otlp-proto-grpc >= 1.24.0
opentelemetry-instrumentation-httpx >= 0.45b0
基本設定:OTLP Exporter初期化
# otel_setup.py
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
def setup_tracer(service_name: str, otlp_endpoint: str = "http://localhost:4317") -> trace.Tracer:
"""
OpenTelemetry TracerProviderを初期化してグローバルに設定する。
Args:
service_name: サービス識別名(例: "llm-chat-api")
otlp_endpoint: OTLPコレクターのエンドポイント
"""
resource = Resource(attributes={
SERVICE_NAME: service_name,
"deployment.environment": "production",
"ai.framework": "langchain", # gen-ai semconv に準拠
})
exporter = OTLPSpanExporter(
endpoint=otlp_endpoint,
insecure=False, # 本番では必ずTLS
)
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
return trace.get_tracer(service_name)
LLMリクエストへのSpan付与
# llm_client.py
import time
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage
tracer = trace.get_tracer("llm-chat-api")
def chat_with_tracing(user_message: str, model: str = "gpt-4o") -> str:
"""
LLMリクエストにOpenTelemetryスパンを付与する。
gen-ai semconv (v1.28) の属性名を使用。
参考: https://opentelemetry.io/docs/specs/semconv/gen-ai/
"""
with tracer.start_as_current_span("llm.chat") as span:
span.set_attribute("gen_ai.system", "openai")
span.set_attribute("gen_ai.request.model", model)
span.set_attribute("gen_ai.request.temperature", 0.7)
start_time = time.monotonic()
try:
llm = ChatOpenAI(model=model, temperature=0.7)
response = llm.invoke([HumanMessage(content=user_message)])
latency_ms = (time.monotonic() - start_time) * 1000
# gen-ai semconv のトークン属性
span.set_attribute("gen_ai.usage.input_tokens",
response.response_metadata.get("token_usage", {}).get("prompt_tokens", 0))
span.set_attribute("gen_ai.usage.output_tokens",
response.response_metadata.get("token_usage", {}).get("completion_tokens", 0))
span.set_attribute("llm.latency_ms", latency_ms)
span.set_status(Status(StatusCode.OK))
return response.content
except Exception as e:
span.set_status(Status(StatusCode.ERROR, str(e)))
span.record_exception(e)
raise
この設定でCollectorにトレースが届くようになる。次にLangSmithとLangfuseへの送信設定を見ていく。
D-4:LangSmithへのトレース送信
LangSmith公式ドキュメントによれば、環境変数を設定するだけでLangChainのすべての呼び出しが自動追跡される。
環境変数設定
# .env(git管理外に置くこと)
LANGCHAIN_TRACING_V2=true
LANGCHAIN_ENDPOINT="https://api.smith.langchain.com"
LANGCHAIN_API_KEY="lsv2_pt_xxxxxxxxxxxxxxxx"
LANGCHAIN_PROJECT="production-llm-app"
コールバックハンドラーによる明示的トレース
環境変数だけでは取得できないカスタムメトリクスが必要な場合、コールバックハンドラーを使う。
# langsmith_callback.py
from langsmith import Client
from langsmith.run_helpers import traceable
from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage
import os
client = Client()
@traceable(
project_name=os.getenv("LANGCHAIN_PROJECT", "default"),
tags=["production", "chat-endpoint"],
)
def traced_chat(user_message: str, session_id: str) -> dict:
"""
LangSmithにトレースを送信する関数。
@traceable デコレーターで自動的にrun_idが付与される。
"""
llm = ChatOpenAI(model="gpt-4o", temperature=0.7)
response = llm.invoke([HumanMessage(content=user_message)])
return {
"output": response.content,
"session_id": session_id,
# カスタムメタデータ(LangSmith UIで検索・フィルター可能)
"metadata": {
"user_segment": "premium",
"feature_flag": "v2-prompt",
}
}
LangSmithで見るべき指標
LangSmithのダッシュボードで最低限監視する指標は以下の4つ。
| 指標 | アラート閾値(経験則) | 意味 |
|---|---|---|
| 平均レイテンシ | > 5,000ms | ユーザー体験が著しく劣化 |
| エラーレート | > 2% | LLMへの接続問題 |
| フィードバックスコア | < 0.7 / 1.0 | 品質低下 |
| トークン/リクエスト | 急増時 | プロンプトインジェクション疑い |
品質スコアの設計についてはLLMアウトプット品質ゲートに詳しい。
D-5:Langfuseへのトレース送信
Langfuse公式ドキュメントでは、OpenTelemetryのOTLP exporterをLangfuseのエンドポイントに向けることで統合できると説明されている。
LangfuseのOTLP Exporter設定
# langfuse_otel_setup.py
import base64
import os
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
def setup_langfuse_tracer(service_name: str) -> trace.Tracer:
"""
LangfuseのOTLPエンドポイントにトレースを送信する設定。
Langfuseはベーシック認証をAuthorizationヘッダーで受け取る。
参考: https://langfuse.com/docs/integrations/opentelemetry/python-otel
"""
public_key = os.environ["LANGFUSE_PUBLIC_KEY"]
secret_key = os.environ["LANGFUSE_SECRET_KEY"]
host = os.environ.get("LANGFUSE_HOST", "https://cloud.langfuse.com")
# Basic認証ヘッダーを生成
auth_token = base64.b64encode(f"{public_key}:{secret_key}".encode()).decode()
exporter = OTLPSpanExporter(
endpoint=f"{host}/api/public/otel/v1/traces",
headers={"Authorization": f"Basic {auth_token}"},
)
resource = Resource(attributes={SERVICE_NAME: service_name})
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
return trace.get_tracer(service_name)
Langfuseでのコスト追跡
Langfuseはトークン消費量とモデル料金を自動計算するモデル定義機能を持つ。
# langfuse_cost_tracking.py
from langfuse import Langfuse
from langfuse.decorators import observe, langfuse_context
from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage
langfuse = Langfuse()
@observe() # Langfuseの自動トレースデコレーター
def generate_response(prompt: str, model: str = "gpt-4o") -> str:
"""
@observe デコレーターでLangfuseにトレースを自動送信する。
トークン消費量はopenai_model_usageから自動取得される。
"""
langfuse_context.update_current_observation(
name="chat-generation",
input=prompt,
metadata={"model": model, "environment": "production"},
)
llm = ChatOpenAI(model=model)
response = llm.invoke([HumanMessage(content=prompt)])
langfuse_context.update_current_observation(
output=response.content,
usage={
"input": response.response_metadata.get("token_usage", {}).get("prompt_tokens", 0),
"output": response.response_metadata.get("token_usage", {}).get("completion_tokens", 0),
}
)
return response.content
セルフホスト版Langfuseの導入手順は公式GitHubリポジトリのDockerComposeファイルを参照。コスト追跡はエージェントの3層アーキテクチャを設計する際にも重要で、AIエージェント三層成熟度モデルと組み合わせると効果的だ。
D-6:アラート設計の3軸
Observability 2.0のアラートは「レイテンシ・コスト・品質」の3軸で設計する。Prometheusのrecording rulesとalert rulesの例を示す。
Prometheusアラートルール設定
# llm-alerts.yaml
groups:
- name: llm_observability
interval: 60s
rules:
# === レイテンシアラート ===
- alert: LLMHighLatency
expr: |
histogram_quantile(0.95,
rate(llm_request_duration_ms_bucket[5m])
) > 5000
for: 5m
labels:
severity: warning
annotations:
summary: "LLMレスポンスのP95レイテンシが5秒を超えています"
description: "{{ $labels.service }} のP95レイテンシ: {{ $value }}ms"
# === コストアラート ===
- alert: LLMTokenCostSpike
expr: |
rate(llm_token_cost_usd_total[1h]) * 3600 > 10.0
for: 15m
labels:
severity: critical
annotations:
summary: "LLMトークンコストが時間あたり$10を超えています"
description: "現在のコスト予測: ${{ $value }}/hour"
# === 品質アラート ===
- alert: LLMQualityScoreDrop
expr: |
avg_over_time(llm_quality_score[30m]) < 0.65
for: 10m
labels:
severity: warning
annotations:
summary: "LLM品質スコアが閾値を下回っています"
description: "30分平均スコア: {{ $value }} (閾値: 0.65)"
品質スコアの算出方法(RAGの場合はfaithfulness、QA精度など)は用途によって異なる。基本的な考え方はLLMガードレール設計の出力検証セクションを参照。
D-7:本番運用での注意点(Pitfalls)
Pitfall 1: プロンプトをそのままログに残すな
ユーザーの入力がPII(個人識別情報)を含む場合、プロンプトをそのままログに残すとGDPR/個人情報保護法に抵触する。
# pii_sanitizer.py
import re
from typing import Optional
def sanitize_prompt_for_logging(prompt: str) -> str:
"""
ログに記録する前にPIIをマスクする。
経験則: email、電話番号、クレジットカード番号を対象とする。
"""
# メールアドレスのマスク
prompt = re.sub(r'[\w\.-]+@[\w\.-]+\.\w+', '[EMAIL]', prompt)
# 電話番号のマスク(日本形式)
prompt = re.sub(r'\d{2,4}-\d{2,4}-\d{4}', '[PHONE]', prompt)
# クレジットカード番号のマスク
prompt = re.sub(r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b', '[CARD]', prompt)
return prompt
Pitfall 2: サンプリングレートの設定を誤る
LLMアプリは1リクエストあたりのトレースデータ量が大きい。本番では100%サンプリングを避け、Head-based samplingを設定する。
# sampling_setup.py
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased, ParentBased
# 本番: 10%サンプリング(公式推奨: 1-10%)
# 参考: https://opentelemetry.io/docs/concepts/sampling/
sampler = ParentBased(root=TraceIdRatioBased(0.1))
Pitfall 3: LangSmithとOTELで二重課金が起きる
LangSmithの自動トレースを有効にしたままOTELのコールバックも設定すると、同じリクエストが2回記録される。環境変数で片方を無効化する。
# LangSmithのみ使う場合
LANGCHAIN_TRACING_V2=true
# OTELは使わない(OTEL_EXPORTER_OTLP_ENDPOINTを設定しない)
# OTELのみ使う場合
LANGCHAIN_TRACING_V2=false
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317
まとめ:Observability 2.0への移行ステップ
- 今週:
otel_setup.pyをコピーしてサービスに組み込み、ローカルで動作確認する - 今月: LangSmithかLangfuseのどちらかを選んで本番に繋ぎ、トークンコストの可視化から始める
- 3ヶ月後: Prometheusアラートルールを3軸(レイテンシ・コスト・品質)で設定し、SLOを定義する
OpenTelemetryのgen-ai semconvは2026年時点でv1.28が策定中(OpenTelemetry GitHubで確認)で、今後属性名が変わる可能性がある。実装時は必ず最新のsemconvドキュメントを参照すること。
FAQ
Q. LangSmithとLangfuseはどちらを選ぶべきか?
LangChainを使っているならLangSmithの統合が最もシンプル。プロバイダー非依存でセルフホストしたい場合はLangfuse(OSS)が適している。両方を試して、チームのワークフローに合う方を選ぶのが現実的。
Q. OpenTelemetryのgen-ai semconvは安定版か?
2026年6月時点でまだExperimentalステータス。本番コードに組み込む場合は属性名の変更が将来起きる前提で、設定を一箇所に集約しておくこと(otel_setup.pyのような初期化ファイルにまとめる)。
Q. トークンコストのアラートは何を閾値にすればいいか?
月予算から逆算するのが基本。月$300が予算なら時間あたり$0.42。最初の1ヶ月は閾値を高めに設定して実態を把握し、その後絞り込む(経験則)。
Q. ローカル開発環境ではどうすればいいか?
ローカルではLANGCHAIN_TRACING_V2=trueとAPIキーを設定するだけで動く。OTELコレクターはDocker Composeで立ち上げる。OpenTelemetry Collector公式イメージを使う。
Q. LLMの品質スコアをどう自動計算するか?
用途によって異なる。RAGの場合はfaithfulness(生成内容がコンテキストと矛盾していないか)、チャットの場合は人間フィードバックのcollect→train→scoreパイプラインが一般的。LangSmithのevaluate()APIが便利。
References
- OpenTelemetry Python SDK公式ドキュメント — トレーサー初期化・エクスポーターの設定
- OpenTelemetry Semantic Conventions for GenAI — gen-ai semconvの属性定義(v1.28 Experimental)
- LangSmith Tracingドキュメント — LangChainとの統合・コールバックハンドラー
- Langfuse OpenTelemetry Integration — OTLPエンドポイント経由の統合方法
- OpenTelemetry Sampling概念ガイド — Head-based/Tail-based samplingの使い分け
- Langfuse GitHub(セルフホスト版) — Docker Composeでのセルフホスト手順
