跳到主要内容

生产环境中的 LLM 可观测性:工程师容易忽略的四个隐性故障

· 阅读需 12 分钟
Tian Pan
Software Engineer

大多数将 LLM 应用推向生产环境的团队,其日志设置常被误认为是可观测性。他们在数据库中存储提示词(prompt)和响应,在表格中跟踪 token 数量,并在 Datadog 中设置延迟告警。然而,当用户反馈聊天机器人已经连续两天给出错误回答时,没人能告诉你原因 —— 因为收集到的数据都没有告诉你模型是否真的正确。

传统监控回答的是“系统是否在线且速度多快?”而 LLM 可观测性回答的是一个更难的问题:“系统是否在做它应该做的事情,以及它在什么时候停止了这种正常行为?”当你的系统行为是概率性的、依赖上下文的,并且经常以不触发任何告警的方式出错时,这种区别就显得至关重要。

四大隐性故障

在构建可观测性基础设施之前,了解你真正想要捕捉的内容会有所帮助。LLM 系统有四种标准监控完全无法察觉的失败方式:

迷之自信的错误(Confident errors)。模型返回了一个错误的答案,但没有任何不确定的迹象。没有异常,没有 4xx 状态码,你的指标中也没有升高的错误率。响应看起来与正确的响应完全一致。一个客服机器人引用了一份已经失效六个月的退货政策,而且语气听起来非常权威。如果没有针对生产流量运行评估(evaluation),这永远不会出现在任何仪表盘中。

隐性漂移(Silent drift)。随着系统周围世界的变化,性能逐渐下降。模型的训练数据变得陈旧。你的产品描述更新了,但 RAG 管道中的上下文没有更新。六个月前运行良好的提示词开始产生偏差,因为其编写时的背景已经发生了变化。你只有在用户投诉时才会注意到,而不是在此之前。

无限制的成本(Unbounded costs)。Token 数量以设计时并不明显的方式复合增长。失败时的重试逻辑使你的支出翻倍或翻三倍,却没有带来任何功能上的改进。随着对话历史的增长,上下文窗口被填满。一个范围界定不良的 agent 循环进行了 40 次工具调用而不是 4 次。这些都不会显示为错误 —— 只会显示在账单上。

不透明的推理(Opaque reasoning)。当 agent 采取了错误的行动或链式调用产生了一个糟糕的输出时,你需要准确追踪是哪一步引入了错误。在标准日志记录下,你只有输入和输出,而没有中间状态:检索了哪些文档、重排序器(reranker)给它们打了多少分、工具调用是否返回了预期结果、模型如何解释结果。调试就像是在考古。

可观测性的真正要求

LLM 可观测性的五大支柱反映了上述系统的失败方式:

可靠性(Reliability) 涵盖了传统监控擅长的部分 —— 延迟百分位数、供应商错误率、速率限制恢复、可用性。这是入场门槛。

质量(Quality) 是大多数团队存在差距的地方。你需要在生产流量上持续衡量事实准确性和依据(grounding)成功率,而不仅仅是在离线评估期间。了解模型是否仍然正确的唯一方法就是对其进行评估。

安全性(Safety) 意味着跟踪越狱尝试、响应中的个人隐私信息(PII)泄露以及有害内容 —— 如果你的系统处理敏感领域或服务于广泛的用户群体,这一点尤为重要。

成本(Cost) 需要按请求进行核算,而不仅仅是每月总额。你需要知道哪些类型的请求是昂贵的,重试风暴发生在何处,以及缓存是否有效。

治理(Governance) 是审计追踪:系统所做出的每一个决策及其原因的完整可追溯性,其可复现程度足以回答来自法律、合规部门或愤怒客户的提问。

同时构建所有这五个支柱是不现实的。实际的实施路线图分为几个阶段:从基础日志记录和追踪 ID(trace ID)开始,标准化遥测(telemetry)并构建仪表盘,增加护栏(guardrails)和评估,然后分层加入多模型路由和 agent 追踪,最后实现治理自动化。

分布式追踪作为基础

对于任何比单个 LLM 调用更复杂的系统,分布式追踪都是结构性基础。OpenTelemetry 已成为标准的插桩(instrumentation)层 —— 它将数据收集与数据存储分离,在防止供应商锁定的同时,允许你将 span 路由到任何合适的后端。

心智模型:一个 trace 代表一个请求的完整生命周期。Span 代表该请求中的单个操作。用户向 RAG 应用提出的问题可能会生成一个 trace,其中包含查询嵌入(query embedding)、文档检索、重排序(reranking)、提示词组装、LLM 调用和响应解析的 span。每个 span 都携带自己的计时、输入、输出和属性。

针对 LLM 调用的最小可行 span 应该捕获以下内容:

from opentelemetry import trace

tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span("llm_call") as span:
span.set_attribute("llm.model", "claude-3-5-sonnet")
span.set_attribute("llm.prompt_tokens", prompt_token_count)
span.set_attribute("llm.temperature", 0.7)
# 将提示词/响应作为事件(event)而非属性(attribute)捕获
# —— 如果作为属性存储,大型负载可能会导致 span 导出器崩溃
span.add_event("prompt", {"content": prompt_text})
response = call_llm(prompt_text)
span.add_event("response", {"content": response.text})
span.set_attribute("llm.completion_tokens", response.usage.completion_tokens)
span.set_attribute("llm.latency_ms", response.latency_ms)

一个重要的实现细节:提示词和响应应该作为 span 上的 事件(events) 而不是 span 属性来捕获。大多数后端的 span 属性都有大小限制;以属性形式存储的 4K-token 提示词会隐式截断或导致导出器崩溃。事件可以正确处理大型负载。

对于像 LangChain 或 LlamaIndex 这样的框架,自动插桩库会自动处理这些:

from openinference.instrumentation.langchain import LangChainInstrumentor

LangChainInstrumentor().instrument()
# 所有的 LangChain 调用现在都会自动发送 span

自动插桩涵盖了常见情况,但往往会遗漏应用层上下文 —— 用户 ID、会话 ID、功能标志(feature flags)、A/B 测试变体。这些需要通过 baggage 或 span 属性手动注入。没有它们,追踪将难以与实际的用户体验相关联。

RAG 调试工作流

RAG 系统至少在五个不同的环节可能出现故障:查询嵌入(query embedding)、检索(retrieval,返回了错误的文档)、重排序(reranking,正确文档评分较低)、上下文准备(context preparation,截断或排序问题),以及 LLM 对检索上下文的理解。没有追踪(traces),一个错误的回答就仅仅是一个错误的回答。有了追踪,你可以准确地看到故障源自何处。

当 RAG 查询返回错误回答时的调试序列:

  1. 获取该特定请求的追踪记录
  2. 检查检索跨度(retrieval span)——返回了哪些文档?它们的相似度得分是多少?查询时索引中是否存在正确的文档?
  3. 如果检索正确,检查上下文组装(context assembly)——相关的段落是否被截断了?它是否处于模型倾向于忽略的位置?
  4. 如果上下文完整,检查 LLM 跨度(LLM span)——模型是使用了检索到的上下文,还是忽略了它并产生了幻觉?内联运行的 Grounding 评估器可以自动标记这一点

只有当你的跨度(spans)捕获了中间状态时,此工作流才有效——包括检索到的文档 ID 和得分、组装好的上下文字符串,以及进入模型前的最终提示词(prompt)。仅记录整个流水线的输入和输出,会使第 2 步和第 3 步变得不可能。

Agent 的可观测性有所不同

Agent 引入了单次调用系统所不具备的可观测性挑战。多步 Agent 不是简单的请求/响应——它是一个包含状态、分支和循环的过程。标准的追踪层级结构无法清晰地映射到这一点上。

对 Agent 而言,重要的指标与单次调用指标不同:

  • 规划效率:Agent 完成一个目标需要多少个推理步骤?随着提示词的偏移(drift),这个数字是否在随时间增加?
  • 工具执行质量:哪些工具正在被调用?重试次数是否在增加?工具是否返回了被 Agent 默默忽略的错误?
  • 目标完成率:Agent 是否真的完成了任务?它在什么比例下会放弃、陷入循环或产生无意义的回答?
  • 每个完成目标的成本:这是真正的单元经济效益问题。不是每个请求的成本,而是每个成功完成的任务的成本。

Agent 可观测性中最困难的部分是,由于 LLM 的输出是非确定性的,对于相同的输入,不同运行次数下的同一条追踪记录可能看起来大不相同。比较两条 Agent 追踪记录需要标准化的指标,而不是原始跨度的比较。

工具调用日志记录尤为重要,但经常被忽略。当 Agent 调用一个函数时,你希望获取函数名称、参数(脱敏 PII 信息)、返回值、延迟以及是否发生了重试。一个常见的故障模式是:Agent 进行工具调用,收到错误,重试了四次,耗尽了上下文窗口,最后返回一个模糊的失败响应。如果没有工具调用跨度,追踪记录只会显示“Agent 失败”。

评估必须在生产流量上运行

离线评估(在部署前针对一组固定的示例运行测试套件)可以告诉你模型在某一时刻是否达到了标准。但它无法告诉你模型在部署后是否能继续保持该标准,因为生产数据的分布会发生偏移,你会更新提示词,底层的模型也会更新。

生产环境评估弥补了这一差距。将一部分真实请求持续通过评估器(LLM 作为评委、嵌入相似度、基于规则的检查)运行,可以为你提供一个质量信号,当某些环节出错时,该信号会降低。像 Langfuse、Braintrust 和 Arize 这样的平台都支持将评估器附加到生产追踪中。

实际的约束在于成本。在 100% 的生产流量上运行 LLM 作为评委的评估器,会使你的推理开销翻倍。标准方法是采样——评估 5-10% 的请求,按请求类型、用户群组或最近的提示词更改进行分层。当部署更改了系统提示词或模型版本时,进行 100% 的评估。

需要关注的指标不是单一的质量得分,而是趋势。一个稳定在 0.78 的质量得分要好于上周是 0.85 而现在是 0.72 的得分。警报应基于斜率(趋势)触发,而不仅仅是阈值。

从哪里开始

常见的错误是试图在任何东西发挥作用之前,一次性对所有内容进行检测。首先从贯穿每个请求的追踪 ID(trace IDs)开始,这样你就可以为任何用户报告的问题获取完整的上下文。仅凭这一点就比大多数仪表板更有价值。

然后添加请求级别的成本追踪。你会立即发现哪些请求类型是昂贵的,以及是否有任何请求超出了预期。

接着在生产流量上添加一个质量评估器——哪怕是简单的基于规则的评估器。回答在应该包含引用时包含了吗?它是否在预期的长度范围内?它是否拒绝了不该拒绝的请求?

其他一切——幻觉检测、Agent 追踪、治理自动化——都叠加在这个基础之上。那些试图在发布任何东西之前构建完整的“五大支柱”系统的团队,往往最终什么也发布不了。

最小可行 LLM 可观测性技术栈包括:追踪 ID、每个请求的 Token 成本,以及生产流量上的一个质量检查。除此之外的其他一切都是优化。


大多数 LLM 应用悄无声息地失败的原因是,它们的构建者将传统的软件可观测性直觉应用到了一个以非传统方式失败的系统上。LLM 在出错时不会抛出异常。它在发生偏移时也不会返回 500 错误。失败模式是一个看起来正确但实际错误的响应——捕捉它的唯一方法是从一开始就将评估构建到你的生产循环中。

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