关于 llm-obs-session-classify
用于判定 Datadog LLM Obs 会话和 Trace 的满意度,输出 yes、partial 或 no。技能按步骤检索各轮内容,并在可用时使用 MCP 工具或 pup 命令行。
主要功能
- 01会话、Trace 和应用模式
- 02输出 yes/partial/no 判定
- 03分阶段检索对话内容
- 04选择 MCP 或 pup 后端
适用场景
- 01评估 Datadog 助手会话
- 02审查单个 Trace
- 03抽样检查 LLM 应用会话
MCP 相关
优先使用 Datadog LLM Obs MCP 工具;也可将操作转换为 pup 命令行调用。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ datadog-labs/ agent-skills --skill llm-obs-session-classify安装后在 Claude Code 中输入/llm-obs-session-classify确认已加载。
详细介绍
后端
检测——每次调用开始时,在采取任何操作之前,确定要使用的后端:
- 如果用户在调用参数中的任意位置传入了
--backend pup→ 立即使用 pup 模式,无论是否存在 MCP 工具。跳过步骤 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 结果不同,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模式:选项 A(aggregate_spans)在 pup 中不可用——跳过该选项,直接执行选项 B。
调用 ID: 每次调用开始时,在进行任何 MCP 工具调用之前,生成一个 8 位十六进制调用 ID(例如 3a9f1c2b)。整个调用过程中保持不变。
意图标记: 每次 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 的 Trace ID |
ml_app | ml_app 模式 | 是 | 要从中采样的 LLM 应用名称 |
timeframe | ml_app 模式 | 否(默认:now-7d) | 向前追溯的时间范围 |
sample_limit | ml_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 App 模式 - 未提供任何内容 → 发出错误(见上文)
输出格式
默认(精简)——所有模式均使用,除非调用方要求详细输出:
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 个轮次,以及任何
input.preview或output.preview标记出异常(错误、纠正循环、重复意图、意外工具)的轮次,运行 C1/C2。跳过其余轮次之前,先扫描所有剩余轮次的预览以检查异常。
并行处理:在一条消息中发出全部 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 筛选器仅对系统提示词内容进行操作,并悄悄返回错误的消息。
要提取有意义的尾部内容(用户查询、工具调用、最终回答):
- 先调用一次,不传入
path,以从响应中读取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 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_idstart_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 的基础上提供更多信息。
查询优先级(信号由强到弱,噪声由少到多):
@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并移除小组件/单元格,或手动写入并覆盖 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 步的基础上提供更多信息。
查询优先级(信号由强到弱,噪声由少到多):
@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 都是一次写入。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 对单个 LLM Obs 追踪进行分类。不使用 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 的
span_kind— 可能是agent、workflow、llm或其他类型 - 提取所有存在的 span 类型:
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 只有 embedding | Embedding 流水线 — 参见下文的不可分类保护条件 |
任意位置存在 span_kind=agent | Agent 应用 |
只有 span_kind=llm 类型的 span | LLM/聊天应用 |
根 span 为 span_kind=retrieval 或 span_kind=workflow,且有 LLM 子 span | 流水线应用 |
查找 agent span(如果有):
agent span 不一定是根 span。搜索所有 span,查找 span_kind=agent — 它可能是 workflow 根 span 的子级或孙级(例如 investigate → Agent workflow → generate-summary)。使用最深层/最有意义的 agent span 来调用 agent 循环。
如果没有找到 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 调用 get_llmobs_span_details(如果 agent span 与根 span 不同,也对 agent span 调用)。提取:
children_ids→ 确认层级结构evaluationsmap → 任何评审判定(遍历所有键;将 WARN 状态的评估视为不适用)content_info→ 存在哪些字段及其大小
步骤 T3 — 读取内容
按照内容检索级联,依次使用 agent span(C1)、LLM 子 span(C2)、根 span(C3),然后是结构信号(C4)。
关于观察到的特定模式的说明:
service:ai_gatewayLLM span(bits-copilot、assistant_api):来自 AI Gateway 代理的独立chat_completion-call或messages-callspan —get_llmobs_span_content(messages)在这些 span 上可靠有效。generate-summaryagent span(error-tracking-investigator):agent 循环有效,但当 IO 跟踪受功能标志控制时,内容通常会被 REDACTED。此时回退到结构信号:子 span 名称(classify、suggest-action、add-markdown-links)、工具调用、来自current_turn标签的轮数,以及整体status。assistantagent span(assistant_api):由于 LLM span 是通过get_answer_from_model_step工作流形成孙级 span,agent 循环返回的迭代为空。使用get_llmobs_span_content(metadata)获取query_string,并使用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 | Agent 跨度是根跨度 | 直接用作分类单元 |
workflow / task | 流水线包装器——Agent 可能嵌套其中 | 在一个样本跟踪记录上调用 search_llmobs_spans(trace_id=<id>),查找嵌套的 Agent 跨度 |
llm | 扁平 LLM 应用,没有编排 | 直接使用 LLM 跨度 |
仅有 embedding | 嵌入流水线 | 不可分类(见上文的保护条件) |
| 不同跟踪记录的类型混杂 | 多个服务或模式 | 按 service: 标签分组并分别处理 |
LLM Experiments 跟踪记录:如果根跨度具有
span_kind: experiment,并包含input、output和expected_output结构化字段,那么你查看的是 Datadog LLM Experiments 跟踪记录。每个实验跨度代表一次数据集记录运行。内容位于根跨度的结构化 I/O 字段中——对根跨度使用get_llmobs_span_content(field="input"/"output"/"expected_output"),而不是沿着 LLM 子跨度消息级联进行查找(C1/C2)。实验跟踪记录中的 LLM 子跨度可能包含存根或占位内容。
分组粒度——会话与跟踪记录:
首先,确定要用于分组的业务 ID 标签。按以下顺序尝试:
**选项 A — aggregate_spans(首选):**如果应用会发送 APM 跨度(大多数 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。如果未返回任何桶,则尝试在 LLM Obs 跨度上观察到的业务 ID 标签(@investigation.id、@chatid、@correlation_id 等)。此操作一次调用即可返回最多 1000 个唯一 ID,并提供每个 ID 的计数——远比对 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 后):
- 存在 UUID 格式的
session_id→ 会话粒度,按session_id分组。 - 存在一致的业务 ID(
investigation.id、chatid、correlation_id等) → 将其用作会话等效 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_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 | 应用只有 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 结果是未包装的字典或列表。