想象你是一家快递公司的调度员:一个包裹从用户手上出发,要经历揽收、分拣、干线运输、派送等多个环节。从前每个环节各记各的账——有人记纸质单、有人发微信、有人打电话,一旦包裹延误,你根本说不清它卡在哪个转运中心。OpenTelemetry(简称 OTel)就是把各环节统一成一套“扫码系统”:给每个包裹贴一张全球通用的物流单号,每个中转站都拿同一款扫码枪记录“到达、处理、发出”,并且把汇总数据自动发回总部大屏。放在 AI 应用里,这个“包裹”就是一次 LLM 请求,而“中转站”是网关、Prompt 处理、向量检索、模型推理、结果校验等模块。OTel 负责把这条请求变成一条完整可追踪的链路,同时用指标衡量吞吐、延迟、token 用量与成本,用日志记录异常细节。
对 AI 从业者而言,掌握 OTel 等于拥有 LLM 应用的“监控操作系统”;它是大模型运维、AgentOps、成本治理与可靠性工程岗位的核心基建能力。
目录
可视化演示
动态演示OpenTelemetry的核心机制,通过源头/管道/节点/终端四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
OpenTelemetry:Open OpenTelemetry三信 SDK Instrumenta 核心机制可视化 OpenTelemetry整体架构与关键组件
1. OTel 是什么
快递公司如果只有一堆分散的纸质单据,调度员永远没法一眼看清全局。OTel 的野心,就是给整个行业定一套“统一包裹单 + 统一扫码枪 + 统一数据仓库”的规范。无论是自研服务、开源中间件还是云厂商组件,只要遵循这个规范,数据就能汇入同一个控制中心。
放在 AI 工程里,一次 LLM 请求会穿过 API 网关、Prompt 模板、向量检索、模型推理、限流与缓存等多个环节。没有统一规范时,各个服务分别写日志,问题发生时只能靠人工逐个翻。OTel 通过 SDK 和协议,把这些环节产生的追踪数据、指标数据、日志数据全部标准化,让“用户问了一句什么”变成一条可回放的完整链路。
OTel 的核心是三个信号:Trace(链路)、Metric(指标)、Log(日志)。它既是一个标准,也是一套开源实现,涵盖多语言 SDK、采集器 Collector、OTLP 协议,以及各类导出器。它不是某个监控产品的替代品,而是监控产品之上的公共管道。
所以,OTel 对 LLM 应用的意义,就像给所有包裹贴上了同一套可识别条码。从此,调度员不再需要看懂各分拣厂自创的编号,只需要一套系统就能追踪全局。
- 统一管道OTel 不是单一信号,而是三大可观测信号的公共载体。
- 并集关系链路、指标、日志各有侧重,合在一起才构成完整真相。
2. 数据模型三支柱
快递员每到一个中转站都会“嘀”一下扫码枪,记录包裹在这个站点的到达和离开。在 OTel 里,这条完整的物流轨迹叫 Trace,而每一次扫码记录就是一个 Span。Span 是 Trace 的基本积木:它有名字、开始时间、结束时间、状态,以及一组可自定义的属性。
一个 LLM 请求的 Trace,可以简单理解成一串 Span。最外层 Span 代表“整个 LLM 请求”,它下面挂着“网关鉴权”“Prompt 构造”“向量检索”“模型调用”“结果校验”等子 Span。子 Span 再往下还可以有“数据库查询”“HTTP 调用”等更细的 Span。这些 Span 通过父子关系挂成一棵树,共同描述一次请求发生了什么。
除了 Trace,OTel 还有 Metric 和 Log。Metric 是“每分钟处理了多少包裹、平均耗时多长”这类可聚合统计;Log 则是“这个包裹包装破损”这类带时间的原始事件。三者在排障、容量规划和审计中缺一不可。
理解数据模型,就理解了 OTel 的“记账语言”。物流公司不会只靠一条总报表管理一切,它需要每个站点、每个包裹、每次异常都有独立记录;同样,LLM 应用也需要 Span 记录每一次调用细节,才能快速定位问题。
- Span 集合Trace 由 n 个 Span 组成,而不是只有一个笼统的时间段。
- 父子关系Span 之间通过 parent 字段形成树状结构,体现调用层级。
3. SDK 接入流程
给包裹贴条形码需要一套工具,这套工具在 OTel 里就是 SDK。使用 SDK 的流程很像“给快递员发放扫码枪并配置无线电网络”:先在应用里安装 OTel 依赖,再初始化一个 Provider,最后让业务代码产生 Span 并把数据交给导出器。
初始化 Provider 时,通常要设置 Service Name、环境、版本等资源信息。比如一个 Python 的 LLM 服务,可以叫 service.name="llm-gateway",这样所有数据都自动带上“这是哪个服务”的标签。SDK 启动后,你可以选择自动埋点,例如监听 HTTP 请求、数据库调用、OpenAI 客户端调用;也可以手动埋点,在关键业务逻辑外用 with tracer.start_as_current_span(...) 包一层。
接下来必须配置导出链:Span 先交给 BatchSpanProcessor 做批量缓冲,再交给 Exporter 发往后端。整个链条很像快递员扫完码后,不是立刻跑一趟总部送数据,而是先累积一批,再统一发车,从而降低对业务性能的影响。
接入 SDK 是 OTel 落地的第一公里。它解决的是“数据从哪来”的问题;数据一旦产生,后续的串联、采样、导出才有素材。
- SDK 注入应用通过 SDK 产生 Span,而不是直接向后端发 HTTP。
- Provider 装配Provider 负责管理 Tracer、Meter、Logger 等入口。
- Exporter 发送最终数据由 Exporter 发往 OTLP 端点或第三方平台。
4. 上下文传播
物流包裹在转运中心换车时,工作人员必须把原包裹单号同步到新的交接单上,否则后续扫码就接不上前一段。OTel 的上下文传播做的就是这件事:当请求从服务 A 调到服务 B 时,自动传递 Trace ID、Span ID、采样标记,以及若干自定义键值。
在 OTel 中,上下文 Context 是一个轻量容器。里面最核心的是 trace_id 和 span_id,它们决定一个新 Span 应该挂在哪棵树上。为了让不同语言、不同框架都能理解,业界普遍使用 W3C 制定的 traceparent HTTP 头来传输这些信息。浏览器、网关、微服务只要都能识别这个头,就能天然串联。
此外,Baggage 负责携带业务键值,比如用户 ID、租户 ID、会话 ID。它像包裹上额外贴的“易碎品”“加急”标签,一路随请求传递。对 LLM 应用来说,把 user.id 放进 Baggage,就能在排障时按用户维度过滤 Trace。
没有上下文传播,OTel 只能拿到一段段孤立的“扫码记录”。有了它,所有 Span 才能拼成一张完整物流地图,这也是全链路追踪的命脉。
- 快照传递下一个服务拿到的是包含 trace_id 和 span_id 的上下文快照。
- 新 Span 并入新 Span 把自己的 span_id 追加进上下文,继续向后传。
5. 采样策略
快递公司的质检组不可能拆开每一个包裹检查,只能按一定比例抽查。OTel 的采样策略也类似:不是每条 Trace 都必须采集入库,而是根据成本与价值做取舍。最常见的三类是 Head Sampling、ParentBased Sampling 和 Tail Sampling。
Head Sampling 是在请求刚进入系统时就决定“这条链路要不要采”,核心是快但粗。ParentBased Sampling 则规定:如果父 Span 被采样,子 Span 也一定被采样;如果父 Span 没采,子 Span 也不采,从而保持链路完整性。Tail Sampling 在链路全部结束后再做决策,可以结合状态码、耗时、是否报错来判断,更准确但需要额外的缓冲存储。
采样会直接影响成本和排障能力。采样率过高,存储费用暴涨;采样率过低,低频故障可能永远看不见。因此生产环境常用“错误链路全量采、正常链路按比例采”的混合策略。
采样不是“偷工减料”,而是用可控成本换取足够真实的全局统计。对 LLM 应用而言,token 费用和存储费用都高,采样策略因此成了运维治理的必答题。
- 采样率 pp 表示保留给后端的 Trace 比例,0 到 1 之间。
- 全量成本被保留的链路需要完整存储、检索与展示。
- 缩减成本未采样的链路只留下极少量摘要或不留。
6. 导出与配置
所有扫码数据最终要发回总部控制中心,快递公司绝不会让司机每次只送一个包裹。OTel 的导出器也有同样的批处理思想:数据先进入队列,攒够一批或到达时间阈值后统一发送。
最常用的导出协议是 OTLP,它支持 HTTP 和 gRPC 两种传输方式。配置导出器时,主要关注三个参数:Endpoint 指向哪,超时多久,是否开启压缩。比如把 OTLP 端点指向 http://collector.example.com:4318,就能让 Collector 统一接收并转发到后端。
网络总会有抖动,OTLP 导出器通常带重试与指数退避。第一次失败后等待 1 秒,第二次 2 秒、第三次 4 秒,直到封顶时间。这个机制很像快递线路拥堵时,司机先在网点排队,而不是反复猛打总部电话。
正确的导出配置,决定数据能否稳定送达。如果只埋点不导出,OTel 就像一套装好了条码机却从不联网的快递站,数据全部困在本地。
- 初始等待第一次重试前等待 τ₀。
- 指数增长第 k 次重试的等待时间为 τ₀ × 2ᵏ。
- 封顶保护等待时间不能超过 t_max。
7. LLM 专属扩展
普通快递和 AI 专线快递需要不同类型的标签:AI 包裹上最好额外写明“模型名称、输入 token 数、输出 token 数、推理耗时”。OTel 社区为此定义了 GenAI 语义约定,专门描述大模型调用。
在一个 LLM Span 上,可以记录 gen_ai.request.model 表示模型名称,记录 gen_ai.usage.input_tokens 和 gen_ai.usage.output_tokens 表示输入输出 token 数,还可以记录 Prompt 摘要和 Completion 摘要。这些信息让链路不再只是“调用成功/失败”,而能回答“这次回答花了多少钱”。
有了 token 数,结合模型单价就能估算成本。假设输入每千 token 单价为 p_in,输出每千 token 单价为 p_out,则一次调用的成本可以写成下面的公式。把这些属性按模型、租户、应用聚合成 Metric,就能实时监控 AI 账单。
LLM 应用最大的运维焦虑之一就是“看不见的 token 账单”。OTel 的 Genre AI 扩展,相当于在快递包裹上自动贴了一张计价小票,让每一次 AI 推理都有据可查、有账可算。
- 输入费用输入 Prompt 的 token 数量乘输入单价。
- 输出费用模型生成内容的 token 数量乘输出单价。
- 总成本两部分相加得到一次请求的模型成本。
8. 运维协作生态
统一物流网建好之后,快递公司往往还会在总部旁边设一个“客户体验分析台”,专门分析哪些包裹延迟、哪些用户投诉。OTel 负责的正是底层物流网;而 Langfuse 这类专用工具,则是在这张网上做 LLM 会话分析、Prompt 版本管理和人工反馈。
Langfuse 可以接收 OTel 数据,并把 Trace 转化成更贴近 LLM 产品的界面:用户会话、评论文本、Token 消耗、模型对比一目了然。也可以把 Langfuse 的 session_id 作为属性写进 Span,再导入通用监控平台,实现“业务视图”与“技术视图”双向切换。
在运维侧,你还能基于 OTel 指标建立告警:当 token 成本超过阈值、P95 延迟超过 3 秒、模型调用错误率超过 5% 时,自动触发值班通知。这样,物流网不再是静态地图,而是一个会主动报警的指挥系统。
OTel 不是终点,而是可观测性的地基。地基上面可以盖 Langfuse、Grafana、Prometheus、云厂商监控等各种建筑;只有地基扎实,LLM 应用才能真正做到“看得清、查得明、省得下”。
- 指标越界例如 token 成本超过预算阈值。
- 持续时间需要持续 d 分钟才告警,避免瞬时抖动。
- 组合条件两者同时满足时,告警位取 1 并通知值班人。
AI 应用场景
LLM 网关全链路排障
阶段:模型服务运维与 AgentOps
解决的问题:传统监控只能看到单服务日志,无法判断一次缓慢的 RAG 回答到底卡在向量检索还是模型推理。
具体案例:某客服助手在早晚高峰响应变慢,工程师通过 OTel 的 Trace 发现 70% 耗时发生在 Embedding 调用,而不是大模型本身;随后把向量库连接池扩容,P95 延迟直接下降一半。
Token 成本看板与预算告警
阶段:成本治理与 FinOps
解决的问题:OpenAI 账单按天延迟,团队无法实时掌握每个业务线的 token 消耗。
具体案例:团队在 OTel 的 LLM Span 上记录模型名、输入输出 token 数,用 OTLP 送到 Prometheus 后按租户聚合;当日成本超过预算 80% 时自动告警,并将用量最高的 Prompt 模板列出优化。
Langfuse 会话分析与产品迭代
阶段:模型评估与产品分析
解决的问题:Trace 能看到技术细节,但看不出用户在同一会话里的反馈和满意度。
具体案例:把 OTel 采集的 LLM 调用下发到 Langfuse,产品经理按用户会话查看每轮 Prompt、结果和用户评分,发现某类问题的负反馈率高达 40%,从而推动 Prompt 模板迭代。
Python 演示
import randomimport timefrom dataclasses import dataclass, fieldfrom typing import Dict, Optional @dataclassclass Span: name: str trace_id: str span_id: str parent_id: Optional[str] start: int end: int = 0 attributes: Dict[str, object] = field(default_factory=dict) def finish(self): self.end = time.perf_counter_ns() return (self.end - self.start) / 1e3 class ParentBasedSampler: def __init__(self, rate: float = 0.5): self.rate = rate def should_sample(self, parent_sampled: Optional[bool]) -> bool: if parent_sampled is True: return True if parent_sampled is None: return random.random() < self.rate return False class ConsoleExporter: def export(self, span: Span) -> None: latency = (span.end - span.start) / 1e3 print( f"trace={span.trace_id} span={span.name} " f"latency={latency:.2f}ms attrs={span.attributes}" ) class Tracer: def __init__(self, sampler: ParentBasedSampler, exporter: ConsoleExporter): self.sampler = sampler self.exporter = exporter self._root_sampled = None def start_span(self, name: str, parent: Optional[Span] = None) -> Span: if parent is None: trace_id = format(random.getrandbits(64), "016x") self._root_sampled = self.sampler.should_sample(None) else: trace_id = parent.trace_id span_id = format(random.getrandbits(64), "016x") parent_id = None if parent is None else parent.span_id return Span(name, trace_id, span_id, parent_id, time.perf_counter_ns()) def end_span(self, span: Span, attrs: Optional[Dict[str, object]] = None) -> None: if attrs: span.attributes.update(attrs) span.finish() if self.sampler.should_sample(self._root_sampled): self.exporter.export(span) def main(): sampler = ParentBasedSampler(rate=0.8) exporter = ConsoleExporter() tracer = Tracer(sampler, exporter) root = tracer.start_span("llm-request") db = tracer.start_span("vector-search", root) tracer.end_span(db, {"db.collection": "docs"}) gen = tracer.start_span("chat-completion", root) tracer.end_span(gen, {"gen_ai.usage.input_tokens": 120, "gen_ai.usage.output_tokens": 80}) tracer.end_span(root, {"http.status_code": 200}) if __name__ == "__main__": main()
class ParentBasedSampler:定义父级采样器:根节点决定整棵链路是否保留。def should_sample(self, parent_sampled: Optional[bool]) -> bool:核心采样逻辑:父节点已采样则子节点跟进,根节点则按概率抽签。span_id = format(random.getrandbits(64), "016x")生成 16 位十六进制 Span ID,模拟 OTel 的全局唯一标识。self._root_sampled = self.sampler.should_sample(None)根 Span 启动时一次性决定整条 Trace 的命运。tracer.end_span(gen, {"gen_ai.usage.input_tokens": 120, "gen_ai.usage.output_tokens": 80})在 LLM Span 上附加 token 属性,作为成本监控的数据来源。
常见误区
误区OTel 能采集 Trace、Metric、Log,还能导出数据,那直接用它就够了,为什么还要 Langfuse、Grafana 这些平台?
正解OTel 本质上只是“采集管线和传输协议”,它不负责长期存储、多维统计、告警通知和产品化界面。就像快递公司建设了统一的扫码网络,但总部还需要专门的调度大屏和客服系统。LLM 应用尤其需要 Langfuse 这样的会话分析平台来辅助评估。
为何常见:因为 OTel 自带 SDK、Collector 和 Exporter,看起来已经是一条完整流水线,容易让人误以为它是“全家桶”。
误区把采样率调成 1% 能省下大量存储费用,反正大部分请求都是正常的。
正解过度降低采样率会让低频错误、边缘用户和特殊 Prompt 成为盲区。生产环境应采用混合策略:错误链路全量采、正常链路按比例采、关键业务用 Tail Sampling 兜底。采样是在“成本”和“可观测性覆盖度”之间做折中,而不是简单一刀切。
为何常见:在演示环境中数据量小,采样影响不明显;一旦线上流量变大,统计失真和盲区问题才会暴露。
误区只要在 Span 上设置了 `gen_ai.usage.input_tokens`,成本账单就会自动出现。
正解OTel 只负责记录“用了多少 token”,它并不知道每个模型每千 token 的价格,也不会自动跑账单。要得到成本,需要自己实现或借助平台根据价格表计算,再按模型、租户、应用聚合。
为何常见:因为“token 属性”和“模型报价”在界面里经常同时出现,容易让人误以为系统已经内置计价器。
误区LLM 应用出问题,看一条 Trace 就能定位,那指标和日志是不是多余?
正解Trace 擅长回答“某一次请求发生了什么”,但回答不了“最近一小时的 token 成本是否飙升”“错误率是否从 1% 涨到 10%”。Metric 提供趋势与聚合,Log 提供导致错误的原始细节,三类信号互相补充,缺一不可。
为何常见:因为 Trace 的瀑布图视觉冲击最强,初学者容易把个案当成全局。
面试题
Q1. OTel 最核心的目标是什么?
- A.统一三类信号采集← 正确答案
- B.训练更大的模型
- C.自动生成业务代码
- D.压缩日志存储成本
答案:A。OTel 的核心是统一 Trace、Metric、Log 三类信号的采集与导出,让可观测性数据标准化。训练模型、生成代码、压缩日志都不是它的职责。
Q2. 一次 LLM 请求的 Trace 由什么组成?
- A.串成链的 Span 集合← 正确答案
- B.单条独立的 Span
- C.一组零散的时间点
- D.一堆无结构的文本
答案:A。Trace 由多个 Span 按父子关系串成树或链。单个 Span 只是其中一环,零散时间点和无结构文本都不具备 Trace 的关联能力。
Q3. SDK 初始化 Provider 的主要作用是什么?
- A.汇总链路并统一导出← 正确答案
- B.训练 Prompt 并部署
- C.压缩日志并做备份
- D.替代 Trace 采集过程
答案:A。Provider 管理 Tracer、Meter、Logger,是 SDK 的核心入口,负责把业务代码产生的可观测性数据汇总并交给 Exporter。它不做模型训练或日志备份,也不是为了替代采集,而是组织采集。
Q4. 上下文传播时 Baggage 主要用于做什么?
- A.跨服务携带业务键值← 正确答案
- B.修改大型模型权重
- C.删除旧的运维日志
- D.生成随机新的 Span
答案:A。Baggage 像包裹上的附加标签,用来跨服务传递用户 ID、租户 ID 等业务信息。它不参与模型权重修改、日志删除,也不负责生成 Span。
Q5. 父级采样策略的基本原则是什么?
- A.子 Span 跟随父采样决策← 正确答案
- B.每个 Span 都重新抛硬币
- C.优先丢弃根 Span 数据
- D.所有请求必须全量采集
答案:A。ParentBasedSampler 规定子 Span 的采样结果跟随父 Span,保证整条 Trace 要么完整保留,要么整体丢弃。多次独立抛硬币会破坏链路完整性。
Q6. 在 LLM Span 上记录 token 成本主要依赖哪类信息?
- A.GenAI 语义约定← 正确答案
- B.随机价格字段
- C.CPU 时钟周期数
- D.网络包大小和
答案:A。OTel 的 GenAI 语义约定了模型名、输入输出 token 数等属性,成本才能基于真实用量计算。随机字段、CPU 周期和网络包大小都不是 token 计费依据。
Q7. 关于 OTel 与 Langfuse 的关系,理解正确的是?
- A.OTel 采数据平台做分析← 正确答案
- B.两者属于互斥替代关系
- C.Langfuse 完全取代 OTel
- D.OTel 无法接入 Langfuse
答案:A。OTel 负责数据采集与传输,Langfuse 负责 LLM 会话分析;Langfuse 可以接入 OTel 数据,两者是协作关系而非替代关系。
Q8. 下列属于 OTel 三大信号的是?
- A.链路与跨度信号← 正确答案
- B.指标聚合信号← 正确答案
- C.结构化日志信号← 正确答案
- D.提示词模板文本
答案:ABC。OTel 三大信号是 Trace、Metric、Log,分别对应链路、指标和日志。Prompt 模板是业务配置,不属于 OTel 信号体系。
Q9. 设计 LLM 应用采样策略时,需要考虑哪些因素?
- A.存储成本与链路量← 正确答案
- B.查询速度与覆盖率← 正确答案
- C.分布式采样一致性← 正确答案
- D.界面主题美观度
答案:ABC。采样影响存储成本、查询速度、数据覆盖率以及多个服务间的采样一致性。界面主题与可观测性系统设计无关。
Q10. 关于 OTel 在 LLM 运维中的应用,下列说法正确的是?
- A.生成请求级 Trace← 正确答案
- B.采集 token 与费用← 正确答案
- C.接入 Langfuse 平台← 正确答案
- D.替换模型推理引擎
答案:ABC。OTel 可为 LLM 请求生成 Trace,通过 GenAI 属性记录 token 用量,并把数据导给 Langfuse 这类平台。它不负责模型推理,也无法替代推理引擎。