HoneycombのAgent TimelineのためのAIエージェント計装方法

AIエージェントは非決定的で、多段階かつ不透明です。本番環境で1つが失敗すると、「モデルが変なことを言った」というのは、インシデント事後分析で最も安価で最も役に立たない一行です。エージェントを実際に動作する形でデバッグするには、すべてを順序立てて捉え、何が起こったかを再構成できる十分なコンテキストを備えたテレメトリーが必要です。

OpenTelemetry GenAI Semantic Conventionsは、まさにそれを行うためのベンダーニュートラルな方法を提供します。適切な属性でエージェントを計装すると、HoneycombのAgent Timelineは会話全体を描画します:モデル呼び出し、ツール呼び出し、エージェントの引き渡し、失敗、そしてそれらがトリガーした下流のAPIやデータベース処理まで、すべてを共有のconversation IDで結びつけます。

以下は、Timelineに表示され、表示された後にデバッグ可能にするためのエージェントの計装方法です。

最初に必要な3つの属性

Agent Timelineは、複数のトレースと複数のエージェントを1つの会話ビューにグループ化します。これを実現するには、エージェントの実行チェーン内のすべてのスパンに3つの属性が必要です:

重要な微妙な点: 「GenAIスパン」は単なるLLM呼び出しではありません。エージェントによってトリガーされた実行チェーン内の任意のスパン、つまり下流のデータベースクエリ、サードパーティAPI呼び出し、またはエージェントがツールを呼び出したために実行されたバックグラウンドジョブなども含まれます。エージェントが何かを行ったためにスパンが存在する場合、そのスパンにはconversation IDが付与されるべきです。

これがTimelineをエンドツーエンドで機能させる理由です。下流システムのスパンにconversation IDが伝播されなければ、専用のAI可観測性ツールが到達するLLM限定ビューになってしまいます。しかし、本番環境では根本原因がLLMであることはほとんどありません。

最小限の実行例

以下は、OpenTelemetry SDKを使用したPythonでの手動計装例です。Honeycombを指すOTLPエクスポーターが既に設定されていることを前提としています。

import json
import uuid
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode

tracer = trace.get_tracer("my-agent")

def run_agent(user_message: str):
    conversation_id = str(uuid.uuid4())

    with tracer.start_as_current_span("invoke_agent support_agent") as span:
        span.set_attribute("gen_ai.conversation.id", conversation_id)
        span.set_attribute("gen_ai.agent.name", "support_agent")
        span.set_attribute("gen_ai.operation.name", "invoke_agent")

        return call_llm(user_message, conversation_id)

def call_llm(message: str, conversation_id: str):
    with tracer.start_as_current_span("chat gpt-4o") as span:
        span.set_attribute("gen_ai.conversation.id", conversation_id)
        span.set_attribute("gen_ai.agent.name", "support_agent")
        span.set_attribute("gen_ai.operation.name", "chat")
        span.set_attribute("gen_ai.request.model", "gpt-4o")

        # ... actual LLM call ...

        span.set_attribute("gen_ai.response.model", "gpt-4o-2024-08-06")
        span.set_attribute("gen_ai.usage.input_tokens", 142)
        span.set_attribute("gen_ai.usage.output_tokens", 87)
        return result

conversation_idをコールスタックを通じてスレッド化し、下流のHTTPクライアント、データベースクエリ、キュー workerを含むすべてのスパンがこれを付与できるようにします。これにより、LLM限定ビューではなく、フルスタックの全体像が得られます。

Timeline UIの残りの部分を点灯させる

以下の属性がTimelineを真に行動可能にするものです:トークン使用量、モデル識別、ツール呼び出しのデバッグ、失敗検出。

トークン使用量

すべてのchatまたはcompletionスパンに以下を設定します:

  • gen_ai.usage.input_tokens
  • gen_ai.usage.output_tokens
  • gen_ai.usage.cache_read.input_tokens
  • gen_ai.usage.cache_creation.input_tokens

これらが高カーディナリティ属性としてクエリ可能になると、トークン消費をモデル、レイテンシ、会話の結果、ユーザー感情と単一のクエリで相関させることができます。

モデル識別

  • gen_ai.request.model - リクエストしたもの
  • gen_ai.response.model - 実際に得られたもの

これらはしばしば異なります。gpt-4oをリクエストして、gpt-4o-2024-08-06のような特定の日付付きバージョンが返されることがあります。両方をキャプチャすることで、プロバイダー側のサイレントモデルアップグレード後の動作変更をデバッグできます。

ツール呼び出し

ツール呼び出しは、エージェント的失敗の多くが発生する場所です。すべてのツール実行スパンを以下のように計装します:

with tracer.start_as_current_span(f"execute_tool {tool_name}") as span:
    span.set_attribute("gen_ai.conversation.id", conversation_id)
    span.set_attribute("gen_ai.agent.name", "support_agent")
    span.set_attribute("gen_ai.operation.name", "execute_tool")
    span.set_attribute("gen_ai.tool.name", tool_name)
    span.set_attribute("gen_ai.tool.call.id", tool_call_id)
    span.set_attribute("gen_ai.tool.call.arguments", json.dumps(args))

    try:
        result = execute(tool_name, args)
        span.set_attribute("gen_ai.tool.call.result", json.dumps(result))
        span.set_attribute("gen_ai.response.finish_reasons", json.dumps(["stop"]))
        return result
    except Exception as e:
        span.set_attribute("error.type", type(e).__name__)
        span.set_status(Status(StatusCode.ERROR, str(e)))
        raise

ツール呼び出しが失敗した場合は、error.typeを設定し、エラーステータスを親スパンに伝播します。Timelineの「Show Failures Only」モードと会話レベルの失敗カウントは、このシグナルに依存しています。これにより、失敗が干し草の中の針ではなく、一級のナビゲーション要素に変わります。さらに、ツール呼び出しが伝播されたgen_ai.conversation.idを受け入れ、OpenTelemetryスパンを送信できる場合、そのツール呼び出し内で何が起こったかを正確に追跡できます。

プロンプトとレスポンス(PIIに関する注意)

  • gen_ai.input.messages - 完全なプロンプト
  • gen_ai.output.messages - 完全なレスポンス

これらは、エージェントに何が伝えられ、何を言ったかを読むことができるため、根本原因調査を劇的に高速化します。また、デフォルトでPIIや機密データをキャプチャします。他の機密ペイロードと同様に扱ってください:アプリケーションレイヤーで編集するか、OpenTelemetry Collectorのプロセッサで除去するか、データ分類ルールに基づいて本番以外の環境にキャプチャを制限します。

埋め込み

埋め込みスパンでは、gen_ai.request.modelgen_ai.usage.input_tokensを設定します。

評価結果

幻覚、バイアス、関連性、または任意のカスタム評価シグナルに対して、gen_ai.evaluation.resultイベントをGenAI操作スパンに付与します。これにより、コスト、レイテンシ、品質の間のループが閉じられます。これらはすべてスパンデータとしてクエリ可能です。

マルチエージェントの計装

マルチエージェントシステムでは、2つのルールがあります:

  1. 各エージェントに独自のgen_ai.agent.nameを割り当てます。 これにより、Timelineでのスイムレーンと引き渡しの可視性が駆動されます。サブエージェントは独自の異なる名前を使用し、親から継承しません。属性が欠落している場合、スパンは「Unknown」として表示され、目的が損なわれます。
  2. 呼び出し側のエージェントがinvoke_agentスパンを発行し、呼び出されるエージェントが発行するのではありません。 呼び出されるエージェントは、独自のgen_ai.agent.nameの下でchatexecute_toolなどの独自のスパンを発行します。これにより、引き渡し自体がトレース内の明示的でクエリ可能なイベントになります。
# Orchestrator agent invoking a specialist agent
with tracer.start_as_current_span("invoke_agent billing_agent") as span:
    span.set_attribute("gen_ai.conversation.id", conversation_id)
    span.set_attribute("gen_ai.agent.name", "orchestrator")   # the caller
    span.set_attribute("gen_ai.operation.name", "invoke_agent")

    # billing_agent emits its own spans under
    # gen_ai.agent.name = "billing_agent"
    return billing_agent.handle(query, conversation_id)

スパン命名規則

一貫したスパン名は重要です。Timelineが操作を正しくグループ化して描画するために使用するためです:

使用中のSDKでこれを行う

手動計装は任意のエージェントで機能しますが、実際にはほとんどのチームがフレームワークまたはベンダーSDK上に構築しています。以下は、それぞれの考え方です。

OpenAI Python SDK. openaiパッケージはネイティブにGenAI semconvスパンを発行しませんが、OpenTelemetry contribの自動計装パッケージが存在し、適切な属性を持つchatembeddingsスパンを箱から出して発行します。計装を導入するだけで、LLMレイヤーのテレメトリーが無料で得られます。ただし、gen_ai.conversation.idgen_ai.agent.nameは自分で設定する必要があります。LLM呼び出しを制御可能な親スパンでラップすると、自動計装が発行する子スパンが会話コンテキストを継承します。

Anthropic Python SDK. OpenTelemetry contribエコシステムに自動計装が存在します。SDK呼び出しが発火したときにconversation IDがスコープ内にあるように、独自のconversation-scopingスパンと組み合わせます。

LangChainとLangGraph. LangChainのコールバックシステムは、コミュニティパッケージを通じてOpenTelemetryに接続できます。自動計装によりLLMとツールのスパンが得られますが、conversation ID、エージェント名(特にすべてのノードが別個のエージェントであるマルチエージェントグラフの場合)、およびLangChainが認識しないカスタムツールや下流サービスへのトレースコンテキストの伝播は依然としてあなたの責任です。

これらすべてに共通するパターンは同じです:フレームワークの計装にLLMレイヤーのスパンを所有させ、エージェントレイヤーと会話レイヤーの属性をあなたが所有する。 自動計装は会話の境界やエージェントのアイデンティティを推測できません。これはアプリケーションのプロパティです。

動作したときに表示されるもの

Agent Timelineのスクリーンショット

これらの属性がHoneycombに流れ込むと、1つの会話のAgent Timelineビューには以下が表示されます:

  • 会話のサマリー:総所要時間、モデル呼び出し、ツール呼び出し、再試行、関与したエージェント、失敗回数
  • エージェントごとの水平スイムレーンと、それらの間の明示的な引き渡し
  • 失敗スパンのインライン強調表示と「Show Failures Only」フィルター
  • プロンプト、レスポンス、トークン使用量、モデル名、終了理由を表示するGen AIパネル
  • 下流のAPIおよびデータベーススパンを含む完全なトレースウォーターフォール。多数の会話を横断するシステム全体の調査のためにCanvasにピボット可能

これが、エージェントが失敗したことを知ることと、なぜ失敗したかを理解することの違いです。モデルが根本原因であることは稀です。LLM呼び出し、ツール呼び出し、引き渡し、下流のシステム処理を含むチェーン全体を計装することで、次に何かが壊れたときに正しい方向を示してくれます。