用于调试 LLM 延迟、成本和 GPU 饱和度的 ES|QL 查询 作者来自 Elastic Jeffrey Rengifo学习如何使用 ES|QL 针对 OpenTelemetry traces 调查 LLM 延迟、token 成本和 GPU 饱和度并获取根本原因而不仅仅是表面症状。仪表板告诉你哪里出问题ES|QL 告诉你为什么。针对 Elasticsearch 中 OpenTelemetry traces 数据的三条查询识别出了模型切换后成本回归 2.4 倍、某个 prompt 模板比另一个多生成 23 倍 output tokens以及 GPU 在 43 个时间窗口中的 42 个窗口里持续高于 90% 负载——这一切都来自同一个集群其中 EDOT 已经在发送 tracesDCGM 已经在发送 GPU metrics。本文将展示如何在你自己的 LLM 工作负载中复现这三类调查。在本文中你将学习如何使用 ES|QL 查询基于 Elastic Distribution of OpenTelemetry EDOT 和 OpenTelemetry Collector 收集的数据回答关于 LLM 工作负载的三个真实调试问题。前置条件Elasticsearch 9.xPython 3.9本地已安装 Ollama v0.5.12本文中的所有查询和配置步骤都可以在配套 notebook 中找到。AI 工作负载中的可观测性鸿沟大多数运行基于 LLM 的应用的团队已经完成了第一步为应用添加埋点以捕获 traces、token 数量和延迟。EDOT、OpenLIT 和 Langtrace 等工具让这件事变得很简单。数据正在持续流入。下一步自然就是当出现问题时如何查询这些数据。预构建的 dashboards 只能回答预先定义好的问题“我的 p95 延迟是多少” 或者 “我今天用了多少 tokens”这些对于监控很有用但调试是另一回事。调试意味着你有一个症状“上周二延迟飙升”然后你需要不断探索数据直到找到原因。这种探索需要的是查询语言而不是 dashboard。这正是 ES|QL 的用武之地。ES|QL 是 Elasticsearch 的基于 pipe 的查询语言它允许你在单个查询中跨 traces 和 metrics 进行聚合、过滤和 join。应用到 LLM telemetry 上它可以让你做如下事情在一个查询中比较不同模型版本的 p95 延迟按自定义 prompt 标识符分组找到消耗 tokens 最多的模板将 LLM trace 数据与 GPU metrics 进行 join判断是否是基础设施瓶颈在其他文章中我们介绍了如何采集 LLM telemetry使用 EDOT、OpenLIT 或 Langtrace。本文则解释当出现问题时如何对这些 telemetry 进行调查。技术栈LLM telemetry 如何进入 Elastic在开始调试之前我们需要理解有哪些数据可用以及它们存在哪里。架构如下该技术栈包含两条数据路径LLM traces应用层你的 Python 应用通过 OpenAI client 调用 Ollama或任何兼容 OpenAI 的端点。EDOT Python 会自动对这些调用进行埋点生成符合 OpenTelemetry GenAI 语义规范的 spans。当通过 Elastic Managed OTLP Endpoint 发送时这些 spans 会进入 Elasticsearch 中的 traces-generic.otel-default 数据流。GPU metrics基础设施层在运行 GPU 推理的主机上NVIDIA 的 DCGM Exporter 会以 Prometheus endpoint 的形式暴露 GPU metrics。OpenTelemetry Collector 会抓取这些 metrics 并发送到 Elasticsearch它们最终进入 metrics-* 数据流。EDOT 自动捕获的内容EDOT Python 包含 elastic-opentelemetry-instrumentation-openai它会自动对 OpenAI client 库的每一次调用进行埋点。由于 Ollama 在 http://localhost:11434/v1/ 上提供 OpenAI 兼容 API因此 EDOT 可以在无需任何代码修改的情况下对 Ollama 调用进行埋点。每一次 LLM 调用都会生成一个 span并包含以下属性遵循 OTel GenAI 语义规范属性捕获内容示例gen_ai.operation.name操作类型chatgen_ai.request.model你请求的模型gemma4:e4bgen_ai.response.model实际响应的模型gemma4:e4bgen_ai.usage.input_tokensprompt token 数量142gen_ai.usage.output_tokenscompletion token 数量89gen_ai.response.id唯一 completion IDchatcmpl-abc123完成埋点后每一次调用都会作为一个 span 出现在 Kibana 中并附带所有这些属性EDOT 还会生成两个 metricsgen_ai.client.token.usage token 数量的 histogram 以及 gen_ai.client.operation.duration请求延迟秒的 histogram 。配置非常简单。将 OpenAI client 指向 Ollama然后使用 EDOT 的自动埋点运行from openai import OpenAI client OpenAI( base_urlhttp://localhost:11434/v1/, api_keyollama, # required by the client but unused by Ollama )如何向 OTel spans 添加自定义 prompt template IDOTel GenAI 语义规范涵盖了模型追踪和 token 使用情况但并不包含 prompt template 标识符。如果你正在运行多个 prompt templates system prompts、few-shot 变体等你需要知道究竟是哪一个导致了问题。当前 OTel 规范中并不存在 gen_ai.prompt.id 这一约定。为了填补这一空白你可以添加一个自定义 span 属性from opentelemetry import trace tracer trace.get_tracer(__name__) with tracer.start_as_current_span(prompt-execution) as span: span.set_attribute(prompt.template.id, summarize-v2) response client.chat.completions.create( modelgemma4:e4b, messages[{role: user, content: prompt}] )这个 prompt.template.id 属性会作为 span 的一部分流入 Elasticsearch你可以像使用任何内置属性一样在 ES|QL 查询中使用它。GPU metrics从 DCGM 到 Elastic对于在 NVIDIA 硬件上运行自托管模型的团队来说GPU metrics 是至关重要的上下文信息。NVIDIA 的 DCGM Data Center GPU Manager Exporter 会以 Prometheus endpoint 的形式在 9400 端口暴露 GPU 利用率、内存使用率、温度以及功耗等 metrics。带有 Prometheus receiver 的 OpenTelemetry Collector 会抓取这些 metrics 并将其转发到 Elastic。resource processor 会为每一个 metric 添加 data_stream.dataset nvidia_gpu 标签从而将数据路由到 metrics-nvidia_gpu.otel-default 数据流中以便与 Elastic 的 NVIDIA GPU OpenTelemetry integration 保持一致receivers: prometheus: config: scrape_configs: - job_name: nvidia_gpu scrape_interval: 10s static_configs: - targets: [localhost:9400] processors: resource/nvidia_gpu: attributes: - key: data_stream.dataset value: nvidia_gpu action: upsert - key: data_stream.namespace value: default action: upsert exporters: otlp: endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT} service: pipelines: metrics: receivers: [prometheus] processors: [resource/nvidia_gpu] exporters: [otlp]Elastic 提供了一流的 NVIDIA GPU OpenTelemetry integration其中包含 Fleet 级 dashboards、六条告警规则例如 thermal throttling 等情况以及一个用于 GPU 热健康状态的 SLO 模板。对于 LLM 调试而言关键的 GPU metrics 包括Metric它告诉你的内容DCGM_FI_DEV_GPU_UTILGPU 计算单元的繁忙程度 % DCGM_FI_DEV_FB_USEDGPU 内存 VRAM 使用量DCGM_FI_DEV_GPU_TEMP是否可能存在 thermal throttling 从而影响性能DCGM_FI_DEV_POWER_USAGE功耗可用于判断是否存在持续高负载注意DCGM 需要 NVIDIA 数据中心 GPU A100、H100、L40S 。对于消费级 GPU基于 NVML 的工具如 nvmlreceiver 可以提供类似的 metrics。而云托管 LLM 提供商 OpenAI、Bedrock、Azure OpenAI 则完全不会暴露 GPU metrics因为底层硬件已被抽象化。问题 1我的新模型版本是否导致了延迟或成本退化场景你一直在生产环境中运行 gemma4:e2b并刚刚部署了 gemma4:e4b 以获得更好的质量。几天后延迟告警触发token 账单也暴涨。问题是模型切换是否是根本原因OpenTelemetry GenAI 规范会自动捕获什么gen_ai.request.model你请求的模型与 gen_ai.response.model实际响应的模型之间的区别非常重要。在使用 Ollama 时这两者通常与指定的 model:tag 一致。但对于使用模型别名的云服务提供商例如 gpt-4o 会解析为某个固定版本response model 可能与 request model 不同。在进行模型版本比较时gen_ai.response.model 是更可靠的字段因为它反映了实际运行的模型。ES|QL 查询FROM traces-generic.otel-default | WHERE attributes.gen_ai.operation.name chat AND timestamp NOW() - 7 days | EVAL is_failure CASE(attributes.event.outcome failure, 1, 0) | STATS request_count COUNT(*), avg_input_tokens AVG(attributes.gen_ai.usage.input_tokens), avg_output_tokens AVG(attributes.gen_ai.usage.output_tokens), p95_duration_us PERCENTILE(transaction.duration.us, 95), error_count SUM(is_failure) BY attributes.gen_ai.response.model | SORT p95_duration_us DESC该查询会为你提供一个并排对比每个模型版本在延迟、token 使用量以及错误率方面的表现。针对从两个 Gemma 4 变体收集的 120 个 chat spans 运行后返回结果如下有两点特别值得注意。首先两边的 prompts 完全相同两者都是 99 个 input tokens 因此延迟差距并不是由 prompt 大小导致的。其次gemma4:e4b 平均实际生成的 output tokens 更少但在第 95 百分位上的耗时却超过了两倍。这说明性能退化来自模型本身而不是其工作负载。使用 LOOKUP JOIN 添加成本分析OTel GenAI 规范并不包含成本属性。虽然 token 数量是可用的但要将其换算为成本则需要知道每个模型的定价。这正是 ES|QL 的 LOOKUP JOIN 发挥作用的地方。首先创建一个包含模型定价信息的 lookup 索引PUT /model_pricing { settings: { index: { mode: lookup } }, mappings: { properties: { attributes.gen_ai.response.model: { type: keyword }, cost_per_1k_input_tokens: { type: float }, cost_per_1k_output_tokens: { type: float } } } }使用你的模型定价数据填充该索引然后扩展查询FROM traces-generic.otel-default | WHERE attributes.gen_ai.operation.name chat AND timestamp NOW() - 7 days | STATS request_count COUNT(*), total_input_tokens SUM(attributes.gen_ai.usage.input_tokens), total_output_tokens SUM(attributes.gen_ai.usage.output_tokens), p95_duration_us PERCENTILE(span.duration.us, 95) BY attributes.gen_ai.response.model | LOOKUP JOIN model_pricing ON attributes.gen_ai.response.model | EVAL estimated_cost (total_input_tokens / 1000.0) * cost_per_1k_input_tokens (total_output_tokens / 1000.0) * cost_per_1k_output_tokens | SORT estimated_cost DESC现在你可以在同一个结果集中同时看到每个模型版本的延迟与成本。LOOKUP JOIN 会在查询时动态丰富你的 trace 数据而无需将定价信息复制到每一个 span 中。假设使用如下示例定价gemma4:e2b 的价格为每 1K tokens 输入 $0.10 / 输出 $0.30而 gemma4:e4b 的价格为输入 $0.25 / 输出 $0.75那么同样每个模型处理 60 个请求后会得到如下结果gemma4:e4b 的工作负载在完成相同任务时成本约高出 2.4 倍尽管它生成的 output tokens 甚至略少。延迟与成本的退化在同一个查询结果中就可以同时看到。何时使用模型版本对比查询当你在评估模型变更时这种模式非常有用例如不同模型版本之间的 A/B 测试、渐进式发布或者基于任务复杂度将请求路由到不同模型的多模型策略。问题 2哪个 prompt 模板导致 token 激增场景你的 token 使用量本周突然上涨 40%但你并没有更换模型。你有三个 prompt 模板在轮换使用summarization、extraction、classification你需要找出到底是哪一个导致了问题。为什么 prompt.template.id 是值得添加的自定义 OTel 属性OTel GenAI 语义规范会追踪哪个模型处理了请求、用了多少 tokens、耗时多少但它不会追踪使用的是哪个 prompt 模板因为 prompt 管理属于应用层逻辑。这是一个关键的调试缺口。如果所有 prompts 都通过同一个 gen_ai.operation.name chat 操作流转那么你无法区分一个表现正常的 summarization prompt 和一个失控的 extraction prompt除非有自定义标识符。添加 prompt.template.id 这个自定义 span 属性如 stack 部分所示可以解决这个问题。这是一种值得尽早采用的模式因为缺失它的代价通常只有在系统出问题时才会显现。ES|QL 查询FROM traces-generic.otel-default | WHERE attributes.gen_ai.operation.name chat AND timestamp NOW() - 7 days | EVAL is_failure CASE(attributes.event.outcome failure, 1.0, 0.0) | STATS request_count COUNT(*), avg_output_tokens AVG(attributes.gen_ai.usage.output_tokens), max_output_tokens MAX(attributes.gen_ai.usage.output_tokens), error_rate AVG(is_failure) * 100 BY attributes.prompt.template.id | SORT avg_output_tokens DESC将该查询运行在我们 120 个 spans 上会得到一个非常明确的“赢家”extraction-v3 每次请求产生的 token 数量约为 summarize-v2 的 5 倍也约为 classify-v1 的 23 倍。max_output_tokens 列也很重要少数极端响应可能会拉高平均值因此同时查看这两个指标可以清楚看出extraction-v3 的高 token 使用是结构性的“过度输出”而不是由单个异常值导致的偏移。将这一模式扩展到其他调试维度prompt.template.id 模式可以扩展到任何你想要切分的调试维度客户等级、使用场景、部署区域等。它可以作为自定义 span 属性加入并在 ES|QL 中进行分组分析。GenAI 规范提供的是模型和 token 层的数据而自定义属性提供的是业务上下文层的数据。问题 3LLM 延迟是否与 GPU 饱和相关场景过去一周推理延迟逐渐上升但应用代码和模型都没有变化。你怀疑是基础设施问题。这个问题是自托管模型特有的。当你使用云端 LLM 提供商 OpenAI、Bedrock、Azure OpenAI 时GPU 资源是完全抽象的。你只能看到延迟上升但无法判断是否是 provider 的 GPU 已经饱和。而在使用 NVIDIA 硬件自托管模型时你可以同时观察问题的两侧。GPU metrics 能告诉你什么来自 DCGM Exporter 的 GPU metrics 为推理引擎提供了一个观察窗口DCGM_FI_DEV_GPU_UTIL 较高超过 90%意味着 GPU 计算单元已饱和新推理请求会排队从而增加延迟。DCGM_FI_DEV_FB_USED 接近总显存容量意味着 GPU 内存压力增大可能需要交换模型层或者 GPU 无法进行更多 batch。升高的 DCGM_FI_DEV_GPU_TEMP 在超过 GPU 的降频阈值后会触发 thermal throttling从而降低时钟频率直接影响推理吞吐量。将 traces 与 GPU metrics 关联挑战在于 LLM traces 和 GPU metrics 位于不同的 indices且 schema 不同。LLM spans 位于 traces-generic.otel-default时间粒度是 request 级别GPU metrics 位于 metrics-*时间粒度是 scrape interval通常 10–15 秒。ES|QL 的 LOOKUP JOIN 可以将两者结合起来。方法是创建 lookup index将 GPU metrics 聚合为按分钟 bucket 的数据然后将 trace 数据与这些 bucket 进行 join。首先创建用于存放聚合 GPU metrics 的 lookup indexPUT /gpu_metrics_by_minute { settings: { index: { mode: lookup } }, mappings: { properties: { time_bucket: { type: date }, gpu_utilization: { type: float }, gpu_memory_used: { type: float }, gpu_temperature: { type: float } } } }然后将原始 DCGM metrics 聚合为按分钟划分的时间桶FROM metrics-* | WHERE metrics.DCGM_FI_DEV_GPU_UTIL IS NOT NULL AND timestamp NOW() - 7 days | EVAL time_bucket DATE_TRUNC(1 minute, timestamp) | STATS gpu_utilization AVG(metrics.DCGM_FI_DEV_GPU_UTIL), gpu_memory_used AVG(metrics.DCGM_FI_DEV_FB_USED), gpu_temperature AVG(metrics.DCGM_FI_DEV_GPU_TEMP) BY time_bucket使用 Elasticsearch bulk API 将聚合后的结果写入 gpu_metrics_by_minute 索引。在生产环境中如果 GPU metrics 持续被采集并写入可以使用 Elasticsearch transform 自动维护这个 lookup 索引使其保持实时更新。FROM traces-generic.otel-default | WHERE attributes.gen_ai.operation.name chat AND timestamp NOW() - 7 days | EVAL time_bucket DATE_TRUNC(1 minute, timestamp) | STATS avg_duration_us AVG(transaction.duration.us), request_count COUNT(*) BY time_bucket | LOOKUP JOIN gpu_metrics_by_minute ON time_bucket | WHERE gpu_utilization IS NOT NULL | EVAL latency_vs_gpu CASE( gpu_utilization 90 AND avg_duration_us 5000000, saturated slow, gpu_utilization 90 AND avg_duration_us 5000000, saturated but ok, gpu_utilization 90 AND avg_duration_us 5000000, slow without gpu cause, normal ) | SORT time_bucket DESC注意由于 GPU metrics 每 10 秒抓取一次而 LLM spans 是按请求粒度记录时间戳因此两者在进行 join 时需要统一粒度。lookup index 会将原始 metrics 聚合为按分钟的平均值而在 trace 侧使用 DATE_TRUNC(1 minute, timestamp) 将 spans 对齐到相同的时间桶。如何解读 latency_vs_gpu 分类latency_vs_gpu 列会对每个时间窗口进行分类“saturated slow”GPU 是瓶颈。你需要扩展 GPU 容量、减少 batch size或者使用更小的模型。“saturated but ok”GPU 已经很忙但延迟仍然可接受。你已经接近上限但还没有超出。“slow without gpu cause”延迟来自其他因素网络、预处理、队列深度。GPU 不是问题所在。“normal”一切正常。将我们的 120 个 chat spans 与按分钟聚合的 GPU buckets 进行 join 后共得到 43 个同时包含 LLM activity 和 GPU coverage 的时间窗口latency_vs_gpu分钟数GPU 利用率范围平均请求耗时saturated slow4290.8% - 98.2%5.98s - 70.5ssaturated but ok193.9%4.77s从问题到调查上面的三个查询只是起点。ES|QL 的管道式语法使它们具有可组合性因此你可以在调查深入时不断组合这些模式。例如你可以将问题 1 和问题 2 结合起来“在新模型版本中哪些 prompt 模板的 token 效率最差”FROM traces-generic.otel-default | WHERE attributes.gen_ai.operation.name chat AND timestamp NOW() - 7 days | STATS avg_output_tokens AVG(attributes.gen_ai.usage.output_tokens), request_count COUNT(*) BY attributes.gen_ai.response.model, attributes.prompt.template.id | SORT avg_output_tokens DESC将数据按两个维度同时切分后会暴露出单一查询无法看到的行为模式。classify-v1 prompt 只要求返回一个单词标签而 gemma4:e4b 在该 prompt 下基本遵守这一约束每次响应约 20 个 tokens。相比之下gemma4:e2b 在同样的 prompt 上平均输出约 121 个 tokens多出约 6 倍因为它倾向于在标签之外额外添加解释。这类退化无法通过平均值发现只有在同时按 model 和 prompt 进行切分时才能显现。从调试走向告警一旦你通过临时 ES|QL 查询识别出某种模式就可以将其转化为检测规则。Elastic 的告警系统支持基于 ES|QL 的规则因此帮助你定位问题的同一条查询也可以变成未来检测问题的告警每个 prompt template 的 token 使用量超过阈值模型版本的延迟退化超过某个百分比GPU 利用率持续高于 90% 且推理延迟下降Kibana 内置的 LLM 可观测性对于希望在 ES|QL 查询之外同时使用预构建视图的团队Elastic 提供开箱即用的 LLM Observability dashboards自 Elastic Observability 9.0 起 GA。这些仪表板覆盖 OpenAI、Amazon Bedrock、Azure AI 和 Google Vertex AI展示 token 使用量、延迟分布以及成本拆解。对于 GPU 基础设施NVIDIA GPU OpenTelemetry integration 提供 Fleet 级 dashboards包含 GPU 利用率、显存、温度和功耗等指标并预置六条针对关键 GPU 状态的告警规则。这些 dashboards 与 ES|QL 方法是互补的。使用 dashboards 进行持续监控和健康检查而当你需要深入分析具体问题时则使用 ES|QL。结论我们涵盖了以下内容缺口LLM telemetry 的采集已经解决但如何调试仍然是难题。ES|QL 通过即席查询弥补了这一缺口。三种调试模式模型版本对比 STATS LOOKUP JOIN 、prompt 模板隔离自定义属性 GROUP BY 、以及 GPU 关联分析跨 trace 与 metric index 的 LOOKUP JOIN。LOOKUP JOIN 的价值在查询时将外部上下文定价、GPU metrics注入 trace 数据而无需修改埋点逻辑。自定义属性在 OTel GenAI 规范之外扩展领域字段如 prompt.template.id以支持规范本身未覆盖的调试维度。该方法适用于任何 OpenAI 兼容的 LLM endpointOllama、vLLM、TGI只要通过 EDOT 进行埋点并且 ES|QL 查询可以在任意支持对应 data streams 的 Elasticsearch 集群中运行。下一步试用配套 notebook完整了解埋点与查询流程探索 Elastic 的 LLM Observability dashboards用于开箱即用的监控视图阅读 ES|QL LOOKUP JOIN了解更多数据增强模式查看 OpenTelemetry GenAI semantic conventions 获取最新属性定义学习基于 OpenTelemetry 与 Elastic 的 ML 与 AI Ops 可观测性原文https://www.elastic.co/observability-labs/blog/esql-llm-opentelemetry-debugging