关于 observability-llm-obs
用于分析已采集的 LLM 和 Agent 数据的 Elastic 可观测性技能。它帮助运维人员调查性能、Token 用量、已埋点的成本、响应质量及基于 Trace 的调用链,并先确认实际数据源与字段。
主要功能
- 01发现可用的 LLM 数据源
- 02识别部署中的实际字段名
- 03分析延迟、吞吐量和错误
- 04报告 Token 与已埋点成本
- 05检查质量信号和调用链
适用场景
- 01比较不同模型的 LLM 性能
- 02查看 Token 总量及可用成本数据
- 03调查故障和内容过滤事件
- 04定位 Trace 工作流中的瓶颈
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ elastic/ agent-skills --skill observability-llm-obs安装后在 Claude Code 中输入/observability-llm-obs确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/esql-recipes.mdgenai-fields.mdintegrations.md详细介绍
LLM 和 Agent 式可观测性
仅根据实际摄取到 Elastic 中的数据,回答有关监控 LLM 和 Agent 式组件的问题——不使用其他信息。此技能回答四类问题:LLM 性能、成本和 Token 利用率、响应质量,以及调用链或 Agent 式工作流编排。
给定部署通常使用一种或多种摄取路径:APM/OTLP 跟踪,和/或集成指标与日志。实际使用哪种路径,需要通过发现来确认,不能想当然地认为两种都存在。有关 ES|QL 语法、命令和查询模式,请使用 elasticsearch-esql 技能。对于非 LLM 特定的服务级延迟和错误排查,请使用 observability-sre-triage 技能。
环境配置
此技能通过 elastic CLI 执行 Elasticsearch 操作。如果未安装
elastic CLI,请告知用户其用途。不要猜测凭据、直接调用 HTTP API,或尝试其他变通方法。
此技能以 HTTP 简写形式引用操作(例如,GET /、GET /_cat/indices、GET /{index}/_mapping、
GET /{index}/_settings/index.mode、POST /_query)。本文末尾的 操作 表格将每个简写映射到相应的 elastic CLI 命令——始终使用 CLI,而不是直接调用 HTTP API。
无法访问集群时的分析
上述 CLI 检查仅用于决定是否可以_查询集群_,不影响分析。如果用户已在问题中提供相关证据(指标值、计数、状态原因、日志行、告警负载、配置),请基于这些证据进行分析并给出结论。
如果确实需要用户尚未提供的数据,请说明要检查什么以及如何检查——指出能够明确回答问题的具体查询、索引和字段,然后再请求设置 CLI。即使无法连接集群,明确指出要检查的内容也有帮助;只要求设置 CLI 则没有帮助。
要完成的任务
- 在查询任何内容之前,发现部署实际使用的 LLM 摄取路径
- 发现此部署中实际使用的 LLM 字段名称,因为 GenAI 属性命名并不一致
- 按模型、服务或时间报告 LLM 延迟、吞吐量和错误率
- 报告 Token 利用率;仅当成本字段确实存在时才报告成本
- 调查响应质量:失败、超时、结束原因、内容过滤器和护栏事件
- 重建 Agent 式调用链,并找出多步骤工作流中的瓶颈 span
- 将发现与基于 LLM 相关数据定义的 SLO 和告警规则关联起来
输出规范
适用于此技能生成的每条回复。
- 给出证据最充分的结论。 当证据指向某个方向时,就明确说明。不要为了显得谨慎而降低置信度——面对明确的证据却含糊其辞,是缺陷,不是谦逊。
- 只在结论中说明一次置信度。 不要在每条要点中重复说明。
- 不要超出证据进行推测。 如果没有观察到某个原因,就不要将它写进答案。说明哪些内容未知,然后到此为止。
- 如实报告缺失情况。 零行意味着数据缺失或未采集,绝不意味着底层情况正常。特别是,如果不存在成本字段,就说明成本未被监测——不要用猜测的每 Token 价格乘以 Token 数量,再把结果当作成本数据呈现。
- 报告实际查到的数字。 引用查询返回的 Token 数量、延迟和错误率。不要将它们四舍五入成模糊的描述,也不要沿用本技能示例中的数字。
- 不要凑字数。 不要复述问题,不要叙述运行了哪些查询(除非结果与之相关),也不要总结总结。
- 以发现结尾。 不要在最后附上“要不要我深入调查?”之类的提议。可执行的后续事项应列在建议清单中,并表述为建议,而不是问题。
数据所在位置
| 接入路径 | 索引模式 | 可回答的问题 |
|---|---|---|
| 通过 OTLP 传输的 OTel / EDOT 跟踪数据 | traces-*.otel-*,或通用模式 traces-* | 单次请求的延迟、Token、模型、结束原因,以及调用链 |
| Elastic APM Agent 跟踪数据 | traces-apm*,或通用模式 traces-* | 单次请求的延迟和结果;如果 SDK 添加了 GenAI 属性,也可获取这些属性 |
| APM/OTel 指标 | metrics-apm*、metrics-*.otel-* | 聚合的服务级速率和延迟 |
| Elastic LLM 集成指标 | metrics-<integration>.*(例如 metrics-aws_bedrock_agentcore.*) | 聚合 Token 数量、调用次数、延迟,有时还包括成本 |
| Elastic LLM 集成日志 | logs-<integration>.* | 提示词/响应记录、护栏和内容过滤事件 |
使用通用的 traces-* 模式查找跟踪数据,无论数据是由 Elastic APM Agent 还是 OpenTelemetry 采集的都适用。监测数据可以由 EDOT、OpenLLMetry、OpenLIT 或 Langtrace 导出到 OTLP——它们都会将 LLM 和 Agent Span 写入跟踪数据流。
只有跟踪数据才能还原调用链。 trace.id、span.id 和 parent.id 是重建 Agent 调用链的唯一方式。集成指标已经过聚合,无法做到这一点——如果问题涉及编排,而现有数据只有集成指标,就说明无法根据现有数据还原调用链。
背景阅读: LLM 和 Agentic AI 可观测性, EDOT LLM 用例, Observability Labs — LLM 可观测性。
流程:确定有哪些可用数据
按顺序执行以下步骤。不要跳过步骤 5。
-
验证连接并检测版本。 调用
GET /。这将决定可用的查询语言功能:build_flavor: "serverless"表示可以使用所有 ES|QL 功能,包括TS、TRANGE和TBUCKET。否则,请使用version.number:在 Stack 上,TS在 9.2 中为预览版,在 9.4 中正式发布;TRANGE在 9.3.0 中正式发布,因此对于完整的时间序列流程,应将 Stack 9.4 视为最低版本;低于此版本时,改用FROM和BUCKET(@timestamp, ...)。判断错误会导致查询无法解析。 -
确定有哪些数据摄取路径。 这将决定此部署是否有 traces、集成数据或两者兼有,也会影响哪些问题根本无法回答。使用
GET /_data_stream/<name>列出数据流(传入traces-*、metrics-*、logs-*),或使用GET /_resolve/index/<pattern>解析模式。查找 trace 数据流、metrics-apm*,以及与已知 LLM 集成数据集匹配的任何metrics-*或logs-*。如果两条路径都没有 LLM 数据,请如实说明并停止——不要根据对模型提供商的一般了解作答。 -
发现实际的 LLM 字段名称。 这将决定要写入 ES|QL 的确切字段路径,不能靠猜测。不同的埋点方式使用的命名各不相同:
gen_ai.*、llm.*或集成专用名称;语义约定本身也发生过变化(较旧的埋点方式会生成gen_ai.system,较新的则会生成gen_ai.provider.name)。使用GET /_field_caps并指定诸如*gen_ai*、*llm*、*token*、*cost*这样的字段模式,或读取GET /<index>/_mapping,然后抽样检查文档,确认字段值已填充。不同数据摄取路径的属性嵌套方式不同——OTel 原生 trace 数据流会将 span 属性公开为attributes.<name>(通常也会以裸字段<name>透传),而不是span.attributes.<name>。编写查询前,请确认哪种形式能够解析。属性目录和解析规则请参阅 references/genai-fields.md。 -
判断是否存在成本字段。 成本不属于 OpenTelemetry GenAI 规范。某些埋点方式会添加诸如
llm.response.cost.usd_estimate这样的自定义属性,某些集成会公开成本指标,但许多部署两者都没有。请在步骤 3 中明确查找。如果不存在,针对成本问题的答案应说明尚未对成本进行埋点——改为报告令牌数量,并指出这一缺口。 -
每个问题选择一个一致的数据源。 当 APM traces 和集成指标同时存在时,选定其中一种,并在整个答案中使用它。混用会导致重复计数,并为同一数量得出两个不同的数字,因为集成会轮询提供商自身的账单数据,而 traces 记录的是客户端观察到的情况。根据问题类型选择:traces 用于单次请求分析、调用链以及任何需要 trace 层级的问题;集成指标 用于较长时间窗口内的令牌和成本汇总。说明答案使用了哪个数据源。
-
在可能相关时检查告警和 SLO。 这将决定性能下降是否已被发现并跟踪。使用
GET kbn:/api/alerting/rules/_find查找规则,使用GET kbn:/api/observability/slos查找 SLO,然后筛选目标为 LLM 相关服务或集成指标的项目——步骤 3 中的字段名称可帮助判断哪些规则相关。正在触发的告警,或状态为已违反或正在退化的 SLO,都是性能下降的证据。请注意,SLO API 的sli.kql.custom指标使用的是 KQL,而不是 ES|QL;这是 API 合约的规定,并不意味着建议在其他地方使用 KQL。
使用场景和查询模式
使用 POST /_query 编写查询。始终限定时间范围,在有 service.name 时添加该字段,并使用 LIMIT 限制结果数量。
如果只需查看趋势,而不是以精细粒度扫描较长时间范围,请使用较粗的时间分桶。
| 问题 | Traces 路径 | Integration 路径 |
|---|---|---|
| 延迟、吞吐量、错误率 | 按 GenAI 操作或模型属性筛选;按时间桶使用 COUNT(*)、AVG(span.duration.us) 统计,并通过 event.outcome == "failure" 统计失败 | 按模型维度统计请求速率、延迟和错误指标 |
| Token 和成本 | 按时间、模型或服务对输入和输出 Token 属性求和;仅在存在成本属性时才添加该属性 | 按时间和模型汇总 Token 和成本指标 |
| 响应质量和安全性 | 使用 event.outcome、error.type 和结束原因属性;仅在提示词和响应已捕获且未被删减时使用它们 | 防护栏拦截、内容过滤事件和策略违规 |
| 调用链和编排 | 仅限 Traces — 按 trace.id 分组,从 parent.id 沿层级追溯到 span.id,按 span 名称或 GenAI 操作聚合 | 无法回答 — 指标已经过预聚合 |
在 Traces 路径中,每个 span 的延迟来自 duration(纳秒,在每个 OTel 原生 span 中都有值)或
span.duration.us(微秒,是一个 APM 兼容字段,在许多 span 中为 null — 按此字段排序可能会在不知情的情况下
漏掉最慢的步骤)。使用前请确认哪个字段有值。最慢的子 span 就是瓶颈。
按 span 名称或 GenAI 操作属性聚合,可查看工作流中各类步骤的分布 —
检索、大模型调用、工具使用。
有关特定集成的数据流和字段名称 — OpenAI、Azure OpenAI、Azure AI Foundry、Amazon Bedrock、Bedrock AgentCore、GCP Vertex AI — 请参阅 references/integrations.md。有关更长的完整查询示例, 包括 Trace 层级追溯和时间序列集成模式,请参阅 references/esql-recipes.md。
示例
以下字段路径仅供参考。运行任何操作前,请先根据步骤 3 确认实际路径——此技能 本身会先发现字段名称,正确的嵌套路径取决于数据摄取方式。
“每种模型消耗了多少令牌?”——确认令牌属性存在且可解析,然后按模型和时间段汇总。 报告返回的总量:
FROM traces-*
| WHERE @timestamp > NOW() - 24 hours AND attributes.gen_ai.request.model IS NOT NULL
| EVAL in_tok = TO_LONG(attributes.gen_ai.usage.input_tokens),
out_tok = TO_LONG(attributes.gen_ai.usage.output_tokens)
| STATS input_tokens = SUM(in_tok), output_tokens = SUM(out_tok)
BY hour = BUCKET(@timestamp, 1 hour), attributes.gen_ai.request.model
| SORT hour
| LIMIT 500
“我们的 LLM 支出是多少?”——查询前,使用 GET /_field_caps 搜索 *cost* 字段。如果没有,
则报告令牌使用情况,并明确说明此部署未采集成本数据;解决方法是在采集层添加成本
属性,或启用能够报告成本的集成。不要用臆测的价格乘以令牌数。
“哪个模型最慢,它是否频繁出错?”——一次查询延迟和错误率,并按模型分组:
FROM traces-*
| WHERE @timestamp > NOW() - 24 hours AND attributes.gen_ai.request.model IS NOT NULL
| STATS request_count = COUNT(*),
failures = COUNT(*) WHERE event.outcome == "failure",
avg_duration_us = AVG(span.duration.us)
BY attributes.gen_ai.request.model
| EVAL error_rate = failures::double / request_count
| SORT avg_duration_us DESC
| LIMIT 100
“为什么我们的 Agent 运行缓慢?”——这需要追踪层级,因此需要追踪数据。找出包含多个 LLM 或工具 span 的
追踪,按这些 span 花费的时间排序,然后通过 trace.id 深入查看耗时最长的追踪,
找出瓶颈 span。由于 WHERE 限定了 LLM span,下面的总和是 LLM 耗时,而非端到端追踪
耗时——请明确标注列名,以免误读为实际延迟:
FROM traces-*
| WHERE @timestamp > NOW() - 3 hours AND attributes.gen_ai.operation.name IS NOT NULL
| STATS llm_span_count = COUNT(*), llm_duration_us = SUM(span.duration.us) BY trace.id
| WHERE llm_span_count > 1
| SORT llm_duration_us DESC
| LIMIT 50
“提示词会被拦截吗?”——检查追踪路径上的完成原因属性和 error.type,或检查
集成的护栏日志事件。诸如内容过滤器之类的完成原因是质量信号,而不是传输错误。
“是否已有针对这个问题的告警?”——查询 GET kbn:/api/alerting/rules/_find 和
GET kbn:/api/observability/slos,并根据步骤 2 中识别的 LLM 相关服务或集成指标进行筛选。
指南
- 仅根据摄取到 Elastic 中的数据作答。 不要描述或依赖其他供应商的 UI、控制台或产品。 如果数据不在 Elastic 中,答案就是数据不在 Elastic 中。
- 先发现,再查询。 通过
GET /_field_caps、GET /<index>/_mapping或样本文档, 确认摄取路径(步骤 2)和字段名称(步骤 3)。切勿猜测属性路径。 - 每个问题只使用一个一致的数据源。 不要在同一个答案中混合使用 APM 追踪和集成指标。
- GenAI 规范中不包含成本。 只有数据中存在成本字段时,才能将成本数值视为可用。
- 只有追踪才能还原链路。 如果问题涉及 Agent 编排,而现有数据只有集成指标, 应说明无法还原链路,而不是根据聚合数据进行推测。
- 根据版本选择时间序列语法。 在 Serverless 或 Stack 9.4+ 上使用
TS、TRANGE和TBUCKET; 在更低版本上改用FROM和BUCKET(@timestamp, ...)。为时间桶设置别名(BY bucket = TBUCKET(1 hour)), 并按别名排序。 - 动态映射的属性可能发生类型冲突。 令牌属性在一个后备索引中经常被映射为
integer, 在滚动更新后的另一个索引中则被映射为long,导致 ES|QL 拒绝该字段。请先在EVAL中使用TO_LONG(...)转换,再进行聚合。 - 不依赖 Kibana UI。 优先使用 ES|QL 和 Elasticsearch API;仅在处理 SLO 和告警时使用 Kibana API。切勿 指示用户打开 Kibana UI。
- ES|QL 语法和查询模式请参考 elasticsearch-esql 技能;在 Stack 9.4+
和 Serverless 上适用
TS命令参考, 其他版本则适用FROM命令参考。
Operations
| HTTP API (shorthand) | elastic CLI command |
|---|---|
GET / | elastic es info |
GET /_data_stream/<name> | elastic es indices get-data-stream --name '<name>' |
GET /_resolve/index/<pattern> | elastic es indices resolve-index --name '<pattern>' |
GET /<index>/_mapping | elastic es indices get-mapping --index '<index>' |
GET /_field_caps | elastic es field-caps --index '<index>' --fields '<fields>' |
POST /_query | elastic es esql query --format tsv --query '<esql>' |
GET kbn:/api/alerting/rules/_find | elastic kb alerting get-alerting-rules-find --filter '<filter>' |
GET kbn:/api/observability/slos | elastic kb slo find-slos-op --space-id '<space>' --kql-query '<kql>' |