はじめに
Observability製品におけるLLM呼び出しを記録する形式(トレース形式)がOpenTelemetry(OTel)によって標準化(GenAI Semantic Conventions)されつつある。
DatadogやGrafanaも標準規約(GenAI Semantic Conventions)のスキーマをサポートしており、GenAI Semantic Conventionsは今後監視ツールのデファクトスタンダードになりそうな気がするので、実際にトレースを出してみる。
GenAI Semantic Conventionsの多くの属性は執筆時点(2026年7月)でまだ Development(実験的)ステータスです。属性名や構造は今後変わる可能性があります。
1. GenAI Semantic Conventions とは
LLMを呼び出したとき、何をどんな名前で記録するかを定めた規約。仕様はopen-telemetry/semantic-conventions-genaiリポジトリで管理されている。
分散トレーシング
分散トレーシング自体は2010年頃に出てきた概念で、マイクロサービスアーキテクチャにおいて処理のどこでどれだけ時間がかかったかを追跡する手法。
LLMアプリ(エージェント)も色々なサービス・ツールが組み合わさっているので、例えば「チャットボットの応答に12秒かかった」というとき、
ある時点の出来事の記録であるログを見るだけだと、時間がかかっているのがLLM本体なのか、文書検索なのか、ツールの実行なのか切り分けが難しい。のでアクションの開始と終了をスパンとしてセットで持つ分散トレーシングが活きてくる。
分散トレーシングの基本単位がスパンとトレーシングで、GenAI Semantic ConventionsはスパンにLLM呼び出しの情報をどんな属性名で書き込むかを標準化したもの。
(参考)
スパン: 開始時刻・終了時刻・その作業の詳細情報(属性)がひとまとまりになったもの
トレース: 1つのユーザーリクエストの中で発生した複数のスパンをつなげたもの
スパン名の規則
{gen_ai.operation.name} {gen_ai.request.model}
例: chat claude-sonnet-5
LLM呼び出しだけでなく、エージェントのツール実行や検索も規約に含まれ、ツール実行は execute_tool {ツール名}、RAGの検索は {operation} {データソースID} という形になる。
属性の例
| 属性 | 要求レベル | 例 | 説明 |
|---|---|---|---|
gen_ai.operation.name |
Required |
chat, embeddings, execute_tool
|
操作名 |
error.type |
Conditionally Required | timeout |
エラー種別 |
gen_ai.input.messages |
Opt-In | - | プロンプト全文 |
gen_ai.output.messages |
Opt-In | - | 応答全文 |
gen_ai.system_instructions |
Opt-In | - | システムプロンプト |
error.typeはざっくりした仕分けラベル操作失敗時にのみ記録。言語によってはエラーがラップされてそのままでは仕分けできないので、その場合はアンラップした中身のエラー型を使う(Pythonでは気にしなくてよい)
プロンプトと応答全文はデフォルトでは記録しない(オプトイン)。
従来のアプリログでは個人情報はマスキングして出すケースが多いが、LLMアプリのログではデフォルト出さない、になりそう。
LLMだとガードレールや出力の強制度合によっては想定しないものが出てしまうことがあるのでデフォルト記録しない、は安心。
2. GenAI Semantic Conventionsに沿った応答を試してみる
OpenAIサーバのフリをするローカルサーバ
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/v1/chat/completions", methods=["POST"])
def chat():
return jsonify({
"id": "chatcmpl-demo123",
"object": "chat.completion",
"created": 1752537600,
"model": "gpt-4o-mini",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "オブザーバビリティとは、外部出力からシステムの内部状態を理解できる性質のことです。",
},
"finish_reason": "stop",
}],
"usage": {"prompt_tokens": 23, "completion_tokens": 40, "total_tokens": 63},
})
if __name__ == "__main__":
app.run(port=8000)
OpenAI SDKの呼び出しをトレースする
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
from openai import OpenAI
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
# 呼び出しを全部トレースする
OpenAIInstrumentor().instrument()
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="dummy",
)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "オブザーバビリティとは何ですか?"}],
)
print("LLM応答:", resp.choices[0].message.content)
provider.force_flush()
スパン全文
LLM応答: オブザーバビリティとは、外部出力からシステムの内部状態を理解できる性質のことです。
{
"name": "chat gpt-4o-mini",
"context": {
"trace_id": "0x7aaf83c8cef21310fb395639ac5483df",
"span_id": "0x9fcd69c0d7b35dc1",
"trace_state": "[]"
},
"kind": "SpanKind.CLIENT",
"parent_id": null,
"start_time": "2026-07-15T08:05:00.608656Z",
"end_time": "2026-07-15T08:05:02.811302Z",
"status": {
"status_code": "UNSET"
},
"attributes": {
"gen_ai.operation.name": "chat",
"gen_ai.request.model": "gpt-4o-mini",
"gen_ai.provider.name": "openai",
"server.address": "localhost",
"server.port": 8000,
"gen_ai.response.finish_reasons": [
"stop"
],
"gen_ai.response.model": "gpt-4o-mini",
"gen_ai.response.id": "chatcmpl-demo123",
"gen_ai.usage.input_tokens": 23,
"gen_ai.usage.output_tokens": 40
},
"events": [],
"links": [],
"resource": {
"attributes": {
"telemetry.sdk.language": "python",
"telemetry.sdk.name": "opentelemetry",
"telemetry.sdk.version": "1.43.0",
"service.instance.id": "4b2fffcd-bf44-4cc2-9c76-3306b4a26da8",
"service.name": "unknown_service"
},
"schema_url": ""
}
}
3. 呼び出しは正常終了だけど品質が低い回答を拾いたい
応答は返ってきてるけど中身がデタラメ(ハルシネーション)っていうケースはどうやって拾うのか?
モデル呼び出しのスパンで判定するのは難しいので、LLM-as-a-Judgeなどの評価を別で行うことが多い。
評価結果の記録用にgen_ai.evaluation.resultというイベントが定義されており、gen_ai.evaluation.name(評価指標名)、gen_ai.evaluation.score.value(評価スコア)、gen_ai.evaluation.score.label(数値のスコアをrelevantのような人間が判別できる形にしたもの)などの属性を持っている。
また、このイベントはgen_ai.response.idで元のLLM呼び出しイベントと手動で紐付ける設計になっている。(評価は非同期でサンプリングして行うことが多いため、手動紐づけになっているものと思われる)
おわりに
今回はダミーサーバで試したので、実際にLLMアプリを作った際にきちんと取り入れたい。
参考