ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI可观测性实战:用OpenTelemetry追踪LLM与RAG应用

AI可观测性实战:用OpenTelemetry追踪LLM与RAG应用 前些天看到一条行业新闻Dynatrace 宣布计划收购 AI 可观测性公司 Arize AI。很多做后端开发和监控运维的同学都在问传统可观测性不是已经有 Prometheus、SkyWalking、ELK 这些方案了吗为什么还要专门收购一家“AI 可观测性”公司其实答案并不是“要监控大模型”这么简单而是现有监控体系在 LLM 应用面前出现了明显缺口。本文不打算讨论收购条款和商业层面的事情而是以这次收购为契机梳理 AI 可观测性要解决的问题、关键数据模型以及如何用 OpenTelemetry 给 LLM / RAG 应用加上可观测能力。你可以把本文当成一套从概念到落地的入门教程包含完整可运行的 Python 示例、项目结构、常见问题和工程建议。读完以后你至少能回答三个问题AI 可观测性到底在观测什么和传统监控有什么区别在自己的项目里怎么快速接入1. 从一次收购看 AI 可观测性为什么成为焦点1.1 Dynatrace 与 Arize AI 是谁先说背景。Dynatrace 是传统的统一可观测性平台大家更熟悉它的应用性能监控、分布式追踪、基础设施监控、日志分析等能力。它的传统强项在于“系统运行状态”比如请求慢在哪、数据库连接池是否打满、某个服务是否异常。Arize AI 则是 AI 可观测性领域比较有代表性的公司产品定位更偏向机器学习模型上线后的表现监控、数据漂移检测、模型评估和 LLM 应用的观测。一个是看“系统跑得稳不稳”一个是看“模型效果好不好”两者互补性很强。从技术视角推测这次收购最直接的价值在于把传统可观测性数据链路和 AI 数据链路打通让同一个平台既能观察服务器的 CPU、内存、调用链路也能观察 LLM 的 Prompt、Completion、Token 消耗、幻觉风险、向量检索命中率等信息。对开发者来说这意味着后续做 LLM 应用时可观测性基础设施不需要重新造轮子。1.2 传统监控解决不了 LLM 的问题很多团队已经有比较成熟的监控体系但遇到 LLM 应用后会明显感到不够用。原因不是传统监控工具不好而是监控对象变了。传统接口的输入输出是结构化的比如 JSON 里的某个字段必须满足类型和范围。我们可以写一条规则如果status ! 200就告警。但 LLM 应用的输入输出是自然语言同一个问题可以有多种完全不同的正确回答一个正则表达式根本没法判断回答是否“正确”。就算在代码里加了很多断言也只能覆盖你能枚举出来的错误无法发现模型在语义层面悄悄跑偏。传统监控关注的是延迟、错误率、吞吐量但 LLM 应用还需要关注新的维度Token 消耗和成本不同 model 的计费差异很大。Prompt 内容和输入长度的变化。上下文窗口是否接近上限。Embedding 向量是否发生漂移。模型是否产生幻觉回答是否忠实于给定资料。是否返回了隐私数据或违规内容。这些信息很难用普通的log.info(model response: %s, resp)搞定因为它们既需要结构化也需要上下文关联。比如一个 RAG 应用用户问了一个问题系统先检索知识库再拼接 Prompt再调用模型最后返回答案。如果答案不对普通日志只能看到“模型返回了某句话”但看不出问题出在检索阶段还是模型生成阶段。AI 可观测性要解决的就是这个“全链路关联分析”问题。1.3 本文你能获得什么接下来的内容会分成几个层次先理解 AI 可观测性的核心概念和数据模型。然后用 OpenTelemetry 搭建一个最小环境给普通 LLM 调用增加分布式追踪。再扩展到一个简单的 RAG 流程把检索、拼 Prompt、调模型分别拆成 Span。最后补充采样、脱敏、Cost 统计和常见排查思路。只要你熟悉 Python并且了解一点 HTTP 接口的基本概念就能跟着实操。即使你所在团队用的是 Java、Go 或 Node.js本文的思路和数据结构也是通用的只需要替换对应语言的 OpenTelemetry SDK。2. AI 可观测性的核心概念与数据模型2.1 可观测性的三大支柱再理解可观测性这个词来自控制理论意思是“通过系统外部输出推断系统内部状态的能力”。在软件领域大家最常说的是三支柱Metrics指标比如请求数、错误率、P99 延迟、Token 消耗。Logs日志比如一条请求的完整记录包含输入输出和关键上下文。Traces追踪一次请求经过多个服务或组件时的完整链路。传统做法是把三者分开用但 AI 可观测性更强调把它们串起来。一次 LLM 调用在 Metrics 层是一个llm.calls.total计数在 Logs 层是一条包含prompt和response的记录在 Traces 层是一个包含多个子阶段的 Span 树。只有三者关联才能回答“为什么这个请求变慢了”和“为什么这个回答质量变差了”这两个问题。2.2 LLM 场景下需要哪些指标LLM 应用的可观测性指标可以分为两大类一类是传统性能指标的扩展另一类是 AI 特有的质量与成本指标。传统性能指标扩展请求量QPS、并发数。延迟首 Token 延迟TTFT、总生成耗时。错误率调用模型失败、超时、限流。资源CPU、内存尤其是本地部署模型时。AI 特有指标Token 使用量输入 Token、输出 Token、总 Token。Cost 预估根据模型单价计算每次调用的成本。上下文利用率当前 Prompt 占模型上下文窗口的比例。检索命中率RAG 中向量检索返回的相关片段是否真的有用。评估分数通过规则或另一个模型判断回答的忠实度、相关性、有害性。PII 泄露检测返回内容是否包含手机号、身份证、银行卡等信息。这些指标不是随便选几个而是要和业务目标绑定。比如你在做客服机器人最该关注的是检索命中率和回答忠实度而不是一味看 P99 延迟。2.3 用一组 JSON 表示一次 LLM Span在 OpenTelemetry 里链路追踪的最小单位是 Span。一个 Span 可以记录名称、开始时间、结束时间、属性、状态和父子关系。我们可以把一次 LLM 调用设计成一个 Span并加入 AI 相关属性。下面是一个示意结构{ name: llm.call, traceId: 3f2d8a1c..., spanId: 9b7e1c4a..., parentSpanId: b1d1c8e2..., attributes: { gen_ai.system: openai, gen_ai.request.model: gpt-4o-mini, gen_ai.request.max_tokens: 1024, gen_ai.request.temperature: 0.2, gen_ai.prompt.0.content: 请用一句话介绍可观测性, gen_ai.completion.0.content: 可观测性是通过日志、指标和追踪推断系统内部状态的能力。, gen_ai.usage.input_tokens: 24, gen_ai.usage.output_tokens: 30, gen_ai.duration_ms: 1860 }, status: { code: OK } }这里gen_ai.*是一套社区在推动的语义约定目的是让不同厂商的 LLM 追踪数据可以互操作。你在自建的时候不一定要用这套前缀但建议统一命名规范后续从自建平台迁移到商业平台时成本会小很多。3. 环境准备与项目结构3.1 环境依赖本文示例使用 Python操作系统不限Windows、macOS、Linux 都可以。建议使用 Python 3.10 及以上版本便于处理类型注解。需要安装以下依赖pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc requests说明一下opentelemetry-api和opentelemetry-sdk是 OpenTelemetry 的 Python 核心库。opentelemetry-exporter-otlp-proto-grpc负责把 Trace 数据通过 OTLP 协议上报给 Collector 或后端平台。requests用于示例中模拟调用 LLM 的 HTTP 接口。版本建议以你安装时的最新稳定版为准。如果你是在公司内网使用还需要确认网络策略允许访问 OTLP 端口。3.2 项目结构我们创建一个名为ai-observability-demo的目录结构如下ai-observability-demo/ ├── requirements.txt ├── telemetry.py # OpenTelemetry 初始化 ├── llm_service.py # 模拟 LLM 调用 ├── app.py # 手动埋点示例 ├── rag_service.py # RAG 链路示例 ├── metrics.py # Metrics 指标示例 └── collector-config.yaml3.3 初始化 OpenTelemetry SDK先写telemetry.py。这个文件负责初始化 TracerProvider并把 Span 同时输出到控制台和 OTLP Collector。控制台输出是为了方便本地调试OTLP 导出是为了对接真实后端。# telemetry.py from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter def init_tracer() - trace.Tracer: resource Resource(attributes{ SERVICE_NAME: llm-observability-demo }) provider TracerProvider(resourceresource) # 控制台导出本地调试时可以看到完整 Span JSON console_processor BatchSpanProcessor(ConsoleSpanExporter()) provider.add_span_processor(console_processor) # OTLP 导出上报给 opentelemetry-collector # 如果没有启动 Collectorapp 启动后不会崩溃只是导出失败 otlp_exporter OTLPSpanExporter( endpointhttp://localhost:4317, insecureTrue, ) otlp_processor BatchSpanProcessor(otlp_exporter) provider.add_span_processor(otlp_processor) trace.set_tracer_provider(provider) return trace.get_tracer(llm-observability-demo)这里使用BatchSpanProcessor是为了异步批量导出避免每次调用 LLM 都阻塞业务线程。控制台导出的处理器可以保留也可以在生产环境去掉。4. 实战一从零为 LLM 调用添加 Trace4.1 设计 LLM 调用接口我们先用一个简单的 HTTP 客户端封装 LLM 调用方便后续埋点。实际项目里你可以替换成 OpenAI SDK、LangChain、或者你自建的模型服务。# llm_service.py import os import requests def call_llm(prompt: str, model: str gpt-4o-mini) - str: 调用 OpenAI 兼容接口。为了示例清晰这里直接使用 HTTP 调用。 api_key os.getenv(OPENAI_API_KEY) if not api_key: raise RuntimeError(缺少 OPENAI_API_KEY 环境变量) response requests.post( https://api.openai.com/v1/chat/completions, headers{Authorization: fBearer {api_key}}, json{ model: model, messages: [ {role: user, content: prompt} ], }, timeout30, ) response.raise_for_status() data response.json() return data[choices][0][message][content]这段代码不是必须的你完全可以换成自己的模型调用逻辑。关键是理解埋点发生在调用前后。4.2 手动创建 Span接下来写app.py。我们用tracer.start_as_current_span创建 Span并在 Span 上记录 Prompt、Completion、耗时和错误信息。# app.py import time from opentelemetry import trace from opentelemetry.trace import StatusCode from llm_service import call_llm from telemetry import init_tracer tracer init_tracer() def answer(prompt: str) - str: with tracer.start_as_current_span(llm.request) as span: span.set_attribute(app.prompt, prompt) start time.perf_counter() try: result call_llm(prompt) span.set_attribute(app.completion, result) span.set_attribute(app.result_length, len(result)) except Exception as exc: span.set_attribute(app.error, str(exc)) span.set_status(StatusCode.ERROR) raise finally: duration_ms (time.perf_counter() - start) * 1000 span.set_attribute(app.duration_ms, duration_ms) return result if __name__ __main__: result answer(用一句话介绍可观测性) print(result)运行后控制台会输出类似下面的 Span 信息{ name: llm.request, context: { trace_id: 0x..., span_id: 0x... }, attributes: { app.prompt: 用一句话介绍可观测性, app.completion: 可观测性是通过日志、指标和追踪推断系统内部状态的能力。, app.result_length: 28, app.duration_ms: 1845.67 } }到这里你已经完成了一次最基本的 LLM 可观测性埋点。相比于写日志这种方式多了 Trace ID、Span ID 和父子关系后续可以和其他调用链关联起来。4.3 本地查看 Trace为了在本地可视化查看 Trace我们需要启动一个 OpenTelemetry Collector。先创建collector-config.yamlreceivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 processors: batch: exporters: debug: verbosity: detailed service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [debug]然后使用 Docker 启动docker run -p 4317:4317 -p 4318:4318 \ -v $(pwd)/collector-config.yaml:/etc/otelcol/config.yaml \ otel/opentelemetry-collector-contrib启动后再运行python app.py你会在 Collector 的日志里看到上报的 Trace。这里演示的是本地 Debug 导出实际生产你可以把debug替换成otlp或dynatrace等 exporter。5. 实战二RAG 全链路可观测性5.1 RAG 链路为什么比单次 LLM 调用更难观测RAGRetrieval-Augmented Generation是目前最常见的 LLM 工程架构。它的流程大致是用户输入问题 - 向量化 - 从知识库检索 - 把检索片段拼到 Prompt - 调用 LLM - 返回答案。问题出在链路太长。用户看到答案不对时你很难直接判断是知识库里没相关内容还是检索到了但排序不对还是 Prompt 拼接导致模型理解错误还是模型本身幻觉。如果每一步都打一条单独的日志日志之间没有关联排查效率会非常低。正确做法是把整个 RAG 流程做成一棵 Span 树。5.2 为检索和生成分别埋点下面写一个简化的rag_service.py。为了便于运行retrieve函数会直接返回固定的片段你可以在真实项目中替换成向量数据库查询。# rag_service.py from telemetry import init_tracer tracer init_tracer() def retrieve(question: str) - list[str]: 实际项目里这里会查询向量数据库并返回片段与相似度分数。 示例中直接返回固定内容。 return [ Dynatrace 是面向云原生应用的可观测性平台。, Arize AI 提供 AI 模型的可观测性、评估和数据分析能力。, ] def build_prompt(question: str, chunks: list[str]) - str: context \n.join(chunks) return f请根据以下资料回答问题。\n资料{context}\n问题{question} def rag_answer(question: str) - str: with tracer.start_as_current_span(rag.pipeline) as root: root.set_attribute(rag.question, question) # 检索阶段 with tracer.start_as_current_span(rag.retrieve) as span: chunks retrieve(question) span.set_attribute(rag.retrieve.chunk_count, len(chunks)) span.set_attribute( rag.retrieve.chunk_ids, ,.join([str(i) for i in range(len(chunks))]) ) # 构造 Prompt 阶段 with tracer.start_as_current_span(rag.build_prompt) as span: prompt build_prompt(question, chunks) span.set_attribute(rag.prompt.length, len(prompt)) # 模型调用阶段 with tracer.start_as_current_span(rag.llm_call) as span: from llm_service import call_llm answer call_llm(prompt) span.set_attribute(rag.answer, answer) span.set_attribute(rag.answer_length, len(answer)) root.set_attribute(rag.status, success) return answer if __name__ __main__: print(rag_answer(Arize AI 是做什么的))运行之后你会看到 Span 的嵌套关系rag.pipeline ├── rag.retrieve ├── rag.build_prompt └── rag.llm_call这样当业务反馈“回答质量下降”时你可以先看rag.retrieve返回的chunk_count是不是变少再看rag.prompt.length是不是太大导致上下文不够最后再看rag.llm_call的耗时和错误。每个环节都有数据而不是靠猜。5.3 加入 Metrics调用次数与 Token 直方图Traces 适合排查单次请求Metrics 适合看整体趋势。我们可以在metrics.py里定义两个指标一个计数器统计 LLM 调用次数一个直方图统计 Token 消耗。# metrics.py from opentelemetry import metrics from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader def init_meter() - metrics.Meter: reader PeriodicExportingMetricReader(ConsoleMetricExporter()) provider MeterProvider(metric_readers[reader]) metrics.set_meter_provider(provider) return metrics.get_meter(llm-meter) meter init_meter() llm_calls_total meter.create_counter( llm.calls.total, unit1, description累计 LLM 调用次数, ) llm_tokens_histogram meter.create_histogram( llm.tokens.usage, unittoken, description每次 LLM 调用消耗的 Token 数, ) def record_llm_call(token_count: int) - None: llm_calls_total.add(1) llm_tokens_histogram.record(token_count)在app.py中调用record_llm_call时你需要从实际 LLM 响应里取出usage.total_tokens或根据字符串长度估算。对于 OpenAI 接口常见字段如下data response.json() usage data.get(usage, {}) total_tokens usage.get(total_tokens, 0)有了这些指标你可以设置告警比如“单次调用 Token 消耗超过 5000”或者“每分钟调用次数突增”这些都是 AI 可观测性早期落地最容易见效的部分。6. 数据如何处理采样、脱敏与上报6.1 用 SpanProcessor 实现自动脱敏LLM 应用中Prompt 和 Completion 可能包含用户隐私数据、公司内部资料。如果全部原样上报到可观测性平台会带来很大的安全风险。建议在上报前对属性做一次脱敏处理。一个比较稳妥的做法是在埋点前统一使用一个脱敏函数而不是把原始数据直接写到 Span 上。下面是一个简单实现# sanitize.py import re # 示例手机号、身份证、连续数字串 SENSITIVE_PATTERNS [ re.compile(r\b1[3-9]\d{9}\b), re.compile(r\b\d{17}[\dXx]\b), ] def redact_text(text: str) - str: for pattern in SENSITIVE_PATTERNS: text pattern.sub([REDACTED], text) return text def truncate(text: str, max_length: int 200) - str: if len(text) max_length: return text[:max_length] ...(truncated) return text然后在埋点处替换span.set_attribute(app.prompt, truncate(redact_text(prompt)))不要嫌麻烦。真实生产环境里直接把用户输入打进日志而引发安全事故的例子并不少见。最佳实践是默认不记录原始 Prompt先记录脱敏后的摘要等确实需要排查某条 Trace 时再在受控环境里按需查看完整数据。6.2 用 Collector 做数据预处理除了在代码里脱敏OpenTelemetry Collector 也支持对 Span 数据做过滤和处理。例如我们可以在 Collector 配置里加上内存限制和批量处理避免高并发时把内存打满。processors: batch: send_batch_size: 1024 timeout: 5s memory_limiter: check_interval: 1s limit_percentage: 80 spike_limit_percentage: 20这里的参数含义send_batch_size每批导出的 Span 数量。timeout攒够一批的最长等待时间。memory_limiterCollector 内存使用过高时主动丢弃部分数据保护自身稳定。这些配置在官方文档里都有详细说明部署到生产前建议先压测。6.3 对接商业可观测性平台的通用方式如果你们团队正在使用 Dynatrace、Arize AI 或其他支持 OTLP 的可观测性平台接入方式基本类似在代码里保持 OTLP 导出不变把上报 endpoint 指向平台提供的地址并加上认证 Header。export OTEL_EXPORTER_OTLP_ENDPOINThttps://your-tenant.example.com:4317 export OTEL_EXPORTER_OTLP_HEADERSAuthorizationApi-Token your_token_here然后在telemetry.py中把OTLPSpanExporter的 endpoint 改成读取环境变量避免把地址硬编码在代码里。import os endpoint os.getenv(OTEL_EXPORTER_OTLP_ENDPOINT, http://localhost:4317) headers os.getenv(OTEL_EXPORTER_OTLP_HEADERS) exporter OTLPSpanExporter(endpointendpoint, headersheaders, insecureTrue)不同平台的认证方式可能不一样有的用 Header有的用 Query 参数。具体以你所用平台的接入文档为准但整体思路是一致的你的应用只负责产生标准 OTLP 数据平台负责存储、分析和可视化。7. 常见问题与排查思路7.1 高频问题列表问题现象常见原因解决思路运行后没有 Trace 输出TracerProvider 没有初始化或初始化顺序不对确认init_tracer()在创建 Span 之前执行Span 之间没有父子关系没有使用start_as_current_span嵌套调用使用同一个 Tracer 的上下文管理器保持嵌套数据上报失败Collector 没启动或 endpoint 端口不对检查 4317 / 4318 端口查看 Collector 日志Token 统计为 0响应中没有usage字段从响应体中解析usage.total_tokens没有则估算Prompt 包含敏感信息埋点直接记录了完整输入使用脱敏函数截断或替换敏感内容延迟值不准把排队时间也算进了模型调用耗时只统计从请求发送到收到首个返回值的时间高并发下上报影响业务SpanExporter 同步导出使用BatchSpanProcessor并配置批量参数7.2 一个完整排查案例假设你收到一个告警RAG 应用最近 1 小时的平均回答长度变短了用户反馈质量下降。直接看日志很难定位因为你不知道是检索结果变少了还是模型被某种安全策略拦截导致输出被截断。按照 AI 可观测性的数据你可以这样排查先看rag.retrieve.chunk_count的分布确认是否从原来的 5 变成了 1。再看rag.prompt.length如果 Prompt 变短了可能是知识库内容更新导致检索片段变少。接着看rag.llm_call的错误状态和gen_ai.usage.output_tokens如果输出 Token 明显变小可能是模型侧设置 max_tokens 被改小。最后拉出该时段的一条具体 Trace对比过去成功 Trace 的 Span 树看差异点。这个排查过程之所以能成立是因为我们提前把每个阶段的关键信息都记到了 Span 上。如果没有这些数据只能靠反复试错和人工复现。8. 最佳实践搭建 AI 可观测性体系的三个层次8.1 指标层先定义业务 SLO不要把 AI 可观测性做成“数据收集大礼包”。先想清楚你的应用最重要的问题是什么再定义可量化的 SLO。如果你做的是在线客服 RAG建议先关注这几个指标检索命中率检索阶段是否返回了有效片段。回答忠实度模型回答是否来源于检索到的知识库内容。平均首 Token 延迟用户感知速度。单次请求成本Token 消耗乘以模型单价。这些指标需要按月或者按周复盘而不是只盯着 CPU 和内存。8.2 追踪层统一 Trace 命名和属性规范Trace 的价值在于关联上下文但如果没有统一规范很快就会变成一团乱麻。比如有人把模型调用 Span 叫llm有人叫openai_request后续检索和看报表都会很痛苦。建议在团队内约定一套属性命名前缀参考社区语义gen_ai.system模型提供商。gen_ai.request.model模型名称。gen_ai.prompt.*Prompt 内容或摘要。gen_ai.completion.*模型输出。gen_ai.usage.*Token 统计。rag.retrieve.*RAG 检索相关字段。命名统一之后无论你用的是 Dynatrace、Arize AI还是自建 ClickHouse 存储都能按同一套维度查询。8.3 评估层把线上监控和离线评估打通AI 可观测性的最终目标是驱动模型和系统持续改进。线上采集到的真实 Prompt 和 Response应该定期抽样进入离线评估流程。一个常见的闭环如下线上平台存储 Trace。根据规则或随机采样抽取样本。离线用自动化评估模型判断 hallucination、answer relevance。评估结果回流到标注或训练数据集。微调或优化 Prompt 后再上线对比指标。这也是 Arize AI 这类平台区别于传统 APM 的核心思路。单纯“看得到”还不够还要能“评得出”和“改得准”。9. 写在最后回到 Dynatrace 收购 Arize AI 这条消息上。对普通开发者来说最重要的信号不是哪家公司又买了哪家公司而是 AI 工程化正在进入深水区光能把模型跑起来远远不够还要能回答“模型效果如何变化”“为什么回答质量下降”“一次推理花了多少钱”。也就是 AI 可观测性。不要等到应用上线后再补监控。如果你正在做一个 LLM 项目建议今天就做两件事第一在代码里用 OpenTelemetry 给一次模型调用加一个 Span第二把 Prompt 和 Response 的脱敏规则写出来。这两步成本很低但能让你在问题发生时少很多手忙脚乱。下一步可以继续研究 OpenTelemetry 的 Baggage、Context Propagation、Agent 工具调用追踪以及模型评测指标的设计。可观测性不是终点而是让 AI 应用变得可信、可控、可优化的基础能力。
返回列表