前置条件- Elasticsearch 9.x- Python 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在 上提供OpenAI兼容API因此EDOT可以在无需任何代码修改的情况下对Ollama调用进行埋点。每一次LLM调用都会生成一个span并包含以下属性遵循OTel GenAI语义规范完成埋点后每一次调用都会作为一个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包括注意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的时间窗口从问题到调查上面的三个查询只是起点。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可观测性