
LLMオブザーバビリティのセッション・トレース・スパン:構造的な階層ではないものが1つある
Datadogの用語ページは、「LLM observability sessions traces spans」というGoogle検索で1位に表示されるページですが、この3つの言葉のうち2つしか定義していません。3つではありません。欠けている1つは gen_ai.conversation.id に対応しており、欠けている理由は、OpenTelemetryの仕様がこれを構造的な階層として一度も定義しなかったからです。そもそもオブザーバビリティそのものの必要性確認したい方は、こちらからどうぞ。この記事は、あの記事が止まった地点、つまりデータモデルの話から始めます。
要点
- スパンはトレースの内側に入れ子になり、トレースはセッションにまとめられます。ネストは内側から外側へ、スパン、トレース、セッションの順です。
- スパンは1回の時間計測されたオペレーション、トレースは1件のエンドツーエンドのリクエスト、セッションは1つのマルチターン会話です。
- OpenTelemetryのGenAIセマンティック規約は、スパンと
gen_ai.conversation.id属性を定義しています。セッションという階層は定義していません。 - トレースIDとスパンIDはコンテキストを通じて自動的に伝播します。セッションIDは伝播しません。毎ターン、自分で設定する必要があります。
セッション vs トレース vs スパン、一目でわかる比較
LLMオブザーバビリティでは、スパンは1回の時間計測されたオペレーション(モデル呼び出し、検索ステップなど)、トレースは1件のリクエストが生成するスパンの木、セッションは同じ会話からの複数のトレースをまとめるものです。ネストは内側に向かいます。スパンはトレースの内側、トレースはセッションの内側です。3つ目のグループ分けこそが、見た目通りではない存在です。
| 階層 | 何を包むか | 存続期間 | IDを設定するのは誰か | 何がわかるか | 会話あたりの典型的な数 |
|---|---|---|---|---|---|
| セッション | 1つのユーザー会話からの複数のトレース | 数分から数日。非アクティブタイムアウトまたは明示的なクローズで終了(ベンダー定義) | あなたが毎ターン手動で設定 | この会話全体は成功したか? | 1 |
| トレース | 1件のエンドツーエンドのリクエストまたはターン | 数ミリ秒から数秒 | 自動(SDK / OTel) | このターンで何が起きたか? | 通常5〜20 |
| スパン | 1回のオペレーション:検索、モデル呼び出し、ツール呼び出し | 1ミリ秒未満から数秒 | 自動(SDK / OTel) | どのステップが遅かったか、間違っていたか、高かったか? | トレースあたりおよそ3〜30 |
上記の数と存続期間の数字は、RAGチャットボットやエージェントループで想定される典型的な範囲であり、管理された実験の実測値ではありません。あなたの数字は異なるでしょう。変わらないのは、セッションの行こそが仕様上は構造的な階層ではないという点であり、「セッション:あなたのツールがおそらく独自に発明した階層」というセクションでそれを証明します。
スパンとは何か?そしてスパンの kind とは?
スパンとは、名前、開始タイムスタンプ、終了タイムスタンプ、ステータスコード、そしてキーと値の属性バッグを持つ、1回の時間計測されたオペレーションです。LLMトレーシングでは、有用なデータは属性側にあります。gen_ai.usage.input_tokens、gen_ai.usage.output_tokens、gen_ai.request.model は、そのオペレーションのコストと、どのモデルが実行したかを教えてくれます。
スパンは1回のオペレーションであり、1回の関数呼び出しではない
すべてのスパンは親スパンIDへのポインタ(ルートスパンでは空)を持ち、これが木を構築します。属性バッグはオープンで、必要なコンテキストを何でも添付できます。OpenTelemetry GenAIスパン規約(ステータス:Development)は、すべてのGenAIスパンに gen_ai.operation.name と gen_ai.provider.name を必須とし、上記のトークン使用量属性を推奨しています。
Datadogの用語ページにある実践的なルールを1つ。LLM、Workflow、Agentのスパンはルートスパンになれますが、Tool、Task、Embedding、Retrievalのスパンはなれません。これはDatadogのルールであって普遍的なものではありませんが、これを明言しているのは同社だけであり、親のないツール呼び出しから始まるトレースを作らずに済みます。
スパンの種類:同じアイデア、5つの語彙
すべてのツールは、「このスパンはモデル呼び出しである」と「このスパンは検索である」を言い分ける方法が必要です。ただ、その言葉で一致していません。
| ツール | 「オペレーションの種類」を表す言葉 | 値 |
|---|---|---|
| OpenTelemetry GenAI | gen_ai.operation.name 属性 | 15の周知の値(chat、embeddings、execute_tool、invoke_agent、retrieval、ほか10個)。該当するものがあれば1つをMUSTで使用し、どれも該当しない場合のみカスタム値が許可される |
| Datadog | Span kind | LLM、Workflow、Agent、Tool、Task、Embedding、Retrieval |
| OpenInference / Phoenix | Span kind | CHAIN、LLM、TOOL、RETRIEVER、RERANKER、EMBEDDING、AGENT、GUARDRAIL、EVALUATOR、PROMPT |
| Langfuse | Observation type | generation、span、event |
| LangSmith | Run type | LLM、chain、tool、retriever |
OpenInferenceの仕様は10種類を挙げます。Datadogは7種類。OTelは第3の道を採ります。そのGenAI属性レジストリは、gen_ai.operation.name に対して15の周知の値(chat、create_agent、create_memory、create_memory_store、delete_memory、delete_memory_store、embeddings、execute_tool、generate_content、invoke_agent、invoke_workflow、plan、retrieval、search_memory、text_completion)を公開し、そのいずれかが該当する場合はその値をMUSTで使用すること、カスタム値はどれも該当しないときに限りMAYで使用できると定めています。つまり、これは半オープンなenumであり、enumの欠如ではありません。3つのリスト、3つの長さ、そして相互の整合はゼロ。ツールを選定中なら、この語彙の差は機能リストより重要です。ダッシュボードとアラートフィルターのキーになるのは、この語彙だからです。
トレースとは何か?なぜ木の形が重要なのか?
トレースとは、1件のリクエストが生成するスパンの木です。1つのルートスパンが頂点にあり、他のすべてのスパンは親スパンIDの辺でその下にぶら下がります。重要なのは木の形そのものです。フラットなログは「何かが遅かった」と教えてくれますが、木は「どのステップが遅かったか」と「どのステップが不正な出力を生んだか」を教えてくれます。
chat_request (root) 2,340ms
├── retrieval 410ms
│ └── rerank 85ms
├── chat gpt-4o 1,720ms
└── tool_call: search_calendar 190msこの木を読めば、診断は一瞬です。レイテンシの74%はモデル呼び出しにあり、検索にはありません。5つのタイムスタンプのフラットなログでも合計は同じですが、要因の帰属はまったくわかりません。
エージェントループでは、この木は単純なRAGリクエストより深く、広くなります。各ツール呼び出しは独自のサブツリーを生成し、5ステップのエージェントターンは1つのルートの下に30以上のスパンを簡単に生みます。これは正常であり、後述のスパン粒度の問題が存在する理由でもあります。
トレーシングとロギングの区別もここで重要です。ロギングはイベントを記録し、トレーシングは因果関係を記録します。何をログに残し何をトレースするかの線引きでまだ迷っているなら、LLMロギングのベストプラクティスの記事でその線引きをしています。
セッション:あなたのツールがおそらく独自に発明した階層
いいえ。セッションはOpenTelemetry GenAI規約における構造的な階層ではありません。仕様が定義しているのはスパンと gen_ai.conversation.id 属性(条件付き必須、「利用可能な場合」、ステータス:Development)であり、この属性はメッセージを相関させるために使う会話またはスレッドの一意な識別子と説明されています。各ベンダーはその属性の上に独自のセッションオブジェクトを構築しています。この検索結果ページ上で仕様のステータスを率直に述べているのは他には誰もいないので、ここで明示します。
その帰結こそが、この記事全体が伝えるために存在する一文です。
セッションはグループ化キーであり、親スパンではない。トレースIDのように伝播せず、毎ターン自分で設定する必要がある。
1ターンでも設定を忘れると、そのターンはセッションからこぼれ落ちます。これに対する自動のコンテキスト伝播はありません。
セッションはいつ始まり、いつ終わるのか?
ベンダー定義です。一部のツールは、新しい会話IDを持つ最初のトレースでセッションを開き、非アクティブタイムアウトで閉じます(Langfuseのデフォルトは設定可能なウィンドウ)。他は明示的なクローズ呼び出しを要求します。仕様がライフサイクルについて何も語らないのは、仕様がセッションをオブジェクトとしてモデル化していないからです。
ターンをまたいで引き継がれるものと、引き継がれないものは?
モデルのコンテキストウィンドウはセッションではありません。セッションは独立したトレースをまたぐグループ化キーです。各ターンは独自のトレース、独自のルートスパン、独自のトークン数を持ちます。引き継がれるのは、各ルートスパンに押印した会話ID属性です。引き継がれないのは、レイテンシ、トークン使用量、スパン構造。これらはトレースごとのものです。
セッションレベルのメトリクスは何を測るのか?
単一のトレースでは測れないものです。解決率(会話はユーザーの問題を解決したか?)、回答までのターン数(ユーザーが必要なものを得るまでに何件のトレースがあったか?)、放棄された会話(クローズシグナルのないセッション)。セッションレベルで本番トレースの評価を実行すれば、ターンごとには正常に見えるマルチターンの障害を捕捉できます。
ベンダー非依存のコード
このスニペットは安定版のOTelプリミティブのみを使用します。ベンダーSDKは使いません。1ターンのルートスパン、検索の子スパン、モデル呼び出しの子スパンを作成し、gen_ai.conversation.id を設定して、3ターンが1つのセッションに入るようにします。
from opentelemetry import trace
tracer = trace.get_tracer("my-llm-app")
SESSION_ID = "conv-8f3a2c" # same value on every turn
def handle_turn(user_message: str):
with tracer.start_as_current_span("chat_request") as root:
# You set this. It does not propagate automatically.
root.set_attribute("gen_ai.conversation.id", SESSION_ID)
with tracer.start_as_current_span("retrieval") as ret:
ret.set_attribute("gen_ai.operation.name", "retrieval")
docs = retrieve(user_message)
with tracer.start_as_current_span("chat gpt-4o") as llm:
llm.set_attribute("gen_ai.operation.name", "chat")
llm.set_attribute("gen_ai.provider.name", "openai")
llm.set_attribute("gen_ai.request.model", "gpt-4o")
response = call_model(user_message, docs)
llm.set_attribute("gen_ai.usage.input_tokens", 1_204)
llm.set_attribute("gen_ai.usage.output_tokens", 312)
return response同じ SESSION_ID で handle_turn を3回呼び出せば、この属性を読むどのバックエンドでも、3つのトレースすべてが1つのセッションにグループ化されます。IDを変えれば、新しいセッションが始まります。仕組みはこれだけです。
5ベンダーのドキュメントを並べて読んだ。一致していない。
2026-07-30に、Langfuse、LangSmith、OpenInference / Phoenix、Datadogの現在のデータモデルドキュメントを並べて読み、さらにOpenTelemetry GenAIスパン仕様も確認しました。5つのうち4つは、同じオブジェクトを別の名前で呼んでいます。セッションを属性ではなくファーストクラスのオブジェクトとして扱っているのは1つだけ。このクエリのGoogle検索で1位のDatadogの用語ページは、セッションをそもそも定義していません。
| 概念 | OTel GenAI semconv | Langfuse | LangSmith | OpenInference / Phoenix | Datadog |
|---|---|---|---|---|---|
| 会話全体 | gen_ai.conversation.id 属性 | Session(トレースの任意のグループ化) | Thread(session_id / thread_id メタデータ経由) | session.id スパン属性 | 用語ページでは未定義 |
| 1件のリクエスト | Trace | Trace | Trace(「runsのコレクション」) | Trace | Trace |
| 1回のオペレーション | Span | Observation(span / generation / event) | Run(「単一の作業単位を表すspan」) | span kindを持つSpan | span kindを持つSpan |
最初の行について出典の注記を1つ。OpenInferenceの session.id は、上記でリンクしたトレース仕様(10種類のスパンkindを扱う)にはありません。これは隣接するOpenInferenceセマンティック規約ファイルで、セッションの一意な識別子として定義されています。2つのファイルで1つの仕様です。
ベンダー横断の比較は私たちの発明ではありません。FutureAGIもOTelとベンダーの対照表を公開しています。私たちが付け加えたのは2点。セッションの行(FutureAGIは省略している)と、「同じ言葉、違う意味」の罠です。Langfuseの「observation」とLangSmithの「run」はスパンと同じオブジェクトであり、一方DatadogとOpenInferenceのスパンのkindは、同じアイデアに対する異なる語彙です。
Langfuseはobservationと呼び、LangSmithはrunと呼び、Datadogはspanと呼ぶ。同じオブジェクトだが、移行すれば壊れるダッシュボードは3つ。
これは移行コストに関する私たちの見解であり、ベンダーの主張ではありません。しかしこれが、「observation」や「run」をキーにした保存済みフィルター、評価設定、アラートルールが、ツールを乗り換えた日に機能しなくなる理由です。フィールドの名前を変えているのではありません。階層の名前を変えているのです。この2つの具体的なツールを比較検討中なら、Langfuse vs LangSmith 比較で分岐点をさらに深掘りしています。
読者のスタックには、すでにOpik、PostHog、Sentry、Weights & Biasesがあるかもしれません。Googleはこの4つすべてを llm tracing と関連付けており、それぞれがこれらの概念を少しずつ異なる形でマップしています。正しいものを選ぶには?オブザーバビリティプラットフォーム比較で全体をカバーしています。
鮮度に関する注記。GenAI規約はメインの semantic-conventions リポジトリから独自のリポジトリへ移動しました。古い opentelemetry.io/docs/specs/semconv/gen-ai/ パスは、現在ではポインタがあるだけです。
どのIDがどこに入るのか?
トレースIDは1件のリクエストを識別し、コンテキストを通じて自動的に伝播します。スパンIDはそのトレース内の1回のオペレーションを識別し、これも自動です。相関ID(またはリクエストID)はトレーシングが始まる前にWeb層から来るもので、トレースIDと最もよく混同されるのがこれです。セッションIDは例外中の例外で、毎ターン、自分で手動設定するものです。
| ID | 設定者 | スコープ | 混同されがちなのは |
|---|---|---|---|
| トレースID | 自動 | 1件のリクエスト。コンテキストを通じて伝播 | Web層からの相関ID |
| スパンID | 自動 | 1回のオペレーション | , |
| 親スパンID | 自動 | 木を構築。ルートスパンでは空 | , |
| セッション / 会話ID | あなたが毎ターン手動で | 複数のトレース | 伝播すると思われている。しない。 |
| ユーザーID | あなたが手動で | 複数のセッション | セッションID |
| リクエスト / 相関ID | トレーシング開始前のWeb層 | 1件のHTTPリクエスト | トレースID(これが最大の混同) |
実践的なルール。gen_ai.conversation.id をスパン属性として毎ターンのルートスパンに付与し、ユーザーIDも併せて押印します。1ターン飛ばすと、セッションレベルのメトリクスはそのターンをサイレントに失います。
カーディナリティについて1つ警告。ユーザーIDとセッションIDはカーディナリティの高い値です。これはバックエンドのインデックス課金に関わる問題であり、次のセクションのテーマです。
スパンの粒度はどの程度が適切か?
失敗モードは2つ、どちらもよくあります。
スパンの過剰。 関数呼び出しごとにスパンを切ると、誰も読めない400スパンのトレースと、誰も承認していないスパン単位の課金が生まれます。ホスティング型バックエンド(Datadog、Langfuse Cloud)はスパン量で課金します。あらゆる文字列結合まで計測するおしゃべりなエージェントループは、フリーティアを午後ひとつで焼き尽くします。
スパンの不足。 「チェーン全体」にスパン1つでは、遅かったことはわかっても場所はわかりません。結局print文を復活させる羽目になり、それはトレーシングが置き換えるはずだったものです。
経験則(あくまで経験則であり、実測値ではありません)。判断や外部呼び出しが起きる境界にスパンを切ります。
- 検索ステップ:スパンを切る。
- Rerank呼び出し:スパンを切る。
- 各モデル呼び出し:スパンを切る。
- 各ツール呼び出し:スパンを切る。
- 各ガードレールチェック:スパンを切る。
- 純粋なプロセス内変換(文字列フォーマット、JSONパース、プロンプト組み立て):親スパンの属性にし、個別のスパンにはしない。
カーディナリティ、サンプリング、保持期間について。
- カーディナリティの高い属性(ユーザーID、完全なプロンプト)はストレージコストを膨張させます。サンプリングまたは切り詰めを。
- ほとんどのバックエンドはトレースレベルでサンプリングできます。エラートレースは100%保持し、正常系はサンプリング。
- 保持期間はまちまち。フリーティアは7日、有料は30〜90日。データが必要になる前に決めておきます。
スパン量とスパン単価の背後にある実際のコストモデルは、LLMコスト監視ガイドを参照してください。ここでは再構築しません。
Techsyのアプローチ
クライアントのエージェント案件では、3つのルールを標準にしています。
- ターンごとに1トレース。エージェントが内部的にループしても、2つのユーザーターンを1つのトレースにマージしない。
- すべてのルートスパンにセッションIDを押印し、アプリケーションコードで設定し、伝播を前提にしない。
- スパンのkindは小さな固定セット(retrieval、inference、tool、guardrail)に保ち、ベンダー変更後もダッシュボードが生き残るようにする。
3つ目はチームが飛ばしがちなルールであり、移行を救うルールでもあります。スパンの語彙が1ベンダーのenumに縛られていると、乗り換えた日にすべてのアラートと保存済みビューが壊れます。
エージェントシステムを構築中で、トレーシングアーキテクチャにセカンドオピニオンが欲しい方は、無料相談をご用命ください。
著者について
Mert BaturはTechsy.ioのCo-Founderであり、チームはB2Bクライアント向けにAIエージェント、自動化システム、音声/SDRパイプラインを提供しています。Techsyチームが実際に本番環境で使っているLLMツーリングスタックについて執筆しています。LinkedInでつながってください。
よくある質問
分散トレーシングにおけるスパンとは何ですか?
スパンとは、時間計測された1つの作業単位です。名前、開始時刻、終了時刻、ステータス、属性のセットを持ちます。スパンは親スパンIDの参照で互いにリンクし、木を形成します。LLMアプリケーションでは、スパンは通常、1回のモデル呼び出し、1回の検索、または1回のツール起動を包みます。
Datadogにおけるスパンとは何ですか?
DatadogのLLM Observabilityでは、スパンは同じく時間計測されたオペレーションですが、Datadogはspan kindの分類を加えています。LLM、Workflow、Agent、Tool、Task、Embedding、Retrievalです。ルートスパンになれるのはLLM、Workflow、Agentのkindだけです。この分類はDatadog固有のもので、OpenTelemetry標準の一部ではありません。
オブザーバビリティの4つの柱とは何ですか?
4つの柱は、ログ、メトリクス、トレース、そして(誰の枠組みかによりますが)プロファイルまたはイベントです。トレースは、この記事が属する柱です。LLMのケースではひとひねり加わります。トークン使用量とモデルIDはトレーススパンの属性であり、独立したメトリクスストリームではないため、2つの柱に相当するものが1つのクエリに収束します。
オブザーバビリティの4つのゴールデンシグナルとは何ですか?
レイテンシ、トラフィック、エラー、サチュレーションです。LLMシステムでは、レイテンシは最初のトークンまでの時間と生成総時間を意味し、トラフィックはモデルごとの秒間リクエスト数、エラーは失敗したスパン(ステータスコードERROR)、サチュレーションはトークンバジェットの枯渇またはキューの深さを意味します。シグナルは同じで、単位が異なります。
セッションはOpenTelemetry仕様の一部ですか?
構造的な階層としては、違います。OTel GenAIスパン規約は、gen_ai.conversation.id を、会話またはスレッド内のメッセージを相関させるための条件付き必須属性(「利用可能な場合」)として定義しています。これはスパンに載ります。LangfuseやLangSmithのようなベンダーは、その上に独自のセッションオブジェクトやスレッドオブジェクトを構築しています。
トレースID、スパンID、相関IDの違いは何ですか?
トレースIDは1件のリクエストを識別し、すべての下流サービスへ自動的に伝播します。スパンIDはそのトレース内の1回のオペレーションを識別します。相関ID(またはリクエストID)はトレーシング開始前にWeb層が生成するもので、トレースIDと最もよく取り違えられる値です。スコープは重複しますが、発生源が異なります。
1つのトレースには何個のスパンが適切ですか?
固定の答えはありませんが、典型的な範囲はRAGリクエストで3〜30、複数のツール呼び出しを伴うエージェントループで10〜50以上です。経験則としては、プロセス内変換ではなく、外部呼び出しと判断ポイントにスパンを切ります。トレースが100スパンを超えるなら、計測しすぎの可能性が高いです。
Langfuseの「observation」はスパンと同じものですか?
はい。LangfuseのobservationはOTelのスパンと同じオブジェクトで、属性を伴う1回の時間計測されたオペレーションです。Langfuseはobservationを3タイプ(generation、span、event)に分けますが、OTelは gen_ai.operation.name を使います。トレースを読み取るツールを評価中なら、LLM評価ツール比較で、両方の語彙を受け付けるツールをカバーしています。
マルチターンのチャットボット会話を1つのセッションにまとめるには?
毎ターンのルートスパンに同じ会話識別子を設定します。OTelの用語では gen_ai.conversation.id です。Langfuseでは、トレース作成時に session_id を渡します。LangSmithでは、session_id または thread_id メタデータを設定します。1ターン逃すと、そのターンはグループ化からこぼれ落ちます。
シングルターンのリクエストしか扱わない場合、セッションは必要ですか?
おそらく不要です。セッションは、複数のトレースを1つの会話に相関させるために存在します。すべてのリクエストが独立しているなら(分類API、ワンショットの要約機など)、トレースレベルのメトリクスで十分です。セッションは、ターンをまたぐメトリクス、つまり解決率、回答までのターン数、会話レベルのコストが必要になったときに追加します。LLM評価ガイドで、セッションレベルの評価が元を取る場面をカバーしています。
短縮版
スパンはトレースの内側に入れ子になり、トレースはセッションにまとめられます。ネストは実在しますが、仕様が構造化しているのは3階層のうち2つだけです。gen_ai.conversation.id は自分で設定する属性であり、伝播する親スパンではありません。そして今日あなたが選ぶベンダーは、18か月後に乗り換えるベンダーとはこれらのオブジェクトを異なる名前で呼んでいます。だからこそ、スパンの語彙は小さく、ポータブルに保ちましょう。
プラットフォームを選定中なら、まずオブザーバビリティプラットフォーム比較からどうぞ。トレースの上に評価を構築するなら、LLM評価ガイドがここから先を引き継ぎます。