ARTICLE DETAIL

资讯详情

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

OpenTelemetry GenAI 规范实战:LLM 可观测性与成本治理

OpenTelemetry GenAI 规范实战:LLM 可观测性与成本治理 AI 应用跑起来之后很多团队会发现自己突然不会做可观测性了。普通 Web 服务用 OpenTelemetry 能把链路追踪做得漂漂亮亮可一到 LLM 应用面前以前那套玩法明显不够用Prompt 发出去了模型内部到底发生了什么为什么同一条业务链路有的 Token 消耗是几万有的只有几千一个 Agent 任务跑下来调用了多少次模型、每次用的什么参数、哪一次偷偷把上下文撑爆了这些如果都靠猜那线上的问题排查基本就是玄学成本核查也只能事后翻账单。OpenTelemetry GenAI 规范就是冲着这个空档来的。它在传统 trace、metrics、logs 的基础上专门定义了一套描述 LLM 调用链路的语义约定把模型调用、参数配置、Token 用量、耗时这类关键数据标准化地送进可观测性平台让调用链追踪从“能看到请求经过了哪些服务”升级成“能看到一次模型请求的全部关键细节和 Token 成本”。这篇文章我会从实战角度拆解这套规范讲清楚它到底定义了什么、怎么接入、怎么在已有的 tracing 基座上做 Token 成本治理。如果你是做 AI 应用开发、SRE 或者负责平台工程这篇文章能省掉不少你自己去翻 spec 和试错的功夫。1. 为什么传统可观测性在 AI 应用面前“失灵”了1.1 传统 APM 能看到什么看不见什么传统 APM 的核心思路是盯着“服务、接口、数据库、消息队列”这些基础设施。一条链路的 span 记录的是HTTP 方法、URL、状态码、下游调用耗时、错误堆栈。这套模型对微服务和 Web 后端来说非常成熟但对 LLM 应用来说关键信息全在传统语义约定的盲区里。举个例子。一次普通接口调用的流程可能是“网关 - 业务服务 - 缓存 - 数据库”最后落到响应体里的是一个业务 JSON。但一次 AI 应用的流程是“业务服务 - Agent 编排 - 多轮 LLM 调用 - 工具调用 - 再回到 LLM”。这里面真正值得关注的不是那个 HTTP 状态码而是这几个问题模型用的是哪个版本temperature 设了多少上下文塞了多少 TokenPrompt 里实际写了什么模型返回的内容是什么这一次调用烧了多少钱。这些语义传统 span 结构里根本没有合适的位置去放。更麻烦的是传统 APM 对“错误”的判断在 AI 场景里经常失真。HTTP 200 不代表 LLM 任务成功模型可能吞吞吐吐生成了垃圾内容、返回了空结果、或者在一个 Agent 循环里反复绕圈出不来。这些业务层面的事故在 APM 里全是一副岁月静好的样子。你要抓的是“答案质量、任务完成率、Token 浪费”不是 5xx 错误率。1.2 GenAI 应用真正需要观测哪些数据那到底什么数据才是有价值的我理解下来大致可以分成四层。第一层是模型调用元数据包括模型名称、部署环境、temperature、top_p、max_tokens、stop 序列这些请求参数。别小看这些东西很多线上问题其实都是参数引起的同一个模型 temperature 从 0 调到 1行为能差出十万八千里没有记录根本没法复盘。第二层是 Token 用量也就是 prompt_tokens、completion_tokens、total_tokens 这一组数字。这组数据既是性能指标也是成本指标更是排查上下文溢出问题的重要线索。每次调用花了多少输入 Token、多少输出 Token必须和具体的业务请求、用户、会话关联上。第三层是模型响应信息比如返回的 response_id、finish_reason、模型实际使用的版本。finish_reason 尤其关键——它是“length”还是“stop”直接决定了这次调用是正常结束还是因为 max_tokens 截断。第四层是 Prompt 和 Completion 的内容本身。这部分最敏感但也最有价值。出了问题要复盘时有原始 Prompt 和原始输出和没有这些排查效率完全两个档次。当然这里要处理好安全和隐私问题这一点后面细说。1.3 从“服务健康”到“模型行为”的范式转变当我真正把一套 GenAI 可观测性接进系统之后最明显的感觉是观测的对象变了。过去看一个服务关心的是 QPS、P99、错误率、CPU现在看一个 AI 应用关心的是模型调用次数、Token 吞吐、上下文窗口利用率、单次任务成本、工具调用成功率。这不是说基础设施指标不重要了而是要多一层“模型行为”视角。你要能把一次用户请求映射到一串模型调用链再映射到一组 Token 消耗和成本数字。这条链路打通之后很多以前没法回答的问题就都能定量回答了某个新 Prompt 模板上线后成本涨了多少、某个 Agent 是不是陷入了无意义循环、某个渠道来的用户是不是在频繁触发超长上下文。OpenTelemetry GenAI 规范做的最核心的一件事就是把上面这四层数据用统一的语义约定定义出来。这样不管底层接的是哪家模型服务你的 trace、metric、log 结构是一致的可观测性平台也能用同一套图谱去展示和聚合。2. OpenTelemetry GenAI 规范到底定义了什么2.1 核心语义约定一次模型调用如何用 span 描述先直接说结论OpenTelemetry GenAI 语义约定定义了一套以 gen_ai 为前缀的 attribute 集合用来描述一次模型交互的输入、输出、配置和用量。所谓“规范”本质上就是大家约定好这些字段叫什么、挂在哪个 span 上、值是什么类型。这样不同的服务、不同的 SDK 上报的数据才能长成一个模样。一次典型的模型调用在 trace 里会体现为一个独立 spanspan 名字通常是操作类型本身规范里给出的示例是 chat、complete、embed 这类。然后在这个 span 上会挂上几组 attribute。请求侧有 gen_ai.operation.name、gen_ai.request.model、gen_ai.request.temperature、gen_ai.request.top_p、gen_ai.request.max_tokens、gen_ai.request.frequency_penalty、gen_ai.request.presence_penalty 这些。响应侧有 gen_ai.response.id、gen_ai.response.model用来记录模型实际返回的标识和可能和请求不一致的模型版本。最关键的一组是 Token 用量也就是 gen_ai.usage.input_tokens 和 gen_ai.usage.output_tokens这两个字段是做成本治理的数据基础。我自己的建议是不要一上来就想把所有字段都配齐优先把这几项落地operation.name、request.model、request.max_tokens、usage.input_tokens、usage.output_tokens、response.finish_reason。这几项覆盖了 80% 的排查场景剩下的字段都是锦上添花。2.2 调用链怎么把“业务请求”和“模型调用”串起来理论上最理想的做法是一次用户请求对应一条业务 trace业务服务里通过 TracerProvider 创建出 root span然后在这个 root span 下每次调用模型都挂一个 GenAI span 作为 child。这样就能沿着 trace_id 把业务上下文、用户身份、模型调用、成本数据一圈一圈串起来。不过实际落地时很多团队不会把 GenAI span 做成 root而是做成现有服务调用链当中的一个环节。比如你的服务先查了数据库再调 LLM再调工具那么模型调用 span 就是你业务流程里的一个片段。这种时候依赖 OpenTelemetry Context 自动传播就行代码里只需要保证在发起模型调用前从当前 context 里拿 tracer创建出来的 span 自然就是当前 trace 的 child。关键点是GenAI span 本身是 internal 还是 client取决于你的接入形式。如果模型调用发生在你的服务进程内通常标 internal如果是网关形式下游有独立的模型网关服务那就可能是 client。规范对这个没有苛刻强约束能表达清楚就行。重要的是 status 的语义只要 HTTP 请求失败了span 应该标记 error如果请求成功但业务层面不可用比如触发了内容安全策略、上下文长度超限也应该把对应的错误类型记到 attribute 里。2.3 除了 traceGenAI 规范还覆盖了指标与日志很多人以为 GenAI 规范只定义了 tracing其实它还顺带定义了指标只是目前成熟度不如 tracing。涉及的指标主要是 Token 相关比如输入 Token 总数、输出 Token 总数、每 Token 延迟这类配合 Metrics API 可以做到以分钟级去统计整个集群的 Token 消耗曲线。日志层面的变化更有意思。以前我们打日志是“这个服务返回了 200”现在规范倾向于用事件的方式来记录 Prompt 和 Completion。OpenTelemetry 的 span 支持 add_eventGenAI 规范里预定义了两个事件名gen_ai.prompt 和 gen_ai.completion事件内容可以是完整文本或者按消息角色拆分的列表。这样做有一个好处既能把内容保存在 trace 细节里又不会污染结构化日志的主流程查询时可以按 span 展开隐私和安全也好做控制。我自己在项目里的做法是小流量环境把 Prompt 全文放进 gen_ai.prompt 事件生产环境只放长度和摘要内容原始数据落到旁路存储。等到真出问题需要重建现场时再从旁路存储捞明细。这是成本和安全之间的务实权衡。3. 实操把 GenAI 观测接入现有应用3.1 环境准备与采集器选择接入之前先把基础设施备齐。最省事的方式是用 OpenTelemetry Collector因为后续不管是自建后端还是用云厂商托管的 APMCollector 都是标准入口。你可以把 trace 先导到 Collector再转发给后端这样即使后端切换业务侧代码不用动。SDK 层面各语言都能从官方拿到 OpenTelemetry 的基础库比如 Python 的 opentelemetry-api、opentelemetry-sdk、opentelemetry-exporter-otlp。GenAI 语义约定目前还在持续演进所以别指望所有语言的 instrumentation 都成熟很多还是自己手动埋点或者靠社区 SDK 半自动支持。我的建议是暂时别追最新版 spec选一个稳定版本把字段定义固定下来做成公司内部规范文档比跟着 upstream 天天改代码强得多。如果你用的是 OpenAI SDK、Anthropic SDK 这类社区里已经有部分自动埋点的方案但成熟度参差不齐而且对自定义 host 和代理场景支持不一定好。我更推荐先手动埋点把规范吃透再考虑自动化。3.2 手动埋点一个极简 LLM 调用链示例用一个最常见的例子演示一下底层是 OpenAI 兼容接口这里把调用封装在一个函数里。import openai from opentelemetry import trace from opentelemetry.semconv.genai import GenAIAttributes # 键名以实际语义约定的版本为准 tracer trace.get_tracer(app.llm) client openai.OpenAI() def chat_with_trace(system_prompt, user_prompt): with tracer.start_as_current_span(chat) as span: span.set_attribute(GenAIAttributes.OPERATION_NAME, chat) span.set_attribute(GenAIAttributes.REQUEST_MODEL, gpt-4o) span.set_attribute(GenAIAttributes.REQUEST_TEMPERATURE, 0.3) span.set_attribute(GenAIAttributes.REQUEST_MAX_TOKENS, 1024) # prompt 内容建议用事件记录避免把大文本塞进 attribute span.add_event(gen_ai.prompt, { role: system, content: system_prompt, }) span.add_event(gen_ai.prompt, { role: user, content: user_prompt, }) resp client.chat.completions.create( modelgpt-4o, messages[ {role: system, content: system_prompt}, {role: user, content: user_prompt}, ], temperature0.3, max_tokens1024, ) choice resp.choices[0] span.set_attribute(GenAIAttributes.RESPONSE_MODEL, resp.model) span.set_attribute(GenAIAttributes.RESPONSE_ID, resp.id) span.set_attribute(GenAIAttributes.USAGE_INPUT_TOKENS, resp.usage.prompt_tokens) span.set_attribute(GenAIAttributes.USAGE_OUTPUT_TOKENS, resp.usage.completion_tokens) # finish_reason 是排查“回答被截断”的重要依据 span.set_attribute(gen_ai.completion.finish_reason, choice.finish_reason) span.add_event(gen_ai.completion, {content: choice.message.content}) return resp这段代码的重点就两个请求参数和响应用量都往 span 上挂。别小看这几行等你在 trace 视图里看到每个模型请求的输入输出 Token再对比一下成本你会知道什么叫“终于睁眼干活了”。有个细节必须提醒add_event 里塞大文本没问题但 attribute 里塞大文本就要谨慎很多后端对 attribute 值的长度有限制超了就直接丢数据。Prompt 原文放到事件里Attribute 里只放 Token 数和关键指标这才是稳妥做法。3.3 自动埋点与框架集成手动埋点学会之后自动埋点就水到渠成了。本质就是拦截 SDK 的请求在外面包一层同样的逻辑。比如你公司里所有 LLM 调用都走一个统一的客户端 SDK那就在这个 SDK 内部实现一次埋点上层业务将不用感知。还有一种方式是用 OpenTelemetry Instrumentation 机制写一个自定义的拦截器或者中间件。以 Python 为例可以包装 OpenAI 客户端的 chat.completions.create 方法在调用前启动 span调用后收集 usage。这类封装要特别注意不要重复埋点不要和框架自带的 GenAI instrumentation 撞车。之前我见过有的团队既手动埋点又开了自动埋点结果一条链路里出现两套 gen_ai span数据从哪来的都说不清。模型网关或者代理也是接入的好位置。如果你用的是内部网关部署在专门的服务上那在这个网关卡点做埋点能达到“一处接入全员受益”的效果业务侧甚至不需要改动。缺点就是看不到应用进程内部的上下文组装过程只能看到网关视角的调用。两种位置各有取舍规模大的团队通常两个位置都埋用不同的 span 层级去区分。4. Token 成本治理从 trace 到账单的闭环4.1 Token 统计的准确口径与成本计算逻辑Token 成本治理的第一步是搞清楚“准确口径”。模型侧返回的 prompt_tokens 和 completion_tokens是服务端统计的数字相对可靠。但如果你想把成本核算到具体业务、具体用户、具体订单就必须把 trace 里的 usage 数据和业务维度的字段做关联。成本计算本身不复杂核心公式是每一类模型的 Token 数乘以单价再求和。比如某模型输入价格是每百万 Token 2.5 美元输出是每百万 Token 10 美元一次调用用了 6000 输入 Token 和 1500 输出 Token那成本就是 6000/1000000×2.5 1500/1000000×10算下来大约 0.03 美元。这个数看起来小但乘上日均百万次调用规模就出来了。所以千万不要嫌单次成本小就不去算积少成多的速度非常吓人。更实用的做法是在业务代码里写好一个 cost 计算函数注册一份模型价格表每拿到一次 usage 就顺手算出一个“预估成本”以一个 span attribute 的方式挂上去字段名可以叫 gen_ai.usage.cost_estimate。这样成本数据天然跟着 trace 走你不需要另起一套系统就能做成本归因。4.2 用 traces 做成本归因的思路成本归因其实是一个多维分析问题。我在实际项目里最常见的需求有三类按用户看成本、按功能模块看成本、按模型版本看成本。按用户看成本靠的是在 root span 上设置 user.id 之类的业务属性然后聚合算数。按功能模块看成本是在创建 GenAI span 时额外加一个 gen_ai.operation.name 或者业务 app.module 属性。按模型版本看成本就是直接按 gen_ai.response.model 分组。这个思路和普通 APM 的维度分析没有本质区别只是数据来源从“服务调用量”变成了“Token 用量和预估成本”。等你在仪表盘上把成本按天、按模型、按模块拉出来你会非常清楚地看到某些模块为什么烧钱——往往不是单次调用贵而是调用次数失控。另一个容易被忽略的维度是“重复计算”。同一份文本多次进上下文在对话里每轮都带上历史Token 消耗会指数级膨胀。这就是为什么 trace 里一定要有完整调用链因为只有纵向看多轮调用的 Token 变化才能发现这种“上下文重复计费”的问题。4.3 从观测到治理限流、降级与调优可观测性不能只停在“看得见”得能推动治理动作。Token 成本治理最有效的三个抓手是限流、降级、调优。限流通常针对用户级别。某个用户单日 Token 消耗异常比如触发循环调用应当在网关或者业务层直接掐断后续请求。这个阈值从哪里来就是从你 trace 聚合出的成本分布来定比如 P95 成本是 X那阈值可以定在 3X避免一刀切误伤正常用户。降级主要针对模型选择。观测数据会发现某些场景用大模型并没有带来质量提升那就可以在保障效果的前提下切换到成本更低的模型。这种决策如果没有 Token 成本数据支撑就只能拍脑袋。调优则是 Prompt 和参数层面的动作。比如同一个任务temperature 从 0.7 降到 0.2Token 输出往往会减少max_tokens 设置得过大模型偶尔会把答案撑长。每次调整后通过 trace 里的 Token 字段对比前后变化这就成了闭环优化。5. 常见问题与排查技巧实录5.1 高频问题速查我把这段时间实操里最容易碰到的问题整理成一张速查表可以直接照着排查。现象可能原因排查思路span 显示成功但模型返回内容为空业务层没记录 response或者是 content filter 拦截检查 finish_reason看是否是 content_filter单独记录空响应事件usage 字段缺失模型服务没返回用量或 SDK 版本太旧换用新版 SDK如果服务端完全不返回用本地 tokenizer 估算并标记为估算值自定义 attribute 丢失值超过后端限制或 key 名拼错先用 stdout exporter 本地验证再查后端字段映射表trace 里有 gen_ai span但 Token 统计对不上手动埋点和自动埋点重复或采样率不一致检查是否有两套 instrumentation 叠加并确认采样策略调用链断链业务 span 和模型 span 对不上Context 传播没生效跨线程丢失确认是否用了异步任务跨线程时手动注入 trace context成本聚合偏大预估单价配置错误或重复统计了重试请求建立价格表字典对 retry 请求做去重标记5.2 实践中踩过的坑第一个坑是数据脱敏。Prompt 里经常带着用户身份信息、业务数据和潜在敏感内容。如果你直接把原文全量塞进可观测性后端安全审计会教你做人。我目前的做法是生产环境对 Prompt 做截断和摘要只保留前 200 字符和长度特征敏感性内容走独立加密存储真正需要排查时再用 trace_id 去关联。第二个坑是采样。GenAI span 的 size 比普通 span 大得多如果照搬传统服务的采样率比如 10% 抽样可能不够看但如果 100% 采样存储成本会让你怀疑人生。我的建议是成本分析场景要保持高频采样甚至全采因为一条 trace 丢了就很难还原成本明细内容类事件可以做低概率采样省下来的空间非常可观。第三个坑是重试带来的重复计数。模型调用失败会自动重试如果你在重试时没有处理好 span 父子关系成本聚合会把同一份请求算好几遍。一定要在重试逻辑里保证 trace_id 不变、span 层级明确并且在最终成本聚合时对终止状态做一次过滤。第四个坑是框架自带的 instrumentation 版本混乱。社区包更新速度极快字段名说改就改今天叫 gen_ai.request.model明天可能就换了。团队里必须有一份锁定的版本清单和字段白名单不然升级依赖后 trace 结构突然变化仪表盘全花。最后再分享一个我自己的习惯每次发布模型相关变更都会同时跑一遍 trace 对比脚本把新旧版本的 Token 分布和成本分布拉出来并排看。这不是什么复杂工具就是把 trace 实时导到一个本地分析任务里做个简单聚合。但它能帮你避免非常多的“模型效果变好了但成本爆炸了”的尴尬事故。可观测性这东西做到最后拼的就是这些细水长流的习惯。
返回列表