基于OpenTelemetry与Bounded实现GenAI应用可观测性实战
如果你正在尝试将 GenAI 应用投入生产那么监控和度量其性能、成本与质量可能是你现在最头疼的问题之一。传统的 APM 工具能告诉你 API 响应慢了但无法告诉你为什么这次 AI 调用慢了是因为提示词Prompt太长还是模型本身“思考”太久这次调用花了多少钱生成的回答质量如何是否偏离了预期这正是今天要介绍的项目Bounded试图解决的核心痛点。它不是一个全新的监控平台而是一个巧妙的“连接器”。Bounded 的核心思路是利用已经广泛采用的OpenTelemetry (Otel)标准来收集 GenAI 应用的链路追踪数据Traces然后从中自动提取、计算并导出有业务意义的GenAI 专属指标Metrics比如每次调用的 Token 消耗、成本、延迟分布甚至是基于原始提示词Raw Prompts的质量评分。简单来说它把 OpenTelemetry 这个“通用监控管道”变成了专为 GenAI 场景服务的“智能分析引擎”。你不需要改造大量业务代码去埋点只需要像往常一样使用 Otel SDK 进行链路追踪Bounded 就能在后台帮你把零散的 Trace 数据聚合成直观的 Metrics 和 Insights。本文将带你深入理解 Bounded 的设计理念、工作原理并通过一个完整的实战示例演示如何快速搭建环境将你应用中的 GenAI 调用无论是 OpenAI、Anthropic 还是本地模型转化为可观测的黄金指标。你会发现GenAI 的可观测性其实可以很简单。1. 为什么 GenAI 的可观测性如此不同且棘手在谈论 Bounded 之前我们必须先理解为什么监控一个 GenAI 应用比监控一个普通微服务要复杂得多。传统的微服务监控三件套——Metrics指标、Logs日志、Traces链路——在 GenAI 场景下遇到了新挑战。挑战一成本与效率的模糊性。对于一个普通的 REST API延迟高通常意味着代码或依赖有问题。但对于一个调用 GPT-4 的接口高延迟可能源于网络延迟。提示词Prompt过于复杂导致模型需要更长的“思考”推理时间。请求或响应的 Token 数量巨大导致序列化/反序列化时间变长。模型服务提供商自身的排队或限流。 如果没有将Prompt Token 数、Completion Token 数与延迟关联起来看你根本无法定位问题的根源。更关键的是Token 数直接关联到调用成本。一次意外的“慢”调用可能意味着一次昂贵的账单。挑战二质量难以量化。HTTP 状态码 200 代表成功但一个 AI 模型返回了 200只代表它完成了推理。这个推理结果的质量如何是否回答了问题是否产生了幻觉Hallucination是否遵循了指令传统的成功/失败二元指标在此完全失效。我们需要能够评估输出相关性、准确性、有害性等维度的质量指标。挑战三数据的高度动态性。GenAI 应用的输入Prompt和输出Completion每次都可能不同且包含大量非结构化的文本。直接将它们全量打印到日志中不仅数据量大、难以分析还可能涉及隐私和安全问题。我们需要一种既能提取关键特征如长度、主题、包含的关键词又能保护原始数据隐私的监控方式。Bounded 的聪明之处在于它没有另起炉灶去打造一套全新的数据收集体系而是选择站在巨人的肩膀上——OpenTelemetry。Otel 已经成为云原生可观测性的事实标准它定义了如何收集和导出 Traces、Metrics、Logs。Bounded 则扮演了一个“后处理器”或“分析器”的角色专门处理 Otel Traces 中与 GenAI 相关的部分。2. Bounded 核心概念从 Traces 到 GenAI Metrics 的桥梁要理解 Bounded你需要掌握三个核心概念OpenTelemetry Traces、GenAI Span和Bounded Processor。2.1 OpenTelemetry Traces 基础在一个分布式系统中一次用户请求可能会经过多个服务。OpenTelemetry 通过Trace追踪来记录整条请求链路。一个 Trace 由多个Span跨度组成每个 Span 代表一个服务内部或一次外部调用如数据库查询、HTTP 请求的工作单元。Span 包含了开始时间、结束时间、状态码、属性Attributes等丰富信息。例如一个“问答机器人”服务在处理用户提问时可能会创建以下 SpanSpan A: 接收 HTTP 请求。Span B: 查询知识库。Span C: 构造 LLM 提示词。Span D: 调用 OpenAI API。Span E: 解析并返回响应。其中Span D就是一次关键的GenAI 调用 Span。2.2 GenAI Span 的语义约定为了让监控工具能识别和理解 GenAI 调用社区需要一套标准的 Span 属性命名约定。OpenTelemetry 社区正在推动GenAI Semantic Conventions。一个标准的 GenAI Span例如调用 OpenAI ChatCompletion可能会包含以下属性gen_ai.system:openaigen_ai.request.model:gpt-4gen_ai.request.max_tokens:500gen_ai.response.finish_reasons:[stop]gen_ai.usage.prompt_tokens:150gen_ai.usage.completion_tokens:320gen_ai.usage.total_tokens:470gen_ai.response.id:chatcmpl-xxx这些属性被记录在 Span 的attributes字段中。Bounded 的工作就是扫描这些 Traces找出带有gen_ai.system等特征的 Span然后进行计算。2.3 Bounded 的处理流程Bounded 作为一个独立的处理器或 Otel Collector 的一个组件其工作流程可以简化为四步接收从你的应用通过 Otel SDK接收 Traces 数据。识别在 Trace 中识别出所有符合 GenAI 语义约定的 Span。计算基于这些 Span 的属性计算出一系列指标。例如genai_operation_duration_seconds(直方图): GenAI 调用的耗时分布。genai_operation_tokens_total(计数器): 累计消耗的 Token 总数按模型、类型拆分。genai_operation_cost_usd(计数器): 累计调用成本根据 Token 数和模型单价计算。genai_operation_quality_score(量表): 输出质量评分需要配置评估逻辑。导出将计算好的 Metrics 导出到你的监控后端如 Prometheus、Datadog 或 OpenTelemetry Protocol (OTLP) 接收器。这个过程对应用代码几乎是透明的。你只需要确保你的 Otel SDK 正确记录了 GenAI 调用的 Span 和属性。3. 环境准备搭建可观测性基础架构在开始使用 Bounded 之前你需要一个基础的 OpenTelemetry 环境。我们将以一个简单的 Python Flask 应用调用 OpenAI API 为例搭建一个从应用到可视化的完整监控链路。架构概览[Python Flask App] --(OTLP Trace)-- [OpenTelemetry Collector] --(Trace)-- [Jaeger/Tempo] --(Metrics)-- [Prometheus] -- [Grafana] ^ | [Bounded Processor]前置条件Docker 和 Docker Compose用于快速搭建后端组件。Python 3.8 环境。一个有效的 OpenAI API Key。3.1 启动后端可观测性服务我们使用 Docker Compose 来一键启动 OpenTelemetry Collector、Jaeger用于查看 Traces、Prometheus 和 Grafana。创建一个docker-compose.yml文件version: 3.8 services: # OpenTelemetry Collector - 接收、处理、转发遥测数据 otel-collector: image: otel/opentelemetry-collector-contrib:latest command: [--config/etc/otel-collector-config.yaml] volumes: - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml ports: - 4317:4317 # OTLP gRPC 接收端口 - 4318:4318 # OTLP HTTP 接收端口 - 8889:8889 # 健康检查/指标端口 depends_on: - jaeger - prometheus # Jaeger - 用于可视化追踪(Traces) jaeger: image: jaegertracing/all-in-one:latest ports: - 16686:16686 # Jaeger UI # Prometheus - 用于抓取和存储指标(Metrics) prometheus: image: prom/prometheus:latest volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml ports: - 9090:9090 # Grafana - 用于可视化指标(Metrics) grafana: image: grafana/grafana:latest environment: - GF_SECURITY_ADMIN_PASSWORDadmin volumes: - ./grafana-dashboards:/etc/grafana/provisioning/dashboards ports: - 3000:3000 depends_on: - prometheus接下来创建 OpenTelemetry Collector 的配置文件otel-collector-config.yaml。这是整个系统的核心它定义了数据如何被接收、处理和导出。请注意此时我们先配置一个基础的 Collector稍后再集成 Bounded。receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 processors: batch: # 将数据批量处理提高效率 exporters: debug: verbosity: detailed jaeger: endpoint: jaeger:14250 tls: insecure: true prometheus: endpoint: 0.0.0.0:8889 namespace: demo const_labels: service: genai-demo service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [jaeger] metrics: receivers: [otlp] processors: [batch] exporters: [prometheus]创建 Prometheus 的配置文件prometheus.ymlglobal: scrape_interval: 15s scrape_configs: - job_name: otel-collector static_configs: - targets: [otel-collector:8889]现在在终端运行docker-compose up -d启动所有服务。访问以下地址确认服务正常Jaeger UI:http://localhost:16686Prometheus:http://localhost:9090Grafana:http://localhost:3000(用户名admin, 密码admin)3.2 准备 Python 应用环境创建一个新的项目目录并设置 Python 虚拟环境。mkdir genai-observability-demo cd genai-observability-demo python -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate # Windows安装必要的 Python 包pip install flask openai opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-flask opentelemetry-instrumentation-openai opentelemetry-exporter-otlpopentelemetry-instrumentation-openai这个包至关重要它能自动为 OpenAI 的 Python 库调用创建符合语义约定的 Span这是我们能使用 Bounded 的前提。4. 编写并运行一个可观测的 GenAI 应用现在我们来编写一个简单的 Flask 应用它提供一个/ask端点接收用户问题调用 OpenAI API 获取回答并自动通过 OpenTelemetry 上报追踪数据。创建一个app.py文件# app.py import os from flask import Flask, request, jsonify import openai from opentelemetry import trace from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.instrumentation.openai import OpenAIInstrumentor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.resources import Resource # 1. 设置 OpenTelemetry resource Resource(attributes{ service.name: genai-flask-demo, service.version: 1.0.0, }) trace.set_tracer_provider(TracerProvider(resourceresource)) tracer_provider trace.get_tracer_provider() # 创建 OTLP Exporter将数据发送到本地的 Collector otlp_exporter OTLPSpanExporter(endpointhttp://localhost:4317, insecureTrue) span_processor BatchSpanProcessor(otlp_exporter) tracer_provider.add_span_processor(span_processor) # 2. 自动检测Auto-instrumentationFlask 和 OpenAI # 这会自动创建 Span 并添加属性 OpenAIInstrumentor().instrument() app Flask(__name__) FlaskInstrumentor().instrument_app(app) # 3. 设置 OpenAI API Key openai.api_key os.getenv(OPENAI_API_KEY) if not openai.api_key: raise ValueError(请设置 OPENAI_API_KEY 环境变量) app.route(/ask, methods[POST]) def ask_question(): 接收用户问题调用 OpenAI 并返回回答 data request.get_json() user_question data.get(question, ) if not user_question: return jsonify({error: 问题不能为空}), 400 # 这个调用会被 opentelemetry-instrumentation-openai 自动追踪 response openai.ChatCompletion.create( modelgpt-3.5-turbo, messages[ {role: system, content: 你是一个乐于助人的助手。}, {role: user, content: user_question} ], max_tokens500, temperature0.7 ) answer response.choices[0].message.content return jsonify({question: user_question, answer: answer}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)在运行应用前设置你的 OpenAI API Keyexport OPENAI_API_KEY你的-api-key-here # 在 Windows CMD 中set OPENAI_API_KEY你的-api-key-here # 在 Windows PowerShell 中$env:OPENAI_API_KEY你的-api-key-here然后运行应用python app.py现在你的 Flask 应用运行在http://localhost:5000并且已经配置为将追踪数据发送到本地的 OpenTelemetry Collector。5. 生成数据并验证基础追踪让我们发送一个请求来生成一些追踪数据。打开另一个终端使用curl命令curl -X POST http://localhost:5000/ask \ -H Content-Type: application/json \ -d {question: 请用简单的话解释什么是机器学习}你应该会收到一个 JSON 格式的 AI 回答。此时追踪数据已经通过 OTLP 发送到了 Collector并最终存储在了 Jaeger 中。打开浏览器访问http://localhost:16686(Jaeger UI)。在 Service 下拉菜单中你应该能看到genai-flask-demo。点击Find Traces。你应该能看到一条新的 Trace。点击它查看详情。在 Trace 详情中你可以展开看到多个 Span其中应该有一个名为openai.chat的 Span这就是我们的 GenAI 调用。点击这个 Span在右侧的Tags部分你应该能看到类似以下的属性gen_ai.system:openaigen_ai.request.model:gpt-3.5-turbogen_ai.usage.prompt_tokens:xxgen_ai.usage.completion_tokens:xxgen_ai.usage.total_tokens:xxhttp.status_code:200恭喜至此你已经成功建立了一个具备基础 GenAI 追踪能力的应用。但是这些 Traces 数据是分散的、一次性的。我们无法直观地看到一段时间内的 Token 消耗趋势、平均延迟或总成本。这就需要 Bounded 出场了。6. 集成 Bounded从 Traces 到 Metrics 的魔法Bounded 的核心是一个 OpenTelemetry Collector 的Processor。我们需要修改 Collector 的配置在 Traces 的处理流水线中插入 Bounded让它来分析 Traces 并生成 Metrics。首先你需要获取 Bounded 处理器。根据其官方文档它通常以一个 Go 二进制文件或 Docker 镜像的形式提供。为了简化我们假设将其作为 Collector 的一个外部处理器来配置。我们需要更新otel-collector-config.yaml。更新后的otel-collector-config.yamlreceivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 processors: batch: # 定义 Bounded 处理器 bounded/genai: # 这里假设 bounded 处理器以 sidecar 模式运行通过 gRPC 通信 # endpoint: bounded:4317 # 如果 bounded 是独立服务 # 另一种常见方式是将 bounded 作为 collector 的 internal component # 以下配置是示意性的具体参数需参考 bounded 官方文档 metrics: enabled: true cost: enabled: true # 配置模型单价示例需根据实际情况更新 pricing: - model: gpt-3.5-turbo input_cost_per_token: 0.0000015 # $0.0015 / 1K tokens output_cost_per_token: 0.000002 # $0.002 / 1K tokens - model: gpt-4 input_cost_per_token: 0.00003 output_cost_per_token: 0.00006 dimensions: - gen_ai.request.model - gen_ai.system - service.name - http.status_code exporters: debug: verbosity: detailed jaeger: endpoint: jaeger:14250 tls: insecure: true prometheus: endpoint: 0.0.0.0:8889 namespace: demo const_labels: service: genai-demo service: pipelines: traces: receivers: [otlp] processors: [bounded/genai, batch] # 在 batch 前加入 bounded 处理器 exporters: [jaeger] metrics: receivers: [otlp] processors: [batch] exporters: [prometheus, debug] # 注意bounded 生成的 metrics 会通过 otlp receiver 进入 metrics pipeline关键改动说明在processors部分我们添加了bounded/genai处理器并配置了它要计算的指标成本、按维度聚合。在traces流水线中我们将bounded/genai处理器添加到了batch之前。这意味着 Collector 收到 Trace 数据后会先交给 Bounded 处理Bounded 分析 Trace 并生成新的 Metrics 数据然后再将 Trace 批量导出到 Jaeger。Bounded 生成的 Metrics 会通过 OTLP 接收器receivers: [otlp]重新进入 Collector 的metrics流水线最终被导出到 Prometheus。由于 Bounded 本身可能是一个需要独立运行的组件上述配置是概念性的。在实际部署中你可能需要使用一个包含了 Bounded 处理器构建的定制化 Collector 镜像。或者将 Bounded 作为一个独立的 gRPC 服务运行并在 Collector 配置中将其指向该服务。为了演示我们假设使用了定制镜像。你需要更新docker-compose.yml中的otel-collector服务使用一个预装了 Bounded 的镜像或者通过卷挂载 Bounded 的二进制文件。简化演示我们重启 Collector 服务以加载新配置。docker-compose stop otel-collector docker-compose up -d otel-collector7. 观察 Bounded 生成的 GenAI 指标重启 Collector 并更新配置后再次通过你的 Flask 应用发送几个不同的请求例如curl -X POST http://localhost:5000/ask -H Content-Type: application/json -d {question:Python 的列表和元组有什么区别} curl -X POST http://localhost:5000/ask -H Content-Type: application/json -d {question:写一个简单的快速排序算法。}现在打开 Prometheus (http://localhost:9090) 并进入Graph页面。在查询框中尝试输入以下指标名称demo_genai_operation_duration_seconds_bucket查看 GenAI 调用耗时的直方图分布。demo_genai_operation_tokens_total查看按模型、Token 类型prompt/completion聚合的总 Token 消耗。demo_genai_operation_cost_usd_total查看累计的 GenAI 调用成本。例如查询rate(demo_genai_operation_tokens_total[5m])可以显示最近5分钟每分钟的 Token 消耗速率。在 Grafana 中可视化登录 Grafana (http://localhost:3000admin/admin)。添加数据源选择 PrometheusURL 填写http://prometheus:9090保存并测试。新建一个 Dashboard添加 Panel。在 Query 中使用 PromQL 语句例如总成本趋势sum(demo_genai_operation_cost_usd_total)各模型平均延迟rate(demo_genai_operation_duration_seconds_sum[5m]) / rate(demo_genai_operation_duration_seconds_count[5m])Token 消耗比例sum by (token_type) (rate(demo_genai_operation_tokens_total[5m]))通过这些面板你就能在一个统一的视图中监控你的 GenAI 应用的核心业务指标而不仅仅是基础设施指标。8. 高级特性基于原始提示词Raw Prompts的质量指标Bounded 项目标题中提到的 “with O raw prompts” 暗示了其另一个强大功能利用原始提示词进行计算。这不仅仅是记录 Token 数而是可以对 Prompt 和 Completion 的内容进行分析生成质量指标。例如你可以配置 Bounded 来计算提示词长度统计字符数或单词数监控是否出现异常长的提示。输出相关性评分通过一个简单的规则如检查输出是否包含特定关键词或集成一个轻量级评估模型如与问题计算余弦相似度为每次输出打分。安全检查检查输出中是否包含敏感词或有害内容。这通常需要在 Bounded 处理器中配置自定义的“分析器”或“插件”。配置可能类似于processors: bounded/genai: metrics: enabled: true analysis: prompts: - name: length_check type: length field: gen_ai.request.prompt # 假设属性中包含原始提示词 thresholds: warn: 1000 error: 5000 - name: keyword_presence type: keyword_match field: gen_ai.response.completion keywords: [错误, 无法回答, 抱歉] metric_name: output_contains_apology重要提示处理原始提示词和补全内容涉及隐私和安全。务必评估必要性是否真的需要将原始文本内容发送到监控管道考虑脱敏Bounded 或 Collector 是否支持对敏感信息如邮箱、手机号进行脱敏处理遵守合规确保数据处理符合 GDPR、HIPAA 等法规要求。使用本地处理尽量让分析逻辑在应用侧或可信的 Collector 侧完成避免明文传输到第三方服务。9. 常见问题与排查思路在集成和使用 Bounded 或类似的 GenAI 可观测性方案时你可能会遇到以下问题问题现象可能原因排查方式解决方案Jaeger 中看不到openai.chatSpanOpenAI 自动检测未生效1. 检查是否安装了opentelemetry-instrumentation-openai。2. 检查OpenAIInstrumentor().instrument()是否在创建 OpenAI 客户端之前被调用。确保 instrumentor 的初始化顺序正确并尝试重启应用。Prometheus 中查询不到demo_genai_*指标Bounded 处理器未正确生成或导出指标1. 检查 Collector 日志 (docker-compose logs otel-collector)。2. 在 Prometheus 的Targets页面检查otel-collector:8889是否健康。3. 检查 Bounded 处理器配置是否正确是否在正确的 pipeline 中。确认 Bounded 处理器配置格式查看其日志确保它成功处理了 Trace 并生成了指标。成本指标为 0 或不准模型单价配置错误或未匹配1. 检查 Bounded 配置中的pricing部分。2. 确认 Span 中的gen_ai.request.model属性值与配置中的model字段完全匹配。更新定价配置确保模型名称大小写一致。考虑使用通配符或默认定价。Trace 数据量巨大导致 Collector 负载高采样率过低所有请求都被追踪1. 检查是否配置了采样Sampling。2. 评估是否所有 GenAI 调用都需要全量追踪。在 Otel SDK 或 Collector 中配置头部采样或概率采样例如只对 10% 的请求进行全量追踪。自定义分析器如质量评分未工作分析器配置错误或依赖未满足1. 检查 Bounded 日志中关于分析器的错误信息。2. 确认分析器所需的字段如gen_ai.request.prompt是否存在于 Span 属性中。确保 Span 包含了分析器所需的原始数据属性。可能需要自定义 Otel Instrumentation 来添加这些属性。10. 生产环境最佳实践与建议将 GenAI 可观测性方案投入生产环境需要考虑更多因素采样策略Sampling全量追踪所有 GenAI 调用可能产生海量数据成本高昂。建议采用头部采样确保错误请求和慢请求被捕获同时对成功且快速的请求进行降采样。数据脱敏与隐私在 Collector 或 Bounded 处理器中配置属性过滤或脱敏规则防止敏感信息如 PII流入下游监控系统。OpenTelemetry Collector 的attributes处理器可以帮助完成这项工作。指标基数控制为指标添加维度如model,status_code时需谨慎。如果维度值过多例如将完整的用户ID作为标签会导致指标基数爆炸严重影响 Prometheus 性能。只添加有聚合分析价值的维度。多模型供应商支持除了 OpenAI确保你的 Instrumentation 也支持 Anthropic、Cohere、Azure OpenAI、本地模型如通过 vLLM等。社区 Instrumentation 可能覆盖不全需要自己封装或寻找第三方实现。将成本与业务关联仅仅知道总成本不够。通过将service.name、deployment.environment甚至自定义的business.unit属性作为维度可以将 GenAI 成本分摊到不同的业务线或团队。设置告警基于 Bounded 生成的指标设置有意义的告警。成本告警当每日成本超过预算阈值时告警。延迟告警当 P95 延迟超过 SLA 时告警。错误率告警当 GenAI 调用错误率如非 200 状态升高时告警。质量告警当输出质量平均分低于阈值时告警如果实现了质量评分。性能开销评估自动检测和 Trace 导出会带来一定的性能开销通常 5%。在流量极高的服务中需要进行压测评估开销是否可接受并优化采样率。通过 Bounded 这样的工具我们将 GenAI 应用从“黑盒”变成了“白盒”。它不仅仅是一个监控工具更是一个成本控制和质量保障的核心基础设施。开始为你的 GenAI 应用注入可观测性吧它将是你在 AI 工程化道路上最值得的早期投资之一。
