【AgentOps実装編】”1エージェント=1トレース”では追えない——マルチエージェント分散トレーシング設計|A2A/MCP境界をまたぐW3C Trace Context伝播・trace_id継承ルール・OpenTelemetry Collectorのサンプリング/PII秘匿/”テレメトリ自体のコスト”制御・まだ”Development”段階のGenAI規約をバージョンピン留めして本番運用する

はじめに——「1エージェント=1トレース」では、本番のマルチエージェントは追えない

AgentOps連載③(オブザーバビリティ編)では、1つのエージェントが「考え→ツールを呼び→また考える」という一連の軌跡(トラジェクトリ)を、1本のトレースに束ねる設計を扱いました。単一エージェントであれば、これで十分に「何が起きたか」を再構成できます。

しかし本番系がマルチエージェントになった瞬間、この前提は崩れます。オーケストレーターが専門エージェントを呼び、そのエージェントがさらに別のエージェントやツールサーバーへ委譲する——プロセスも、ネットワーク境界も、プロトコルもまたぐこの構造では、「1エージェント=1トレース」は、単にバラバラの短いトレースが大量に生まれるだけの状態を意味します。どのトレースとどのトレースが同じユーザーリクエストに属するのか、繋ぎ直せなくなるのです。

本稿は、③の「その先」です。テーマは2つに絞ります。ひとつは、複数エージェントが呼び合う本番系を、1本のトレースに繋ぐこと(A2A/MCPの境界をまたぐtrace_idの伝播)。もうひとつは、そのトレースが流れていく「配管」層——OpenTelemetry Collectorの設計(サンプリング/PII秘匿/テレメトリ自体のコスト制御)です。設計論としての協調パターンや、SLI/SLO・トークン予算といった③・既出記事の内容は「参照」に留め、ここでは分散トレーシングの実装に踏み込みます。

この記事が埋める空白:OpenTelemetryのGenAIセマンティック規約は、いまも「単一エージェント内部」の計装しか事実上規定していません。エージェント間でtrace_idをどう継承するかの標準ルールは未定義に近く、MCPのトレース断絶も最近になってようやく手当てが始まった段階です。しかも規約は依然として「Development(開発中)」ステータスで、安定化の公開スケジュールは示されていません。つまりこの領域は、標準に頼り切れず、自分たちで伝播ルールとバージョン運用を決めなければ本番投入できない。本稿はそこを実装レベルで埋めます。

1. なぜマルチエージェントは「1トレース」にならないか——trace_id継承と3つの断絶

1-1. 「1本のトレース」を成立させている条件

分散トレーシングでは、1リクエストにひとつのtrace_idが振られ、その中で個々の処理単位がspan_idを持ちます。子スパンは親スパンのspan_idparent_span_idとして引き継ぐことで、木構造が復元されます。この「同じtrace_idを、境界を越えても運び続ける」のがコンテキスト伝播(context propagation)で、単一プロセス内なら言語ランタイムが自動でやってくれます。

問題は、エージェントが別のエージェントやツールサーバーを呼び出した先が、別プロセス・別ネットワーク・別プロトコルだという点です。呼び出し先が「今のリクエストはtrace_id=X、親スパンは=Yだ」という情報を受け取れなければ、そこで新しいトレースが始まってしまう。境界ごとに、この受け渡しを明示的に配線する必要があります。

1-2. 断絶の3類型

実運用でトレースが途切れる箇所は、おおむね次の3つに整理できます。原因が違えば対処も違うため、まず自分の系がどの型で切れているかを切り分けます。

どこで切れるか典型的な症状対処の方向
① プロトコル境界断絶A2A/MCPなど、HTTPヘッダ以外の経路で呼ぶとき呼び出し先が別トレースとして立ち上がり、親子が繋がらないtraceparenttracestateを、そのプロトコルのメタデータに載せて運ぶ
② 非同期・キュー断絶メッセージキュー、バックグラウンドジョブ、Webhookコールバック投入側と処理側が別トレースになり、遅延の因果が追えないメッセージのヘッダ/属性にコンテキストを埋め込み、消費側でリンク(span link)として復元
③ 計装ギャップ断絶自動計装が対応していないSDK・自作クライアント途中の1ホップだけスパンが欠落し、木が浅くなるそのホップに手動計装を挿し、上流のコンテキストを注入・抽出する

マルチエージェント特有で最も厄介なのがです。HTTPの世界ならtraceparentヘッダを載せるという合意がありますが、A2AやMCPは「アプリケーション層の独自プロトコル」であり、その中にどうトレースコンテキストを埋め込むかは、まだ各実装・各SDKの裁量に委ねられている部分が大きいのです。

2. A2A/MCP境界をまたぐW3C Trace Context伝播の実装

2-1. 運ぶべきものは2つのヘッダだけ

境界を越えて運ぶ実体はシンプルで、W3C Trace Contextのtraceparenttracestateです。traceparentは次の4フィールドをハイフンで連結した1行です。

traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
             │  │                                │                │
      version │            trace-id              │  parent-id     │ trace-flags
             (2桁)        (32桁hex=trace_id)  (16桁hex=span) (sampled等)

やることは、呼び出し側で「現在のコンテキスト」をこの文字列に直列化(inject)して送り、受け側でそれを取り出して(extract)自分のコンテキストの親に据える——これだけです。tracestateはベンダー固有情報(サンプリング判断の引き継ぎなど)を運ぶ補助で、あれば一緒に転送します。難しいのは概念ではなく、「A2A/MCPのメッセージのどこにこの2行を載せるか」という配線箇所の特定です。

2-2. A2A境界での伝播

A2A(Agent-to-Agent)はHTTP+JSON-RPCベースでエージェント間のタスク委譲を行うため、トランスポートがHTTPである強みを使えます。すなわち、A2Aリクエストを送出するHTTPクライアント層でtraceparenttracestateをリクエストヘッダに注入し、受け側のA2Aサーバーがハンドラ入口でそれを抽出して、以降のスパンをその配下にぶら下げます。

実装の勘所は「A2Aのタスクは長寿命になりうる」点です。委譲されたタスクが非同期で進み、後からコールバックやポーリングで結果が返る場合、単純な親子(child span)では表現できません。この場合は、タスク投入時のコンテキストを保存しておき、結果受信側でspan link(トレースをまたいだ関連付け)として繋ぐ設計が現実的です。「同一リクエスト内の同期呼び出しは親子、非同期の委譲はlink」と役割を分けると、トレースが破綻しません。

2-3. MCP境界での伝播——最近まで残っていた断絶

MCP(Model Context Protocol)はクライアントとツールサーバーの間のプロトコルで、ここが長らくトレースの「断絶点」でした。MCPのメッセージ自体はトレースコンテキストの運搬を前提に設計されていなかったため、エージェント→MCPサーバー→外部API、という経路でトレースが途切れがちだったのです。仕様・SDKレベルでの手当ては比較的最近始まった段階で、本記事執筆時点でも「使っているMCP SDK/トランスポート(stdio/HTTP/SSE)がコンテキスト伝播に対応しているか」は、必ずバージョン単位で確認すべき項目です。

対応が不十分なトランスポートを使わざるを得ない場合の実務的な回避策は、MCPリクエストの_metaのような拡張フィールド、あるいはツール引数のラッパにtraceparentを明示的に載せ、MCPサーバー側の入口で手動extractするという「アプリ層での自前伝播」です。標準化を待つのではなく、境界ごとに『注入する場所・抽出する場所』をコードで固定し、契約としてドキュメント化しておく——これがマルチエージェント本番系の最初の基礎工事になります。

設計原則:伝播は「自動計装に期待する」ものではなく「境界の契約として明示的に配線する」もの、と割り切る。自動で繋がったらラッキー、繋がらない前提で inject/extract のフックを各境界に置いておく。これがトレース断絶のデバッグ時間を最も短くします。

3. gen_ai.* 規約の「Development」問題と、バージョン運用

3-1. なぜ「規約のバージョン」が本番の関心事になるのか

OpenTelemetryのGenAIセマンティック規約は、gen_ai.system(モデルの提供元)、gen_ai.request.modelgen_ai.usage.input_tokensgen_ai.usage.output_tokensgen_ai.operation.nameといった属性の「名前」を標準化するものです。ダッシュボードやアラート、コスト集計は、これらの属性名に依存して組まれます。

ところが、この規約は依然として「Development(開発中)」ステータスにあります。安定版(Stable)ではないということは、マイナーバージョンの更新で属性名やセマンティクスが変わりうることを意味します。SDK/計装ライブラリを何気なくアップグレードしただけで、昨日までgen_ai.usage.input_tokensで拾えていた値が別名になり、ダッシュボードが静かに空になる——これが本番で最も起きやすい事故です。

3-2. OTEL_SEMCONV_STABILITY_OPT_IN による「ピン留め」

OpenTelemetryは、規約の移行期にどのバージョンの属性を出すかを明示的に選ぶための環境変数OTEL_SEMCONV_STABILITY_OPT_INを用意しています。これはHTTP規約の安定化のときに確立した仕組みで、「旧属性のみ」「新属性のみ」「両方を並行出力(dup)」を選べるのが要点です。両方出しの期間を設けることで、ダッシュボードの切り替えを無停止で行えます。

GenAI領域でも、この「opt-inで挙動を固定する」考え方をそのまま適用します。運用ルールとして次を推奨します。

  1. 属性名は環境変数でピン留めする。ライブラリの既定挙動に任せず、「この系はこの規約バージョンの属性を出す」と明示的に固定する。
  2. アップグレードは必ず「並行出力(dup)」を挟む。新旧両方の属性を一定期間出し、ダッシュボード/アラートを新属性へ移し替えてから旧属性を落とす。
  3. ダッシュボード側に互換レイヤーを1枚噛ませる。生の属性名を直接参照せず、「入力トークン数」という意味に対して「新旧いずれかの属性名を吸収するビュー/クエリ」を挟む。規約が動いても、直すのはこの1枚だけで済む。

要は、「Developmentステータスの規約に、本番のダッシュボードを直結させない」こと。属性名という揺れる地面の上に、自分たちで安定した床(互換レイヤー)を張るのが、本番運用の作法です。

4. OpenTelemetry Collectorのパイプライン設計——サンプリング・PII秘匿・content capture

ここからが「配管」層です。各エージェントが吐いたテレメトリは、いったんOpenTelemetry Collectorに集約してから、バックエンドへ送るのが本番の定石です。Collectorを挟むことで、サンプリング・秘匿・整形・コスト制御を、アプリのコードを触らずに一元管理できます。パイプラインはreceivers → processors → exportersの3段で、勝負は真ん中のprocessorsです。

4-1. サンプリング戦略——head-based と tail-based

全トレースを保存すればコストが青天井になり、無作為に間引けば肝心の失敗トレースを捨ててしまいます。2つの方式を使い分けます。

方式判断のタイミング長所マルチエージェントでの弱点
head-basedprobabilistic_samplerトレース開始時に確率で採否を決定軽量・低コスト。判断が最上流で完結結果(エラー/遅延)を見る前に捨てるので、失敗トレースを取りこぼす
tail-basedtail_samplingトレース完了後、全スパンを見てから決定「エラーを含む」「閾値超の遅延」など結果に基づく採択ができる1トレースの全スパンが揃うまでCollectorで保持が必要=メモリ・構成の負荷が高い

マルチエージェントでは、tail-basedを主軸に、「エラーを含むトレースは100%残す/正常トレースは低確率でサンプリング」という方針が現実解です。ただしtail_samplingは「同一trace_idの全スパンが同じCollectorインスタンスに集まる」ことが前提です。Collectorを水平スケールさせるなら、trace_idでルーティングして同一トレースを同じ後段に寄せる「load-balancing exporter → tail_sampling Collector」の2段構成が必要になります。これを知らずにCollectorを増やすと、tail samplingが不完全なトレースを対象に誤判定を始めます。

4-2. PII秘匿と「content capture off」

GenAIのトレースは、放っておくとプロンプトと生成テキストそのもの——つまりユーザーの入力全文——をスパン属性に載せてしまう危険があります。多くのGenAI計装は、この本文キャプチャを環境変数(例:OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT)で制御し、既定はオフにしています。本番の初期値は次を鉄則にします。

  • content captureは既定オフのまま本番投入する。デバッグで一時的に有効化する場合も、環境・期間・対象を限定し、恒常オンにしない。
  • Collector側でも二重に防御する。アプリ設定を信用しきらず、redactionプロセッサやattributestransformプロセッサで、メールアドレス・電話番号・トークン全文などのパターンをdelete/hashする。「アプリで載せない」+「Collectorで削ぎ落とす」の二段構えにする。
  • 属性の許可リスト(allowlist)方式を検討する。「危険な属性を消す」より「通してよい属性だけ残す」ほうが、新しい計装が予期せぬ本文を吐いても事故りにくい。

監査観点:テレメトリは「ログの一種」として、データ保持・越境移転・アクセス権限の対象になります。トレースにユーザー本文が混入していないかは、セキュリティ/プライバシーレビューの明示的なチェック項目に含めておくと安全です。

5. 「テレメトリ自体のコスト」をどう抑えるか——本番で見落とされがちな支出

マルチエージェントは、1リクエストあたりのスパン数が単一エージェントの数倍〜数十倍に膨らみます。エージェントが増え、各エージェントがツールを何度も呼べば、スパン・メトリクス・ログの量が乗算的に増える。結果として、「観測のためのコストが、観測したいワークロードのコストに迫る」という逆転が本番で起こります。抑えどころは主に3つです。

  1. カーディナリティ(属性値の種類数)を制御する。コスト爆発の主因はスパンの「数」だけでなく、メトリクス属性の「組み合わせ爆発」です。user_idやリクエスト固有のIDをメトリクスの次元にしない。高カーディナリティな値はトレース(=サンプリングされる)側に置き、メトリクス(=全量集計される)側から外すのが原則です。
  2. 不要な属性・スパンをCollectorで落とす。デバッグ由来の冗長属性、内部リトライの微細スパンなどをfilterattributesプロセッサで送信前に削減する。「出す量を絞る」よりCollectorで「送る量を絞る」ほうが、アプリ改修なしで効きます。
  3. tail samplingで「価値の低い正常トレース」を積極的に間引く。失敗・遅延・高コストのトレースは残し、平凡な成功トレースは低確率に。前述のとおり結果ベースで選べるのがtail-basedの利点で、これは品質を落とさずに保存量を落とせる数少ない手段です。

目安として、「テレメトリ費用を、対象ワークロードのLLM/インフラ費用に対する割合として定点観測する」ダッシュボードを1枚持つことを勧めます。この比率が上振れしたら、それはサンプリング率かカーディナリティを見直す合図です。テレメトリのコストは、放置すると誰も見ないまま膨らむ「静かな支出」になりがちです。

6. 本番トレースを「評価」と「インシデント対応」へ還流する

分散トレーシングは、集めて終わりでは半分しか価値がありません。マルチエージェントのトレースは、AgentOpsの他の工程へ還流させて初めて投資が回収されます。接続先は主に2つです。

評価(連載②)への還流。本番トレースは、最良のテストデータセットの供給源です。tail samplingで残した「失敗トレース」「想定外の委譲経路をたどったトレース」を、そのまま評価スイートのケースへ昇格させる。本番で実際に壊れた入力・経路を、回帰テストに取り込むループを作ることで、評価が机上のものでなくなります。ここでgen_ai.*属性が安定していること(第3章)が効いてきます。

インシデント対応(連載⑧)への還流。マルチエージェント障害の一次切り分けは、「どのエージェント/どの境界で、どのスパンが失敗したか」をトレースから即座に読めるかで、初動速度が決まります。1本に繋がったトレース(第1〜2章)があれば、「オーケストレーターは正常、委譲先AがMCP境界でタイムアウト」といった因果が数十秒で見える。逆にトレースが断絶していると、各エージェントのログを突き合わせる作業から始まり、MTTRが跳ね上がります。本稿の伝播設計は、突き詰めればインシデント初動時間への投資です。

まとめ——標準に頼り切れない領域を、契約と互換レイヤーで運用する

マルチエージェント本番系の分散トレーシングは、「標準を入れれば繋がる」段階にまだありません。押さえるべきは次の3点です。

  1. trace_idの継承は「境界の契約」として明示的に配線する。A2AはHTTPヘッダ、MCPは_meta等への自前伝播も辞さず、各境界で inject/extract の場所をコードで固定しドキュメント化する。自動計装に依存しない。
  2. 「Development」の規約に本番ダッシュボードを直結しない。OTEL_SEMCONV_STABILITY_OPT_INで属性をピン留めし、並行出力を挟んで移行し、互換レイヤーで属性名の揺れを吸収する。
  3. Collectorを「配管」として設計に組み込む。tail samplingで価値あるトレースを残し、content captureは既定オフ+Collectorでの二重秘匿、カーディナリティ制御で「テレメトリ自体のコスト」を定点観測する。

そして、集めたトレースは評価とインシデント対応へ還流させる。ここまでやって初めて、マルチエージェントの可観測性は「大量の短いトレースの山」から「1リクエストを1本で追え、次の改善に回る仕組み」へと変わります。1エージェント=1トレースの先には、境界をまたいで繋ぎ直す地道な配線作業がある——それを設計として引き受けるのが、本番のAgentOpsです。

よくある質問(FAQ)

Q1. まずはhead-basedサンプリングだけで始めてはダメですか?
小規模なら可です。ただしhead-basedは「結果を見る前に捨てる」ため、失敗トレースの取りこぼしが起きます。エラー率が事業指標に直結する本番では、早い段階でtail-basedへ移行し、少なくとも「エラーを含むトレースは100%残す」ルールを敷くことを勧めます。

Q2. A2AとMCPで、伝播の難易度に差はありますか?
あります。A2AはHTTPトランスポートを使うためtraceparentヘッダに素直に載せられます。MCPはトランスポート(stdio/HTTP/SSE)とSDKバージョンによって対応状況がまちまちで、非対応の場合は_meta等への自前伝播が必要です。導入前に「使うMCP SDK/トランスポートがコンテキスト伝播に対応しているか」をバージョン単位で確認してください。

Q3. gen_ai.* 属性が「Development」なら、使わないほうが安全では?
いいえ、使うべきです。避けるべきは「規約バージョンに無防備に直結すること」であって、規約自体は標準化の最有力です。OTEL_SEMCONV_STABILITY_OPT_INでピン留めし、ダッシュボードに互換レイヤーを噛ませておけば、Developmentステータスでも安全に運用できます。

Q4. content captureをオフにすると、デバッグに困りませんか?
恒常オフのまま、必要時だけ環境・期間・対象を絞って一時有効化するのが定石です。加えて「プロンプトのハッシュ」「トークン数」「ツール名」など、本文を載せずに追える属性を充実させておくと、多くの障害は本文なしで切り分けられます。

Q5. Collectorを水平スケールしたらtail samplingがおかしくなりました。
tail samplingは「同一trace_idの全スパンが同じCollectorに集まる」前提です。スケールアウトする場合は、trace_idでルーティングするload-balancing exporterを前段に置き、その後段でtail_samplingを行う2段構成にしてください。これがないと、不完全なトレースに対してサンプリング判定が走り、誤って捨てられます。


関連:本稿はAgentOps連載③(オブザーバビリティ編)の続編(実装編)です。単一エージェントのトラジェクトリ計装・SLI/SLO・バーンレートは③を、評価は連載②、インシデント対応は連載⑧を参照してください。

コメント

タイトルとURLをコピーしました