尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
OpenTelemetry GenAI语义约定实战:调用链追踪与Token成本治理
1. 从一次线上事故说起为什么GenAI应用需要专门的追踪体系去年底我接手了一个内部知识库问答系统的运维工作上线第一周就遇到了一个非常典型的问题用户反馈回答越来越慢但翻遍服务端的监控面板CPU、内存、网络IO全部正常P99延迟却从1.8秒悄悄爬到了7秒。更诡异的是账单上的模型调用费用比预估高了将近三倍却完全说不清钱花在了哪里。这个场景几乎是所有AI应用团队都会撞上的墙。传统微服务的可观测性三板斧——指标、日志、链路追踪——在GenAI场景下会集体失灵。原因很直接一次用户请求背后可能触发多轮模型调用、若干次向量检索、工具函数回调每一轮调用消耗的Token数量、模型版本、缓存命中情况都不一样而这些信息在标准的HTTP span里根本看不到。你看到的只是一个POST请求耗时5秒但不知道这5秒里模型思考占了4.2秒还是检索占了3秒。OpenTelemetry的GenAI语义约定Semantic Conventions for Generative AI就是冲着这个缺口来的。它定义了一套标准化的属性命名规范把模型名称、Token用量、请求参数、响应内容等关键信息挂到span上让调用链追踪能够真正看懂AI请求。配合Token成本治理这套体系能回答三个核心问题钱花在哪、慢在哪、错在哪。这篇文章适合正在做AI应用落地、被成本和延迟问题困扰的后端工程师、SRE和平台负责人。我会从规范本身讲起拆解调用链埋点的具体做法再落到Token成本治理的实操方案。全文基于我在实际项目中的落地经验包含大量规范文档里不会写的坑。2. GenAI语义约定到底规定了什么2.1 属性命名空间的设计逻辑OpenTelemetry的GenAI规范核心是一组以gen_ai.为前缀的属性。这个前缀不是随便定的它遵循了OpenTelemetry语义约定一贯的域隔离思路——不同领域的属性用不同前缀区分避免命名冲突。比如HTTP相关的是http.数据库是db.GenAI自然就是gen_ai.。规范里最核心的几个属性我列一下这些是埋点时必须覆盖的属性名类型含义是否必填gen_ai.systemstring模型提供方标识如openai、anthropic必填gen_ai.request.modelstring请求的模型名称必填gen_ai.response.modelstring实际响应的模型版本建议gen_ai.usage.input_tokensint输入Token数必填gen_ai.usage.output_tokensint输出Token数必填gen_ai.operation.namestring操作类型如chat、embeddings必填gen_ai.request.temperaturedouble温度参数可选gen_ai.request.max_tokensint最大输出Token限制可选这里有个容易踩的坑gen_ai.system和gen_ai.request.model是两个独立维度。很多人图省事只填model结果做成本分析时发现同一个模型名可能来自不同的提供方比如通过聚合网关调用费用单价完全不同账就算不清了。我的做法是两者都填system用提供方的稳定标识model用请求时的原始字符串。2.2 Span的层级结构该怎么设计规范本身没有强制span的嵌套方式但社区实践里形成了一套比较合理的结构。一次完整的AI请求我建议至少拆成三层span最外层是业务span代表一次用户可见的操作比如回答用户问题。这一层挂业务属性比如用户ID、会话ID、租户ID方便后续按维度聚合成本。中间层是编排span代表一次完整的Agent执行流程。如果用了LangChain、LlamaIndex这类框架这一层通常对应一次chain调用。它下面会挂多个子span包括检索、模型调用、工具调用。最内层是模型调用span这是GenAI属性真正落地的地方。每一次对模型API的调用都应该有独立的span挂上gen_ai.*属性。为什么要分这么细因为成本治理需要精确归因。如果只埋一层span你只能知道这次请求花了0.05元但不知道是检索阶段烧的钱还是模型生成烧的钱。分层之后你可以按span类型聚合一眼看出成本大头在哪。2.3 和传统追踪的关键差异传统HTTP追踪关注的是请求-响应的时序关系GenAI追踪在此基础上多了两个维度内容维度和计量维度。内容维度指的是prompt和completion。规范里定义了gen_ai.prompt和gen_ai.completion相关属性但这里有个隐私红线——生产环境绝对不能把完整的用户输入和模型输出明文写进span。我的做法是只记录Token数量、内容哈希和长度原始内容走独立的加密存储通过trace_id关联。这样既满足调试需求又不违反数据合规。计量维度就是Token用量。这是GenAI追踪区别于一切传统追踪的地方。传统追踪里一次请求是原子单位GenAI里一次请求可能消耗几千个Token成本差异巨大。所以Token属性必须精确到每一次模型调用不能只在最外层汇总。3. 埋点实操从零搭起一条可用的追踪链路3.1 环境准备与SDK选型先说技术栈选择。OpenTelemetry的SDK覆盖了主流语言Python和Java的生态最成熟。如果你的AI应用是Python写的大概率是直接用opentelemetry-sdk加上opentelemetry-instrumentation系列包就行。安装依赖这块最小集合是这几个pip install opentelemetry-api \ opentelemetry-sdk \ opentelemetry-exporter-otlp \ opentelemetry-instrumentation如果你用的是OpenAI的官方SDK社区已经有现成的instrumentation包能自动给chat.completions.create这类调用打点。但我不建议完全依赖自动埋点原因后面会讲。手动埋点虽然麻烦但可控性高得多。后端存储我推荐用支持OTLP协议的方案比如Jaeger、Tempo或者商业APM。选型时重点看两个能力能不能按属性过滤比如筛出所有gen_ai.request.modelgpt-4的span能不能做属性聚合比如按模型统计Token总量。这两点直接决定了成本治理能不能落地。3.2 手动埋点的完整代码骨架下面是我在实际项目里用的埋点骨架做了简化但保留了核心逻辑。先初始化TracerProviderfrom opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource resource Resource.create({ service.name: ai-qa-service, service.version: 1.2.0, deployment.environment: production, }) provider TracerProvider(resourceresource) exporter OTLPSpanExporter(endpointhttp://otel-collector:4317) provider.add_span_processor(BatchSpanProcessor(exporter)) trace.set_tracer_provider(provider) tracer trace.get_tracer(__name__)Resource里的service.name很关键它是后续按服务维度聚合成本的基础。deployment.environment用来区分生产和测试避免测试流量污染成本数据。然后是模型调用的埋点。这里我封装了一个装饰器把GenAI属性统一注入def trace_llm_call(system: str, model: str, operation: str chat): def decorator(func): def wrapper(*args, **kwargs): with tracer.start_as_current_span( f{operation} {model}, kindtrace.SpanKind.CLIENT, ) as span: span.set_attribute(gen_ai.system, system) span.set_attribute(gen_ai.request.model, model) span.set_attribute(gen_ai.operation.name, operation) # 记录请求参数 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]) try: response func(*args, **kwargs) # 从响应里提取Token用量 if hasattr(response, usage): 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.model, response.model) return response except Exception as e: span.set_attribute(error.type, type(e).__name__) span.record_exception(e) span.set_status(trace.Status(trace.StatusCode.ERROR)) raise return wrapper return decorator这段代码有几个设计决策值得说明。第一span的kind设为CLIENT因为模型调用本质是一次外部服务调用这样在追踪视图里能和内部函数调用区分开。第二异常处理里同时设置了error.type和record_exception前者用于聚合统计后者用于详细排查。第三Token用量从response的usage字段提取这是OpenAI兼容接口的标准字段其他提供方可能有差异需要适配。3.3 自动埋点的边界与手动补全前面提到不建议完全依赖自动埋点具体原因有三个。第一自动埋点拿不到业务上下文。它知道调用了模型但不知道这次调用属于哪个用户、哪个会话、哪个租户。而这些恰恰是成本分摊的关键维度。解决办法是在自动span的基础上通过trace.get_current_span()拿到当前span手动补业务属性。第二自动埋点对Token用量的提取不一定完整。有些提供方的响应结构不标准或者用了流式返回usage信息在最后一个chunk里自动埋点可能漏掉。流式场景我建议手动在流结束时补一次Token属性。第三自动埋点无法覆盖自定义的编排逻辑。比如你自己实现的RAG流程检索、重排、生成三个阶段自动埋点只能看到生成那一步前两步是黑盒。这时候必须手动埋点把整个链路串起来。我的实践是自动埋点作为兜底保证不漏手动埋点作为主力保证够细。两者结合用trace_id关联。3.4 流式响应的追踪处理流式响应是埋点里最麻烦的场景。因为Token用量通常在流的最后一个chunk才返回而span需要在流结束时才能关闭。处理思路是在开始流式调用时创建span但不立即结束在流迭代过程中累积内容在流结束时提取usage设置属性然后结束span。用Python的生成器包装大概是这个结构def traced_stream(system, model, stream_func, **kwargs): span tracer.start_span(fchat {model}, kindtrace.SpanKind.CLIENT) span.set_attribute(gen_ai.system, system) span.set_attribute(gen_ai.request.model, model) span.set_attribute(gen_ai.operation.name, chat) span.set_attribute(gen_ai.request.stream, True) input_tokens 0 output_tokens 0 try: for chunk in stream_func(**kwargs): if hasattr(chunk, usage) and chunk.usage: input_tokens chunk.usage.prompt_tokens output_tokens chunk.usage.completion_tokens yield chunk finally: span.set_attribute(gen_ai.usage.input_tokens, input_tokens) span.set_attribute(gen_ai.usage.output_tokens, output_tokens) span.end()这里用finally保证即使流中途异常span也能正常关闭并带上已统计的Token。注意gen_ai.request.stream这个属性规范里没有强制但加上之后在追踪视图里能快速区分流式和非流式调用排查延迟问题时很有用。4. Token成本治理把追踪数据变成省钱依据4.1 成本归因的三个维度有了追踪数据成本治理的第一步是建立归因模型。我通常从三个维度切按模型维度不同模型的单价差异巨大。同样是1000个输出Token旗舰模型可能是轻量模型的几十倍。按gen_ai.request.model聚合Token总量乘以对应单价就能看出哪个模型最烧钱。很多时候你会发现一些本该用轻量模型的简单任务被错误路由到了旗舰模型。按业务维度通过业务span上的租户ID、功能模块等属性聚合能看出哪个客户、哪个功能最耗成本。这对SaaS产品尤其重要因为成本直接关系到定价策略。按调用类型维度区分检索、生成、重排等不同阶段的Token消耗。我遇到过检索阶段因为chunk切分不合理导致每次检索塞进去上万个Token的情况生成阶段反而很省。不拆开看根本发现不了。4.2 单价配置与成本计算成本计算需要维护一张单价表。这张表要按gen_ai.system和gen_ai.request.model组合来配置因为同一个模型名在不同提供方那里价格可能不同。systemmodel输入单价(元/千Token)输出单价(元/千Token)openaigpt-4o0.0180.072openaigpt-4o-mini0.0010.004anthropicclaude-3-5-sonnet0.0210.105计算逻辑很简单成本 输入Token/1000 * 输入单价 输出Token/1000 * 输出单价。但要注意几个细节。第一输入和输出单价通常不同输出往往贵好几倍所以优化输出长度比优化输入更划算。第二有些提供方有缓存机制命中缓存的输入Token单价更低追踪数据里如果有缓存命中标识要单独计算。第三单价会变配置表要能热更新不能硬编码在代码里。我一般会在OTel Collector里加一个processor把Token属性转换成成本属性这样在追踪后端直接就能按成本聚合不用每次查询都算。4.3 用追踪数据定位成本异常成本异常排查是追踪数据最有价值的应用场景。分享一个我实际处理过的案例。某天账单突然涨了40%但请求量没变。我按gen_ai.request.model聚合Token总量发现gpt-4o的输出Token总量翻了一倍。进一步按业务span的会话ID聚合定位到某个租户的请求输出特别长。再往下钻发现这个租户的问题触发了模型的过度解释——因为prompt里没有明确限制回答长度模型对简单问题也长篇大论。修复方案是在system prompt里加了输出长度约束同时给这个租户单独设置了max_tokens上限。改完之后这个租户的成本降了60%。这个排查链路能走通靠的就是追踪数据里同时有模型维度、业务维度和Token维度的信息。如果只有传统监控你只能看到请求量没变但账单涨了完全无从下手。4.4 成本优化的几个实操手段基于追踪数据我总结了几个见效最快的优化手段。模型降级路由根据任务复杂度动态选择模型。简单分类、抽取任务用轻量模型复杂推理才用旗舰模型。追踪数据能帮你识别哪些任务其实不需要旗舰模型——如果某个任务的输出Token很少且质量评分稳定就可以考虑降级。Prompt压缩追踪数据里能看到输入Token的分布。如果发现某些请求的输入Token异常大通常是检索塞了太多无关内容。优化检索的召回策略或者加一层重排过滤能显著降低输入Token。输出长度控制在prompt里明确要求简洁回答或者设置合理的max_tokens。追踪数据能告诉你当前输出长度的分布据此设定阈值。缓存复用对于重复性高的查询把结果缓存起来命中缓存就不调用模型。追踪数据能帮你识别哪些查询模式重复率高。5. 落地过程中踩过的坑与应对5.1 属性基数爆炸问题OpenTelemetry的属性默认会被索引如果属性值基数太高比如把用户ID、会话ID直接作为属性会导致存储爆炸和查询变慢。我踩过的坑是把完整的prompt文本作为属性结果一个span的属性大小到了几十KB追踪后端直接扛不住。解决办法是高基数属性用户ID、会话ID只保留在业务span上且设置采样内容类属性一律不存明文只存哈希和长度模型调用span只挂低基数的GenAI标准属性。5.2 采样策略的选择生产环境不可能100%采样但AI请求的采样不能简单随机。我的策略是分层采样错误请求100%采样高成本请求Token超过阈值100%采样正常请求按1%采样。这样既控制了存储成本又保证了异常和成本分析的数据完整性。实现上可以在OTel Collector里配置tail-based sampling根据span属性决定是否保留。关键是采样决策要基于完整的trace而不是单个span否则会丢掉链路上下文。5.3 多提供方适配的差异不同模型提供方的响应结构差异很大。OpenAI的usage字段是prompt_tokens和completion_tokensAnthropic是input_tokens和output_tokens有些自建模型服务干脆不返回usage。我的做法是写一层适配器把各家响应统一转换成标准结构再提取Token属性。对于不返回usage的服务用tokenizer本地估算虽然不精确但聊胜于无。适配器要能配置新增提供方时不用改核心埋点代码。5.4 追踪数据与账单的对账追踪数据统计的Token量和实际账单经常对不上差异来源有几个追踪采样导致漏统计、流式响应中途断开导致Token没记录、提供方的计费口径和API返回的usage不一致。我的做法是定期对账用追踪数据估算的成本和实际账单做比对差异超过阈值就排查。对账不是为了精确到分而是为了发现系统性问题。如果差异稳定在5%以内说明追踪数据可信如果突然跳到20%说明埋点出了问题。6. 从追踪到治理的闭环把追踪数据用起来关键是形成闭环。我的团队现在的流程是追踪数据每天聚合一次生成成本报表报表按模型、业务、调用类型三个维度展示超过预算阈值的维度自动告警告警触发后用追踪链路定位具体请求分析原因制定优化方案优化上线后继续用追踪数据验证效果。这个闭环里追踪数据是唯一的事实来源。没有它成本治理就是拍脑袋有了它每一分钱花在哪里都清清楚楚。最后分享一个小心得GenAI的可观测性不要追求一步到位。先把最核心的模型调用span埋起来把Token用量统计准这一步就能解决80%的成本可见性问题。然后再逐步补业务属性、加采样策略、做成本归因。我见过太多团队一上来就想搭全套结果埋点复杂度太高最后不了了之。小步快跑先让数据流动起来比什么都重要。
RELATED

相关推荐

开源项目Obsidian-Skills:CEO亲授的Obsidian进阶路径

开源项目Obsidian-Skills:CEO亲授的Obsidian进阶路径

说实话,Obsidian 的教程我在网上看过不少,从“五分钟上手”到“打造第二大脑”的都有,但大多数内容都停留在功能介绍层面:这个按钮是干嘛的、那个插件怎么装。真正让我觉得“讲到根子上了”的,反而是最近在 GitHub 上看…

📅 2026/10/8 17:34:09
DeepSeek Harness (dsh) 插件开发实战:从零写一个自动调用 Codex 的插件

DeepSeek Harness (dsh) 插件开发实战:从零写一个自动调用 Codex 的插件

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/8 17:34:09
claude-mem 实战:为对话式 AI 构建持久化记忆系统

claude-mem 实战:为对话式 AI 构建持久化记忆系统

1. 从“聊完就忘”说起:claude-mem 到底想解决什么如果你用 Claude 这类对话式 AI 做过稍微长期一点的事情,大概率遇到过这种尴尬:昨天花了半小时跟它对齐的项目背景、代码规范、命名习惯,今天开个新会话,它全忘了&…

📅 2026/10/8 17:29:06
MORE NEWS

更多资讯

📰

电子保险丝与单片机协同实现工业电源路径保护

做嵌入式和工业控制器,电源路径保护这四个字,很多人觉得是保险丝该干的事。但我自己在电源入口吃过亏:保险管没跳变,后级DC-DC已经热击穿;换过自恢复保险丝,结果动作时间太慢,板子还是挂了。后来…

📰

运动耳机哪个牌子好?2026年运动耳机品牌排行榜前十名实测对比

不少运动爱好者挑选耳机时都很纠结,市面上运动耳机品类繁多,骨传导、开放式挂耳等款式让人眼花缭乱,各类宣传卖点也真假难辨。很多耳机看着参数好看,实际跑步容易滑落、出汗容易故障,或是风噪、漏音问题突出&#xff0…

📰

智能体工程化转型:行为审计、评测基准与成本优化实战

1. 从本周趋势榜看智能体赛道的真实转向过去大半年,只要聊到智能体,圈子里最常见的讨论还是"哪个框架更顺手""提示词怎么写更稳""怎么让模型别乱调工具"。但这一周的 GitHub Trending 中文区给了我一个很明确的信号&#…

📰

FDE实战:用Agent Harness、RAG与MCP串起AI应用全流程

从去年开始,我身边越来越多同事的title里出现了FDE三个字母。有人以为这是前端工程师(Front-End Developer)的缩写,也有人觉得是某个新职级。其实在我做的这条业务线里,FDE指的是Feature/Full-cycle Development Engin…

📰

DeepSeek Harness桌面端详解:内网部署、插件配置与避坑指南

前段时间 DeepSeek Harness 悄悄更新了桌面端 Release,我看到消息的时候先愣了一下——这项目不是一直在终端里跑的吗?等到自己下载下来用了两天,又把安装目录、配置文件、插件机制从头到尾翻了一遍,才意识到这个"桌面端&quo…

📰

命令行参数:main函数的参数

345 命令行参数:main函数的参数 你有没有注意过,Linux命令后面总能跟各种参数?ls -la、gcc -o main main.c、python script.py arg1 arg2。这些参数是怎么传递给程序的呢?答案就藏在main函数的参数里。 一、main函数的完整形式 int main(int argc, char *argv[])参数 类…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬