mcpField
A
agent-observability-session-classify

agent-observability-session-classify

github.com/datadog-labs测试与质量180540 次安装6 天前更新
代码仓库

关于 agent-observability-session-classify

用于判定 Datadog Agent Observability 会话和跟踪的用户满意度。它分阶段检索对话内容,并通过 MCP 或 pup 获取数据,输出 yes、partial 或 no 判定及理由。

主要功能

  1. 01判定会话和跟踪的满意度
  2. 02从 LLM 应用抽样会话
  3. 03通过分阶段回退检索内容
  4. 04支持 Datadog MCP 和 pup 后端

适用场景

  1. 01评估 Datadog 助手会话
  2. 02审查特定跟踪的结果
  3. 03抽样 LLM 应用跟踪以供评估

MCP 相关

检测到 Datadog MCP 工具时,技能会调用它们检索 span 和对话内容;否则可改用 pup 命令行。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/datadog-labs/agent-skills --skill agent-observability-session-classify

安装后在 Claude Code 中输入/agent-observability-session-classify确认已加载。

详细介绍

后端

检测 — 每次调用开始时,在执行任何操作之前,确定要使用哪个后端:

  1. 如果用户在调用参数中的任意位置传入了 --backend pup → 立即使用 pup 模式,无论是否存在 MCP 工具。跳过第 2–4 步。
  2. 检查当前可用工具列表中是否存在 MCP 工具。判断的标准是:可用工具中是否出现 mcp__datadog-llmo-mcp__search_llmobs_spans。
  3. 如果存在 MCP 工具 → 全程使用 MCP 模式。严格按照此技能各工作流章节中的名称调用 MCP 工具。
  4. 如果不存在 MCP 工具 → 检查 pup 是否可执行:通过 Bash 运行 pup --version。包含 "version" 的 JSON 响应表示 pup 可用。
  5. 如果 pup 有响应 → 全程使用 pup 模式。使用此文件底部的工具参考附录,将每次 MCP 工具调用转换为对应的 pup 调用。
  6. 如果两者都不可用 → 停止并告知用户:

    "Datadog MCP 服务和 pup CLI 均不可用。请连接 MCP 服务(claude mcp add --scope user --transport http datadog-llmo-mcp 'https://mcp.datadoghq.com/v1/mcp?toolsets=llmobs,rum')或安装 pup。"

--backend pup 可出现在调用参数的任意位置;传递剩余参数给技能逻辑之前,会先将其移除。

pup 调用规则:

  • 通过 Bash 调用:pup llm-obs <subcommand> [flags]
  • pup 始终输出 JSON。直接解析即可——无需像 MCP 结果那样解包内容块;MCP 结果可能会包装成 [{"type": "text", "text": "<json>"}]。
  • 如果 pup 返回身份验证错误,告知用户运行 pup auth login,然后停止。
  • 并行处理:在同一条消息中发出多个 Bash 工具调用(每个调用执行一条 pup 命令)。
  • 时间标志: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)使用 pup rum aggregate --user-email EMAIL 代替 analyze_rum_events——参见工具参考。步骤 4b(审计跟踪)由 pup 原生支持,并通过 OAuth 查询当前用户所属组织。
  • trace_id 模式:与 MCP 模式完全兼容。
  • ml_app 模式:pup 不支持选项 A(aggregate_spans)——跳过该选项,直接继续选项 B。

调用 ID: 每次调用开始时,在调用任何 MCP 工具之前,生成一个 8 位十六进制调用 ID(例如 3a9f1c2b)。在整个调用过程中保持该 ID 不变。

意图标记: 每次调用 MCP 工具时,都要在 telemetry.intent 前加上 skill:agent-observability-session-classify[<inv_id>] — ,后接调用该工具的原因描述。仅第一次 MCP 工具调用使用 skill:agent-observability-session-classify:start[<inv_id>] — (注意 :start 后缀)。首次调用示例:skill:agent-observability-session-classify:start[3a9f1c2b] — Step 1: enumerate turn root spans for session abc-123

技能:eval-session-classify

用于对 Datadog Agent Observability 会话和跟踪进行分类的技能。会生成满意度判定(yes / partial / no)以及简短的理由。可独立调用,也可由 eval-pipeline 调用。


输入

输入模式必需描述
session_idsession_id 模式是Datadog CMD+I 助手会话的 UUID
trace_idtrace_id 模式是Agent Observability 中的 Trace ID
ml_appml_app 模式是要从中抽样的 LLM 应用名称
timeframeml_app 模式否(默认:now-7d)抽样的时间范围
sample_limitml_app 模式否(默认:20,上限:50)要分类的会话或 Trace 数量

如果未提供 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 模式 → 转到 Trace 模式
  • 提供了 ml_app(未提供 session_id 和 trace_id)→ ml_app 模式 → 转到 ML 应用模式
  • 未提供任何内容 → 输出错误(见上文)

输出格式

默认(简洁)——除非调用方请求详细模式,否则所有模式均使用此格式:

verdict: yes | partial | no
reason: <one sentence>

详细——完整的 Markdown 报告(见各模式部分末尾)。在输入中包含 verbose: true 或请求详细报告,即可启用详细模式。

在 ml_app 模式下,每个 Trace 的简洁区块后始终附加摘要表。


内容检索流程

所有模式下,读取对话内容都遵循此流程。在形成判断之前,对步骤 1(会话模式)或步骤 T1(Trace 模式)中识别出的每个轮次根 span 执行此流程。

完整性门槛:在所有轮次都执行完此流程之前,不得继续执行 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 轮执行 C1/C2,以及 input.preview 或 output.preview 标记异常(错误、修正循环、重复意图、意外工具)的轮次。跳过其余轮次之前,先扫描所有剩余轮次的预览,检查是否存在异常。

并行执行:在同一条消息中发出全部 N 个 get_llmobs_agent_loop 调用——每轮调用一次,并在同一批次中执行。对于 N 轮会话,这意味着同时发出 N 个 C1 调用。不要按顺序逐轮处理,也不要在拿到所有结果之前形成部分判断。只有当某轮的 C1 返回 iterations: [] 时,才对该轮执行 C2。

C1 — get_llmobs_agent_loop(trace_id, agent_span_id)

最丰富的信息来源:每次迭代的完整系统提示词、用户消息、工具调用参数和结果、助手回复及 Token 使用情况。对每个 Agent span,首先尝试此方法。

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 span 经过中间工作流层(例如 get_answer_from_model_step),而不是作为 Agent span 的直接子项。继续执行 C2。
  • 404 → Span ID 在 Trace 存储中无法解析。继续执行 C2。
  • 内容字段包含 <REDACTED_INPUT> 或 <MASKED_STREAMING_RESPONSE> → 功能标志禁用了 IO 跟踪。结构信息(迭代次数、工具名称、Token 使用情况)仍有用——请注明。继续执行 C2 以获取实际内容。

当 C1 结果过大,无法放入上下文并被持久化到磁盘时(长会话中很常见):不要尝试读取完整文件——这会超出 Token 限制。运行定向的 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 子 span 调用 get_llmobs_span_content(field="messages")

当 C1 返回 iterations: [] 时,LLM span 通常位于根 Agent span 下方 2 层——处于工作流包装器之下(例如 get_answer_from_model_step),而 Agent 循环 API 不会深入该包装器。每轮对应的具体树结构:

<root agent span>                          ← one per turn
  <workflow wrapper>                       ← one per LLM round-trip
    <llm span, e.g. anthropic.request>     ← call `field="messages"` here

使用 expand_llmobs_spans 导航到 LLM span ID——不要使用 get_llmobs_trace,它只返回深度为 1 的子节点,无法访问嵌套在工作流下的 LLM span:

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 调用 span 通常以数 KB 的系统提示词开头,因此,较低的 max_tokens 会导致 JSONPath 筛选器仅对系统提示词内容进行操作,并悄悄返回错误的消息。

要提取有意义的尾部内容(用户查询、工具调用、最终回答):

  1. 先调用一次,不传入 path,以从响应中读取 total_tokens_approx。
  2. 再次调用时,设置 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 tracing 已禁用。继续执行 C3。
  • content_info 映射中不包含 messages → 不是聊天 span。继续执行 C3。
C3 — 对根 span 调用 get_llmobs_span_content(field="input") 和 field="output"

根 span 通常会携带由应用写入的合成摘要(例如,将原始用户查询作为 input、将最终响应作为 output,或写入 "Investigate error for issue: <id>" / "Investigation completed with status: completed")。可用信号不多,但足以确认任务内容及其是否已完成。

C4 — 仅使用结构信号

当所有内容都无法访问时,仅根据 span 元数据进行分类:

  • status(ok / error)、stop_reason、response_truncated
  • 子 span 名称(工具名称、工作流步骤名称,例如 classify、generate-summary、suggest-action)
  • iteration 标签数量(LLM 轮数总计)
  • 可用时的持续时间和 token 数量
深入检查——针对可疑工具结果进行定向提取

完成摘要后,深入检查工具列表中包含客户端渲染工具、写入操作或标记了 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 — 枚举轮次根 span
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

使用 tags={...}(而不是 query 字符串形式)。自由格式的 query="session_id:<id>" 会触发全文搜索行为,为每轮返回整个追踪包(span 数量是 5 倍,结构筛选条件会被静默忽略),并在较长会话中耗尽行数预算。tags={"session_id": "<id>"} 会使用已索引的标签筛选,并遵循其他参数。

root_spans_only=True 每个用户轮次返回一个 span(通常是根 Agent span),而不是返回每个工作流、大模型或工具的后代 span。如果会话超过 500 轮,请通过 next_cursor 翻页。

对于每个返回的根 span,记录:

  • trace_id、span_id
  • start_ms、duration_ms
  • 标签:user_handle、user_id、org_id、product_area、message_id
  • 根 span 的 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 — 无需查看 Agent 循环,即可了解用户的问题和他们当时所在的页面。

步骤 3 — 阅读完整对话

对于步骤 1 中枚举的每个轮次根 span,请遵循内容检索级联。按照级联中的并行规则,将所有轮次的 C1 调用(get_llmobs_agent_loop)作为一个并行批次发出,然后对任何 C1 返回 iterations: [] 的轮次继续尝试 C2。

对于大模型 span 位于工作流包装器下的应用(例如 assistant_api,其中 anthropic.request 是通过 get_answer_from_model_step 访问的孙代),C1 通常会返回空迭代 — 这是预期行为,级联会通过 expand_llmobs_spans 导航将这些轮次转至 C2。如果没有可用的大模型调用内容,C3(根 span 上的元数据和输出)是最后的手段。

步骤 4a — 解析 RUM 会话 ID

在执行主要的 RUM 查询之前,在首轮 start_ms 前后 ±2 分钟的窄时间范围内进行查询,以找出用户使用的确切浏览器会话。这样可以隔离 RUM session_id,避免仅按 @usr.email 在较宽时间范围内筛选时出现的跨会话数据混杂(用户一天中在多个标签页操作时,会返回所有标签页的事件)。

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。使用 pup rum aggregate --help 确认 --group-by 标志名称。

步骤 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 和 <rum_filter>(来自步骤 4a)定义时间窗口:

  • 前:[start_ms − 30min, start_ms]
  • 期间:[start_ms, start_ms + session_duration_ms]
  • 后:[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 缺失或不完整属于覆盖缺口,而非行为证据——将其视为负面证据是已知的失败模式。

条件操作
会话中有 0 行 RUM 数据,且用户在较宽的 30 天时间窗口内有一些 RUM 数据(会话日期存在 Web RUM 缺口)降级处理。继续执行,不使用 RUM;依赖跟踪证据(步骤 3)和审计信号(如适用,步骤 4b)。在输出中设置 rum_coverage: partial — web RUM gap on session date。
analyze_rum_events / pup rum 在一个或多个查询中返回权限或身份验证错误(外部组织、采用 SOC2 隔离的租户中较常见)降级处理。使用有结果返回的 RUM 查询;缺失部分则回退到跟踪和审计数据。在输出中设置 rum_coverage: partial — auth error on <query>。
会话中有 0 行 RUM 数据,且用户在 30 天内的任何位置都没有 RUM 数据降级处理。继续使用跟踪和审计数据。在输出中设置 rum_coverage: unavailable — no RUM data for user。请勿将 rum_unavailable 作为终止错误返回。

**硬性规则:**绝不可将缺少 RUM 作为负面判定的证据。如果跟踪和审计信号表明用户满意,缺少 RUM 不会推翻这些信号;如果跟踪信号含糊不清,则默认采用仅依据跟踪数据的判定,不得因缺少行为数据而作出负面评价。

rum_unavailable 错误代码仅适用于无法恢复的情况:分类协议要求必须使用 RUM(例如跟踪内容经过高度脱敏,RUM 是唯一剩余的信号),但又不存在任何 RUM 数据。其他情况下,请以降级模式继续。

信号解读框架

RUM 操作的证据效力并不相同。将它们分为三个层级,并在步骤 5 中根据层级为各项证据赋予不同权重。以下类别与 Agent 无关——请将特定应用中的 RUM 操作名称映射到这些类别。对于无法识别的操作,在掌握其与满意度的相关性证据之前,一律视为不明确。

Tier-A——单独即可作为充分证据(出现时,可改变或确认轨迹判定)

抽象类别方向操作名称模式示例
响应完成前触发流式传输中止负面*.chat-cancel、*.stream-abort、*.cancel-generation
流式传输期间关闭面板/窗口,且 1s 内未重新打开负面Agent 的 LLM span 仍处于活动状态时触发 *.panel.close
跳转到包含 Agent 在响应中明确提及的资源 ID 的 URL正面查看 Agent 引用的确切资源路径
会话结束后 60s 内对 Agent 输出执行资源操作正面对生成的资源执行导出、保存、修改或分享
15 分钟内以相同意图开启新会话(根据步骤 1b 的分类器判定为重试)负面通过相邻会话分析检测到
全盘否定 Agent 的计划(reject_all 或等效操作)负面批量拒绝操作,或流式传输期间执行“停止”操作

Tier-B——可作为佐证,但单独使用时不能作为决定性证据

抽象类别方向
在 Agent 视图上停留时间较短(总计 < 30s)负面
会话结束后跳转到外部帮助页面(文档、支持、社区)负面
收到任何响应前,多次打开面板/视图负面
在 Agent 视图上停留时间较长(> 60s)正面
点击 Agent 呈现的 Markdown 链接正面
会话结束后跳转到主题相关资源(并非与确切资源匹配)正面

不明确——不得作为唯一证据

抽象类别含糊原因规则
展开中间状态的交互(工具调用面板、推理块)既会出现在投入阅读模式中,也会出现在点赞前审视模式中;没有合理的规则可以区分两者配合同方向的 Tier-A 信号或匹配的 Tier-B 信号使用
单次批准工具调用(在 N 个提议中对一个执行 accept)这是预期的 UX 步骤——即使响应质量一般,用户也会点击接受视为弱正面信号;绝不能作为判定“是”的主要证据
单次拒绝工具调用(在 N 个提议中对一个执行 reject)可能意味着“用户引导到了更好的路径”,而非“用户不满意”仅当用户也停止发送消息,或之后的所有调用都遭到拒绝时,才计为负面
缺少流式传输中途上下文时关闭面板/视图无法区分“会话自然结束”和“用户放弃”需要流式传输活动时间信息——参见 Tier-A

**针对你的 Agent:**分类前,请先将应用中的 RUM 操作名称映射到这些类别。Datadog CMD+I 助手的参考映射见 @rum-actions-bits-assistant.md——请将其用作模板,了解应在自身应用的 RUM 事件分类体系中查找哪些信息。无法识别的操作默认归为不明确;只有在掌握相关证据(来自已标注会话或产品知识),确认该操作与满意度的相关性后,才将其提升至 Tier-B 或 Tier-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 的基础上提供更多信息。

查询优先级(信号由强到弱,噪声由少到多):

  1. @asset.id — 包含完整差异的修改/删除历史。权威性最高。已知资产 ID 时必须查询。
  2. Q3(定向 HTTP 路径) — 同一资产的写入次数和时间。已知资产 ID 时必须查询。
  3. 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 / accessed
  • usr.email
  • asset.type(dashboard、notebook、monitor,……)、asset.id、asset.name
  • asset.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 并移除小组件/单元格,或手动写入并覆盖 Agent 生成的内容。这些操作直接体现了用户意图,可以改变判定结果。后端症状信号(接受后写入次数为零、缺少 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 都算一次写入。200/201 = 已确认成功。status_code: 0 表示浏览器已发送请求,但在收到响应前就离开了页面——服务器几乎可以确定已处理该请求;应将其与 200 一并计数。

解读:

  • during PUT 200 的计数应与跟踪中的 upsert 工具调用次数一致。
  • 时间戳在 session_end 后 10–30 秒,且计数与最后一轮预期写入次数一致的写入,属于工具异步执行延迟——不是用户发起的操作。
  • session_end 后超过 30 秒的写入属于用户手动编辑。将其计为会话后写入:短时间内集中出现少量写入(例如,约 1 分钟内有 5–10 次 PUT)= 正常微调(略微积极——用户保留了结果并加以改进);会话后 PUT 次数为 0 = 已明确接受。
  • 已确认取消的会话,其 during PUT 事件数为 0。
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 步的基础上提供更多信息。

查询优先级(信号由强到弱,噪声由少到多):

  1. @asset.id — 包含完整差异的修改 / 删除历史。最具权威性。已知资源 ID 时必需。
  2. Q3(定向 HTTP 路径) — 同一资源的写入次数和时间。已知资源 ID 时必需。
  3. 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 / accessed
  • usr.email
  • asset.type(dashboard、notebook、monitor 等)、asset.id、asset.name
  • asset.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 都是一次写入。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,并继续使用 trace + RUM。某些外部客户的审计跟踪(尤其是 AP1 / SOC2 隔离组织)无法通过 pup 访问。仅凭 Trace + 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

跟踪模式

按 trace_id 对单个 Agent Observability 跟踪进行分类。不使用 RUM。适用于任何 LLM 应用。

步骤 T1 — 获取 span 结构
search_llmobs_spans(
  query  = "trace_id:<TRACE_ID>",
  from   = "now-30d",
  to     = "now",
  limit  = 50
)

根据结果,映射完整的 span 层级:

  • 找出根 span(parent_id: undefined 的那个)
  • 记录根 span 的 span_kind——可能是 agent、workflow、llm 或其他值
  • 提取所有存在的 span kind: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

根据 span 结构确定应用类型:

信号应用类型
根 span 只有 embeddingEmbedding 流水线 — 请参阅下方的不可分类保护规则
任意位置存在 span_kind=agentAgent 应用
仅有 span_kind=llm 的 spanLLM/聊天应用
根 span 为 span_kind=retrieval 或 span_kind=workflow,且有 LLM 子项流水线应用

查找 agent span(如果存在): agent span 不一定是根 span。搜索所有 span,查找 span_kind=agent——它可能是 workflow 根 span 的子级或孙级(例如 investigate → Agent workflow → generate-summary)。选用最深层或最有意义的 agent span 来调用 agent loop。

如果未找到 span → 停止,并返回错误 llmobs_not_found。

**不可分类保护规则:**如果所有 span 的 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 — 获取 span 详情和评估结果

对根 span(以及与根 span 不同的 agent span)调用 get_llmobs_span_details。提取:

  • children_ids → 确认层级关系
  • evaluations 映射 → 获取所有裁判判定(遍历所有键;将状态为 WARN 的评估视为不适用)
  • content_info → 确认存在的字段及其大小
步骤 T3 — 读取内容

使用 agent span(C1)、然后是 LLM 子 span(C2)、根 span(C3),最后是结构信号(C4),按照 内容检索级联流程 执行。

关于观察到的特定模式的说明:

  • service:ai_gateway LLM span(bits-copilot、assistant_api):来自 AI Gateway 代理的独立 chat_completion-call 或 messages-call span——get_llmobs_span_content(messages) 在这些 span 上可稳定工作。
  • generate-summary agent span(error-tracking-investigator):agent loop 可以运行,但当 IO 跟踪受功能标志控制时,内容通常会被 REDACTED。改用结构信号:子 span 名称(classify、suggest-action、add-markdown-links)、工具调用次数、current_turn 标签中的轮数,以及整体 status。
  • assistant agent span(assistant_api):agent loop 返回的迭代为空,因为 LLM span 是通过 get_answer_from_model_step 工作流形成的孙级 span。使用 get_llmobs_span_content(metadata) 获取 query_string,并使用 get_llmobs_span_content(output) 获取响应。
步骤 T4 — 分类

使用内容和评估结果:

满意度标准:

  • yes:输出直接且完整地完成任务。没有错误、截断或拒绝。
  • partial:完成了部分任务,内容正确但不完整,或质量有所下降。
  • no:未能完成任务——存在幻觉、错误、空响应、错误的工具使用,或在结构上无法完成。
  • 当内容完全 REDACTED 时,根据结构信号进行分类:status:error → no;正常完成且具有预期的子 span 名称 → 倾向于 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 — 抽样 span 并了解应用
search_llmobs_spans(
  query           = "@ml_app:\"<ml_app>\" @status:ok",
  root_spans_only = true,
  from            = "<timeframe>",
  to              = "now",
  limit           = 100,
)

如果未找到 span → 停止并返回:

{
  "error": "no_traces_found",
  "detail": "No spans found for ml_app '<ml_app>' in the given timeframe. Check the app name and timeframe."
}

**不可分类保护条件:**如果所有根 span 的 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."
}

**合理性检查:**分类前浏览抽样的根 span。确认它们代表预期的应用——检查 service:、span 名称和标签。继续之前,剔除看起来无关的异常值。

根 span 类型分析:

根 span 类型含义操作
agentAgent span 是根 span直接将其用作分类单位
workflow / task流水线封装层——Agent 可能嵌套其中在示例轨迹上调用 search_llmobs_spans(trace_id=<id>),查找嵌套的 Agent span
llm扁平 LLM 应用,没有编排直接使用 LLM span
仅 embedding嵌入流水线不可分类(见上文保护条件)
不同轨迹中类型混杂多个服务或模式按 service: 标签分组并分别处理

LLM Experiments 轨迹:如果根 span 的 span_kind: experiment,且包含结构化字段 input、output 和 expected_output,则说明这是一个 Datadog LLM Experiments 轨迹。每个实验 span 代表一次数据集记录运行。内容位于根 span 的结构化 I/O 字段中——在根 span 上使用 get_llmobs_span_content(field="input"/"output"/"expected_output"),而不是沿着 LLM 子 span 消息级联链路查找(C1/C2)。实验轨迹中的 LLM 子 span 可能包含存根或占位内容。

分组粒度——会话还是轨迹:

首先,确定要按哪个业务 ID 标签分组。按以下顺序尝试:

**选项 A — aggregate_spans(首选):**如果应用会生成 APM span(大多数 Go/Java/Python 服务都会生成),使用 APM 聚合,通过一次调用获取所有唯一 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。如果结果桶为空,则尝试你在 Agent Observability span 上观察到的业务 ID 标签(@investigation.id、@chatid、@correlation_id 等)。这会在一次调用中返回最多 1000 个唯一 ID,并附带每个 ID 的计数——比对 Agent Observability 搜索结果进行分页高效得多。

**选项 B — search_llmobs_spans 后备方案:**如果应用没有 APM 埋点,或 aggregate_spans 没有返回结果,则对 Agent Observability 根 span 分页,并在客户端按业务 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 或其他在各 span 中保持一致的标签的唯一值。

粒度决策(收集 ID 后):

  1. 存在 session_id(UUID 格式) → 会话粒度,按 session_id 分组。
  2. 存在 一致的业务 ID(investigation.id、chatid、correlation_id 等)→ 将其用作会话等效标识。
  3. 两者均不存在 → 轨迹粒度:一个根 span = 一个分类单位。

从收集的 ID 集合中抽取最多 sample_limit 个唯一 ID。后备方案级联会自动执行——无需用户输入。

步骤 M2 — 对每个单元进行分类(并行)

在一条消息中发出所有内容检索调用。对于每个抽样单元:

会话粒度: 对每个 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>
  • 原因列:使用紧凑块中的一句分类理由。对于 verdict 为 yes 的情况,使用 none。

错误输出模式

{
  "mode": "session_id | trace_id | ml_app",
  "id": "<the input id or app name>",
  "error": "<error_code>",
  "detail": "<human-readable explanation>"
}

错误代码:

代码触发条件
missing_input未提供 session_id、trace_id 或 ml_app
llmobs_not_foundsearch_llmobs_spans 未返回任何跨度
llmobs_content_expiredget_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_foundml_app 模式:在给定应用和时间范围内未找到跨度
unclassifiable_app_type应用只有 embedding 跨度,或是没有对话内容的自动化流水线

工具参考

本附录仅适用于 pup 模式。在 MCP 模式下,直接使用工作流章节中的工具名称。

跨度和追踪
MCP 工具pup 命令
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] — 始终使用 --query "@ml_app:A" 按 ml_app 筛选;--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 确认事件类型筛选标志。

审计跟踪(Step 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(Step M1 Option A)不可用。跳过 Option A,直接执行 Option B。

操作规则

  • MCP 结果解析安全性:编写任何会遍历 MCP 工具结果或访问其字段的脚本(Python、jq 等)之前,先检查原始结构——检查 type(result)、顶层键,以及载荷是否嵌套在内容块中(例如 [{'type': 'text', 'text': '<json>'}])。如有需要,提取内部载荷并使用 json.loads(),然后再进行解析。切勿假设 MCP 结果是未包装的字典或列表。

评论

支持 Markdown