跳到主要内容

生产环境中的 LLM 可观测性:追踪不可预测的行为

· 阅读需 12 分钟
Tian Pan
Software Engineer

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

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

这正是 LLM 可观测性(Observability)所要解决的问题 —— 而这一领域在过去 18 个月里已经显著成熟。

为什么传统追踪(Tracing)不够用

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

LLM 突破了这一假设的每一个层级。模型是一个从概率分布中采样的黑盒。Temperature(温度)、Nucleus Sampling(核采样)以及模型更新都意味着即使输入完全一致,输出也会有所不同。在智能体(Agentic)系统中,这种方差会进一步复合:如果 LLM 在第一步选择了不同的工具,它会遇到不同的中间结果,从而改变第二步的决策,以此类推。到了第五步,Trace(追踪)看起来已经与昨天的“相同”请求完全不同。

这种变化带来的影响是重大的:

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

LLM 的 OpenTelemetry 词汇表

OpenTelemetry 社区一直在开发 gen_ai.* 语义约定(Semantic Conventions),为工程师提供了一套厂商中立的 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 Event(Span 事件),而不是 Span Attribute(Span 属性)。可观测性后端的有效负载大小限制使得大型补全作为属性变得不切实际。包含完整消息内容的自选事件(Opt-in Event)可以保持 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 调用

这个 Trace 告诉你:总耗时、哪一步贡献了最多的延迟、子智能体被问了什么,以及每个模型消耗了多少 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 time)决定 — 即处理输入提示词所需的时间 — 这也是用户感知的响应速度。TPOT 是解码阶段:生成开始后 Token 到达的速度。这两者有不同的优化杠杆(更短的提示词可以改善 TTFT;KV 缓存命中可以同时改善两者),将它们合并为一个单一的 “LLM 延迟” 指标会掩盖这一区别。

功能层级的成本归因

直到月度账单寄到才被发现的 Token 成本,是第一天起就没有将成本归因接入追踪系统的必然结果。可扩展的模式:

  1. 在顶层标记追踪。 在创建每个追踪时附带 user_idfeature_nameenvironmentexperiment_id。OpenTelemetry 的行李传播(baggage propagation)会自动将这些信息带入整个调用链,因此每个子跨度(child span)都会继承这些元数据。

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

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

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

无法复现时的调试

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

每个推理跨度(inference span)的最小可行复现记录:

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

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

对于 Agent 路径调试,在执行前将工具选择决策记录为带有工具参数的跨度事件(span events)。当你将来自相同输入的 “坏” 追踪与 “好” 追踪进行对比时,工具选择中的第一个分歧点通常就是根因所在。

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

选择你的工具

该领域已划分为几个具有不同权衡的独特类别:

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

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

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

基于 OTel 的自动插桩: OpenLIT 和 Traceloop 的 OpenLLMetry 通过极少的代码更改,在流行的 LLM 框架中为你提供原生 OpenTelemetry 插桩,然后让你将遥测数据路由到你已经使用的任何后端。

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

可观测性作为反馈循环

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

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

基础设施层面的问题已基本解决。目前待解决的问题集中在会话层级 —— 即在多轮对话中聚合质量信号,而非仅关注单个 span —— 以及成本归因层级,目前大多数团队仍在追踪单用户成本,而非单功能成本。这些正是下一代工具所关注的领域,也是良好的埋点 (instrumentation) 投入能最直接获得回报的地方。

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