跳到主要内容

LLM 生产环境可观测性:追踪那些你无法预测的行为

· 阅读需 12 分钟
Tian Pan
Software Engineer

你的监控堆栈会告诉你关于请求率、CPU 和数据库延迟的一切。但它几乎无法告诉你,你的 LLM 是否刚刚幻觉出了一个退款政策,为什么一个面向客户的智能体(Agent)循环调用了三次工具才回答了一个简单的问题,或者你产品中的哪个功能正在每天悄无声息地烧掉价值 800 美元的 Token。

传统的可观测性是围绕确定性系统构建的。LLM 在结构上完全不同 —— 同样的输入,每次的输出都可能不同。它的失败模式不是 500 错误或超时;而是一个听起来很有道理、非常自信但恰好错误的回答。成本不再是稳定且可预测的;当一个配置错误的 Prompt 遇到流量高峰时,成本会激增。调试也不再是“在堆栈跟踪中查找异常”;而是“重构为什么智能体在周二凌晨 2 点选择了这个工具路径”。

这就是 LLM 可观测性要解决的问题 —— 并且在过去的 18 个月里,这一领域已经显著成熟。

为什么传统追踪不再适用

分布式追踪基于一个核心假设:给定相同的代码和相同的输入,执行路径是相同的。你对代码进行插桩(Instrument),Span 会告诉你发生了什么,你可以在测试环境中重现该行为来进行调试。

LLM 在各个层面上都打破了这一假设。模型是一个从概率分布中采样的黑盒。Temperature、核采样(Nucleus Sampling)和模型更新都意味着即使输入完全相同,输出也会有所不同。在智能体(Agentic)系统中,这种差异会产生复合效应:如果 LLM 在第一步选择了不同的工具,它会遇到不同的中间结果,从而改变第二步的决策,以此类推。到了第五步,追踪结果可能与昨天“完全相同”的请求追踪截然不同。

其影响是深远的:

  • 错误出现在评估中,而非异常中。 幻觉不会抛出 TypeError。它会返回 HTTP 200 以及一个听起来很自信的谎言。检测需要下游评估 —— 由另一个模型对输出评分、用户反馈信号或基于规则的检查器 —— 而不是 try/catch 块。
  • 延迟是双峰的且取决于输入。 一个 50 Token 的 Prompt 和一个 10,000 Token 的 Prompt 访问同一个端点,但具有截然不同的延迟特征。将平均延迟作为你的 SLO 几乎没有意义;你需要按输入长度分桶来跟踪 p95/p99。
  • 成本是运行时变量,而非固定开销。 一个请求的花费取决于它生成的 Token 数量,而这随模型、Prompt 和任务而变化。一个带有工具调用的智能体可能会从一个 500 Token 的请求膨胀成一个 15,000 Token 的多步链条。

LLM 的 OpenTelemetry 词汇表

OpenTelemetry 社区一直在制定 gen_ai.* 语义约定,为工程师提供了一套供应商中立的 LLM 遥测词汇表。这些约定定义了整个智能体堆栈中的标准 Span 类型和属性。

你将进行插桩的核心 Span 操作:

操作Span 名称覆盖内容
LLM 推理chat {model}实际的模型 API 调用
嵌入 (Embedding)embeddings {model}向量生成
检索 (Retrieval)retrieval {data_source_id}从向量数据库中获取 RAG 数据
工具执行execute_tool {tool_name}任何工具调用
智能体调用invoke_agent {agent_name}调用子智能体

每个 Span 上的关键标准属性:gen_ai.request.modelgen_ai.usage.input_tokensgen_ai.usage.output_tokensgen_ai.request.temperaturegen_ai.response.finish_reason。为了跟踪成本,你还需要 gen_ai.usage.cache_read_input_tokensgen_ai.usage.cache_creation_input_tokens —— 缓存的 Token 通常便宜 80–90%,因此缓存效率是一等性能指标。

一个重要的设计决策:Prompt 和 Completion 应该放入 Span 事件 (Span Events),而不是 Span 属性 (Span Attributes)。可观测性后端的 Payload 大小限制使得将大型 Completion 作为属性变得不切实际。包含完整消息内容的选择性事件可以保持 Span 的轻量化,并允许你按环境控制内容日志记录。

gen_ai.* 约定目前仍处于开发(Development)状态而非稳定(Stable)状态,但已被广泛实现 —— Traceloop、OpenLIT、LangSmith 和 Langfuse 都支持它们。随着规范的固化,工具之间的碎片化已大大减少。

追踪智能体链 (Agentic Chains)

单个 LLM 调用很容易进行插桩。更难的问题是在多步智能体工作流中进行分布式追踪,其中单个用户请求会在不同服务中生成子智能体、工具调用和检索操作。

W3C Trace Context 标准(traceparent 标头)是传播机制。每个 Span 都知道其父级,从而跨越服务和模型调用边界维护因果链。一个完整追踪的智能体请求在你的追踪可视化中看起来像这样:

Trace: user-request-id
└── invoke_agent orchestrator
├── chat claude-3-5-sonnet ← 第一次 LLM 调用
├── execute_tool web_search ← 工具调用
│ └── HTTP GET search-api
├── retrieval docs-vectordb ← RAG 获取
└── invoke_agent specialist-agent ← 子智能体生成
└── chat gpt-4o ← 子智能体的 LLM 调用

这个追踪会告诉你:总持续时间、哪个步骤贡献了最多的延迟、子智能体被要求执行什么任务,以及每个模型消耗了多少 Token。如果没有这种结构,你将只有一堆断开连接的 Span,无法将最终响应中的幻觉归因于三步之前糟糕的检索结果。

传播中断的地方:HTTP 调用通过 OpenTelemetry 的 HTTP 插桩自动传播追踪上下文。模型上下文协议 (Model Context Protocol, MCP) 服务器和自定义 RPC 机制通常不会 —— 你必须在服务器端手动提取 traceparent 标头并显式创建子 Span。这是智能体插桩中的一个常见缺口。

会话连续性:对于多轮对话,追踪上下文需要跨越请求。这通常意味着将 session_idconversation_id 作为 Baggage 项目与 traceparent 一起传播。像 Langfuse 这样的工具显式地对 Session → Trace → Span 层次结构进行建模,让你能够分析会话级的故障率,而不仅仅是单个请求的故障率。

真正重要的指标

值得根据你的响应方式进行区分的三个层级的指标。

第一层级 — 运维指标(针对这些设置告警):

  • Token 吞吐量(输入 + 输出 Token/秒,按模型细分)
  • 按类型细分的错误率:4xx(提示词策略违规)、5xx(供应商错误)、超时
  • p95/p99 延迟 — 对于流式传输接口,细分为 TTFT(首个 Token 时间)和 TPOT(每个 Token 的平均输出时间)
  • 每小时支出率和单用户成本

第二层级 — 质量指标(监控趋势):

  • 缓存命中率:cache_read_input_tokens / input_tokens — 突然下降标志着提示词的变化使你的缓存前缀失效了
  • Agent 工作流中的工具调用成功率和重试率
  • 来自你的评估流水线的幻觉率
  • 会话级任务完成率(需要评估,而不仅仅是追踪)

第三层级 — 优化信号:

  • 输出与输入 Token 的比率 — 异常高值表示提示词生成的响应过于冗长,可以加以限制
  • 模型路由分布 — 流量中有多少百分比流向了你最昂贵的模型层级
  • RAG 流水线中的检索相关性评分

对于流式应用,TTFT 和 TPOT 值得特别关注。TTFT 主要由 Prefill(预填充)时间决定 — 即处理输入提示词所需的时间 — 这也是用户感知的响应速度。TPOT 是 Decode(解码)阶段:一旦开始生成,Token 到达的速度有多快。这两者有不同的优化杠杆(更短的提示词可以改善 TTFT;KV 缓存命中可以同时改善两者),将它们合并为一个单一的 “LLM 延迟” 指标会掩盖这种区别。

功能层级的成本归因

在收到月度账单之前不可见的 Token 成本,是由于没有从第一天起就将成本归因接入到你的 Trace(追踪)中而导致的预料之中的后果。可以扩展的模式如下:

  1. 在顶层标记追踪。 在创建时为每个 Trace 附加 user_idfeature_nameenvironmentexperiment_id。OpenTelemetry 的 baggage 传播会自动将这些信息带入整个调用链,因此每个子 Span 都会继承这些元数据。

  2. 在摄取时计算成本,而不是在查询时。 由于价格会变动,成本不在 OTel 规范中。标准方法是:在摄取 Span 时计算 cost = (input_tokens × price_per_million) + (output_tokens × price_per_million),并将其存储为自定义属性。对于缓存命中与标准输入使用不同的费率。

  3. 针对速率告警,而不是总额。 每日支出告警太慢了。应针对超过阈值的每小时成本进行告警,并按功能进行细分。你要防范的失败模式是单个功能的流量激增消耗了整个产品的预算。

像 Helicone 这样基于代理的工具通过将所有 LLM API 调用路由经过日志代理来处理此问题 — 无需更改代码,即可立即查看每个用户和每个功能的成本。像 Langfuse 这样基于 SDK 的工具需要接入代码,但在如何构建成本层级结构方面为你提供了更大的灵活性。

无法复现时的调试

调试 Bug 的标准流程是:在本地复现它、添加日志、单步执行。对于非确定性的 LLM 行为,这套流程完全失效。LLM 系统的等效方案是在每个 Trace 中构建足够的信号,以便你可以重建模型所经历的过程,即使你无法完全重现它。

针对每个推理 Span 的最小可行复现记录:

  • 完整的提示词(系统消息 + 用户消息)
  • 模型版本(不仅仅是模型名称 — gpt-4o-2024-11-20gpt-4o-2024-08-06 是有意义的区别)
  • Temperature、top_p 以及 seed(如果已设置)
  • 供应商返回的 gen_ai.response.id

这六个字段让你能够重建模型采样的确切分布。你不会得到完全相同的输出,但你会得到来自相同分布的输出 — 而这通常足以复现一类故障。

对于 Agent 路径调试,在执行前将工具选择决策记录为带有工具参数的 Span 事件。当你将来自相同输入的 “坏” Trace 与 “好” Trace 进行对比时,工具选择中的第一个偏差通常就是根本原因所在。

在这里,基于尾部的采样(Tail-based sampling)至关重要。不要采用统一的 10% 采样率 — 这样你会丢失最需要调查的确切 Trace。对于存在错误、高成本、耗时长或评估分数低的 Trace,要 100% 保留。对于路径正常、低风险的流量,可以进行激进的采样。

选择你的工具

该领域的格局已演变为几个具有不同权衡的独特类别:

基于代理(无需更改代码): Helicone 将所有 LLM 调用路由通过一个日志层。即插即用,立即获得可见性,最适合希望以最少配置获得基础成本和延迟监控的团队。

带评估功能的基于 SDK: Langfuse、LangSmith、BrainTrust 和 Arize Phoenix 都需要 SDK 接入,但在追踪的同时为你提供更丰富的评估工作流、提示词版本控制和质量看板。对于有数据驻留要求的团队,Langfuse 和 Phoenix 是可以私有化部署的。

基础架构原生: 如果你已经在运行 Datadog 或 Honeycomb,它们的 LLM 可观测性扩展值得考虑。Datadog 具有原生的幻觉检测功能,可以标记每个 Span 中的矛盾和不支持的主张。Honeycomb 的高基数查询使得即席追踪分析特别强大。

基于 OTel 的自动接入: OpenLIT 和 Traceloop 的 OpenLLMetry 为流行的 LLM 框架提供了原生支持 OpenTelemetry 的接入,只需极少的代码更改,然后让你将遥测数据路由到你已经使用的任何后端。

决策通常取决于你是想要一个专门构建的 LLM 可观测性平台(Langfuse、Arize、LangSmith),还是想要使用 LLM 特定的规范来扩展现有的可观测性技术栈(Datadog、Honeycomb + OTel)。

可观测性作为反馈循环

从 LLM 可观测性中获益最多的团队不仅仅将其作为一种运维工具。他们正在利用 Trace 数据来辅助提示词工程决策、模型路由规则以及缓存策略。

当你看到某个特定的提示词模板缓存命中率为 12%,而另一个类似的模板命中率为 64% 时,你就知道该在哪里投入精力进行提示词重构。当你发现 40% 的 p99 延迟来自同一个检索步骤时,你就知道该在哪里优化向量索引。当你的评估流水线标记出一组源自相同数据源的幻觉时,你就知道哪个知识库需要更新。

基础设施问题已基本解决。目前尚未解决的问题在于会话层面 —— 即如何在多轮对话而非单个 span 之间聚合质量信号 —— 以及成本归因层面,目前大多数团队仍在追踪单用户成本,而不是单功能成本。这些正是下一代工具所关注的领域,也是高质量插桩投入回报最直接的地方。

References:Let's stay in touch and Follow me for more thoughts and updates