llm-obs-session-classify について
Datadog LLM Obs のセッションやトレースを、満足・部分的・不満足に分類するスキルです。ターン内容を段階的に取得し、利用可能な MCP ツールまたは pup CLI を使います。
主な機能
- 01セッション、トレース、アプリの各モード
- 02yes/partial/no の判定
- 03段階的な会話内容の取得
- 04MCP または pup の選択
活用シーン
- 01Datadog アシスタントセッションの評価
- 02単一トレースの確認
- 03LLM アプリのセッションのサンプリング
MCP 対応
利用可能な場合は Datadog LLM Obs MCP ツールを使い、ない場合は pup CLI に処理を置き換えます。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ datadog-labs/ agent-skills --skill llm-obs-session-classifyインストール後、Claude Code で/llm-obs-session-classifyを入力して読み込まれたか確認します。
詳細
バックエンド
検出 — 各呼び出しの開始時に、何らかのアクションを実行する前に、使用するバックエンドを判断します。
- ユーザーが呼び出しのどこかで
--backend pupを渡した場合 → MCP ツールの有無にかかわらず、ただちに pup モードを使用します。手順 2~4 はスキップします。 - アクティブなツール一覧に MCP ツールがあるか確認します。判断基準となるのは、利用可能なツールに
mcp__datadog-llmo-mcp__search_llmobs_spansがあるかどうかです。 - MCP ツールがある場合 → 全体を通して MCP モードを使用します。このスキルのワークフローセクションに記載された名前どおりに MCP ツールを呼び出します。
- MCP ツールがない場合 →
pupが実行可能か確認します。Bash 経由でpup --versionを実行します。"version"を含む JSON 応答があれば、pup を利用できます。 - pup が応答する場合 → 全体を通して pup モードを使用します。このファイルの末尾にあるツールリファレンスの付録を使い、すべての MCP ツール呼び出しを対応する pup 呼び出しに置き換えます。
- どちらも利用できない場合 → 停止し、ユーザーに次のように伝えます。
"Datadog MCP サーバーも pup CLI も利用できません。MCP サーバーを接続する(
claude mcp add --scope user --transport http datadog-llmo-mcp 'https://mcp.datadoghq.com/api/unstable/mcp-server/mcp?toolsets=llmobs,rum')か、pup をインストールしてください。"
--backend pup は呼び出し引数のどこにあっても受け付けられます。スキルのロジックに残りの引数を渡す前に取り除きます。
pup の呼び出しルール:
- Bash 経由で呼び出します:
pup llm-obs <subcommand> [flags] - pup は常に JSON を出力します。直接解析してください(MCP の結果のように
[{"type": "text", "text": "<json>"}]に JSON がラップされることはありません)。 - pup が認証エラーを返した場合は、
pup auth loginを実行するようユーザーに伝え、停止します。 - 並列化:1 回のメッセージで複数の Bash ツール呼び出しを発行します(各呼び出しにつき pup コマンドは 1 つ)。
- 時間フラグ:pup は期間を表す文字列(
1h、7d、30m)と RFC3339 タイムスタンプを受け付けます。now-で始まる文字列は使わないでください。スキルの--timeframe引数から変換する場合は、接頭辞を取り除きます:now-7d→7d、now-24h→24h、now-30d→30d。 pup llm-obs spans searchの--summaryは、ペイロードフィールドを必要最小限のメタデータのみに絞ります。コンテンツが不要な一括処理や検索のフェーズで使用します。
エントリーモード別の pup モードに関する注意事項:
session_idモード:手順 1~3 と手順 5 は完全に動作します。手順 4(RUM)ではanalyze_rum_eventsの代わりにpup rum aggregate --user-email EMAILを使用します。ツールリファレンスを参照してください。手順 4b(監査証跡)は pup 固有の機能で、OAuth 経由で現在のユーザー自身の組織を照会します。trace_idモード:MCP モードと完全に同等です。ml_appモード:オプション A(aggregate_spans)は pup では利用できません。スキップして、オプション B に進みます。
呼び出し ID: 各呼び出しの開始時に、MCP ツールを呼び出す前に、8 文字の 16 進数の呼び出し ID(例:3a9f1c2b)を生成します。呼び出し全体を通して同じ ID を使用します。
インテントタグ付け: すべての MCP ツール呼び出しで、telemetry.intent の先頭に skill:llm-obs-session-classify[<inv_id>] — を付け、その後にツールを呼び出す理由を記述します。最初の MCP ツール呼び出しのみ、代わりに skill:llm-obs-session-classify:start[<inv_id>] — を使います(:start サフィックスに注意してください)。最初の呼び出しの例:skill:llm-obs-session-classify:start[3a9f1c2b] — Step 1: enumerate turn root spans for session abc-123
スキル:eval-session-classify
Datadog LLM Obs のセッションとトレースを分類するスキルです。簡潔な理由説明とともに、満足度の判定(yes/partial/no)を生成します。単独で呼び出すことも、eval-pipeline から呼び出すこともできます。
入力
| 入力 | モード | 必須 | 説明 |
|---|---|---|---|
session_id | session_id モード | はい | Datadog CMD+I アシスタントセッションの UUID |
trace_id | trace_id モード | はい | LLM Observability のトレース ID |
ml_app | ml_app モード | はい | サンプリング対象の LLM アプリ名 |
timeframe | ml_app モード | いいえ(デフォルト:now-7d) | どれくらい前までサンプリングするか |
sample_limit | ml_app モード | いいえ(デフォルト:20、上限:50) | 分類するセッションまたはトレースの数 |
session_id、trace_id、ml_app のいずれも指定されていない場合 → 直ちに停止して、次を返してください:
{
"error": "missing_input",
"detail": "Provide one of: session_id, trace_id, or ml_app."
}
フェーズ 0 — モードの検出
session_idが指定されている → session_id モード → セッションモードへ進むtrace_idが指定されている(session_idは指定されていない)→ trace_id モード → トレースモードへ進むml_appが指定されている(session_id、trace_idは指定されていない)→ ml_app モード → ML App モードへ進む- 何も指定されていない → エラーを出力する(上記を参照)
出力形式
デフォルト(コンパクト)— 呼び出し元が詳細表示を要求しない限り、すべてのモードで使用します:
verdict: yes | partial | no
reason: <one sentence>
詳細表示 — 完全な Markdown レポート(各モードのセクション末尾を参照)。入力に verbose: true を含めるか、詳細なレポートを求めることで詳細表示を要求できます。
ml_app モードでは、トレースごとのコンパクトなブロックの後に、常にサマリーテーブルが追加されます。
コンテンツ取得のカスケード
会話コンテンツの読み取りは、すべてのモードでこのカスケードに従います。判定を行う前に、ステップ 1(セッションモード)またはステップ T1(トレースモード)で特定したすべてのターンのルートスパンに対して、カスケードを実行してください。
完全性ゲート:すべてのターンでカスケードが実行されるまで、RUM(セッションモード)または分類(すべてのモード)に進まないでください。
search_llmobs_spansのプレビュー(input.preview/output.preview、約 200 文字に切り詰められたもの)は、このゲートを満たしません。プレビューでわかるのはターンの境界だけです。カウントされるのは、get_llmobs_agent_loop、expand_llmobs_spans+get_llmobs_span_content、またはget_llmobs_span_content(field="input"/"output")から取得した実際のコンテンツのみです。短いセッション(20 ターン以下):すべてのターンで C1/C2 を実行します。選別や「重要なターン」というヒューリスティックは使用しません。
長いセッション(20 ターン超):最初の 5 ターンと最後の 5 ターンに加えて、
input.previewまたはoutput.previewで異常(エラー、修正の繰り返し、同じ意図の繰り返し、予期しないツール)が示されたターンに対して C1/C2 を実行します。残りのすべてのターンについても、スキップする前にプレビューを確認し、異常がないか調べてください。
並列処理:N 個すべての get_llmobs_agent_loop 呼び出しを、1 つのメッセージで発行してください。ターンごとに 1 回呼び出し、すべてを同じバッチに含めます。N ターンのセッションでは、N 回の C1 呼び出しを同時に行います。ターンを順番に処理したり、すべての結果が揃う前に部分的な判定を行ったりしないでください。あるターンで C1 が iterations: [] を返した場合に限り、そのターンの C2 に進んでください。
C1 — get_llmobs_agent_loop(trace_id, agent_span_id)
最も情報量の多いソースです。各イテレーションの完全なシステムプロンプト、ユーザーメッセージ、ツール呼び出しの引数と結果、アシスタントの応答、トークン使用状況を取得できます。すべてのエージェントスパンで、最初にこれを試してください。
get_llmobs_agent_loop(
trace_id = "<TRACE_ID>",
span_id = "<AGENT_SPAN_ID>",
from = "now-90d",
to = "now",
max_content_length = 2000
)
iterations: []かつtimeline: null→ アプリの LLM スパンは、エージェントスパンの直接の子ではなく、中間のワークフローレイヤー(例:get_answer_from_model_step)を経由しています。C2 に進みます。- 404 → スパン ID がトレースストアで解決されません。C2 に進みます。
- コンテンツフィールドに
<REDACTED_INPUT>または<MASKED_STREAMING_RESPONSE>が含まれている → 機能フラグにより IO トレースが無効になっています。構造(イテレーション数、ツール名、トークン使用状況)は引き続き有用なので、その旨を記録してください。実際のコンテンツを取得するには C2 に進みます。
C1 の結果が大きすぎてコンテキストに収まらず、ディスクに保存された場合(長いセッションでよく発生):ファイル全体を Read しようとしないでください。トークン上限を超えてしまいます。対象を絞った Python 抽出を実行し、ターンごとの構造化サマリーを作成してください:
import json
with open('<persisted_path>') as f:
data = json.loads(json.load(f)[0]['text'])
iters = data['iterations']
for it in iters:
tcs = [tc['name'] for tc in it.get('tool_calls', [])]
content = it.get('content', '') or ''
# Skip the system prompt body — it's verbose and not what we're classifying.
if len(content) > 4000 and content.lstrip().startswith('#'):
content = '[system prompt]'
print(f"iter {it['iteration']} [{it.get('status')}] tools={tcs}"
f" err={it.get('error_message','')} content={content[:300]}")
大きくても解析可能な C1 の結果は実際のコンテンツです。C1 が iterations: [] を返した場合に限り、C2 に進んでください。
C2 — LLM 子跨度上的 get_llmobs_span_content(field="messages")
C1 で iterations: [] が返される場合、LLM スパンは通常、ルートエージェントスパンの 2 階層下にあります。つまり、エージェントループ API がたどらないワークフローラッパー(例:get_answer_from_model_step)の下です。各ターンの具体的なツリーは次のとおりです。
<root agent span> ← one per turn
<workflow wrapper> ← one per LLM round-trip
<llm span, e.g. anthropic.request> ← call `field="messages"` here
get_llmobs_trace は深さ 1 の子のみを返し、ワークフローの下にネストされた LLM スパンには到達できないため、LLM スパン ID をたどるには expand_llmobs_spans を使用します。
expand_llmobs_spans(
trace_id = "<TRACE_ID>",
span_ids = ["<ROOT_AGENT_SPAN_ID>"],
max_depth = 2, # root → workflow → llm span
from = "now-90d", # required: default is now-1d, silently returns empty for older spans
to = "now"
)
返されたツリーから、span_kind=llm かつ has_input=true のすべてのノードを収集します(一般的な名前は anthropic.request、openai.request、chat_completion-call、messages-call です)。それぞれに対して get_llmobs_span_content(field="messages") を呼び出します。
JSONPath と切り詰めに関する注意点:path は max_tokens による切り詰めの後に適用されます。各 LLM 呼び出しスパンは通常、複数 KB のシステムプロンプトで始まるため、max_tokens の値が小さいと、JSONPath フィルターはシステムプロンプトのみの内容に対して処理され、誤ったメッセージが気付かれないまま返されます。
意味のある末尾(ユーザークエリ、ツール呼び出し、最終回答)を抽出するには:
pathを指定せずに 1 回呼び出し、レスポンスからtotal_tokens_approxを読み取ります。path = "$.[-5:]"およびmax_tokens = total_tokens_approx + 500を指定して、再度呼び出します。
get_llmobs_span_content(
trace_id = "<TRACE_ID>",
span_id = "<LLM_SPAN_ID>",
field = "messages",
path = "$.[-5:]",
max_tokens = <total_tokens_approx + 500>
)
これにより、最後の 5 つのメッセージが返されます。通常は[ユーザーのコンテキスト+クエリ、推論、アシスタントのテキスト、アシスタントの tool_call、ツールの結果]で、ターンの内容を理解するには十分です。
- メッセージが
"REDACTED"になっている → IO トレーシングが無効です。C3 に進みます。 content_infoマップにmessagesが含まれていない → チャットスパンではありません。C3 に進みます。
C3 — ルートスパンで get_llmobs_span_content(field="input") および field="output" を使用
ルートスパンには、アプリが書き込んだ合成サマリーが含まれていることがよくあります(例:input としての生のユーザークエリ、output としての最終レスポンス、または "Investigate error for issue: <id>" / "Investigation completed with status: completed")。得られる情報は最小限ですが、タスクの内容と完了したかどうかを確認するには十分です。
C4 — 構造的なシグナルのみ
すべてのコンテンツにアクセスできない場合は、スパンのメタデータのみから分類します。
status(ok / error)、stop_reason、response_truncated- 子スパンの名前(ツール名、
classify、generate-summary、suggest-actionなどのワークフローのステップ名) iterationタグの数(LLM のラウンド総数)- 利用可能な場合は、所要時間とトークン数
ドリルダウン — 疑わしいツール結果を対象を絞って抽出
サマリーを確認した後、ツール一覧にクライアントでレンダリングされるツール、書き込み操作、または error_message が付いた反復処理が含まれるターンを詳しく確認します。これらのチェックにより、サマリーの確認ループでは検出できないサイレントな失敗を見つけられます。
トリガー:
-
クライアントでレンダリングされるツール(ブラウザーで処理されるもの。例:
show_content、create_datadog_notebook、edit_datadog_notebook)— 存在する場合は必ず確認してください。サーバーがイベントを発行し、クライアントが表示を処理します。結果から、レンダリングが委任されたのか、確認されたのかがわかります。"This tool is handled by the client application..."→ ブラウザーに委任されており、ユーザーが何かを見たというサーバー側の確認はありません。次のターンの入力にclient_tool_responseがあるか確認してください。存在しない場合、または次のターンでユーザーが困惑している様子が見られる場合は、出力が表示されなかったものとして扱ってください。これは強い否定的シグナルです。- それ以外の結果 → サーバー側で実行され、出力はレスポンスストリームに含まれています。
-
書き込み操作(例:
call_datadog_api、upsert_datadog_dashboard、upsert_widget、モニター/ノートブックの作成・更新)— アシスタントは実際の HTTP の結果にかかわらず、「X を更新しました」と説明することがよくあります。次の結果を確認してください。403/"not allowlisted"/"PUT not allowlisted"→ 書き込みがブロックされており、変更は適用されていません。404→ アセットが見つかりません。存在しないものを編集しようとしています。500/"internal server error"→ 書き込みの成否は不明で、反映されていない可能性があります。"Session not found"→ ツールの実行前にリソースの有効期限が切れています。- ペイロードを伴う
200/201→ 成功が確認されています。
-
結果が空のデータクエリ(例:
search_datadog_metrics、get_datadog_metric、search_datadog_logs、search_datadog_spans)— サマリーに同じクエリツールの反復処理が多数(5 回超)表示される場合は、結果が空だったか確認してください。NO_DATA/空の配列をもとに推論したアシスタントが、もっともらしく聞こえるものの捏造された分析を出すことがあります(hallucinationの失敗モード)。 -
エラーが発生した反復処理 —
error_messageが設定されている反復処理をすべて確認し、正確なエラー、エラーを引き起こしたツール、アシスタントが再試行して成功したか、または断念したかを確認してください。一時的なエラーへの再試行は中立的です。恒久的な API 制限や同一の失敗の繰り返しは否定的です。
CLIENT_TOOLS = {'show_content', 'create_datadog_notebook', 'edit_datadog_notebook'}
WRITE_TOOLS = {'call_datadog_api', 'upsert_datadog_dashboard', 'upsert_widget'}
FAIL_STRINGS = {'403', '404', '500', 'not allowlisted', 'not found', 'session not found', 'error'}
for it in iters:
if it.get('error_message'):
print(f"ERROR iter={it['iteration']}: {it['error_message'][:200]}")
for tc in it.get('tool_calls', []):
name = tc['name']
result = (tc.get('result') or '')
if name in CLIENT_TOOLS:
print(f"CLIENT iter={it['iteration']} {name}: {result[:300]}")
elif name in WRITE_TOOLS or any(s in result.lower() for s in FAIL_STRINGS):
print(f"WRITE iter={it['iteration']} {name}: {result[:300]}")
解釈の早見表:
| 結果のパターン | 評価のシグナル |
|---|---|
"This tool is handled by the client application" | レンダリング未確認 → 次のターンの client_tool_response を確認 |
403/"not allowlisted" | 書き込みがブロック — アシスタントでは本来の目的を達成できない可能性があります |
"Session not found" | リソースの有効期限切れ — ツール呼び出しの効果はありません |
データクエリでの NO_DATA/空のリスト | 実質的なシグナルなし — アシスタントの回答にハルシネーションがないか確認 |
書き込み操作での 500 | 不確実 — 反映されているかどうかは不明です |
ペイロードを伴う 200/201 | 成功が確認されています |
上記のツール名一覧は、Datadog のアシスタントアプリ向けに調整した例です。他のアプリでは、C1 の tool_calls に表示されるツール名を確認し、その意味(クライアントでのレンダリング、サーバー側の副作用、読み取り専用のクエリ)を考慮してください。
セッションモード
単一の Datadog CMD+I アシスタントセッションを分類します。RUM の行動シグナルを使って、トレースに基づく評価を確認します。
ステップ 1 — ターンのルートスパンを列挙する
search_llmobs_spans(
tags = {"session_id": "<SESSION_ID>"},
root_spans_only = True,
from = "now-90d",
to = "now",
limit = 500
)
pup モード:
pup llm-obs spans search --query "@session.id:<SESSION_ID>" --root-spans-only --limit 500 --from 90d
query 文字列形式ではなく、tags={...} を使用してください。自由形式の query="session_id:<id>" は全文検索の動作を引き起こし、各ターンについてトレースバンドル全体を返します(スパン数が 5 倍になり、構造フィルターは黙って無視されます)。その結果、長いセッションでは行の上限に達します。tags={"session_id": "<id>"} はインデックス化されたタグフィルターに一致し、ほかのパラメーターも適用されます。
root_spans_only=True を指定すると、すべてのワークフロー/LLM/ツールの子孫スパンではなく、ユーザーの各ターンにつき 1 つのスパン(通常はルートエージェントスパン)が返されます。セッションに 500 ターンを超えるターンがある場合は、next_cursor を使ってページネーションしてください。
返された各ルートスパンについて、次の情報を記録します:
trace_id、span_idstart_ms、duration_ms- タグ:
user_handle、user_id、org_id、product_area、message_id - ルートの
nameとspan_kind(通常はagent/assistantですが、アプリによって異なります) input.preview、output.preview(ターン境界の特定にのみ使用します。以下の警告を参照してください)
ターンを start_ms の昇順に並べます。最初のターンのタグから、セッション単位の識別情報(user_handle、user_id、org_id、product_area、セッションの start_ms)を取得します。
プレビューの切り詰めに関する警告:
input.previewとoutput.previewは約 200 文字に切り詰められています。これらはターン境界の特定や明らかな異常の検出には使えますが、判定の根拠としては不十分です。プレビューを見てすでに「結論が明らか」に見えるからといって、コンテンツ取得カスケードを省略しないでください。
search_llmobs_spans が結果を返さない場合 → 停止し、エラー llmobs_not_found を返します。
ターンごとの構造的な詳細(反復回数、ツール名、stop_reason、mcp フラグ、response_truncated)は、後ほどステップ 3(C1)で各ターンの get_llmobs_agent_loop の結果から取得します。
ステップ 2 — 評価とメタデータを取得する
get_llmobs_span_details(
trace_id = "<TRACE_ID>",
span_ids = ["<AGENT_SPAN_ID>"],
from/to = <same window>
)
evaluations マップのすべてのキーを順に確認し、各判定者の .value、.reasoning、.tags を記録します。
content_info.metadata からは、query_string、referrer_path、referrer_url、entities_json、user_info_json を取得します。
get_llmobs_span_details が失敗するか、空の結果を返す場合 → 何もせずにスキップし、ステップ 3 に進みます。
content_info に metadata が存在する場合は、get_llmobs_span_content(field="metadata") を使って取得し、実際の query_string と referrer_path を確認します。これにより、エージェントループを参照せずに、ユーザーの質問とアクセスしていたページを把握できます。
ステップ 3 — 会話全体を読む
ステップ 1 で列挙したすべてのターンのルートスパンについて、コンテンツ取得カスケードに従います。カスケードの並列処理ルールに従い、ターンごとのすべての C1 呼び出し(get_llmobs_agent_loop)を 1 回の並列バッチで実行し、C1 が iterations: [] を返したターンについては C2 に進みます。
LLM スパンがワークフローラッパーの配下にあるアプリ(例:get_answer_from_model_step を介して anthropic.request が孫スパンとなる assistant_api)では、C1 が空の反復結果を返すのはよくあることで、想定内です。カスケードは、それらのターンを C2 の expand_llmobs_spans ナビゲーションに振り分けて処理します。LLM 呼び出しのコンテンツを取得できない場合は、C3(ルートスパンのメタデータと出力)が最後の手段になります。
ステップ 4a — RUM セッション ID を特定する
メインの RUM クエリを実行する前に、最初のターンの start_ms を基準に前後 2 分の範囲を絞り込んで検索し、ユーザーが利用していた正確なブラウザーセッションを特定します。これにより RUM の session_id を切り分け、時間範囲を広くして @usr.email のみでフィルターした場合に発生するセッション間の混入を防げます(1 日の間に複数のタブを使っているユーザーの場合、それらすべてのイベントが返されます)。
analyze_rum_events(
event_type = "action",
filter = "@usr.email:<user_handle> @action.type:custom",
from = <start_ms - 120000>, # 2 minutes before first turn's start_ms (epoch ms)
to = <start_ms + 120000>, # 2 minutes after
sql_query = 'SELECT DISTINCT session_id FROM rum LIMIT 10'
)
重複しない session_id の値をすべて抽出し、rum_session_ids に保存します。以降のすべてのクエリで使う RUM フィルター(以下の <rum_filter>)は、結果に応じて次のように決定します。
- 結果が 0 件 →
<rum_filter>=@usr.email:<user_handle>(メールアドレスと時間範囲にフォールバックします)。 - 結果が 1 件 →
<rum_filter>=@session.id:<rum_session_id>。 - 結果が複数件 →
<rum_filter>=@session.id:(<id1> OR <id2>)。
pup モード:
pup rum aggregate --user-email <user_handle> --query "@action.type:custom" --from <start_ms - 120000> --to <start_ms + 120000> --compute count --group-by @session.idを実行します。--group-byフラグ名はpup rum aggregate --helpで確認してください。
ステップ 4 — RUM の行動シグナルを取得する
pup モード:
analyze_rum_eventsの呼び出しをpup rum aggregateの呼び出しに置き換えます(「ツールリファレンス」を参照)。ページビュー:pup rum aggregate --user-email USER_HANDLE --from START_MS --to END_MS --compute count --group-by @session.id。カスタムアクション:pup rum aggregate --user-email USER_HANDLE --query "@action.type:custom" --from START_MS --to END_MS --compute count --group-by @evt.name。API がクエリを拒否した場合は、イベントタイプのフィルターフラグをpup rum aggregate --helpで確認してください。
start_ms と(ステップ 4a の)<rum_filter> を使い、時間範囲を次のように定義します。
- pre:
[start_ms − 30min, start_ms] - during:
[start_ms, start_ms + session_duration_ms] - post:
[start_ms + session_duration_ms, start_ms + session_duration_ms + 60min]
並行して実行します。
RUM クエリ A — ページビューのタイムライン:
analyze_rum_events(
event_type = "view",
filter = "<rum_filter>",
from/to = <pre to post>,
sql_query = "SELECT timestamp, view_url, \"@view.time_spent\" FROM rum ORDER BY timestamp LIMIT 200",
extra_columns = [{"name": "@view.time_spent", "type": "int64"}]
)
@view.time_spent の単位はナノ秒です。
RUM クエリ B — カスタムアクション(製品領域で絞り込み):
analyze_rum_events(
event_type = "action",
filter = "@action.type:custom <rum_filter>",
from/to = <pre to post>,
sql_query = """
SELECT timestamp, "@action.name", view_url FROM rum
WHERE (
"@action.name" LIKE 'command-assistant%'
OR "@action.name" LIKE 'workbench%'
OR "@action.name" LIKE 'ai-experiences%'
OR "@action.name" = 'click on Bad response'
OR "@action.name" = 'click on Incorrect result'
OR "@action.name" = 'click on Submit'
)
ORDER BY timestamp LIMIT 200
""",
extra_columns = [{"name": "@action.name", "type": "string"}]
)
上記の LIKE フィルターは Datadog アシスタントの例です。分類対象のアプリで意味のあるアクション名のプレフィックスに合わせて絞り込むか、フィルターを削除してセッション上のすべてのカスタムアクションを読み取ってください。
いずれかのクエリで 0 行が返された場合、または権限/認証エラーが発生した場合も、分類を中断してはいけません。RUM の欠落や不完全なデータはカバレッジの不足であり、行動の証拠ではありません。これを否定的な証拠として扱うことは、文書化されている失敗パターンです。
| 条件 | 対応 |
|---|---|
| セッション上の RUM 行が 0 件で、ユーザーにはより広い 30 日間の範囲で SOME RUM データがある(セッション日の Web RUM の欠落) | 劣化モードで処理します。RUM なしで続行し、トレースの証拠(ステップ 3)と監査シグナル(該当する場合はステップ 4b)を使用します。出力に rum_coverage: partial — web RUM gap on session date を設定します。 |
analyze_rum_events/pup rum が 1 つ以上のクエリで権限または認証エラーを返す(外部組織、SOC2 で分離されたテナントでよく発生) | 劣化モードで処理します。行が返された RUM クエリを使用し、不足部分はトレースと監査にフォールバックします。出力に rum_coverage: partial — auth error on <query> を設定します。 |
| セッション上の RUM 行が 0 件で、30 日間にユーザーの RUM データがどこにもない | 劣化モードで処理します。トレースと監査を使って続行します。出力に rum_coverage: unavailable — no RUM data for user を設定します。終端エラーとして rum_unavailable を出力してはいけません。 |
厳守事項: RUM が存在しないことを、否定的な判定の証拠として使ってはいけません。トレースと監査のシグナルが満足を示している場合、RUM がないことでその判定を覆してはいけません。トレースのシグナルが曖昧な場合は、行動データが欠落していることを理由に不利な判断をせず、トレースのみの判定をデフォルトとします。
rum_unavailable エラーコードは、分類プロトコルで RUM が必須であり(たとえば、トレースの内容が大幅にマスキングされ、RUM だけが残されたシグナルである場合)、かつ RUM が存在しないという、復旧不可能なケースに限って使用します。それ以外の場合は、劣化モードで続行します。
シグナル解釈フレームワーク
RUM アクションは、どれも同じ証拠になるわけではありません。3 つのティアに分類し、各アクションの重みをステップ 5 でティアに応じて調整します。以下のカテゴリはエージェントに依存しません。ご利用のアプリに固有の RUM アクション名を、これらのカテゴリに対応付けてください。満足度との相関を示す証拠が得られるまでは、認識されないアクションを「曖昧」として扱ってください。
ティア A — 単独で十分な証拠(存在する場合、トレースの判定を覆す、または確定できる)
| 抽象カテゴリ | 方向 | アクション名のパターン例 |
|---|---|---|
| 応答の完了前にストリームの中断が発生 | ネガティブ | *.chat-cancel、*.stream-abort、*.cancel-generation |
| ストリーム中にパネル/ウィンドウが閉じられ、1 秒以内に再度開かれない | ネガティブ | エージェントの LLM スパンがまだアクティブな状態での *.panel.close |
| エージェントが応答内で明示したアセット ID を含む URL へのナビゲーション | ポジティブ | エージェントが参照した正確なリソースパスの表示 |
| セッション終了から 60 秒以内に、エージェントの出力に対して行われた成果物アクション | ポジティブ | 生成されたアセットを対象としたエクスポート、保存、変更、共有 |
| 同じインテントで 15 分以内に新しいセッションを開始(ステップ 1b の分類器による再試行) | ネガティブ | 近接セッション分析で検出 |
エージェントの計画を全面的に拒否(reject_all または同等の操作) | ネガティブ | 一括拒否の操作、ストリーム中の「停止」アクション |
ティア B — 補強材料。単独では決定的な証拠にならない
| 抽象カテゴリ | 方向 |
|---|---|
| エージェントのビューの滞在時間が短い(合計 30 秒未満) | ネガティブ |
| セッション後に外部のヘルプ(ドキュメント、サポート、コミュニティ)へ移動 | ネガティブ |
| 応答前にパネル/ビューを繰り返し開く | ネガティブ |
| エージェントのビューの滞在時間が長い(60 秒超) | ポジティブ |
| エージェントが表示したマークダウンリンクをクリック | ポジティブ |
| セッション後にトピック関連のリソースへ移動(正確なアセットとの一致はなし) | ポジティブ |
曖昧 — 単独の証拠として使用しないでください
| 抽象カテゴリ | 曖昧な理由 | ルール |
|---|---|---|
| 中間状態(ツール呼び出しパネル、推論ブロック)の表示/展開操作 | 熱心に読み込んでいる場合にも、低評価を付ける前に精査している場合にも発生します。両者を区別する合理的なルールはありません | 同じ方向を示すティア A、または対応するティア B のシグナルと組み合わせてください |
ツール呼び出しを 1 件承認(N 件の提案のうち 1 件に対する accept) | 期待される UX 上の手順です。平凡な応答でもユーザーは「承認」をクリックします | 弱いポジティブとして扱い、「はい」の主要な証拠には決してしないでください |
ツール呼び出しを 1 件拒否(N 件の提案のうち 1 件に対する reject) | 「ユーザーが不満」ではなく、「ユーザーがより良い方法へ誘導した」可能性があります | ユーザーがその後メッセージを送らなくなった場合、または後続の呼び出しがすべて拒否された場合にのみ、ネガティブとしてカウントしてください |
| ストリーム中であることが不明な状態でのパネル/ビューのクローズ | 「セッションが自然に終了した」と「途中で放棄された」を区別できません | ストリームがアクティブだったタイミングの情報が必要です。ティア A を参照してください |
ご利用のエージェントの場合: 分類する前に、アプリの RUM アクション名をこれらのカテゴリに対応付けてください。Datadog CMD+I アシスタントの対応付け例は @rum-actions-bits-assistant.md にあります。ご利用のアプリの RUM イベント分類で確認すべき項目のテンプレートとして使用してください。認識されないアクション名は、デフォルトで「曖昧」とします。満足度との相関を示す証拠(ラベル付きセッションまたは製品知識)が得られた後に限り、ティア B またはティア A に昇格させてください。
Datadog アシスタントのアクション名を具体的に解釈する際は、@rum-actions-bits-assistant.md を参照してください。
ステップ 4b — 監査証跡のシグナルを取得する(アセットの作成・編集があった場合)
監査証跡には、アシスタントの操作がサーバーで確認された結果(反映された書き込み、定着した変更、削除、後続の編集)と、同じアセットに対するセッション後のユーザーの行動が記録されます。アシスタントが Datadog アセット(ダッシュボード、ノートブック、モニター、SLO など)を作成または編集したセッションでは、これは最も信頼性の高い行動シグナルです。RUM のクリックよりも直接的です。
主なツールは pup audit-logs search です。OAuth を使って、アクティブなユーザー自身の組織を照会します(pup auth login)。
このステップを実行するタイミング:
- トレース内のツール呼び出しに書き込み操作(
upsert_*、PUT/POSTを伴うcall_datadog_api、モニター/ノートブックの作成または更新)が含まれる場合 → 実行します。 - セッションに既知のアセット ID(ツール呼び出しの引数、
dashboard_idタグ、セッションのreferrer_path、またはユーザーのクエリから取得)がある場合 → 実行します。 - トレースが完全な読み取り専用(書き込みなし、アセット ID なし)の場合 → スキップします。ステップ 4 に加える監査情報はありません。
クエリの優先順位(シグナルが強い順、ノイズが少ない順):
@asset.id— 完全な差分を含む変更/削除履歴です。最も信頼性が高い情報です。アセット ID が既知の場合は必須です。- Q3(対象を絞った HTTP パス) — 同じアセットに対する書き込み回数とタイミングです。アセット ID が既知の場合は必須です。
- Q1/Q2(広範なシグナルを対象とする HTTP 監査) — 代替手段としてのみ使用します。アクティブなユーザーではノイズが多くなります。
@asset.id — 変更履歴
pup audit-logs search \
--query '@asset.id:<asset_id>' \
--from "<session_start - 30d, ISO>" --to now --limit 50
各行の本文は row["attributes"]["attributes"] に格納されています。フィールド:
action—created/modified/deleted/accessedusr.emailasset.type(dashboard、notebook、monitor、…)、asset.id、asset.nameasset.new_value/asset.prev_value— 変更前後の状態全体をネストされた辞書として格納します(そのまま使用でき、JSON の解析は不要です)timestamp— ISO8601 形式の文字列
Pup はデフォルトで降順の結果を返します。セッションの時系列を確認できるよう、昇順に並べ替えます。
rows = sorted(d["data"], key=lambda r: r["attributes"]["attributes"]["timestamp"])
シグナル:
session_end 後のイベント | 判定のシグナル |
|---|---|
deleted イベント | 強い否定的シグナル — アセットが削除されました |
ユーザーによる modified で、ウィジェット/セル/項目の数が prev_value より少ない | 否定的シグナル — ユーザーがアシスタントの変更を元に戻しました |
ユーザーによる modified で、項目の数が prev_value より多い | 肯定的シグナル — ユーザーがアシスタントの作成物を拡張しました |
クエリ/しきい値が変更された modified(モニター) | 否定的シグナル — ユーザーはアシスタントが設定した値に同意しませんでした |
メッセージ/タグのみが変更された modified | 中立 — 外観上の変更です |
| イベントなし | 中立から肯定的 — アセットに変更はありません |
監査シグナルを過剰に解釈しないでください。 監査で測定するのはバックエンドの正確性であり、ユーザーの満足度ではありません。書き込みが見当たらないことやイベントがないことだけでは、不満の証拠にはなりません。例外はユーザーによる明示的な操作です。
deletedイベント、ウィジェット/セルを削除する手動のmodified、またはエージェントの生成内容を上書きする手動の書き込みは、ユーザーの意図を直接示すため、判定を覆す可能性があります。書き込みがゼロなのに受け入れられた場合や PUT が見当たらない場合など、バックエンドの症状を示すシグナルは、トレースと RUM を裏付けるためにのみ使用し、それだけで判定を覆してはいけません。
ダッシュボードでは、ウィジェット数の差分が最も役立ちます。
nv = row["attributes"]["attributes"]["asset"]["new_value"]
pv = row["attributes"]["attributes"]["asset"]["prev_value"]
delta = len(nv["dashboard_definition"]["widgets"]) - len(pv["dashboard_definition"]["widgets"])
Q3 — 対象を絞った HTTP パス(書き込み数とタイミング)
@asset.id で差分を確認できる一方、Q3 ではリソースへの書き込みの HTTP イベント数を確認できます。トレース内のツール呼び出しを照合したり、セッション後に手動で編集された箇所を見つけたりするのに役立ちます。
# Dashboard:
pup audit-logs search \
--query '@usr.email:<user_handle> @http.url_details.path:/api/v1/dashboard/<dashboard_id>' \
--from "<session_start - 30min, ISO>" --to "<session_end + 2h, ISO>" --limit 200
# Notebook:
pup audit-logs search \
--query '@usr.email:<user_handle> @http.url_details.path:/api/v2/notebook/<notebook_id>' \
--from ... --to ... --limit 200
イベントをフェーズごとに分類すると、セッション中とセッション後に何が起きたかを確認できます。
from collections import Counter
phase_method = Counter()
rows = sorted(d["data"], key=lambda r: r["attributes"]["attributes"]["timestamp"])
for r in rows:
a = r["attributes"]["attributes"]
ts = a["timestamp"]; m = a["http"]["method"]; s = a["http"]["status_code"]
if ts < session_start_iso: phase = "pre-session"
elif ts < session_end_iso: phase = "during"
elif ts < feedback_iso: phase = "session_end->feedback" # only when feedback time is known
else: phase = "post"
phase_method[(phase, m, s)] += 1
PUT 1 件が 1 回の書き込みです。200/201 は成功が確認されたことを示します。status_code: 0 は、ブラウザーからリクエストが送信されたものの、レスポンスが届く前にページ遷移したことを意味します。サーバーではほぼ確実に処理されているため、200 と合わせて数えてください。
解釈:
during PUT 200の件数は、トレース内の upsert ツール呼び出し数と一致するはずです。session_endの 10~30 秒後に記録され、最終ターンで予想される書き込み数と一致する書き込みは、非同期ツール実行の遅延によるものです。ユーザーが開始したものではありません。session_endの 30 秒後を超えて発生した書き込みは、ユーザーによる手動編集です。セッション後の書き込みとして数えてください。短時間にまとまって発生する少数の書き込み(例:1 分ほどで 5~10 件の PUT)は微調整です(やや良好な兆候です。ユーザーは結果を採用し、さらに改善しています)。セッション後の PUT がゼロなら、問題なく受け入れられたことを示します。- キャンセルが確認されたセッションでは、
during PUTイベントはゼロです。
Q1/Q2 — 広範なシグナルを対象にした HTTP 監査(代替手段)
アセット ID が不明な場合、または対象を絞ったクエリの後に再試行やサポートチケットのシグナルを探す場合にのみ使用してください。
# Q1: broad signal, session ± window
pup audit-logs search \
--query '@usr.email:<user_handle>' \
--from "<session_start - 15min, ISO>" --to "<session_end + 30min, ISO>" --limit 200
# Q2: post-session, strip the highest-volume noise paths
pup audit-logs search \
--query '@usr.email:<user_handle> [email protected]_details.path:/api/ui/query/scalar [email protected]_details.path:/api/ui/query/timeseries' \
--from "<session_end, ISO>" --to "<session_end + 30min, ISO>" --limit 200
アクティブユーザーではノイズが大半を占めます。 200 行の上限は実際に適用され、ページネーション用のカーソルもありません。必要に応じて除外する、その他の高ボリュームなパス:
/api/ui/frontend_telemetry/metrics、/api/v1/logs-analytics/aggregate、/api/v1/logs-analytics/list、/api/v1/logs-analytics/facet_info、/api/v2/watchdog/insights/search/*、/api/v2/notifications/notify/*、/api/v2/notifications/presence/*。これらを除外しても上限に達する場合は、時間範囲を狭めてください。
次の項目を探してください:サポートチケットの作成(POST /api/v2/cases)、アシスタント呼び出しの再試行(POST /api/v2/assistant)、ドキュメントへの移動(ここではなく RUM のステップ 4 で確認できます)。
レスポンス抽出ヘルパー
二重にネストされたエンベロープ(row["attributes"]["attributes"])は冗長です。スクリプトごとに一度正規化してください。
import json, subprocess
def pup_audit(query, frm, to, limit=200):
res = subprocess.run([
"pup", "audit-logs", "search",
"--query", query, "--from", frm, "--to", to, "--limit", str(limit),
], capture_output=True, text=True, check=True)
d = json.loads(res.stdout)
rows = []
for r in d.get("data", []):
a = r["attributes"]["attributes"]
rows.append({
"ts": a.get("timestamp"),
"method": a.get("http", {}).get("method"),
"path": a.get("http", {}).get("url_details", {}).get("path"),
"status": a.get("http", {}).get("status_code"),
"action": a.get("action"),
"asset": a.get("asset", {}),
"usr": a.get("usr", {}).get("email"),
})
rows.sort(key=lambda r: r["ts"])
return rows
グレースフルデグラデーション
クエリの結果が 0 行で、かつセッションが外部顧客の組織で発生している場合は、エラーにしないでください。詳細出力に audit_unavailable を記録し、トレースと RUM による調査を続けてください。一部の外部顧客の監査証跡(特に AP1/SOC2 分離組織)は pup からアクセスできません。トレースと RUM だけでも判定には十分です。
ステップ 4b — 監査証跡のシグナルを取得する(アセットが作成または編集された場合)
監査証跡では、アシスタントのアクションによってサーバーで確認された結果(反映された書き込み、適用された変更、削除、後続の編集)と、同じアセットに対するセッション後のユーザー行動が確認できます。アシスタントが Datadog アセット(ダッシュボード、ノートブック、モニター、SLO など)を作成または編集したセッションでは、これは最も信頼性の高い行動シグナルであり、RUM のクリックよりも直接的です。
主なツール:pup audit-logs search。OAuth(pup auth login)を使用して、現在のユーザー自身の組織にクエリします。
このステップを実行する場合:
- トレースのツール呼び出しに書き込み操作(
upsert_*、PUT/POSTを伴うcall_datadog_api、モニター/ノートブックの作成または更新)が含まれている場合 → 実行してください。 - セッションのアセット ID が判明している場合(ツール呼び出しの引数、
dashboard_idタグ、セッションのreferrer_path、ユーザーのクエリから特定できる場合) → 実行してください。 - トレースが読み取り専用のみ(書き込みなし、アセット ID なし)の場合 → スキップしてください。ステップ 4 に追加できる監査情報はありません。
クエリの優先順位(シグナルが強い順、ノイズが少ない順):
@asset.id— 詳細な差分を含む変更/削除履歴。最も信頼性が高い情報です。アセット ID が判明している場合は必須です。- Q3(対象を絞った HTTP パス) — 同じアセットの書き込み数とタイミング。アセット ID が判明している場合は必須です。
- Q1/Q2(広範なシグナルを対象にした HTTP 監査) — 代替手段としてのみ使用します。アクティブユーザーではノイズが多くなります。
@asset.id — 変更履歴
pup audit-logs search \
--query '@asset.id:<asset_id>' \
--from "<session_start - 30d, ISO>" --to now --limit 50
各行の本文は row["attributes"]["attributes"] に格納されています。フィールド:
action—created/modified/deleted/accessedusr.emailasset.type(dashboard、notebook、monitor、…)、asset.id、asset.nameasset.new_value/asset.prev_value— 変更前後の状態全体をネストされた辞書として格納しています(そのまま使用でき、JSON の解析は不要です)timestamp— ISO8601 形式の文字列
Pup はデフォルトで降順に返します。時系列でセッションの推移を確認するには、昇順に並べ替えます。
rows = sorted(d["data"], key=lambda r: r["attributes"]["attributes"]["timestamp"])
シグナル:
session_end 後のイベント | 判定シグナル |
|---|---|
deleted イベント | 強いマイナス — アセットが削除されました |
ユーザーによる modified で、prev_value よりウィジェット/セル/アイテムが少ない | マイナス — ユーザーがアシスタントの変更を取り消しました |
ユーザーによる modified で、prev_value よりアイテムが多い | プラス — ユーザーがアシスタントの作成物を拡張しました |
クエリ/しきい値が変更された modified(monitor) | マイナス — ユーザーがアシスタントの値に同意しませんでした |
メッセージ/タグのみが変更された modified | 中立 — 外観上の変更です |
| イベントなし | 中立からプラス — アセットに変更はありません |
ダッシュボードでは、ウィジェット数の差分が最も有用です:
nv = row["attributes"]["attributes"]["asset"]["new_value"]
pv = row["attributes"]["attributes"]["asset"]["prev_value"]
delta = len(nv["dashboard_definition"]["widgets"]) - len(pv["dashboard_definition"]["widgets"])
Q3 — 対象を絞った HTTP パス(書き込み数+タイミング)
@asset.id で差分を確認できる一方、Q3 ではリソースへの書き込みに関するHTTP イベント数を確認できます。トレース内のツール呼び出しを照合したり、セッション後の手動編集を特定したりするのに役立ちます。
# Dashboard:
pup audit-logs search \
--query '@usr.email:<user_handle> @http.url_details.path:/api/v1/dashboard/<dashboard_id>' \
--from "<session_start - 30min, ISO>" --to "<session_end + 2h, ISO>" --limit 200
# Notebook:
pup audit-logs search \
--query '@usr.email:<user_handle> @http.url_details.path:/api/v2/notebook/<notebook_id>' \
--from ... --to ... --limit 200
イベントをフェーズごとに分類して、セッション中とセッション後に何が起きたかを確認します:
from collections import Counter
phase_method = Counter()
rows = sorted(d["data"], key=lambda r: r["attributes"]["attributes"]["timestamp"])
for r in rows:
a = r["attributes"]["attributes"]
ts = a["timestamp"]; m = a["http"]["method"]; s = a["http"]["status_code"]
if ts < session_start_iso: phase = "pre-session"
elif ts < session_end_iso: phase = "during"
elif ts < feedback_iso: phase = "session_end->feedback" # only when feedback time is known
else: phase = "post"
phase_method[(phase, m, s)] += 1
各 PUT は1回の書き込みです。200/201 は成功が確認されたことを示します。status_code: 0 は、ブラウザーがリクエストを送信したものの、応答が届く前に画面遷移したことを意味します。サーバーではほぼ確実に処理されているため、200 と合わせて数えます。
解釈:
during PUT 200の件数は、トレースの upsert ツール呼び出し回数と一致するはずです。session_endの10~30秒後に記録され、件数が最後のターンで予想される書き込み数と一致する書き込みは、非同期ツール実行の遅延です。ユーザーが開始したものではありません。session_endの30秒を過ぎてからの書き込みは、ユーザーによる手動編集です。セッション後の書き込みとして数えます。短時間に集中した少数の書き込み(例:1分ほどで5~10件の PUT)は微調整です(ややプラス — ユーザーは結果を維持し、改善しました)。セッション後の PUT がゼロなら、問題なく受け入れられています。- キャンセルが確認されたセッションでは、
during PUTイベントはゼロです。
Q1 / Q2 — 広範なシグナルを対象とした HTTP 監査(フォールバック)
アセット ID が不明な場合、または対象を絞ったクエリの後に再試行/サポートチケットのシグナルを探す場合にのみ使用します。
# Q1: broad signal, session ± window
pup audit-logs search \
--query '@usr.email:<user_handle>' \
--from "<session_start - 15min, ISO>" --to "<session_end + 30min, ISO>" --limit 200
# Q2: post-session, strip the highest-volume noise paths
pup audit-logs search \
--query '@usr.email:<user_handle> [email protected]_details.path:/api/ui/query/scalar [email protected]_details.path:/api/ui/query/timeseries' \
--from "<session_end, ISO>" --to "<session_end + 30min, ISO>" --limit 200
アクティブユーザーではノイズが大半を占めます。 200行の上限は固定で、ページネーションカーソルはありません。必要に応じて除外する、他の大量に発生するパス:
/api/ui/frontend_telemetry/metrics、/api/v1/logs-analytics/aggregate、/api/v1/logs-analytics/list、/api/v1/logs-analytics/facet_info、/api/v2/watchdog/insights/search/*、/api/v2/notifications/notify/*、/api/v2/notifications/presence/*。除外後も上限に達する場合は、時間範囲を狭めてください。
次の項目を探します:サポートチケットの作成(POST /api/v2/cases)、アシスタント呼び出しの再試行(POST /api/v2/assistant)、ドキュメントへの移動(ここではなく RUM Step 4 に表示されます)。
応答抽出ヘルパー
二重にネストされたエンベロープ(row["attributes"]["attributes"])は冗長です。スクリプトごとに一度だけ正規化します:
import json, subprocess
def pup_audit(query, frm, to, limit=200):
res = subprocess.run([
"pup", "audit-logs", "search",
"--query", query, "--from", frm, "--to", to, "--limit", str(limit),
], capture_output=True, text=True, check=True)
d = json.loads(res.stdout)
rows = []
for r in d.get("data", []):
a = r["attributes"]["attributes"]
rows.append({
"ts": a.get("timestamp"),
"method": a.get("http", {}).get("method"),
"path": a.get("http", {}).get("url_details", {}).get("path"),
"status": a.get("http", {}).get("status_code"),
"action": a.get("action"),
"asset": a.get("asset", {}),
"usr": a.get("usr", {}).get("email"),
})
rows.sort(key=lambda r: r["ts"])
return rows
グレースフルデグラデーション
クエリの結果が0行で、かつセッションが外部顧客の組織に属している場合は、エラーにせず、詳細出力に audit_unavailable を設定して、トレース+RUM による処理を続行します。一部の外部顧客の監査証跡(特に AP1/SOC2 で分離された組織)は pup 経由では参照できません。判定にはトレース+RUM だけでも十分です。
ステップ 5 — 分類
会話(ステップ 3)、評価(ステップ 2)、RUM シグナル(ステップ 4)、監査シグナル(ステップ 4b。実行した場合)を使用します。
満足度の判定:
yes:最終応答がユーザーの意図に直接応えており、否定的なフィードバックや離脱の兆候がないpartial:部分的に正しい、またはユーザーが追加の努力をしてようやく問題を解決できたno:否定的なフィードバックがあった、ユーザーが離脱した、または中核的な意図を構造的に実現できない
失敗モードコード(yes の場合は省略):
| コード | 意味 |
|---|---|
wrong_answer | 事実に反する主張 |
incomplete_answer | 正しいが、重要な経路を見落としている |
broke_existing_state | ユーザーが持っていたものを損傷した |
excessive_turns | 目標は達成したが、やり取りが多すぎた |
context_loss | 以前のコンテキストを忘れた、または同じ間違いを繰り返した |
wrong_tool_use | ツールまたはパラメーターを誤って使用した |
hallucination | ツールの結果にない ID、URL、事実を捏造した |
other: <describe> |
簡潔な出力(デフォルト):
verdict: yes | partial | no
reason: <one sentence>
詳細な出力(要求された場合のみ):
# Classification: <session_id>
## Session metadata
- **Trace ID:** <trace_id>
- **Agent span ID:** <span_id>
- **RUM session ID(s):** <resolved in Step 4a, or "fallback to email filter" if Step 4a returned 0>
- **Start:** <UTC timestamp>
- **Duration:** <seconds>s
- **Turns:** <N>
- **User:** <user_handle>
- **Product area:** <tag value>
- **Model:** <matched_model_name>
- **Iterations:** <N> (stop reason: <end_turn|tool_use>)
- **Tools called:** <names, counts>
- **Evaluations:** <name: value — "reasoning excerpt"> for each judge
- **Referrer page:** <referrer_path>
- **Content source:** agent_loop | span_content_messages | span_content_io | structural
## User intent
One sentence.
## What the assistant did
- Bullet 1
- Bullet 2
## Was the core intent satisfied?
**yes / partial / no** — one sentence justification.
## Failure mode
- `code`: explanation (omit if yes)
## RUM behavioral signals
### Pre-session context (from ROUTE_CONTEXT or referrer_path)
What the user was working on before the session.
### Assistant panel actions
| Time | Action | Page |
|------|--------|------|
### Post-session navigation
| Time | URL | Dwell |
|------|-----|-------|
### RUM verdict
One sentence: does behavioral evidence support or contradict the trace-only verdict?
## Audit-trail findings (if Step 4b ran)
- **Asset:** <asset_id> (<type>)
- **In-session writes:** <PUT count> (<status breakdown>)
- **Post-session manual edits:** <count, with timing>
- **Asset deleted or torn down:** yes / no
- **Diff signal:** <e.g. "widget count: 8 → 11 (user extended)" or "audit_unavailable">
## Revised satisfaction verdict (with RUM + audit)
yes / partial / no
トレースモード
単一の LLM Obs トレースを trace_id で分類します。RUM は使用しません。あらゆる LLM アプリで動作します。
ステップ T1 — スパン構造を取得する
search_llmobs_spans(
query = "trace_id:<TRACE_ID>",
from = "now-30d",
to = "now",
limit = 50
)
結果から、スパン階層全体を対応付けます。
- ルートスパン(
parent_id: undefinedのもの)を特定します - ルートスパンの
span_kindを確認します。agent、workflow、llmなどの場合があります - 存在するすべてのスパンの種類を抽出します:
agent、llm、tool、workflow、retrieval、embedding - タグを抽出します:
ml_app、service、session_id、investigation.id、issue_id、user_handle、org_id、matched_model_name、stop_reason、current_turn、iteration
スパン構造からアプリの種類を判定します:
| シグナル | アプリの種類 |
|---|---|
ルートが embedding のみ | Embedding パイプライン — 下記の分類不能ガードを参照 |
span_kind=agent がどこかに存在する | エージェントアプリ |
span_kind=llm のスパンのみ | LLM/チャットアプリ |
ルートが span_kind=retrieval または span_kind=workflow で、LLM の子スパンがある | パイプラインアプリ |
エージェントスパンを探します(存在する場合):
エージェントスパンが常にルートとは限りません。すべてのスパンで span_kind=agent を検索します。workflow ルートの子または孫である場合があります(例:investigate → Agent workflow → generate-summary)。エージェントループの呼び出しには、最も深い、または最も意味のあるエージェントスパンを使用します。
スパンが見つからない場合 → 停止し、エラー llmobs_not_found を返します。
分類不能ガード: すべてのスパンが span_kind=embedding で、messages の内容がない場合は、停止して次を返します。
{
"error": "unclassifiable_app_type",
"detail": "All spans are embedding kind with no conversational content. Use status:ok/error rates instead."
}
ステップ T2 — スパンの詳細と評価を取得する
ルートスパン(およびルートと異なる場合はエージェントスパン)に対して get_llmobs_span_details を呼び出します。次を抽出します。
children_ids→ 階層を確認しますevaluationsマップ → 判定者による判定があるか確認します(すべてのキーを確認し、WARN ステータスの評価は該当なしとして扱います)content_info→ 存在するフィールドとそのサイズを確認します
ステップ T3 — 内容を読み取る
エージェントスパン(C1)、次に LLM の子スパン(C2)、次にルートスパン(C3)、最後に構造シグナル(C4)の順に、コンテンツ取得カスケードに従います。
確認された特定のパターンに関する注意事項:
service:ai_gatewayの LLM スパン(bits-copilot、assistant_api):AI Gateway プロキシからのスタンドアロンのchat_completion-callまたはmessages-callスパンです。これらではget_llmobs_span_content(messages)が安定して機能します。generate-summaryエージェントスパン(error-tracking-investigator):エージェントループは機能しますが、IO トレーシングが機能フラグで制御されている場合、内容が REDACTED になることがよくあります。構造シグナルに切り替えます。子スパン名(classify、suggest-action、add-markdown-links)、ツール呼び出し、current_turnタグのターン数、全体のstatusを確認します。assistantエージェントスパン(assistant_api):LLM スパンがget_answer_from_model_stepワークフロー経由で孫スパンになっているため、エージェントループから返されるイテレーションは空です。query_stringにはget_llmobs_span_content(metadata)を、応答にはget_llmobs_span_content(output)を使用します。
ステップ T4 — 分類
コンテンツと評価を使用します。
満足度の基準:
yes:出力がタスクに直接かつ完全に対応しています。エラー、途切れ、拒否はありません。partial:タスクの一部に対応しています。正しいものの不完全であるか、品質が低下しています。no:タスクに対応できていません。幻覚、エラー、空の応答、誤ったツールの使用、構造上実現不可能な場合が該当します。- コンテンツが完全に REDACTED の場合は、構造上のシグナルから分類します。
status:error→no、期待される子スパン名を伴う正常な完了 →yes寄りとし、信頼度が低いことに留意してください。
失敗モードコード: セッションモードと同じ分類体系です(上記を参照)。
簡易出力(デフォルト):
verdict: yes | partial | no
reason: <one sentence>
confidence: high | medium | low ← include when content was partially or fully unavailable
詳細出力(要求された場合):
# Classification: <trace_id>
## Trace metadata
- **Trace ID:** <trace_id>
- **Root span:** <name> (<kind>)
- **Agent span:** <name> (<span_id>) — or "none"
- **ML app:** <ml_app>
- **Service:** <service>
- **Start:** <UTC timestamp>
- **Duration:** <seconds>s
- **Status:** ok | error
- **Session/business ID:** <session_id or investigation.id or issue_id if present>
- **Org:** <org_id if present>
- **Models:** <matched_model_name values>
- **Evaluations:** <name: value — "reasoning excerpt"> or "none"
- **Content source:** agent_loop | span_content_messages | span_content_io | structural
## Task / User query
One sentence.
## What the app produced
- Bullet 1
- Bullet 2
## Was the task completed?
**yes / partial / no** — one sentence justification.
## Failure mode
- `code`: explanation (omit if yes)
ML アプリモード
指定された LLM アプリから複数のセッションまたはトレースをサンプリングして分類します。チームごとに異なる多様な計装パターンに対応します。
ステップ M1 — スパンをサンプリングしてアプリを把握する
search_llmobs_spans(
query = "@ml_app:\"<ml_app>\" @status:ok",
root_spans_only = true,
from = "<timeframe>",
to = "now",
limit = 100,
)
スパンが見つからない場合 → 停止して、次を返します:
{
"error": "no_traces_found",
"detail": "No spans found for ml_app '<ml_app>' in the given timeframe. Check the app name and timeframe."
}
分類不能のガード: すべてのルートスパンが span_kind=embedding の場合 → 停止して、次を返します:
{
"error": "unclassifiable_app_type",
"detail": "<ml_app> only contains embedding spans. No conversational content to classify. Use status:ok/error rates instead."
}
妥当性チェック: 分類の前に、サンプリングしたルートスパンにざっと目を通します。service:、スパン名、タグを確認し、想定したアプリを表していることを確かめます。無関係に見える外れ値は、先に除外してください。
ルートスパンの種類の分析:
| ルートスパンの種類 | 意味 | アクション |
|---|---|---|
agent | エージェントスパンがルートです | そのまま分類単位として使用します |
workflow / task | パイプラインのラッパーです。エージェントがネストされている場合があります | サンプルトレースに対して search_llmobs_spans(trace_id=<id>) を呼び出し、ネストされたエージェントスパンを探します |
llm | オーケストレーションのないフラットな LLM アプリです | LLM スパンをそのまま使用します |
embedding のみ | 埋め込みパイプラインです | 分類不能です(上記のガードを参照) |
| トレース間で混在 | 複数のサービスまたはモードがあります | service: タグでグループ化し、個別に扱います |
LLM Experiments のトレース:ルートスパンの
span_kindがexperimentで、input、output、expected_outputの構造化フィールドを持つ場合、Datadog LLM Experiments のトレースを確認しています。各 experiment スパンは、データセットのレコードを 1 件実行したものを表します。コンテンツはルートスパンの構造化 I/O フィールドにあります。LLM サブスパンのメッセージをたどる(C1/C2)のではなく、ルートスパンに対してget_llmobs_span_content(field="input"/"output"/"expected_output")を使用してください。experiment トレース内の LLM サブスパンには、スタブやプレースホルダーのコンテンツが含まれている場合があります。
グループ化の粒度 — セッションとトレース:
まず、グループ化に使用するビジネス ID タグを特定します。次の順序で試してください。
オプション A — aggregate_spans(推奨): アプリが APM スパンを出力する場合(ほとんどの Go/Java/Python サービスが該当します)、APM 集約を使用して、1 回の呼び出しですべての一意な ID を取得します。pup モードでは aggregate_spans を使用できません。オプション A をスキップして、オプション B に進んでください。
aggregate_spans(
query = "service:<service_name> resource_name:<root_span_name>",
computes = [{ field: "*", aggregation: COUNT, output: "count" }],
group_by = { fields: ["@session_id"], limit: 1000 }, # or @investigation.id, @chatid, etc.
from = "<timeframe>",
to = "now"
)
まず @session_id を試します。バケットが空の場合は、LLM Obs スパンで確認したビジネス ID タグ(@investigation.id、@chatid、@correlation_id など)を試してください。これにより、ID ごとの件数とともに最大 1000 件の一意な ID が 1 回の呼び出しで返されます。LLM Obs の検索結果をページネーションするより、はるかに効率的です。
オプション B — search_llmobs_spans によるフォールバック: アプリに APM 計装がない場合、または aggregate_spans が何も返さない場合は、LLM Obs のルートスパンをページネーションし、クライアント側でビジネス ID タグの重複を排除します。
search_llmobs_spans(
query = "@ml_app:\"<ml_app>\"",
root_spans_only = true,
from = "<timeframe>",
to = "now",
limit = 50 # max per page; use cursor to paginate
)
session_id、investigation.id、chatid、またはスパン間で一貫しているタグの一意な値を収集します。
粒度の決定(ID の収集後):
session_id(UUID 形式)がある → セッション粒度とし、session_idでグループ化します。- 一貫したビジネス ID(
investigation.id、chatid、correlation_idなど)がある → セッションと同等の ID として使用します。 - どちらもない → トレース粒度:ルートスパン 1 件を分類単位 1 件とします。
収集した ID の中から、一意な ID を最大 sample_limit 件サンプリングします。フォールバックの順序は自動で適用されるため、ユーザー入力は不要です。
ステップ M2 — 各ユニットを分類する(並列)
コンテンツ取得の呼び出しは、すべて1つのメッセージで発行します。サンプリングした各ユニットについて、次の手順を実行します。
セッション粒度: 各 session_id について、セッションモードの手順1~5を実行します。user_handle が利用可能な場合のみ RUM を含め、利用できない場合は RUM を問題なくスキップします。
トレース粒度: 各トレースについて、コンテンツ取得カスケードを含むトレースモードの手順 T1~T4 を実行します。
どちらの場合も、コンテンツ取得カスケード(C1→C4)が適用されます。各ユニットの結果は完了次第出力し、すべてのユニットが完了するまで待たないでください。
ユニットごとのコンパクトなブロック:
[session|trace]: <id_short> verdict: yes | partial | no reason: <one sentence> content: <agent_loop|messages|io|structural>
ステップ M3 — サマリーを出力する
すべてのユニットの分類が完了したら、サマリーを出力します。# Session Classification Summary ヘッダーは後続のスキル(eval-trace-rca、eval-pipeline)が検出に使用する検出センチネルです。次のとおり正確に出力してください。
# Session Classification Summary
**App:** `<ml_app>` | **Timeframe:** <from> → now | **Granularity:** session | trace | **Sampled:** <N>
**Root span kind:** <kind> | **Content source:** <dominant source across units>
## Verdict Distribution
| Verdict | Count | % |
|---------|------:|:-:|
| yes | N | % |
| partial | N | % |
| no | N | % |
## Failure Mode Frequency
| Failure Mode | Count | % of failures |
|-------------|------:|:-------------:|
| <mode> | N | % |
## Per-Unit Details
| Trace/Session ID | Type | Verdict | Failure Mode | Reason | Content source |
|-----------------|------|---------|-------------|--------|----------------|
| [<first8>...<last8>](<url>) | session\|trace | yes\|partial\|no | none\|<mode> | <one sentence> | agent_loop\|messages\|io\|structural |
- ID 列:
<first8>...<last8>(表示文字数16文字)の形式で表示し、完全なトレース/セッション URL へのリンクを設定します。- セッションリンク:
https://app.datadoghq.com/llm/traces?query=session_id:<full_id> - トレースリンク:
https://app.datadoghq.com/llm/traces?query=trace_id:<full_id>
- セッションリンク:
- Reason 列:コンパクトなブロックに記載した、分類理由の1文を使用します。判定が
yesの場合はnoneを使用します。
エラー出力スキーマ
{
"mode": "session_id | trace_id | ml_app",
"id": "<the input id or app name>",
"error": "<error_code>",
"detail": "<human-readable explanation>"
}
エラーコード:
| Code | Trigger |
|---|---|
missing_input | session_id、trace_id、ml_app のいずれも指定されていない |
llmobs_not_found | search_llmobs_spans がスパンを返さなかった |
llmobs_content_expired | get_llmobs_agent_loop が404を返し、他のすべてのコンテンツソースも失敗した |
llmobs_content_masked | すべてのコンテンツフィールドが <REDACTED_INPUT>/<MASKED_STREAMING_RESPONSE> を返し、構造的なシグナルも利用できない |
rum_unavailable | 予約済み — プロトコル上 RUM が必須の場合(例:トレースが完全にマスクされ、RUM が唯一残されたシグナルの場合)に限り出力し、かつ RUM が存在しない場合に使用します。一般的な0行または認証エラーの場合は、このエラーを出力しないでください。代わりに縮退モードでトレース+監査を続行し、出力で rum_coverage: partial | unavailable を設定します(ステップ4を参照)。 |
audit_unavailable | 外部顧客の組織でステップ4bが0行を返した。致命的なエラーではありません — 詳細出力にフラグを設定し、トレース+RUMに基づく分類を続行します。 |
no_traces_found | ml_app モード:指定されたアプリと期間でスパンが見つからなかった |
unclassifiable_app_type | アプリに埋め込みスパンしかない、または会話コンテンツのない自動パイプラインである |
ツールリファレンス
この付録はpup モードでのみ適用されます。MCP モードでは、ワークフローの各セクションに記載されたツール名を直接使用してください。
スパンとトレース
| MCP Tool | pup Command |
|---|---|
search_llmobs_spans(query, ml_app, from, to, limit, cursor, root_spans_only, span_kind, summary) | pup llm-obs spans search --query "@ml_app:A [other_filters]" [--from F] [--to T] [--limit N] [--cursor C] [--root-spans-only] [--span-kind K] [--summary] — ml_app で絞り込むには、必ず --query "@ml_app:A" を使用してください。--ml-app A フラグは信頼できず、他のアプリのスパンを黙って返すことがあります。 |
get_llmobs_span_details(trace_id, span_ids, from, to) | pup llm-obs spans get-details --trace-id T --span-ids S1,S2,... |
get_llmobs_span_content(trace_id, span_id, field, path) | pup llm-obs spans get-content --trace-id T --span-id S --field F [--path P] |
get_llmobs_trace(trace_id, include_tree) | pup llm-obs spans get-trace --trace-id T [--include-tree] |
get_llmobs_agent_loop(trace_id, span_id) | pup llm-obs spans get-agent-loop --trace-id T [--span-id S] |
find_llmobs_error_spans(trace_id) | pup llm-obs spans find-errors --trace-id T |
expand_llmobs_spans(trace_id, span_ids, max_depth, filter_kind) | pup llm-obs spans expand --trace-id T --span-ids S1,S2,... [--max-depth N] [--filter-kind K] |
RUM
| MCP ツール | pup コマンド |
|---|---|
analyze_rum_events(event_type="view", filter="@usr.email:EMAIL", ...) | pup rum aggregate --user-email EMAIL --from F --to T --compute count --group-by @session.id |
analyze_rum_events(event_type="action", filter="@action.type:custom @usr.email:EMAIL", ...) | pup rum aggregate --user-email EMAIL --query "@action.type:custom" --from F --to T --compute count --group-by @evt.name |
--user-email EMAIL はクエリに @usr.email:EMAIL を先頭に追加し、すでに設定されている --query と適切に組み合わせます。必要に応じて、イベントタイプのフィルターフラグを pup rum aggregate --help で確認してください。
監査証跡(ステップ 4b)
どちらのバックエンドでも監査証跡は pup ネイティブです。MCP サーバーには analyze_audit_events に相当する機能はありません。MCP が利用できるかどうかにかかわらず、常に Bash 経由で pup audit-logs search を実行してください。
| 用途 | pup コマンド |
|---|---|
@asset.id の変更履歴 | pup audit-logs search --query '@asset.id:<id>' --from <iso|duration> --to <iso|now> --limit 50 |
| Q3 対象 HTTP パス | pup audit-logs search --query '@usr.email:<u> @http.url_details.path:<path>' --from F --to T --limit 200 |
| Q1 広範なシグナル | pup audit-logs search --query '@usr.email:<u>' --from F --to T --limit 200 |
| Q2 セッション後のノイズを除去した結果 | pup audit-logs search --query '@usr.email:<u> [email protected]_details.path:/api/ui/query/scalar [email protected]_details.path:/api/ui/query/timeseries' --from F --to T --limit 200 |
レスポンスの形式:data[].attributes.attributes.{action,asset,http,usr,timestamp,...} — attributes の下に二重にネストされています。timestamp は ISO8601 形式です。asset.new_value/asset.prev_value はネストされた辞書です。Pup はデフォルトで降順に返すため、コード内で sorted() を使って昇順にしてください。ページネーションカーソルはありません。上限は --limit です(デフォルトは 100。広範なクエリでは 200 を指定してください)。
pup に相当する機能がないツール
| MCP ツール | pup モードでの動作 |
|---|---|
get_llmobs_bits_session | スキップしてください。bits 以外のセッションでは不要です。 |
aggregate_spans(ステップ M1 オプション A) | 利用できません。オプション A をスキップして、オプション B に直接進んでください。 |
運用ルール
- MCP 結果の解析に関する安全性:MCP ツールの結果のフィールドを反復処理またはアクセスするスクリプト(Python、jq など)を書く前に、生の構造を確認してください。
type(result)、トップレベルのキー、ペイロードがコンテンツブロック(例:[{'type': 'text', 'text': '<json>'}])内にネストされているかを調べます。必要に応じて、解析前に内側のペイロードを抽出し、json.loads()で読み込んでください。MCP の結果がそのままの辞書やリストであると決めつけないでください。