ARTICLE DETAIL

资讯详情

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

OpenTelemetry GenAI 可观测性实战:Token 成本治理与调用链追踪

OpenTelemetry GenAI 可观测性实战:Token 成本治理与调用链追踪 1. 为什么 AI 应用需要一套专属的可观测性方案1.1 从传统 APM 到 GenAI 可观测性的范式转移做过微服务监控的同学对调用链追踪这套东西应该不陌生。一个 HTTP 请求从网关进来经过鉴权、业务逻辑、缓存、数据库最后返回结果每个环节的耗时、状态码、异常堆栈都能被完整记录下来。这套方法论在过去十年里已经非常成熟OpenTelemetry 也基本成了事实标准。但当你把监控对象换成一个大语言模型应用时会发现传统那套东西突然不够用了。原因很简单传统 APM 关注的是“请求有没有成功、耗时多少、有没有报错”而 AI 应用真正烧钱和出问题的地方藏在语义层面。一次对话请求可能 HTTP 200 返回得漂漂亮亮但背后消耗了 8000 个输入 token、生成了 2000 个输出 token成本是普通接口的几十倍用户可能连续追问了五轮每一轮都把历史上下文重新塞进去token 用量呈平方级增长模型可能因为提示词里某个措辞触发了异常长的推理链导致响应时间从 2 秒飙到 30 秒。这些问题的共同点是它们在传统指标里几乎不可见。你看到的是“接口正常”但账单在悄悄爆炸。这就是 GenAI 可观测性要解决的核心矛盾——把语义层的消耗和行为翻译成可度量、可追踪、可归因的工程指标。OpenTelemetry 社区在 2024 年前后开始推进 GenAI 语义约定Semantic Conventions for GenAI目的就是给这类应用定义一套统一的埋点规范。它规定了 span 该怎么命名、属性该怎么填、token 用量该放在哪个字段、模型参数该怎么记录。有了这套规范不同厂商的 SDK、不同的模型服务、不同的编排框架才能被同一套后端接住做统一分析。1.2 这套方案到底解决谁的痛点我把实际会遇到这个问题的角色分成三类你可以对号入座。第一类是应用开发者。你写了一个基于大模型的客服机器人或者代码助手上线之后老板问你“这个月 API 花了多少钱、哪个功能最费钱、有没有异常调用”你一脸茫然因为你的日志里只有请求和响应没有成本维度。你需要的是在代码里埋点把每次调用的模型名、token 数、耗时、会话 ID 都记录下来。第二类是平台/SRE 工程师。你负责维护公司内部的 AI 网关或者模型路由层多个团队共用你的服务。你需要知道哪个团队、哪个应用、哪个用户消耗了多少资源出了故障要能快速定位是模型侧的问题还是调用方的问题。你需要的是端到端的调用链把网关、编排层、模型调用串起来。第三类是成本负责人或者技术管理者。你不写代码但你要为 AI 预算负责。你需要一张能按天、按模型、按业务线拆分的成本报表还需要在用量异常时收到告警。你需要的是后端分析能力和可视化面板。这三类人的需求层层递进但底层依赖的是同一套埋点数据。所以正确的做法不是各做各的而是从一开始就按 OpenTelemetry GenAI 规范来埋点让数据一次采集、多方复用。1.3 核心概念先对齐Trace、Span 与 GenAI 属性在往下讲实操之前有必要把几个关键概念用大白话过一遍不然后面看属性名会晕。Trace追踪是一次完整请求的生命周期。比如用户问了一句“帮我写个快排”从请求进来到答案返回整个过程就是一个 trace。它有一个全局唯一的 trace_id。Span跨度是 trace 里的一个环节。一次 AI 调用通常至少有三个 span一个是应用层的入口 span一个是编排/检索 span如果你用了 RAG一个是真正的模型调用 span。span 之间有父子关系串起来就是调用链。GenAI 语义约定是 OpenTelemetry 给 AI 相关 span 定义的一套属性命名规则。比如模型调用 span 的名字建议用{operation} {model}的格式像chat gpt-4otoken 用量放在gen_ai.usage.input_tokens和gen_ai.usage.output_tokens里模型名称放在gen_ai.request.model。这些名字看着啰嗦但好处是标准化——任何支持这套规范的后端都能直接识别不用你写一堆解析规则。理解了这三个概念后面的埋点和分析就有了共同的坐标系。2. OpenTelemetry GenAI 规范的核心字段拆解2.1 模型调用 Span 的必备属性清单规范里定义的属性不少但真正在日常分析中高频用到的其实就那么十几个。我把它们按用途分成四组你埋点的时候照着这张表填基本就够了。属性名类型用途是否必填gen_ai.systemstring供应商标识如 openai、anthropic必填gen_ai.request.modelstring请求的模型名必填gen_ai.response.modelstring实际响应的模型名建议gen_ai.operation.namestring操作类型chat/text_completion/embeddings必填gen_ai.usage.input_tokensint输入 token 数必填gen_ai.usage.output_tokensint输出 token 数必填gen_ai.request.temperaturedouble温度参数建议gen_ai.request.max_tokensint最大生成 token建议gen_ai.response.finish_reasonsstring[]结束原因stop/length/content_filter建议gen_ai.conversation.idstring会话 ID用于多轮归因强烈建议这里我要特别强调gen_ai.conversation.id。规范早期版本里没有这个字段很多团队埋点时就漏了结果后面想做多轮对话的成本分析时发现数据串不起来。多轮对话是 token 消耗的大头因为每一轮都要把历史消息重新发一遍如果你不记录会话 ID就无法回答“这个用户这一整段对话总共花了多少钱”这种问题。我的建议是只要你的应用支持多轮这个字段一定要填哪怕规范里标的是可选。另一个容易踩坑的是gen_ai.usage.input_tokens和output_tokens的取值来源。有些 SDK 返回的 usage 字段里input_tokens 是包含缓存命中的部分的有些则分开算。如果你同时用了提示词缓存一定要看清楚供应商文档否则成本计算会偏差很大。我后面会专门讲这个问题。2.2 Span 命名与层级设计别让调用链变成一团乱麻Span 的命名看起来是小事但它直接决定了你在后端能不能快速筛选和聚合。规范建议模型调用的 span 名用{operation} {model}格式比如chat gpt-4o、embeddings text-embedding-3-small。这样你在追踪系统里按名字一搜就能把所有同类调用捞出来。层级设计上我推荐一个三层结构实测下来最清晰第一层应用入口 span名字用业务语义比如handle_user_query、generate_code_suggestion。这一层记录用户 ID、会话 ID、业务场景标签。第二层编排 span如果你有 RAG 检索、工具调用、多步推理每个步骤一个 span。这一层记录检索命中的文档数、工具调用次数。第三层模型调用 span按规范命名记录所有 GenAI 属性。为什么要分三层而不是把所有信息塞进一个 span因为分析需求是分层的。看整体成本时你聚合第一层看检索效率时你聚合第二层看模型选型是否合理时你聚合第三层。如果全塞一起属性会爆炸查询也会变慢。注意span 的父子关系一定要正确设置。我见过有团队因为异步调用没传 context导致模型 span 变成了根 span调用链直接断掉排查了半天才发现是 context 没透传。2.3 Token 计量的三个陷阱Token 计量是成本治理的地基但这里面的坑比想象中多。我总结了三个最常见的。陷阱一把字符数当 token 数。有些人图省事用字符串长度除以 4 来估算 token这在英文里勉强能用中文直接崩盘。中文一个汉字通常对应 1 到 2 个 token具体取决于分词器。正确做法是直接用供应商返回的 usage 字段或者用对应的 tokenizer 库精确计算。陷阱二忽略系统提示词的消耗。很多应用的系统提示词写得很长几百上千 token每次调用都要带上。如果你只统计用户输入会严重低估成本。埋点时要确保 input_tokens 包含完整的请求内容。陷阱三缓存命中与未命中混为一谈。现在主流供应商都支持提示词缓存缓存命中的 token 价格可能只有未命中的十分之一。如果你的 usage 字段里不区分这两者成本报表就会失真。建议在 span 里额外加一个自定义属性比如app.cache_hit_tokens把缓存部分单独记出来。这三个陷阱我在不同项目里都踩过尤其是第三个等到月底对账发现报表和实际账单差了一大截回头查才发现是缓存没区分。3. 从零搭建调用链追踪的实操流程3.1 环境准备与 SDK 选型动手之前先把工具链定下来。核心就三样OpenTelemetry SDK、一个追踪后端、一个模型调用库。SDK 方面Python 用opentelemetry-sdk加opentelemetry-exporter-otlpNode.js 用opentelemetry/sdk-node。版本上建议用 1.20 以上的因为 GenAI 语义约定在早期版本里支持不完整。安装命令很直接pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http pip install opentelemetry-instrumentation追踪后端的选择取决于你的部署环境。自建的话 Jaeger 或者 Grafana Tempo 都行云上的话各家都有 OTLP 兼容的服务。选型的核心标准是能不能按属性做聚合查询因为成本分析需要 group by 模型名、会话 ID 这些维度。有些轻量后端只支持按 trace_id 查做不了聚合那就不适合。模型调用库这块如果你用官方 SDK很多已经内置了 OpenTelemetry 埋点比如某些版本的 OpenAI SDK 可以通过环境变量开启。但内置埋点往往不够灵活属性可能不全我的建议是自己包一层在包装函数里手动创建 span 并填充属性这样可控性最强。3.2 初始化 TracerProvider 的关键配置初始化这块有几个参数必须配对配错了要么数据发不出去要么性能受影响。from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource resource Resource.create({ service.name: ai-assistant, service.version: 1.2.0, deployment.environment: production, }) provider TracerProvider(resourceresource) exporter OTLPSpanExporter(endpointhttp://your-collector:4318/v1/traces) processor BatchSpanProcessor( exporter, max_queue_size2048, schedule_delay_millis5000, max_export_batch_size512, ) provider.add_span_processor(processor) trace.set_tracer_provider(provider)这里service.name一定要填它是后端区分不同应用的依据。BatchSpanProcessor的几个参数值得说一下max_queue_size是队列容量AI 应用在高并发下 span 产生速度很快队列太小会丢数据schedule_delay_millis是批量发送间隔设太短会增加网络开销设太长会影响实时性5000 毫秒是个平衡点max_export_batch_size是单批发送量别超过后端接收上限。提示如果你的应用是短生命周期的比如 Serverless 函数BatchSpanProcessor 可能来不及发送就退出了这种情况要用 SimpleSpanProcessor 或者在函数结束前手动 flush。3.3 给模型调用包一层带埋点的封装这是整个方案的核心环节。我以一个通用的 chat 调用为例展示怎么把 span 和属性填进去。import time from opentelemetry import trace tracer trace.get_tracer(genai.instrumentation) def traced_chat_completion(client, model, messages, conversation_idNone, **kwargs): span_name fchat {model} with tracer.start_as_current_span(span_name) as span: span.set_attribute(gen_ai.system, openai) span.set_attribute(gen_ai.operation.name, chat) span.set_attribute(gen_ai.request.model, model) if conversation_id: span.set_attribute(gen_ai.conversation.id, conversation_id) if temperature in kwargs: span.set_attribute(gen_ai.request.temperature, kwargs[temperature]) if max_tokens in kwargs: span.set_attribute(gen_ai.request.max_tokens, kwargs[max_tokens]) start time.time() try: response client.chat.completions.create( modelmodel, messagesmessages, **kwargs ) elapsed time.time() - start span.set_attribute(gen_ai.response.model, response.model) span.set_attribute(gen_ai.usage.input_tokens, response.usage.prompt_tokens) span.set_attribute(gen_ai.usage.output_tokens, response.usage.completion_tokens) span.set_attribute(gen_ai.response.finish_reasons, [c.finish_reason for c in response.choices]) span.set_attribute(app.latency_ms, int(elapsed * 1000)) return response except Exception as e: span.record_exception(e) span.set_status(trace.Status(trace.StatusCode.ERROR, str(e))) raise这段代码有几个设计意图值得说明。第一span 名用chat {model}格式符合规范方便聚合。第二所有属性都在调用前后填充确保异常时也能记录到部分信息。第三record_exception会把异常堆栈记进 span排查时不用再去翻日志。第四我额外加了一个app.latency_ms自定义属性因为有些后端的 span duration 精度不够自己记一份更可靠。如果你用的是流式响应情况会复杂一些。流式调用下 usage 字段往往在最后一个 chunk 才返回你需要累积所有 chunk 后再填属性。而且流式的首 token 延迟和总延迟是两个不同指标建议分别记录首 token 延迟用app.ttft_ms总延迟用app.latency_ms。3.4 多轮对话的会话串联多轮对话的成本分析是重点也是难点。核心思路是给同一个会话的所有 span 打上相同的gen_ai.conversation.id。会话 ID 的生成时机很关键。我的做法是在用户第一次发起对话时生成一个 UUID存在会话上下文里后续每一轮都复用。如果你用的是无状态的 HTTP 接口可以把会话 ID 放在请求头或者请求体里由前端维护。有了会话 ID 之后你就能在后端做这样的查询按 conversation_id 聚合算出这段对话的总 token 消耗、总轮数、平均每轮消耗。这个数据非常有用因为多轮对话的 token 增长往往是非线性的——第二轮带一轮历史第三轮带两轮历史到第十轮时输入 token 可能是第一轮的十倍。如果你发现某些会话的 token 曲线异常陡峭说明用户可能在问需要长上下文的问题或者你的历史裁剪策略有问题。注意会话 ID 不要用可预测的递增数字也不要把用户隐私信息编进去。用随机 UUID 最稳妥既唯一又不泄露信息。4. Token 成本治理的分析与优化实战4.1 成本归因模型从 span 数据到账单采集到数据只是第一步真正产生价值的是把 span 数据转化成成本数字。这里需要一个归因模型核心公式很简单单次调用成本 输入 token 数 × 输入单价 输出 token 数 × 输出单价但实际落地时单价不是固定的。不同模型单价不同同一模型在不同时间段可能有不同价格缓存命中的 token 单价又不一样。所以你需要维护一张价格表并且这张表要能随供应商调价而更新。模型输入单价每百万 token输出单价每百万 token缓存命中输入单价模型 A15601.5模型 B3150.3模型 C0.51.50.05有了价格表你就可以在后端写一个聚合查询按天、按模型、按业务线算出成本。我建议至少做三个维度的报表按时间看趋势、按模型看选型是否合理、按业务场景看哪个功能最费钱。这里有个实操细节价格表不要硬编码在代码里做成配置或者数据库表方便随时更新。我见过有团队把价格写死在代码里供应商调价后忘了改导致成本报表连续几个月都是错的。4.2 用数据驱动模型选型优化成本治理最有价值的产出是帮你做出更聪明的模型选型决策。举几个我实际遇到过的场景。场景一简单任务用了贵模型。通过按业务场景聚合我发现某个“意图分类”功能一直在用最贵的模型但它的输入输出都很短任务也很简单。换成便宜模型后准确率几乎没降成本直接降了 90%。这种优化如果没有数据支撑你根本不知道从哪里下手。场景二输出 token 失控。有个功能的输出 token 数波动特别大有时候几百有时候几千。查了 span 发现是提示词里没有限制输出长度模型有时候会“话痨”。加上 max_tokens 限制后成本变得可预测了。场景三缓存没生效。系统提示词很长理论上应该能命中缓存但数据显示缓存命中率很低。排查发现是提示词里有个时间戳每次都变导致缓存永远失效。把时间戳挪到用户消息里之后缓存命中率上去了成本降了一大截。这三个场景的共同点是问题在传统监控里完全看不见只有把 token 数据按维度拆开才能发现。这也是为什么我反复强调埋点要全、维度要细。4.3 实时告警与异常检测成本治理不能只做事后分析还要有实时告警。我建议设置三类告警规则。第一类是单次调用异常。比如单次调用的 token 数超过某个阈值比如 50000或者单次成本超过某个金额。这类告警能帮你抓住提示词注入、死循环之类的极端情况。第二类是速率异常。比如某个应用在 5 分钟内的 token 消耗速率突然翻了好几倍。这可能是流量突增也可能是代码 bug 导致的重复调用。第三类是日/周预算告警。设定一个预算上限当累计消耗达到 80% 时预警达到 100% 时告警。这类告警适合给管理者看。告警规则的阈值怎么定我的经验是先用一周的数据跑出基线然后按基线的 2 到 3 倍设阈值。设太紧会天天误报设太松又起不到作用。基线本身也要定期更新因为业务在增长。提示告警要带上足够的上下文比如 trace_id、会话 ID、模型名这样收到告警的人能直接跳转到对应的调用链去排查不用大海捞针。4.4 提示词与上下文的成本优化技巧除了选型和告警日常开发中还有很多能省钱的技巧这些技巧配合可观测性数据使用效果最好。技巧一动态历史裁剪。多轮对话不要无脑带上全部历史。可以根据 token 预算动态裁剪比如保留最近 N 轮或者保留最近 M 个 token 的历史。裁剪策略的效果可以通过会话维度的 token 曲线来验证。技巧二提示词压缩。系统提示词能精简就精简。我见过一个系统提示词写了 2000 多 token里面一半是重复的说明。精简到 800 token 后每次调用省了一大半输入成本。技巧三分级模型路由。简单问题走便宜模型复杂问题走贵模型。可以用一个轻量分类器先判断问题难度再决定路由。这个分类器本身的成本很低但能省下大量模型调用费用。技巧四批量处理。如果业务允许把多个独立请求合并成一次调用能显著降低开销。比如一次要处理 10 条文本与其调 10 次不如拼成一个 batch 调一次。这些技巧单独看都不复杂但组合起来配合可观测性数据持续验证效果成本能降到一个很健康的水平。我在一个项目里把这几个技巧都用上月度成本从最初的数字降到了三分之一左右而且用户体验没有明显下降。5. 常见问题排查与避坑实录5.1 数据采集阶段的典型故障问题一span 发不出去后端看不到数据。排查顺序是这样的先确认 exporter 的 endpoint 对不对OTLP 的 HTTP 端口通常是 4318gRPC 是 4317别搞混再确认网络通不通用 curl 测一下然后看 SDK 有没有报错日志BatchSpanProcessor 发送失败时会有 warning最后检查 resource 配置有些后端要求 service.name 必填缺了会拒收。问题二调用链断裂模型 span 变成根 span。这几乎都是 context 没透传导致的。异步调用、线程池、消息队列这些场景特别容易出问题。解决办法是在跨边界的地方手动传递 context比如用trace.set_span_in_context把父 span 的 context 传下去。问题三token 数对不上。先确认你取的是哪个字段不同 SDK 的字段名可能不一样。再确认是否包含了系统提示词。如果用了缓存确认缓存部分有没有单独统计。最后拿一次调用去供应商的控制台对一下看数字是否一致。5.2 成本分析阶段的常见困惑困惑一报表数字和账单对不上。差异通常来自几个地方一是价格表没更新二是缓存 token 没区分三是有些调用没埋点比如测试环境的调用混进了生产数据四是供应商的计费粒度和你的统计粒度不一致。建议先做一次全量对账把差异来源一个个排除。困惑二某个模型成本突然飙升。先看是调用量涨了还是单价涨了。调用量涨的话按应用和会话拆开看是哪个业务线的问题。单价涨的话可能是供应商调价也可能是你从缓存命中变成了未命中。困惑三优化之后成本没降。这种情况往往是优化点选错了。比如你优化了输出 token但输入 token 才是大头。这时候要回到数据看清楚成本构成再决定优化方向。5.3 一份可直接抄的排查速查表现象可能原因排查动作后端无数据endpoint 错误/网络不通检查端口、curl 测试调用链断裂context 未透传检查异步边界token 数偏低未含系统提示词核对请求内容成本报表偏高价格表过期更新价格配置缓存命中率低提示词含变量检查提示词模板告警频繁误报阈值设置过紧重设基线流式调用无 usagechunk 未累积累积后统一填属性这张表是我从多次踩坑中总结出来的基本覆盖了 80% 的常见问题。遇到新问题时先对照这张表过一遍能省不少时间。5.4 几个容易被忽视的细节最后分享几个细节都是我在实际项目里踩过或者见别人踩过的。细节一采样策略要慎重。全量采集成本高但采样又会丢数据。我的建议是模型调用 span 全量采集因为它是成本分析的核心其他辅助 span 可以按比例采样。如果实在要采样至少保证错误请求和异常高消耗请求被完整采集。细节二属性值不要放敏感信息。提示词和响应内容可能包含用户隐私不要直接塞进 span 属性。如果确实需要记录做脱敏处理或者只记录长度和哈希值。细节三span 数量要控制。一次请求产生几十个 span 会让后端压力很大。只对关键环节埋点细枝末节的东西用日志记录就够了。细节四定期清理历史数据。追踪数据量增长很快如果不做保留策略存储成本会失控。一般保留 30 到 90 天的明细更早的数据做聚合后归档。细节五埋点代码要测试。我见过埋点代码本身有 bug导致属性填错分析出来的结论全是错的。埋点上线前一定要验证数据正确性拿几次调用去核对。这套方案从埋点到分析到优化我在几个不同规模的项目里都跑通过。核心体会是可观测性不是装个 SDK 就完事而是要围绕业务问题去设计数据维度。你埋什么点决定了你能回答什么问题。如果一开始没想清楚要分析什么埋出来的数据往往用不上。所以动手之前先想清楚你的成本归因要细到什么程度、你的告警要覆盖哪些场景再倒推需要哪些属性。这个顺序反了后面返工的成本会很高。
返回列表