AIエージェントのトレーシング実装|全経路を追ってトークン浪費と失敗箇所を可視化

AIエージェントに関する記事のアイキャッチ画像 - AIエージェントのトレーシング実装|全経路を追ってトークン浪費と失敗箇所を可視化 AI×コーディング

AIエージェントのトレーシングとは、各ステップの処理を記録し、失敗と無駄を追跡可能にする手法。

AIエージェントの本番運用で最初に詰まるのは「動かないこと」ではなく「動いているように見えて静かに失敗していること」です。Webアプリなら500エラーが出れば気づけます。ところがエージェントはエラーを吐かず、無限ループに入り、誤ったツールを呼び、古いコンテキストを掴んだまま、それでも自信ありげな回答を返してきます。最終レスポンスのログだけ眺めても、どこで無駄が出たのか分かりません。この記事では、その「見えない経路」をOpenTelemetryで計装し、トークン浪費と失敗箇所を1本のトレースに束ねて追えるようにするまでを、基礎から実装まで通しで扱います。

この記事の要点

  • AIエージェントはクラッシュせず「静かに」失敗するため、最終ログだけでは原因が掴めない
  • OpenTelemetryのGenAIセマンティック規約(gen_ai.*属性)で、推論・ツール呼び出し・検索の各ステップをスパンとして記録できる
  • 属性名はexperimental段階のため版を固定し、OTel公式の1系統に絞るのが事故回避の前提

トレーシングという言葉は分散システムの世界では古くからあります。ただAIエージェントに適用すると、追う対象が「サービス間の通信」から「モデルが下した隠れた判断」へと変わります。なお、何をなぜ観るかという観測の設計・戦略そのものはLLMワークフローのオブザーバビリティ設計で扱っており、本記事はその実装=OpenTelemetryでの計装手順に絞ります。前半パートでは、なぜ追う必要があるのか、何を追うのか、そしてどう計装を始めるのかまでを順番に見ていきます。コード例はOpenTelemetryのGenAI規約に沿った最小構成のイメージで、概念理解を優先したものとして添えます。実運用のAPI仕様・モデルID・属性名は更新が速いため、各公式ドキュメントで最新版を確認してください。

  1. AIエージェントのトレーシングが必要になる理由(静かな失敗の正体)
    1. 通常のログ監視では見えない4つの異常
    2. 「最終レスポンスだけ正しく見える」が一番危険
  2. ユーザー要求からモデル・ツール・最終出力までの全経路
    1. 1リクエストが「隠れた決定の木」になる
    2. トレース・スパン・属性の関係(用語整理)
  3. 失敗はモデルでなく周辺レイヤーで起きる(context / harness)
    1. プロンプト・コンテキスト・ハーネスの役割分担
    2. 計装すべきは「モデルの外側」
  4. ReActループをトレース対象として分解する(思考・行動・観察)
    1. 思考・行動・観察の3ステップとスパンの対応
    2. ループ回数とツール呼び出し履歴を残す
  5. OpenTelemetry GenAI規約とは(gen_ai.* 属性の全体像)
    1. span命名規則 {operation} {name}
    2. experimentalという前提と版固定の作法
  6. 計装の手順|OpenTelemetry / OpenLLMetry を入れる
    1. SDKと自動計装の最小セットアップ
    2. 既存のLangChain / CrewAI / AutoGen への差し込み
  7. スパン設計|gen_ai.* 属性を手で組むコード例
    1. LLM呼び出しスパンに付ける属性
    2. ツール実行・エージェント実行をネストさせる
  8. トークンとレイテンシのメトリクス取得
  9. 検証と再生(replay)でトレースを使い切る
  10. 観測した後どう止めるか(権限レイヤーとwrite-path制御)
  11. 本番投入前に確認する観測の7領域
  12. つまずきやすい落とし穴
  13. まとめ
  14. よくある質問
  15. 参考資料

AIエージェントのトレーシングが必要になる理由(静かな失敗の正体)

普通のサーバー監視は「APIが生きているか」「どのエンドポイントが遅いか」「どのサービスが例外を投げたか」を見ます。AIエージェントの観測は、もっと答えにくい問いに向き合わなければなりません。なぜそのツールを選んだのか。どのドキュメントが回答を変えたのか。リトライが失敗を覆い隠していないか。タスクは本当に解けたのか。エラーコードという分かりやすい手がかりがない場所で、無駄と誤動作を探すことになります。

通常のログ監視では見えない4つの異常

AIエージェントは、Webアプリのようにきれいに落ちてくれません。代表的な失敗の形は4つあります。

まず無限ループ。エージェントが同じ思考とツール呼び出しを何度も繰り返し、抜け出せなくなる症状です。出力は最終的に返ってくるので、表面上は成功に見えます。次に誤ツール選択。検索すべき場面で計算ツールを呼んだり、書き込みツールを読み取りのつもりで使ったりします。3つめが古いコンテキストの取得。RAG(検索拡張生成、外部の文書を検索して回答に使う仕組み)が陳腐化した文書を引いてきて、それを根拠に堂々と間違えます。最後がトークンの過剰消費。想定の数倍に膨れ上がることがあり、コストとして跳ね返ります。

この「数倍」を具体的な倍率として断定はしません。実測に基づく確かな係数があるわけではなく、「想定外に膨らむことがある」という定性的な事実として捉えるのが妥当です。実害は、ループ回数とトークン量を掛け合わせて把握します。

これら4つに共通するのは、どれもHTTPステータスコードでは表現されない点です。ステータスは200のまま、中身だけが壊れています。ログを「最後の1行」だけ残す設計だと、これらは全て素通りします。

「最終レスポンスだけ正しく見える」が一番危険

最終出力だけをログに残すのは、映画を最後の1フレームだけ見てストーリーを推測するようなものです。結末は映っていても、そこに至る分岐や伏線は一切見えません。エージェントの場合、その「途中の分岐」こそが障害の発生源になります。

しかも厄介なのは、最終レスポンスが「それらしく」見えてしまう点です。言語モデルは流暢な文章を返すのが得意なので、内部で誤ったツールを5回呼んでいても、出力は整った日本語で返ってきます。レビューする人間が違和感を覚えにくい。これがWebアプリの障害と決定的に違うところで、500エラーなら誰でも異常と分かりますが、自信ありげな誤答は見逃されやすいのです。

だからこそ、ユーザー要求からモデル呼び出し、ツール実行、最終出力までの全経路を可視化する必要があります。途中の判断を1つずつ記録しておけば、後から「3回目のツール呼び出しで古い文書を引いた」と特定できます。トレーシングの動機は、この「途中を残す」という一点に集約されます。エージェントの自己申告そのものを疑う観点については、信頼スコアリングを扱った別記事でも掘り下げています。

ユーザー要求からモデル・ツール・最終出力までの全経路

トレーシングが追う対象を、もう少し具体的に描いてみます。ユーザーが1回操作するだけで、内部では多数のコンポーネントが連鎖して動きます。アプリサーバーがリクエストを受け、ベクトルDB(文書を数値ベクトルに変換して類似検索するデータベース)が関連文書を引き、LLMプロバイダがモデル呼び出しに応え、各種ツールが外部APIを叩く。この連鎖は一直線ではなく、枝分かれする木の構造になります。

1リクエストが「隠れた決定の木」になる

1回のユーザー操作が、アプリサーバー・ベクトルDB・LLMプロバイダ・ファイルパーサー・ブラウザツール・CRM API・課金システム・通知キューと、いくつものレイヤーに触れます。エージェントは各段で「次に何をするか」を自分で決めるので、その判断の連なりが隠れた意思決定の木になります。

たとえば「先月の請求書を集計して」という1つの依頼を考えてみてください。エージェントはまず請求書を探す検索を実行し、見つかったPDFをパーサーに渡し、抽出した数値を計算ツールで合算し、結果を整形してから返します。ここで検索が古いインデックスを引けば、集計の前提が崩れます。パーサーが1枚だけ読み損ねれば、合計が静かにずれます。どの枝で問題が起きたのかは、最終出力の「合計額」を見ているだけでは絶対に分かりません。

この木構造を1つの単位として記録するのがトレーシングの役割です。バラバラのログ行ではなく、「このユーザー操作に紐づく一連の処理」として束ねることで、初めて経路全体を俯瞰できます。

1トレース=モデル呼び出し・ツール実行・検索の子スパンの木 1回のユーザー要求が親スパン(invoke_agent)になり、その下にモデル呼び出し(chat)、ツール実行(execute_tool)、検索(retrieval)などの子スパンがぶら下がる木構造。各スパンにgen_ai.*属性(トークン数・モデル名・所要時間)を載せて全経路を追える。 1トレース=隠れた処理の木を全部残す(gen_ai.* 属性で追う) invoke_agent(親スパン) 1回のユーザー要求=1トレース chat gpt-4o(モデル呼び出し) 属性: gen_ai.usage.input/output_tokens execute_tool web_search(ツール) 属性: gen_ai.tool.name・引数・結果 retrieval(検索) 属性: 引いた文書ID・更新日時 chat(再度の推論) 属性: ループ回数・終了理由 最終出力だけでは この木が見えない ※最終レスポンスのログだけでは「どの子スパンで古い文書を引いたか/何回ループしたか」が追えない。全経路をスパンで残すのがトレーシング。 属性名(gen_ai.*)は2026-06時点でexperimental。版を固定して扱う。
1回のユーザー要求が親スパンになり、モデル呼び出し・ツール実行・検索が子スパンとしてぶら下がる。各スパンの属性で全経路を追う。

トレース・スパン・属性の関係(用語整理)

ここで3つの用語を整理しておきます。初出なので丁寧に説明します。

トレース(trace)は、1回のユーザー操作に対応する処理全体のことです。先ほどの「請求書を集計して」の依頼1件が、1本のトレースになります。スパン(span)は、そのトレースの中の個々の処理ステップを指します。検索が1スパン、モデル呼び出しが1スパン、計算ツールの実行が1スパン、という具合です。スパンは親子関係を持てるので、「エージェント実行」という親スパンの下に、複数の子スパンがぶら下がる木になります。属性(attribute)は、各スパンに付ける付加情報のキーと値のペアです。たとえばモデル呼び出しスパンに「使ったモデル名」「入力トークン数」「終了理由」を属性として乗せます。

用語 意味 補足
トレース 1回のユーザー操作に対応する処理全体 依頼1件=1トレース
スパン トレース内の個々の処理ステップ 検索・モデル呼び出し・ツール実行など
属性 スパンに付けるキー・値の付加情報 モデル名・トークン数・終了理由など

この3つの関係を押さえておくと、後半で出てくるOpenTelemetryのスパン設計がそのまま腑に落ちます。トレースが木の幹、スパンが枝、属性が枝に付いた葉、というイメージで掴んでおいてください。OpenTelemetryでは、親スパンにエージェント実行を、子スパンに各LLM呼び出しやツール実行、検索を割り当てる構造が基本になります。

失敗はモデルでなく周辺レイヤーで起きる(context / harness)

「出力がおかしいのはプロンプトが悪いからだ」という発想は、半分正しくて半分外れています。プロンプトの改善で直る問題もありますが、エージェントの失敗の多くはモデル本体の外側、つまり周辺レイヤーで発生します。トレースで属性を細かく付ける理由も、ここにあります。

プロンプト・コンテキスト・ハーネスの役割分担

Dev.toのある技術記事では、AIシステムの最適化を3つのレイヤーに分けて整理しています。プロンプトエンジニアリング、コンテキストエンジニアリング、ハーネスエンジニアリングの3つです。この区分は1つの記事が提示する枠組みであって業界標準の分類ではありませんが、観測対象を考えるうえで見通しがよくなります。

プロンプトエンジニアリングは指示の明確化に関わります。ただし、モデルが参照できる情報が手元になければ、いくら指示を磨いても機能しません。コンテキストエンジニアリングは、RAGやメモリのように「モデルが参照すべき情報」を適切に渡すことに焦点を当てます。ハーネスエンジニアリングは、プロンプトやコンテキストの最適化を超えて、AIシステム全体の信頼性とスケーラビリティを担う層です。

同じプロンプトを投げているのに出力が毎回ぶれる、多段階のワークフローでエージェントが混乱する。こうした症状が出るとき、原因はプロンプト自体ではなく、最適化すべきレイヤーがそもそも違っている可能性が高いと、その記事は指摘しています。プロトタイプから本番運用へ移すと、この周辺レイヤーの脆さが一気に表面化します。プロトタイプが本番で詰まる構造的な理由については、別記事「LovableやBoltで作ったアプリが本番で詰まる理由|プロトタイプを実運用へ移す移行手順」でも具体的に扱っています。

計装すべきは「モデルの外側」

この役割分担を踏まえると、トレースで記録すべき対象が見えてきます。モデル呼び出しの中身、つまり入出力のテキストだけを残しても足りません。何をコンテキストとして渡したのか、どのツールを選んだのか、検索でどの文書を引いたのか。モデルの外側で起きた判断こそ、属性化して残す価値があります。

具体的には、検索スパンに「引いた文書のIDや更新日時」を、ツール実行スパンに「選んだツール名と引数」を、コンテキスト構築スパンに「渡したトークン量」を乗せます。こうしておけば、出力がぶれたときに「古い文書を引いていた」「想定外のツールを選んでいた」と切り分けられます。プロンプトを何度書き直しても直らなかった問題が、実は周辺レイヤーの記録を見れば一目だった、という展開は珍しくありません。計装の重心は、モデルの内側ではなく外側に置くのが実用的です。

ReActループをトレース対象として分解する(思考・行動・観察)

エージェントが「自分で考えてツールを使う」とき、その内部では一定のパターンが回っています。代表的なのがReActという枠組みです。トレースで追う内部構造を理解するには、このループを知っておくと設計がぶれません。

思考・行動・観察の3ステップとスパンの対応

ReActは、推論(Thought)と行動(Action)と観察(Observation)を交互に繰り返すループです。Shunyu Yaoらによる論文(プリンストン大学とGoogle Research、arXiv:2210.03629として公開、ICLR 2023で発表)が提案しました。推論トレースが行動計画の立案・追跡・例外処理を助け、行動が外部環境からの情報取得を可能にする、という構造になっています。

流れを追うと、まずモデルが「次に何をすべきか」を考えます(Thought)。次にツールを実行します(Action)。その結果を受け取ります(Observation)。そしてその観察を次の思考に反映させ、また考える。この反復で複雑なタスクを段階的に解いていきます。

トレーシングの視点では、このThought・Action・Observationの各ステップが、そのまま子スパンに対応します。思考のステップはモデル呼び出しスパン、行動のステップはツール実行スパン、観察のステップはツールの返り値を記録する属性。ReActの構造とスパン設計が、ほぼ1対1で対応するわけです。ここではReActそのものの仕組みには深入りせず、トレースで分解する対象としての側面に絞ります。ただし、実運用で「Thought」に相当する内容をそのまま保存する必要はありません。後から原因分析に使えるのは、モデル呼び出し・ツール名・ループ回数・入力/出力トークン・エラー・参照文書IDといったメタデータです。会話本文やツールの引数・結果は機微情報を含み得るため、Opt-In・マスキング・保持期間の制御を前提にします。エージェント開発で起きうる情報漏洩リスクと検出・防止策も併せて押さえておくと安全です。

注意したいのは、ReActはエージェントの設計パターンであって、OpenTelemetryのトレーシングとは別レイヤーの話だという点です。ReActを採用していないエージェントでも計装はできますし、逆にReActを使っていても計装しなければ中身は見えません。両者は直交する関係にあります。

ループ回数とツール呼び出し履歴を残す

ReActループをトレースで追う実益は、2つあります。1つは「どの観察で判断が狂ったか」を特定できること。観察ステップの属性にツールの返り値を残しておけば、「3回目の観察で古い在庫数を受け取り、そこから集計が崩れた」と後追いできます。

もう1つが「何回ループを回したか」を可視化できること。これは静かな失敗の検知に直結します。正常なら3回で終わるタスクが15回ループしていたら、明らかに異常です。ループ回数とツール呼び出し履歴をスパンとして残しておけば、無限ループや迷走を「回数」という分かりやすい指標で捕まえられます。最終出力は同じように見えても、ループ回数を見れば内部の苦戦が一目で分かる。これがトレーシングの効きどころです。

OpenTelemetry GenAI規約とは(gen_ai.* 属性の全体像)

ここからが実装の技術核です。各社バラバラの形式でログを取ると、後で集計も比較もできません。そこで標準スキーマが要ります。OpenTelemetry(分散システムの計装を標準化するオープンソースのフレームワーク)が定めるGenAIセマンティック規約が、その役割を担います。

span命名規則 {operation} {name}

OpenTelemetryのGenAIセマンティック規約は、プロンプト・モデル応答・トークン使用量・ツール/エージェント呼び出し・プロバイダ情報を記録するための標準スキーマを定めています。スパンの命名規則は {operation} {name} の形を取ります。操作名とその対象を空白で繋ぐ書式です。

具体例を挙げると、チャット呼び出しなら chat gpt-4o、ツール実行なら execute_tool web_search、エージェント実行なら invoke_agent {agent.name} という形になります。規約ではエージェント関連のスパンとして create_agent / invoke_agent / execute_tool が定義されています。invoke_agent はクライアント側ではCLIENT、フレームワーク内部の処理ではINTERNALというスパン種別(span kind、そのスパンがクライアント呼び出しか内部処理かを区別する分類)を取ります。

共通の属性には、操作名を表す gen_ai.operation.name、プロバイダを表す gen_ai.provider.name、エージェント名の gen_ai.agent.name、ツール名の gen_ai.tool.name が含まれます。トークン使用量は gen_ai.usage.input_tokensgen_ai.usage.output_tokens という属性で記録します。input_tokens はキャッシュ読み出し分・キャッシュ生成分を含む、全入力トークンを含めるべきとされています。この規約への対応は、可観測性バックエンドやエージェントフレームワークの側でも進んでいます。たとえばDatadogはGenAI Semantic Conventionsへの対応を公式に表明しています。各バックエンドの個別機能や価格には踏み込みませんが、「規約に対応する動きが広がっている」点を押さえておけば十分です。フレームワーク側の対応状況は後半(計装の手順)で個別に触れます。

experimentalという前提と版固定の作法

ここで最も大切な前提を共有します。GenAIセマンティック規約は、2026年6月時点でも全体としてDevelopment(実験)段階にあります。モデル呼び出し・ツール実行・エージェント関連のスパンはいずれも実用上は使われ始めているものの、属性名・イベント形式・スパン設計は今後変更され得ます。実装時は、参照するsemconvバージョンと利用するライブラリの出力形式を固定して扱うのが安全です。

GenAI規約の属性名(gen_ai.usage.input_tokens など)を「確定仕様」として扱わないでください。GenAI規約は2026年6月時点で全体がDevelopment段階であり、版が上がると属性名やスパン設計が変わる可能性があります。記事内のコードや属性名は概念理解のための例として捉え、実装時は必ず参照している規約バージョンを固定し、公式の最新仕様を確認してください。

experimentalだからといって使えないわけではありません。むしろ実運用では十分に機能します。鍵になるのは、依存する規約バージョンを明示的に固定することです。対応するinstrumentationでは、OTEL_SEMCONV_STABILITY_OPT_IN という環境変数を使い、旧名・新名の発行を切り替えて移行互換を保てます。仕様が動くことを前提に、こちらの版を先に決めておく。この姿勢が、experimentalな規約と付き合ううえでの基本作法になります。

なお、属性体系はOpenTelemetry公式の gen_ai.* 一系統に固定するのを強く推奨します。後で詳しく触れますが、別系統の規約と属性名を混ぜると、バックエンドが認識できずに集計が壊れます。最初に1系統と決めて、最後までそれを貫いてください。

計装の手順|OpenTelemetry / OpenLLMetry を入れる

規約の全体像を掴んだので、実際に計装を始めます。まず最小構成で動く状態を作り、そこから広げていく進め方が安全です。ここで示すコマンドやコードは概念理解用の最小例であり、そのままの完動を約束するものではありません。

SDKと自動計装の最小セットアップ

最短の入り口は、OpenTelemetryのSDKに加えて、自動計装ライブラリを入れることです。OpenLLMetry(Traceloopが提供、OpenTelemetryの上に構築されたオープンソースのトレーシング)を使うと、主要なLLMプロバイダやフレームワークの呼び出しを自動でスパン化できます。自動計装に加えて、カスタムスパンによる拡張もできるので、最初の一歩として扱いやすい選択肢です。ただし、利用するバージョンやinstrumentationによっては、出力される属性名が現行のOpenTelemetry GenAI規約と完全には一致しないことがあります。実運用では、実際に出力されたスパン属性を確認し、gen_ai.usage.input_tokens / gen_ai.usage.output_tokens / gen_ai.input.messages などが参照するsemconvバージョンと揃っているかを確かめてください。

導入はパッケージのインストールから始めます。

pip install opentelemetry-sdk opentelemetry-exporter-otlp traceloop-sdk

初期化のコードも最小限で済みます。次は概念を示すための最小例です。

from traceloop.sdk import Traceloop

# OTLPエクスポータ経由でトレースを送る最小初期化
Traceloop.init(
    app_name="my-agent",
    # 収集先のエンドポイントは環境に合わせて設定する
)

# 以降、対応プロバイダ/フレームワークの呼び出しが自動でスパン化される

最新版の属性を発行したい場合は、先ほど触れた環境変数を設定します。

export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental

利用するinstrumentationが対応している場合、この設定で新しいGenAIセマンティック規約の gen_ai.* 属性へ出力を切り替えられます。ただし、すべてのライブラリがこの環境変数に対応するわけではないため、各instrumentationのドキュメントと実際のスパン出力を確認してください。エクスポータはOTLP(OpenTelemetry Protocol、トレースを収集先に送る標準プロトコル)を使い、収集先のバックエンドへスパンを送信します。ここまでで、ひとまず「トレースが流れる」状態が出来上がります。最初は出力先をコンソールにして、スパンが実際に発行されているか目視するところから始めると、つまずきが減ります。

既存のLangChain / CrewAI / AutoGen への差し込み

ゼロからエージェントを書いている人ばかりではありません。すでにLangChainやCrewAI、AutoGenでエージェントを組んでいるケースも多いはずです。既存フレームワークでもOpenTelemetry連携は進んでいます。AutoGenは公式にOpenTelemetryベースのトレーシングを備え、ツールには execute_tool、エージェントには create_agent / invoke_agent スパンを使うと説明されています。LangChain/LangGraphはLangSmithのOTel連携経由でトレーシングを有効化できます。CrewAIもOpenLITなどを通じてOpenTelemetry-nativeな観測連携を利用できます。ただし、実際に出るスパン属性名やsemconvバージョンはフレームワーク・instrumentation・バージョンで変わるため、導入時に実スパンを確認してください。

差し込みの基本は、エージェントのメイン処理が走る前に計装を初期化しておくことです。対応フレームワークでは、モデル呼び出し・ツール実行・エージェント呼び出しなどが自動でスパン化される場合があります。ただし、ReActループの各内部ステップがどこまで分解されるかは、フレームワークやinstrumentationの実装に依存します。ループ回数・選択ツール・参照文書IDなどは、必要に応じて手動スパンや属性で補います。自前のオーケストレーション(複数のモデルやツールを役割分担で組み合わせる制御)を書いている場合も、手動スパンを足せば同じ木構造に乗せられます。

ここまでが前半の到達点です。まずは自動計装で「最小構成で動く」状態を作り、トレースが流れることを確認しました。後半では、この自動計装に頼り切らず、gen_ai.* 属性を手で組んでスパンを設計する方法、トークンとレイテンシをコストとして数値化する方法、そして記録したトレースを開発ループに戻す再生(replay)の実装へと進みます。自動で取れる範囲を押さえたうえで、手動でどこまで踏み込むか。その線引きが、エージェントの「黙って失敗・浪費している箇所」を実際に追えるかどうかを分けます。

スパン設計|gen_ai.* 属性を手で組むコード例

自動計装で取れない情報は、手動スパンで補います。たとえば自前のオーケストレーションでツールを選ぶ分岐や、リトライの判断ロジック。ここはフレームワークのフックの外側なので、手で gen_ai.* 属性を載せたスパンを作る必要があります。

LLM呼び出しスパンに付ける属性

OpenTelemetryのGenAIセマンティック規約では、モデル呼び出しに gen_ai.operation.name(操作名)、gen_ai.request.model(モデル名)、gen_ai.usage.input_tokens / gen_ai.usage.output_tokens(入出力トークン)、gen_ai.response.finish_reasons(終了理由)といった属性を載せます。下記は概念理解用の最小構成イメージで、そのままの動作を保証するものではありません。属性名は2026年6月時点で実験段階(experimental)のため、版が変われば変わり得る前提で読んでください。

from opentelemetry import trace
tracer = trace.get_tracer("agent")

with tracer.start_as_current_span("chat gpt-4o") as span:
    span.set_attribute("gen_ai.operation.name", "chat")
    span.set_attribute("gen_ai.request.model", "gpt-4o")
    resp = call_model(messages)
    span.set_attribute("gen_ai.usage.input_tokens", resp.usage.input)
    span.set_attribute("gen_ai.usage.output_tokens", resp.usage.output)
    span.set_attribute("gen_ai.response.finish_reasons", resp.finish)

スパン名が {operation} {name} 形式(例 chat gpt-4o)になっている点に注目。規約が命名規則を定めているので、これに揃えるとバックエンド側で集計しやすくなります。

ツール実行・エージェント実行をネストさせる

規約はエージェント関連スパンとして create_agent / invoke_agent / execute_tool を定義しています。親スパンを invoke_agent、その下にモデル呼び出しと execute_tool(ツール実行)を子スパンとしてぶら下げる。こうすると、1回のエージェント実行が木構造の1本のトレースにまとまります。gen_ai.agent.namegen_ai.tool.name を載せておけば、どのエージェントがどのツールを呼んだかが後から追えます。

トークンとレイテンシのメトリクス取得

数値で語れるようになると、コストの議論が一気に具体的になります。gen_ai.usage.* を集計すれば、テナント(利用テナント・契約単位)ごとのトークン消費を説明できる状態になる。なお input_tokens はキャッシュ読み出し・キャッシュ生成分を含む全入力トークンを含むべきとされている点に注意。

  • テナント単位の集計: tenant_id を属性に足し、想定の数倍に膨れたケースを早期に拾う
  • レイテンシ分解: 各スパンの所要時間から、モデル・検索・ツール・後処理のどの段が遅いかを特定する

ここでも倍率そのものは断定しません。ループ回数とトークン量を掛け合わせて、「黙ったループ」が生むトークン浪費を炙り出す運用に落とすのが現実的です。

検証と再生(replay)でトレースを使い切る

集めたトレースは、読んで終わりにしないこと。記録したトレースとスパンを使えば、失敗ケースを再生(replay)して、どの観察(Observation)・どの属性で結果が変わったかを切り分けられます。これを評価・回帰チェックに繋ぐと、修正が別のケースを壊していないかを確認できる。トレース自体が自動で改ざん不能になるわけではないので、監査用ログとして扱うなら、追記専用(WORM)ストレージ・アクセス制御・保持期間・改ざん検知を組み合わせる設計を検討します。

観測した後どう止めるか(権限レイヤーとwrite-path制御)

可視化のゴールは、危険な動作を止めること。読み取りや要約(read-path)は事故りにくい一方、CRMのレコード作成・更新・削除といった書き込み(write-path)は、人間の確認なしだと事故になり得ます。ある運用者の指摘では、エージェント駆動のワークフローには人間が操作するUIのような確認画面がなく、APIキーに制限がなければ意図しない一括更新や削除が起こり得る、という懸念が挙がっています。

プロトタイプから本番へ移す段階で、この穴は表面化しがちです。試作で動いたものが本番のデータを触り始めるとき何に注意すべきかは、本番移行で詰まる理由を扱った記事も参考になります。

破壊的なアクション(削除・一括更新・外部送金など)の直前には承認ゲートを挟む設計を検討してください。トレースで「危険な書き込み」を可視化しても、止める仕組みがなければ事故は防げません。

本番投入前に確認する観測の7領域

本番投入前に確認しておきたい観測領域は、7つに整理できます。ここまでの実装と対応づけて並べます。

領域 追う指標 対応するspan・属性
トレース 全経路の再生可否 invoke_agent / execute_tool 木構造
コスト テナント別トークン gen_ai.usage.input/output_tokens
品質評価 回帰・評価結果 replay用トレース
信頼性 エラー率・リトライ回数 ステップ単位のエラー属性
セキュリティ PII漏洩・注入検知 content記録のオプトイン管理
レイテンシ 段ごとの所要時間 各span duration
ガバナンス write-path承認 危険アクションのspan

表で並べると網羅的に見えますが、外せないのはトレース・コスト・信頼性の3つ。ここが欠けると「黙った失敗・浪費」を追う土台が崩れます。

つまずきやすい落とし穴

最後に、知らないと事故る論点を集約します。

属性名の系統を混ぜないこと。OpenTelemetry公式の gen_ai.* と、OpenInferenceの llm.* は別系統の規約で属性名が異なります。1記事ならぬ1システムで1系統に固定しないと、バックエンドが属性を認識できず集計が崩れます。また、規約が実験段階である前提で版を固定し、gen_ai.input.messages のように会話本文を記録する設定はPII漏洩源になるため、content記録はオプトインで管理してください。

OpenTelemetryの公式ドキュメントも、この段階での扱いを明示しています。

These conventions are currently in Development.

(出典: OpenTelemetry GenAI Semantic Conventions, https://github.com/open-telemetry/semantic-conventions-genai / 公式サイトの旧 GenAI ページは移動案内になっているため、最新仕様はこのリポジトリ側を参照)

まとめ

全経路を1本のトレースに束ね、gen_ai.* 属性で計装し、トークンとレイテンシを数値化し、replayで開発ループに戻す。この流れを通すと、エージェントが黙って失敗・浪費している箇所を実際に追えるようになります。まずは自動計装で最小構成を動かし、足りない分岐だけ手動スパンで補う。属性は1系統に固定し、実験段階の仕様は時点付きで扱う。小さく始めて、見える範囲を少しずつ広げていけば十分です。

よくある質問

Q. OpenTelemetryのGenAI規約は正式版ですか?

いいえ、2026年6月時点でもGenAI規約は全体がDevelopment(実験)段階です。モデル呼び出し・ツール実行・エージェント関連のスパンはいずれも実用では使われていますが、属性名・スパン設計は変わり得るため、参照するsemconvバージョンと利用ライブラリの出力形式を固定して扱うのが安全です。

Q. 既存のLangChainやn8nにも入れられますか?

AutoGenは公式にOpenTelemetryトレーシングを備えます。LangChain/LangGraphはLangSmithのOTel連携で、CrewAIはOpenLIT等を通じてOpenTelemetry系の観測連携を利用できます。多くの場合は最小限の初期化で計測を始められますが、既存コードの修正量や出力される属性名は、利用フレームワーク・instrumentation・バージョンによって変わります。n8nはワークフロー自動化プラットフォームで、AI機能とLangChainエージェント連携をサポートします。

Q. OpenInferenceとどちらを使うべきですか?

どちらか1系統に固定するのが原則です。両者はOpenTelemetry上に構築された別規約で属性名が異なります。混在させるとバックエンドが認識できなくなるため、本記事ではOTel公式の gen_ai.* に統一しています。

参考資料

  • OpenTelemetry GenAI Semantic Conventions(open-telemetry/semantic-conventions-genai, GitHub) — 公式サイトの旧 GenAI ページは別リポジトリへの移動案内になっているため、実装時はこのリポジトリ側の最新仕様を参照
  • OpenTelemetry Blog: Inside the LLM Call (GenAI Observability)
  • Datadog: LLM Observability supports OpenTelemetry GenAI Semantic Conventions
  • Traceloop: OpenLLMetry ドキュメント
  • Shunyu Yao et al.: ReAct (arXiv:2210.03629)
タイトルとURLをコピーしました