构建 LLM 可观测性系统:OpenTelemetry 分布式追踪与成本归因

发布时间:2026/8/26 21:50:45

构建 LLM 可观测性系统:OpenTelemetry 分布式追踪与成本归因
你的 Agent 链跑了 47 秒花了 $0.87 —— 但钱到底花在哪一步哪个模型调用是瓶颈本文用 Go OpenTelemetry 从零搭建 LLM 调用全链路追踪系统。一、为什么 LLM 应用需要专门的观测体系传统微服务有成熟的观测三板斧Metrics / Traces / Logs但 LLM 应用有三个独特需求是 Prometheus Grafana 的通用模板覆盖不了的需求传统 APM 能解决LLM 专属痛点Token 用量归因❌一次 Agent 链可能触发 20 次 LLM 调用需要知道哪个 prompt 最烧钱延迟拆解❌网络 RTT vs 首 token 延迟 vs 生成延迟 — 需要拆到这三个维度模型对比❌A/B 两个模型跑同一批请求需要按模型维度聚合质量指标工具调用链路部分Agent → LLM → tool_call → LLM → tool_result → LLM → response的多跳追踪本节我们用 Go 实现一个可插拔的 LLM 观测层核心设计原则零侵入— 不改业务代码通过http.RoundTripper拦截所有 LLM HTTP 请求OpenTelemetry 原生— Span 可导出到 Jaeger / Grafana Tempo / Datadog成本实时归因— 每个 Span 带 Token 用量和$成本二、核心架构┌─────────────────────────────────────────────────────┐ │ Your Agent Code │ │ agent.Run(ctx, 帮我分析这个 PR) │ └────────────────────┬────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ ObservableRoundTripper │ │ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │ │ │ 创建 Span │→│ 注入 W3C │→│ http.RoundTrip() │ │ │ │ │ │ TraceCtx │ │ │ │ │ └──────────┘ └───────────┘ └────────┬─────────┘ │ │ │ │ │ ┌──────────┐ ┌───────────┐ ┌───────▼─────────┐ │ │ │ 计算成本 │←│ 解析响应 │←│ OpenAI / Claude │ │ │ │ 设置属性 │ │ Token 用量 │ │ HTTP Response │ │ │ └──────────┘ └───────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ OTLP Exporter → Jaeger / Grafana │ │ 每条 Trace: │ │ ├── agent.run (root) │ │ │ ├── llm.call (modelgpt-4o, tokens3200) │ │ │ ├── tool.search_code │ │ │ ├── llm.call (modelgpt-4o-mini, tokens800) │ │ │ └── llm.call (modelgpt-4o, tokens2100) │ └─────────────────────────────────────────────────────┘三、完整实现3.1 依赖go get go.opentelemetry.io/otel go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc go get go.opentelemetry.io/otel/sdk/trace go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp3.2 核心类型LLMSpan// llmspan/span.gopackagellmspanimport(contextencoding/jsonionet/httpstringstimego.opentelemetry.io/otelgo.opentelemetry.io/otel/attributego.opentelemetry.io/otel/codesgo.opentelemetry.io/otel/trace)// LLMCallSpan 封装一次 LLM 调用的所有观测数据typeLLMCallSpanstruct{span trace.Span providerstringmodelstringstartAt time.Time}// StartLLMCall 创建 LLM Span提取 provider model 信息funcStartLLMCall(ctx context.Context,req*http.Request)(*LLMCallSpan,context.Context){tracer:otel.Tracer(llm-client)provider,model:parseProviderModel(req)ctx,span:tracer.Start(ctx,llm.call,trace.WithAttributes(attribute.String(llm.provider,provider),attribute.String(llm.model,model),attribute.String(llm.url,req.URL.Hostreq.URL.Path),attribute.String(http.method,req.Method),),)returnLLMCallSpan{span:span,provider:provider,model:model,startAt:time.Now()},ctx}// parseProviderModel 从请求体中解析 model 名称支持 OpenAI / Anthropic / 兼容格式funcparseProviderModel(req*http.Request)(provider,modelstring){// 从 URL 推断 providerhost:req.URL.Hostswitch{casestrings.Contains(host,openai):provideropenaicasestrings.Contains(host,anthropic):provideranthropicdefault:providerhost}// 读取请求体获取 model生产环境建议用 buffer poolbodyBytes,err:io.ReadAll(req.Body)iferr!nil{modelunknownreturn}req.Bodyio.NopCloser(strings.NewReader(string(bodyBytes)))// 恢复 bodyvarbodystruct{Modelstringjson:model}ifjson.Unmarshal(bodyBytes,body)nilbody.Model!{modelbody.Model}else{modelunknown}return}// RecordResponse 从 LLM 响应中提取 Token 用量并写入 Spanfunc(l*LLMCallSpan)RecordResponse(resp*http.Response,respBody[]byte,errerror){deferl.span.End()duration:time.Since(l.startAt)l.span.SetAttributes(attribute.Float64(llm.duration_ms,float64(duration.Milliseconds())))// 记录 HTTP 状态ifresp!nil{l.span.SetAttributes(attribute.Int(http.status_code,resp.StatusCode))}// 处理错误iferr!nil{l.span.RecordError(err)l.span.SetStatus(codes.Error,err.Error())return}ifresp.StatusCode400{l.span.SetStatus(codes.Error,resp.Status)return}// 解析 Token 用量usage:parseUsage(respBody,l.provider)ifusage!nil{l.span.SetAttributes(attribute.Int(llm.usage.prompt_tokens,usage.PromptTokens),attribute.Int(llm.usage.completion_tokens,usage.CompletionTokens),attribute.Int(llm.usage.total_tokens,usage.TotalTokens),attribute.Float64(llm.cost.usd,usage.Cost),)}l.span.SetStatus(codes.Ok,)}// TokenUsage Token 用量 成本typeTokenUsagestruct{PromptTokensintCompletionTokensintTotalTokensintCostfloat64}// parseUsage 解析不同 provider 的 token usage 字段funcparseUsage(body[]byte,providerstring)*TokenUsage{varrespstruct{Usagestruct{PromptTokensintjson:prompt_tokensCompletionTokensintjson:completion_tokensTotalTokensintjson:total_tokens}json:usage}iferr:json.Unmarshal(body,resp);err!nil{returnnil}u:TokenUsage{PromptTokens:resp.Usage.PromptTokens,CompletionTokens:resp.Usage.CompletionTokens,TotalTokens:resp.Usage.TotalTokens,}// 成本计算各模型定价u.CostcalculateCost(u.PromptTokens,u.CompletionTokens,provider)returnu}// calculateCost 按模型定价计算成本USDfunccalculateCost(prompt,completionint,providerstring)float64{// 示例定价2025 Q2生产环境应从配置读取prices:map[string]struct{prompt,completionfloat64}{gpt-4o:{2.50,10.00},gpt-4o-mini:{0.15,0.60},claude-3.5-sonnet:{3.00,15.00},claude-3-haiku:{0.25,1.25},deepseek-chat:{0.14,0.28},}p,ok:prices[provider]if!ok{pprices[gpt-4o-mini]// 默认按低价算}return(float64(prompt)/1e6)*p.prompt(float64(completion)/1e6)*p.completion}3.3 HTTP Transport 拦截层// llmspan/transport.gopackagellmspanimport(bytesionet/http)// ObservableTransport 包装 http.RoundTripper自动为所有 LLM 请求创建 SpantypeObservableTransportstruct{base http.RoundTripper}// NewObservableTransport 创建可观测的 HTTP TransportfuncNewObservableTransport(base http.RoundTripper)*ObservableTransport{ifbasenil{basehttp.DefaultTransport}returnObservableTransport{base:base}}func(t*ObservableTransport)RoundTrip(req*http.Request)(*http.Response,error){lls,ctx:StartLLMCall(req.Context(),req)reqreq.WithContext(ctx)// 调用真正的 HTTP 客户端resp,err:t.base.RoundTrip(req)// 读取响应体获取 token 信息varrespBody[]byteifresp!nilresp.Body!nil{respBody,_io.ReadAll(resp.Body)resp.Body.Close()resp.Bodyio.NopCloser(bytes.NewReader(respBody))}// 记录到 Spanlls.RecordResponse(resp,respBody,err)returnresp,err}3.4 初始化 OTLP Exporter// telemetry/otel.gopackagetelemetryimport(contextfmtosgo.opentelemetry.io/otelgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpcgo.opentelemetry.io/otel/propagationgo.opentelemetry.io/otel/sdk/resourcesdktracego.opentelemetry.io/otel/sdk/tracesemconvgo.opentelemetry.io/otel/semconv/v1.21.0)// InitTracer 初始化 OTLP Tracer导出到 Jaeger / Grafana TempofuncInitTracer(ctx context.Context,serviceNamestring)(*sdktrace.TracerProvider,error){endpoint:os.Getenv(OTEL_EXPORTER_OTLP_ENDPOINT)ifendpoint{endpointlocalhost:4317// gRPC}exporter,err:otlptracegrpc.New(ctx,otlptracegrpc.WithEndpoint(endpoint),otlptracegrpc.WithInsecure(),// 开发环境生产用 TLS)iferr!nil{returnnil,fmt.Errorf(create OTLP exporter: %w,err)}tp:sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter),sdktrace.WithResource(resource.NewWithAttributes(semconv.SchemaURL,semconv.ServiceName(serviceName),)),sdktrace.WithSampler(sdktrace.AlwaysSample()),// 生产环境建议用概率采样)otel.SetTracerProvider(tp)otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{},propagation.Baggage{},))returntp,nil}3.5 使用示例// main.gopackagemainimport(contextfmtnet/httptimeyour-project/llmspanyour-project/telemetrygithub.com/sashabaranov/go-openai)funcmain(){ctx:context.Background()// 1. 初始化追踪tp,err:telemetry.InitTracer(ctx,my-agent)iferr!nil{panic(err)}defertp.Shutdown(ctx)// 2. 创建可观测的 OpenAI 客户端obsTransport:llmspan.NewObservableTransport(http.DefaultTransport)httpClient:http.Client{Transport:obsTransport}client:openai.NewClientWithConfig(openai.DefaultConfig(sk-...))// 注入自定义 HTTPClient如果 SDK 支持// 或者直接修改 DefaultConfig 的 HTTPClient// 3. 开始你的 Agent 逻辑tracer:otel.Tracer(agent)ctx,span:tracer.Start(ctx,agent.analyze_pr)deferspan.End()// span.SetAttributes(attribute.String(pr.url, prURL))// ... 调用 LLM每次调用自动生成子 Spanresp,err:client.CreateChatCompletion(ctx,openai.ChatCompletionRequest{Model:gpt-4o,Messages:[]openai.ChatCompletionMessage{{Role:user,Content:Review this code diff...},},})iferr!nil{span.RecordError(err)return}fmt.Printf(Response: %s\nCost: tracked in trace\n,resp.Choices[0].Message.Content)// 完成后在 Jaeger UI 查看完整调用链// http://localhost:16686}四、更进阶构建 Agent 专属仪表盘有了上述 Span 数据你可以在 Grafana 里构建 LLM 专属面板4.1 关键指标 SQLTimestream / ClickHouse / PostgreSQL-- 每个模型的 P50 / P95 / P99 延迟SELECTmodel,quantile(0.50)(duration_ms)asp50,quantile(0.95)(duration_ms)asp95,quantile(0.99)(duration_ms)asp99FROMllm_spansWHEREcreated_atnow()-interval24 hoursGROUPBYmodel;-- 每小时成本趋势SELECTdate_trunc(hour,created_at)ashour,model,sum(cost_usd)astotal_costFROMllm_spansWHEREcreated_atnow()-interval7 daysGROUPBY1,2ORDERBY1;-- 各 Agent 步骤的 Token 消耗分布SELECTspan_name,-- agent.analyze_pr, llm.call, tool.search, etc.sum(prompt_tokens)asprompt,sum(completion_tokens)ascompletion,count(*)ascallsFROMllm_spansWHEREtrace_idIN(SELECTtrace_idFROMllm_spansWHEREspan_nameagent.analyze_pr)GROUPBY1;4.2 自定义 Span 属性加上业务维度// 在业务代码中为 Span 添加业务属性方便按客户/项目聚合span.SetAttributes(attribute.String(business.customer_id,customerID),attribute.String(business.workflow,code_review),attribute.String(llm.quality_score,qualityScore),// 后续评估的结果)这样你就能回答这些问题“昨天 code_review 流程花了多少钱”“哪个客户触发了最多的 LLM 调用”“gpt-4o 在 review 场景的延迟是否比 sonnet 更稳定”五、生产环境 Checklist项目建议采样策略使用TraceIDRatioBased(0.1)— 生产环境 10% 采样可覆盖绝大部分排查场景Span 属性大小不要把完整 prompt / response 写入 Span 属性超过 8KB 会截断。存引用 ID 即可OTLP 端点用 gRPC 而非 HTTP减少连接开销加 retry backoff计费精度按实际 API 返回的 usage 计费不要自己数 tokenTokenizer 差异可达 20%多 Provider抽象parseUsage支持所有 provider 的 response schema写单元测试覆盖六、总结本文实现了一个零侵入的 LLM 调用观测层核心只有三个文件llmspan/span.go— Span 创建 Token 解析 成本计算llmspan/transport.go— HTTP 拦截器自动创建 Spantelemetry/otel.go— OTLP 导出初始化投入约 200 行代码你就拥有了每次 LLM 调用的全链路追踪含 Token 用量和美元成本与 Jaeger / Grafana Tempo 的原生集成按模型、工作流、客户等维度的成本归因这套方案已经在我们的生产环境跑了 3 个月帮助发现了几个隐藏成本泄漏点——比如某个 prompt 模板因为历史遗留的 system message 每次都多烧 3000 tokens。下一步你可以把 Logs 和 Metrics 也接到 OpenTelemetry用otel.SetLogger()otel.SetMeter()实现真正的三合一观测。代码完整可运行依赖见 3.1 节。

相关新闻

Unity Pico MR开发核心注意事项与实战技巧

Unity Pico MR开发核心注意事项与实战技巧

2026/8/26 17:26:11

1. Unity Pico MR开发核心注意事项解析作为一名长期从事MR开发的工程师,我在使用Unity对接Pico设备进行混合现实开发时积累了不少实战经验。Pico作为国内领先的VR/MR硬件厂商,其SDK迭代速度较快,不同版本间的兼容性和功能差异往往成为开发者最…

【PyTorch】TensorBoard实战:从训练曲线、参数分布到模型架构的可视化全解析

【PyTorch】TensorBoard实战:从训练曲线、参数分布到模型架构的可视化全解析

2026/8/26 22:59:30

1. TensorBoard与PyTorch的完美结合如果你正在用PyTorch训练神经网络,却对黑箱般的训练过程感到不安,TensorBoard就是你的最佳拍档。这个最初为TensorFlow设计的可视化工具,如今已经成为PyTorch生态中不可或缺的调试利器。想象一下&#xff0…

Unity渲染性能优化:Draw Call与SetPass Call实战解析

Unity渲染性能优化:Draw Call与SetPass Call实战解析

2026/8/26 7:54:03

1. 性能优化核心指标解析在Unity游戏开发中,Draw Call和SetPass Call是衡量渲染性能的两个关键指标。Draw Call指CPU向GPU发送的绘制指令次数,而SetPass Call则表示着色器状态切换的次数。这两个指标直接影响着游戏的帧率和运行效率。我经历过一个移动端…

Fiddler弱网测试实战:模拟真实网络环境,提升应用健壮性

Fiddler弱网测试实战:模拟真实网络环境,提升应用健壮性

2026/8/26 22:56:54

1. 项目概述:为什么我们需要模拟弱网环境? 在移动应用和Web服务开发测试的日常工作中,我们常常会陷入一个“温室”误区:所有测试都在公司高速、稳定的Wi-Fi或千兆有线网络下进行。测试工程师点击按钮,页面瞬间加载&…

Mac软件“已损坏”提示的根源与解决方案:从Gatekeeper到终端命令

Mac软件“已损坏”提示的根源与解决方案:从Gatekeeper到终端命令

2026/8/26 22:56:54

1. 问题根源:为什么Mac总说我的软件“已损坏”?如果你是从Windows或Linux转投Mac阵营的用户,第一次遇到“无法打开‘XXX’,因为它来自身份不明的开发者”或者“XXX已损坏,无法打开。您应该将它移到废纸篓”这类弹窗时&…

腾讯云Token Plan个人版套餐选择指南:从AI编程到API调用的成本优化策略

腾讯云Token Plan个人版套餐选择指南:从AI编程到API调用的成本优化策略

2026/8/26 22:56:54

1. 项目概述:Token Plan个人版套餐选择困境 最近在AI编程和API调用圈子里,腾讯云的Token Plan个人版套餐讨论热度很高。很多开发者,无论是独立开发者、学生,还是小团队的成员,都在纠结同一个问题:面对39元、…

Jetson Orin升级JetPack 7.2:解锁DeepStream多路视频分析性能瓶颈

Jetson Orin升级JetPack 7.2:解锁DeepStream多路视频分析性能瓶颈

2026/8/26 22:56:54

1. 项目概述:一次关键的固件与软件栈升级如果你正在使用NVIDIA Jetson Orin系列平台进行DeepStream相关的开发,无论是做多路视频分析、智能边缘计算盒子,还是机器人视觉中枢,那么最近NVIDIA官方发布的Jetpack 7.2绝对是一个值得你…

WorkBuddy:企业AI原生应用平台,6-9个月实现50%-80%效率跃迁

WorkBuddy:企业AI原生应用平台,6-9个月实现50%-80%效率跃迁

2026/8/26 22:56:54

1. 项目概述:当AI助手成为你的“工作伙伴”最近和几个不同行业的朋友聊天,发现一个挺有意思的现象:大家嘴上都在谈“AI转型”,但真把AI用起来、用出效果的团队,其实没想象中那么多。很多公司要么是采购了昂贵的SaaS服务…

LibreOffice命令行实现Word转PDF:服务器端批量转换与工程化实践

LibreOffice命令行实现Word转PDF:服务器端批量转换与工程化实践

2026/8/26 22:46:53

1. 从“另存为”到“工程化”:为什么Word转PDF远不止点个按钮 如果你在办公室里问一个同事怎么把Word转成PDF,十有八九他会告诉你:“简单啊,Word里直接另存为不就行了?” 这话没错,对于偶尔处理一两份文档的…

[光学原理与应用-521]:对光的错误理解与纠偏

[光学原理与应用-521]:对光的错误理解与纠偏

2026/8/26 1:50:39

首先光是一种能量的载体和形态,宏观上观察到的光是由无数个微观的光量子组成的,每个光子在产生的瞬间,其在真空的空间中以确定不变的速度沿着一个初始的方向一直向前,在微观层面,每个光量子的运动轨迹是以波函数所展现…

SIP通话转接原理与REFER方法实战解析

SIP通话转接原理与REFER方法实战解析

2026/8/26 1:49:16

1. 通话转接不是“挂断再拨号”,而是SIP会话的动态重定向你有没有遇到过这样的场景:客服坐席A正在和客户通电话,突然需要把这通对话无缝转给专家坐席B,客户完全感知不到中间的断连——既没听到忙音,也没被要求重新拨号…

Kolla-ansible单节点OpenStack部署实战:从环境准备到排坑指南

Kolla-ansible单节点OpenStack部署实战:从环境准备到排坑指南

2026/8/26 17:50:58

1. 为什么选择Kolla-ansible来部署单节点OpenStack?如果你正在寻找一种能把OpenStack从“概念”快速变成“可用的实验环境”的方法,那么Kolla-ansible几乎是当前最主流、最省心的选择。我见过太多人卡在手动编译依赖、配置服务、处理版本冲突的泥潭里&am…

Python random 模块常用函数详解:从入门到实战

Python random 模块常用函数详解:从入门到实战

2026/8/26 0:05:45

目录 1. 引言2. 准备工作3. 基础随机函数4. 序列相关函数5. 随机种子与复现6. 实战案例7. 注意事项8. 常见问题与排查9. 总结 1. 引言 摘要: 本文系统介绍 Python 标准库 random 模块中最常用的随机数生成函数。内容涵盖基础随机函数(random()、unifor…

Hermes接入团队协作后,我推翻了三个效率假设

Hermes接入团队协作后,我推翻了三个效率假设

2026/8/26 0:05:45

聊《Hermes真能提效吗?先看流程里最慢的那一步》之前,先说一句实在的:别急着背概念,先看它在真实项目里到底解决什么问题。摘要团队把 Hermes 接进项目三个月后,交付速度没有提升反而慢了。复盘后发现,最先…

免费AI大模型调教指南:打造专属网文写作助手

免费AI大模型调教指南:打造专属网文写作助手

2026/8/26 0:05:45

1. 先搞清楚“AI小说扩展模式”到底能帮你做什么如果你是一个刚开始写网文、或者卡在L3级别以下的作者,最头疼的可能是情节推进不下去、人物对话干瘪,或者世界观设定不够丰满。自己对着空白文档硬憋,效率很低。这时候,一个能理解你…

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

2026/8/22 2:02:26

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具,覆盖选题构思、文献整理、内容生成、格式排版等核心场景,真正帮你高效搞定论文难题。 一、全流程王者:一站式搞定论文全链路(一天定稿首…

导师推荐!2026最新AI论文工具测评与实用推荐

导师推荐!2026最新AI论文工具测评与实用推荐

2026/8/26 18:07:30

2026年真正好用的AI论文工具,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

告别游戏崩溃:XCOM 2模组管理器的智能革命

告别游戏崩溃:XCOM 2模组管理器的智能革命

2026/8/26 17:57:52

告别游戏崩溃:XCOM 2模组管理器的智能革命 【免费下载链接】xcom2-launcher The Alternative Mod Launcher (AML) is a replacement for the default game launchers from XCOM 2 and XCOM Chimera Squad. 项目地址: https://gitcode.com/gh_mirrors/xc/xcom2-lau…