尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Agent 可观测性基石:OpenTelemetry 学习与实践

Agent 可观测性基石:OpenTelemetry 学习与实践 一、背景单次 LLM 调用这种形态下链路线性日志即全景打一行日志就记录「调用了什么模型、花了多久、返回了什么」就结束了用户提问 ──→ 调用一次模型 ──→ 返回答案而 Agent 具备多轮推理、工具嵌套及强因果依赖等特征执行过程呈树状拓扑且耗时跨度大扁平日志无法承载层级耗时统计与因果依赖关系的表达。从 1 次调用变成几十次日志行数变多人眼看不过来从毫秒变成分钟耗时本身成了需要定位的问题从线性变成有因果第 3 轮依赖第 2 轮的工具结果平铺日志表达不了这种关系。日志记录的是「发生了什么」而 Agent 的执行还有一层结构做了什么、按什么顺序做、时间花在哪一步。OpenTelemetry简称 OTel它用一套标准的数据模型把一次执行记录成一棵有层级、有因果、带耗时的树Trace / Span并统一描述、采集和传输。Agent 的执行结构本身就是一棵树Agent 关心的「层级 因果 耗时」正好是 span 树的三个能力有父子层级、有调用因果、有起止耗时。二、核心概念2.1 OTel 的定位与核心数据OTel 有点像 HTTP 协议HTTP 定义了请求/响应长什么样、怎么传但它不是浏览器、也不是服务器OTel 定义了运行数据长什么样、怎么传。OTel 记录的最小单元叫span{ name: claude, trace_id: 4bf92f3577b34da6a3ce929d0e0e4736, span_id: 00f067aa0ba902b7, parent_id: 1c987a38b1408550, start_time: 2026-09-21T03:15:55.631Z, end_time: 2026-09-21T03:15:56.736Z, attributes: { gen_ai.request.model: claude }, events: [ { name: first-token } ], status: UNSET, kind: CLIENT }name这个 span 在干什么显示在瀑布图上trace_id属于哪一次完整调用整棵树共享span_id自身编号parent_id父 span 的编号start_time / end_time时间区间相减就是耗时attributes附加的结构化信息events瞬时事件statusUNSET / OK / ERRORkindCLIENT / SERVER 等后端靠它画拓扑图。两个关键点span 是时间区间不是时间点。 这是它和日志最根本的区别。parent_id 是拼出一棵树的唯一依据。 trace 一次完整调用产生的所有 span 拼成的树trace_id表示属于哪棵树把散落的 span 归拢成一个 tracespan_id标识自身parent_span_id指向父节点是拼树的唯一依据。后端只做两件事按 trace_id 分组 → 按 parent_id 建树判断根节点很简单看它有没有父节点没有父节点的就是根。2.2 上下关系确定在同一个程序里OTel 会自动把新 span 接到当前的父节点上with tracer.start_as_current_span(turn-0): # 当前是 turn-0 with tracer.start_as_current_span(llm): # llm 自动成为 turn-0 的子节点 pass跨进程就不一样两个程序互相看不见各记各的。比如 Agent 要查订单它自己查不了得让订单服务去查。在 OTel 里这两个值叫trace_id和span_id通常放在 HTTP 请求头里SDK 会自动带上和读取。跨进程时如果没有这个传递两边的记录就永远对不上只会得到两棵互不相干的树。2.3 三大信号Trace、Metric、LogOTel 只定义三类数据叫信号它们通过trace_id关联形成从粗到细的下钻链Trace 和 Metric 的成本差异很直接100 万个请求不可能每个都存一棵完整的树metrics 只存聚合数字100 万个请求也只是几个数。2.4 组件与链路从 SDK 到 CollectorOTel 分两个包opentelemetry-api只有接口opentelemetry-sdk给应用依赖。如果 openai 库内部埋点、依赖的是api应用装了 SDK埋点生效没装API 调用变成空操作什么都不发生也不报错。这样库可以零成本内置埋点。五个核心组件对应代码resource Resource.create({service.name: my-agent}) # ← Resource provider TracerProvider(resourceresource) # ← Provider provider.add_span_processor(BatchSpanProcessor(exporter)) # ← Processor Exporter trace.set_tracer_provider(provider) tracer trace.get_tracer(__name__) # ← TracerOTLP 和 Collector。OTLP 是传输协议。Collector 是独立进程生产环境常用它做四件事接收统一入口、处理采样、脱敏、富化、路由一份数据发多个后端、缓冲。改策略不用改应用——把采样率从 10% 调到 5%改 Collector 配置即可不用重新部署 Agent。完整架构三、解决的问题3.1 日志与 Trace 的分工出错看堆栈、看某个变量的值 → 日志看某一步花了多久、时间花在哪一步 → trace在几十条运行里找最慢的、统计工具失败率、对比两个模型 → trace。日志回答「发生了什么」trace 回答「为什么慢、谁拖累了谁」。两者都真实存在不是二选一。OTel 不取代现有日志。挂一个 handler业务代码不用改日志同时进两个地方OTel 那份自动带上trace_id一条带trace_id的日志把散落的文本变成树上的一个锚点可以从日志直接跳到对应的 trace。3.2 从日志到执行树同样是记录一次运行日志是平铺文本trace 是有层级的树日志记录发生了什么但很难表达这些事情之间的关系哪个 LLM 调用属于哪一轮、哪个 Tool 是哪次决策触发的、整个 Agent 在哪里耗时。一个排障例子① Metric 发现异常P99 从 80ms 涨到 210ms慢在模型调用 ② Trace 定位到具体会话turn-1 里 tool:query_order 被调了 3 次每次都 1.5s ③ Log 找到根因从 trace_id 一键跳过来 ERROR query_order 超时第 3 次放弃 → 不是模型慢是订单服务接口超时模型被迫反复重试3.3 Attributes记录属性信息span 自带的信息只有四样name → 这是什么操作只能有 1 个值 start / end → 什么时候、多久 parent_id → 谁在谁里面 attributes → 其余一切name必须低基数它是聚合主键只能承担分类。实际要记录的还包括模型、token 数、轮次、输入输出、失败原因这些放在 attributes 里。同一棵 span 树跑两遍A 不 set 属性、B set 属性结构、名字、耗时完全一样差别在能回答的问题哪一步最慢两者都能答这是唯一只靠时间就能答的token花费、上下文第几轮开始涨、哪个工具最容易失败只有 B 能答。只看 span 结构能分析的只有耗时要分析业务就得靠 attributes。属性之间有依赖位置也有要求要画 token 增长曲线turn.index和gen_ai.usage.input_tokens必须同时存在少一个就只剩三个孤立数字排不出顺序属性都记了但挂错 span分析也用不了。Trace 决定能不能还原过程Attributes 决定能不能分析过程。四、编码与实测4.1 字段规范核心是用行业约定的名字后端靠字段名自动渲染面板——用model_name而不是gen_ai.request.model后端的面板、成本计算、LLM 视图都会失效。LLM / Agent 场景已有标准OTel 语义约定模型 spangen_ai.request.model、gen_ai.usage.input_tokens、gen_ai.usage.output_tokens、gen_ai.server.time_to_first_token工具 spantool.name、tool.arguments输入输出input.value / output.value自定义业务字段加自己的前缀比如cs.intent、cs.prompt_version避免和规范撞名。是 span 还是 attribute用两步判断第一步这一步有自己的耗时吗 是 → 开 span 第二步这是某一步的产出/结果吗 是 → 挂 attribute有耗时的步骤用 span步骤的产出用 attribute。比如智能客服前置三步——意图识别、query 改写、槽位抽取——各自都耗时所以各是一个 span各自的产出意图、槽位是 attributewith tracer.start_as_current_span(cs:intent) as s: intent classify_intent(text) # ← 耗时发生在这一步 s.set_attribute(cs.intent, intent.name) # ← 产出挂属性如果压成一个 span 三个属性就丢掉了「哪一步最慢」而调优时需要这个信息。4.2 完整示例一个智能客服 Agent初始化一个程序只做一次from opentelemetry import trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter resource Resource.create({service.name: my-agent-runner}) provider TracerProvider(resourceresource) provider.add_span_processor( BatchSpanProcessor(OTLPSpanExporter(endpointhttp://localhost:4318/v1/traces)) ) trace.set_tracer_provider(provider) # ← 全进程只能调一次 tracer trace.get_tracer(__name__)把前面的规则串起来规范字段和业务自定义字段混用def handle_session(user_text: str) - str: # 根 span一次会话的边界。业务属性走自己的 cs. 命名空间 with tracer.start_as_current_span(agent:customer-service) as agent: agent.set_attribute(openinference.span.kind, AGENT) agent.set_attribute(cs.tenant_id, t-001) agent.set_attribute(cs.prompt_version, v3.2) # user.id / session.id 是高基数 → 放进日志不塞属性 # 前置 pipeline每一步都有耗时 → 各自一个 span产出挂属性 with tracer.start_as_current_span(cs:intent) as s: intent classify_intent(user_text) s.set_attribute(cs.intent, intent.name) with tracer.start_as_current_span(cs:slot_filling) as s: slots fill_slots(user_text) s.set_attribute(cs.slots, json.dumps(slots)) # 多轮生成规范字段挂在模型 span 上 for i in range(MAX_TURNS): with tracer.start_as_current_span(fturn-{i}) as turn: turn.set_attribute(turn.index, i) with tracer.start_as_current_span(claude) as llm: reply call_model(user_text, slots) llm.set_attribute(gen_ai.request.model, claude) llm.set_attribute(gen_ai.usage.input_tokens, reply.usage.input) llm.set_attribute(gen_ai.server.time_to_first_token, reply.ttft_ms) llm.set_attribute(llm.cost.usd, reply.cost_usd) llm.set_attribute(output.value, reply.text[:10240]) # 截断 for call in reply.tool_calls: with tracer.start_as_current_span(ftool:{call.name}) as tool: tool.set_attribute(tool.name, call.name) tool.set_attribute(status, ok if execute(call).ok else error)生成的树每个字段对应一个具体问题这次会话花了多少tokengen_ai.usage.* llm.cost.usdtoken 花在哪一轮上面 turn.index必须同层前置三步谁最慢三个 cs:* span 的时长本身用户意图分布cs.intent哪个工具最容易挂tool.name statuswith tracer.start_as_current_span(turn-0) as turn:这行和with open()结构一致括号里的字符串 → 是名字进后端别人看 → 有约束低基数 as 后面的变量 → 是把手只在代码里用 → 随便取零语义 with → 退出时自动结束 span三个细节with 就是 span 的开始和结束。不需要自己调 end()也不要漏掉结束——不 end() 的 span 永远不会被导出而且不报错。as 后面的名字随便取它只是个 Python 局部变量括号里的字符串才是 span 的名字。树长什么样完全由代码缩进决定。4.3 三信号协同Metric 和 Log 怎么配合真实系统里三条信号一起用同一个事件写三遍各写各的。tracer trace.get_tracer(__name__) meter metrics.get_meter(__name__) logger logging.getLogger(__name__) # instruments模块级各建一次别放在函数里 tokens meter.create_counter(gen_ai.client.token.usage, unit{token}) latency meter.create_histogram(gen_ai.client.operation.duration, units) with tracer.start_as_current_span(agent:customer-service) as agent: logger.info(会话开始) # ③ log —— 自动带上 trace_id with tracer.start_as_current_span(MODEL) as span: t0 time.monotonic() reply call_model(user_text) dt time.monotonic() - t0 span.set_attribute(gen_ai.usage.input_tokens, reply.usage.input) # ② metric —— 同一个事件换个记法从这一次变成一个数字 lbl {gen_ai.request.model: MODEL} tokens.add(reply.usage.input reply.usage.output, lbl) latency.record(dt, lbl)跑完之后手上同时有三样东西靠trace_id缝合trace是一棵树metric是几条数字log是几行带trace_id的文本。4.4 实测一次 Agent Trial链路是完整的SDK 埋点 → OTLP/HTTP → Jaeger 进程 → Jaeger 的 HTTP API → 取回来出图。中间隔着一个真实的后端进程验证的是端到端链路不是内存里的对象。结果service.name agent-eval14 个 span端到端771.8ms。其中 LLM 调用总共 120.1ms占比约 15.6%这次实验里 LLM 不是主要耗时来源。没有 Trace容易先怀疑模型有 Trace 可以先确认时间花在哪。4.5 能回答的问题与模型对比常用查询模式都是按某个字段聚合哪一步最慢 → 按 span name 聚合耗时排序哪个工具最容易失败 → 按 tool.name 分组统计 status ERRORtoken 花在哪 → 按 turn.index 排开 input_tokens 看增长曲线。对比对评测场景尤其有用最终分数 轮数 总 token 工具调用 模型 A 0.85 3 2.1k 2 模型 B 0.85 12 11.4k 9 看起来一样 差 4 倍 差 5 倍 差 4.5 倍分数一样成本差 5 倍这个差异日志里看不出来span 树里能直接看到。形状本身也是信息4.6 后端选型与落地节奏后端选型Jaeger入门、看瀑布图开箱即用适合起步MLflow适合实验对比Langfuse是 LLM 专用面板字段约定不同要验证渲染效果自建 Collector 存储等量上来再说。OTel 的失败大多是「数据没到」而不是「程序崩了」需要先有一个能看数据的地方否则问题出在哪都不好判断。分阶段落地① 先让一条轨迹能看 ← 最重要成本低② 再批量导入历史数据③ 再定字段规范④ 最后建对比工作流、上生产加 Collector、配采样、接多后端OTel 的价值在第一次跑的时候不明显只看到一棵树会像「好看点的日志」。价值出现在有第二个版本可以对比的时候。所以先跑通、先有数据字段可以之后再补。五、总结OpenTelemetry 把一次 Agent 执行记录成一棵有层级、有因果、带属性的树Trace / Span 还原「做了什么、什么顺序、花了多久」Attributes 记录「用的什么、token花了多少、为什么失败」Metric / Log 分别回答「整体怎么样」和「那一瞬发生了什么」靠 trace_id 串起来。一次 771.8ms 的 Agent TrialLLM 只占 120.1ms15.6%慢的不一定是模型。当 Agent 从「调用一个模型」变成「自主完成一项任务」理解它怎么跑就成了可观测性的核心问题。学AI大模型的正确顺序千万不要搞错了2026年AI风口已来各行各业的AI渗透肉眼可见超多公司要么转型做AI相关产品要么高薪挖AI技术人才机遇直接摆在眼前有往AI方向发展或者本身有后端编程基础的朋友直接冲AI大模型应用开发转岗超合适就算暂时不打算转岗了解大模型、RAG、Prompt、Agent这些热门概念能上手做简单项目也绝对是求职加分王给大家整理了超全最新的AI大模型应用开发学习清单和资料手把手帮你快速入门学习路线:✅大模型基础认知—大模型核心原理、发展历程、主流模型GPT、文心一言等特点解析✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑✅开发基础能力—Python进阶、API接口调用、大模型开发框架LangChain等实操✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经以上6大模块看似清晰好上手实则每个部分都有扎实的核心内容需要吃透我把大模型的学习全流程已经整理好了抓住AI时代风口轻松解锁职业新可能希望大家都能把握机遇实现薪资/职业跃迁这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】
返回列表