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

发布时间:2026/7/11 10:20:37

构建 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/7/7 23:28:17

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

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

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

2026/7/11 20:05:28

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

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

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

2026/7/11 14:10:40

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

王国保卫战 1-3 部 Lua 脚本反编译:LuaJIT LJD 工具实战,7步修改英雄属性

王国保卫战 1-3 部 Lua 脚本反编译:LuaJIT LJD 工具实战,7步修改英雄属性

2026/7/12 7:04:03

王国保卫战 Lua 脚本深度修改实战:从反编译到英雄属性定制1. 游戏数据修改的技术背景与工具准备在塔防游戏领域,《王国保卫战》系列以其精巧的平衡性和丰富的英雄系统著称。但对于追求个性化游戏体验的玩家而言,理解并修改游戏底层数据无疑能…

3步构建企业级Windows Syslog服务器:从被动应对到主动预防的日志管理解决方案

3步构建企业级Windows Syslog服务器:从被动应对到主动预防的日志管理解决方案

2026/7/12 7:04:03

3步构建企业级Windows Syslog服务器:从被动应对到主动预防的日志管理解决方案 【免费下载链接】visualsyslog Syslog Server for Windows with a graphical user interface 项目地址: https://gitcode.com/gh_mirrors/vi/visualsyslog 面对分散在多台设备的海…

学了不忘,忘了能查:超级学习 Skill + LLM Wiki 双引擎学习系统

学了不忘,忘了能查:超级学习 Skill + LLM Wiki 双引擎学习系统

2026/7/12 7:04:03

“Obsidian 是 IDE,LLM 是程序员,Wiki 是代码库。” —— Andrej Karpathy 0.1 为什么需要这篇文章? 传统的笔记工具(Notion、OneNote、甚至 Obsidian 本身)有一个共同问题:你写了笔记,但很少回…

通信数字化分销调研:172号卡渠道层级与账号权限底层规则解析

通信数字化分销调研:172号卡渠道层级与账号权限底层规则解析

2026/7/12 7:04:03

通信数字化分销调研:172号卡渠道层级与账号权限底层规则解析数字化线上通信分销是近几年兴起的轻资产副业赛道,依托运营商正规流量卡资源,平台搭建线上代理体系,普通用户完成实名即可参与推广分润。172号卡是行业内运营时间较长、…

IDEA 2024.1 性能调优:5项关键全局设置,降低内存占用20%

IDEA 2024.1 性能调优:5项关键全局设置,降低内存占用20%

2026/7/12 7:04:03

IDEA 2024.1 性能调优实战:5项关键设置释放20%内存每次启动IDEA时风扇狂转的噪音,或是编码过程中突然卡顿的界面,都让开发者感到烦躁。尤其在中低配设备上,这些问题会直接影响开发效率。但大多数用户并不知道,只需调整…

AI测试学习路线图 2026版:从智能体到性能测试的6阶段进阶与薪资对标

AI测试学习路线图 2026版:从智能体到性能测试的6阶段进阶与薪资对标

2026/7/12 6:54:03

AI测试学习路线图 2026版:从智能体到性能测试的6阶段进阶与薪资对标当ChatGPT开始编写测试用例、大模型自动生成边界值分析时,传统测试工程师的转型窗口正在快速关闭。2026年的测试领域,不会Prompt工程就像20年前不懂SQL——你依然能工作&…

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践

2026/7/12 0:03:42

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator XUnity.AutoTranslator作为Unity游戏社区中最成熟的文本翻译解决方…

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持

2026/7/12 0:03:42

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持 【免费下载链接】raspberrypi-kernel It provides openEuler kernel source for Raspberry Pi 项目地址: https://gitcode.com/openeuler/raspberrypi-kernel 前往项目官网免费下载&…

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧

2026/7/12 0:03:42

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧 【免费下载链接】integration-test The repo contains test suits for system integration test 项目地址: https://gitcode.com/openeuler/integration-test 前往项目官网免费下载:…

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践

2026/7/12 0:03:42

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator XUnity.AutoTranslator作为Unity游戏社区中最成熟的文本翻译解决方…

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持

2026/7/12 0:03:42

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持 【免费下载链接】raspberrypi-kernel It provides openEuler kernel source for Raspberry Pi 项目地址: https://gitcode.com/openeuler/raspberrypi-kernel 前往项目官网免费下载&…

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧

2026/7/12 0:03:42

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧 【免费下载链接】integration-test The repo contains test suits for system integration test 项目地址: https://gitcode.com/openeuler/integration-test 前往项目官网免费下载:…