AI Agent 幻觉、死循环、烧钱:OpenTelemetry GenAI 把黑盒拆成三根可观测的柱子
过去一年我经手了三个 LLM Agent 生产落地项目,有两个在第二周就被迫回滚。原因不是模型能力不行,是我们在盲目飞行。Agent 在用户面前自信地编造数据,在后台反复调用同一个工具十七次,账单从每天两百块飙到四千块,而 APM 面板上一片绿色。传统可观测性擅长回答“服务挂了没有”,回答不了“Agent 到底在干什么”。OpenTelemetry GenAI 语义约定正在把这个黑盒拆开。
为什么传统 APM 在 Agent 面前失效
Prometheus 的 RED 指标和分布式追踪是为确定性系统设计的。一个 HTTP 请求进一个服务,出一个响应,延迟和错误码就是全部真相。Agent 不是这样运转的。一个 Agent 回合可能触发五次模型调用、三次工具执行、两次检索,每一步的输入输出都是自然语言,每一步都有概率偏离轨道。
Dynatrace 在收购 Arize 时把这个问题讲得很清楚:AI 会静默失败,产生一个自信、看似合理但完全错误的响应,不触发任何错误码。Agent 可能完成了工作流但走错了路径,可能基于过时上下文行动,可能更新了错误的记录。服务表面健康,结果已经错了。
传统 APM 的 span 里没有 gen_ai.operation.name,没有 token 用量,没有工具调用的参数和返回值。你看到一条八百毫秒的 trace,无法判断它是“检索后回答”还是“模型自己编了一个答案”。OpenTelemetry GenAI 语义约定补的就是这块。
回到 2025 年,业界的普遍做法是把 LLM 调用当成黑盒:上报一个总耗时,记录一个 token 总数,再附一句模型名字。这套做法在 RAG 单轮问答里勉强够用,一进 Agent 场景立刻破功。一个像样的研究型 Agent 会在一次任务里调用检索、读写文件、调外部 API、再回头修订上一轮的输出,每一环都用到模型。把这一切揉成一条记录,跟只看一辆卡车的总里程表就想知道引擎、刹车、转向各自健康状况没什么两样。
更棘手的是,Agent 的失败模式往往不是技术栈层面的,是语义层面的。模型没有报错,HTTP 状态码全是 200,但它给出的答案是错的。传统的错误率面板上一片祥和,业务侧已经出了问题。这种错位是过去一年里大多数 Agent 团队被迫自建可观测性方案的真正动因。
三根柱子分别盯什么
OpenTelemetry 把可观测性拆成 traces、metrics、logs。映射到 Agent 场景,每一根柱子负责一类失控。
Traces 抓幻觉的传播路径
幻觉很少凭空产生。它通常有源头:检索回来一段无关的文档,模型基于错误上下文推理;或者工具返回了异常结果,模型选择“圆过去”而不是报错。没有 trace,你只看到最终的错误回答,看不到它是从哪一步开始烂的。
OpenTelemetry GenAI 约定定义了一套 Agent 特有的 span 类型。2026 年 6 月的 OTel walkthrough 展示了一条标准 Agent 轨迹:invoke_agent 作为父 span,下面挂 chat 子 span,再下面挂 execute_tool。这个层次结构让检索、推理、工具执行各自可见。Arize Phoenix 和 Langfuse 都基于这套结构做轨迹可视化,Langfuse 的 Graph 视图甚至能把循环展开成 DAG,直观看到 Agent 在哪个节点反复打转。
Metrics 管烧钱速率和死循环检测
Token 消耗不是一个需要事后审计的账单项目,它是一个可以实时告警的时序指标。OpenTelemetry GenAI 约定定义了 gen_ai.client.token.usage 直方图,按模型、操作类型、token 方向拆维度。在 Grafana 里画一条按 model 分组的 token 速率曲线,烧钱异常通常在两分钟内就能定位到具体模型或具体 Agent。
死循环检测更依赖 metrics 的形态。一个正常的 Agent 回合,工具调用次数分布集中在个位数。当某个 trace 的 execute_tool 计数突破二十,或者同一 Agent 的 chat span 速率在时间窗口内出现周期性峰值,这就是循环的指纹。OpenLIT 和 Elastic 的 Agent 可观测性面板都内置了这类成本异常检测。
Logs 承载语义级判断
Token 数字告诉你“花了多少”,日志告诉你“花得值不值”。OpenTelemetry GenAI 约定允许把评估结果作为 log record 发出,事件名是 gen_ai.evaluation.result。这意味着你可以把幻觉检测器、事实一致性打分、安全护栏命中情况都写进同一个日志管道。
Elastic 在其 LLM 可观测性方案里把 guardrails 监控做进了日志层,检测敏感数据泄露、有害内容、提示注入,以及幻觉。Langfuse v4 在 2026 年 8 月引入的多模态评估器可以直接对 trace 里的音频、图像、PDF 打分,评估结果以 score 的形式挂在 span 上。
一张表看传统 APM 和 GenAI 可观测性的差距
| 维度 | 传统 APM | OpenTelemetry GenAI |
|---|---|---|
| Span 语义 | HTTP/gRPC 方法、状态码 | gen_ai.operation.name、模型、token 用量 |
| 父子关系 | 服务调用链 | invoke_agent → chat → execute_tool |
| 成本信号 | 不存在 | gen_ai.client.token.usage 直方图 |
| 质量信号 | 错误码 | gen_ai.evaluation.result 事件 |
| 工具可见性 | 无 | 工具名、参数、结果、错误类型 |
| 幻觉定位 | 不可能 | 通过检索 span 和 chat span 对比溯源 |
可落地的代码:用 OTLP 手动上报 GenAI 语义约定
下面这段 Python 代码不依赖 LangChain 或任何框架,直接构造符合 OTel GenAI 约定的 span 和 metrics。它演示了一个 Agent 回合的完整上报:先开 invoke_agent span,再开 chat span,记录 token 用量和评估结果。
需要先安装依赖,用 pip 装 opentelemetry-api、opentelemetry-sdk、opentelemetry-exporter-otlp-proto-http 三个包。
import time
import uuid
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
trace_exporter = OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
span_processor = BatchSpanProcessor(trace_exporter)
provider = TracerProvider()
provider.add_span_processor(span_processor)
trace.set_tracer_provider(provider)
metric_exporter = OTLPMetricExporter(endpoint="http://localhost:4318/v1/metrics")
reader = PeriodicExportingMetricReader(metric_exporter, export_interval_millis=5000)
meter_provider = MeterProvider(metric_readers=[reader])
metrics.set_meter_provider(meter_provider)
tracer = trace.get_tracer("agent.observability")
meter = metrics.get_meter("agent.observability")
token_usage = meter.create_histogram(
name="gen_ai.client.token.usage",
unit="{token}",
description="Token usage per GenAI operation"
)
def run_agent_turn(user_query: str):
agent_span = tracer.start_span("invoke_agent")
agent_span.set_attribute("gen_ai.operation.name", "invoke_agent")
agent_span.set_attribute("gen_ai.agent.name", "research_agent")
chat_span = tracer.start_span("chat", parent=trace.set_span_in_context(agent_span))
chat_span.set_attribute("gen_ai.operation.name", "chat")
chat_span.set_attribute("gen_ai.request.model", "gpt-4o")
chat_span.set_attribute("gen_ai.provider.name", "openai")
input_tokens = 420
output_tokens = 180
chat_span.set_attribute("gen_ai.usage.input_tokens", input_tokens)
chat_span.set_attribute("gen_ai.usage.output_tokens", output_tokens)
token_usage.record(input_tokens, {"gen_ai.operation.name": "chat", "gen_ai.token.type": "input"})
token_usage.record(output_tokens, {"gen_ai.operation.name": "chat", "gen_ai.token.type": "output"})
time.sleep(0.8)
chat_span.end()
tool_span = tracer.start_span("execute_tool", parent=trace.set_span_in_context(agent_span))
tool_span.set_attribute("gen_ai.operation.name", "execute_tool")
tool_span.set_attribute("gen_ai.tool.name", "web_search")
tool_span.set_attribute("gen_ai.tool.call.id", str(uuid.uuid4()))
time.sleep(1.2)
tool_span.set_attribute("gen_ai.tool.call.result", "retrieved: 3 documents")
tool_span.end()
agent_span.set_attribute("gen_ai.evaluation.hallucination_score", 0.12)
agent_span.end()
if __name__ == "__main__":
run_agent_turn("对比 OTel GenAI 和传统 APM 的差异")
time.sleep(6)
这段代码的关键设计:invoke_agent 作为根 span 锚定整个回合,chat 和 execute_tool 作为子 span 保留因果链。token 用量同时写入 span 属性和直方图指标,前者用于单次追溯,后者用于聚合告警。评估分数挂在 Agent span 上,回溯时可以直接过滤“幻觉分数超过 0.5 的 trace”。
生产部署时还有两个细节容易被忽略。第一,BatchSpanProcessor 的批量队列上限要按 Agent 的并发度调整,默认的 2048 在高并发 Agent 场景下会丢 span,一旦丢的是 execute_tool 这种关键节点,整条 trace 的因果链就断了。第二,metric 的 export_interval_millis 不宜太短,5 秒是个合理的下限,再低会让 Collector 端的 OTLP 流量翻倍,反而把可观测性自身变成了成本项。
另外一个反直觉的经验:不要把 token 用量的所有维度都打到指标里。模型名、操作类型、token 方向这三个维度足够定位 90% 的成本问题。把用户 ID 或会话 ID 这种高基数值当 metric 标签是 Prometheus 和 OTel Metrics 共同的反模式,标签基数爆炸会让指标后端在几天内崩溃。高基数信息放到 span 属性里,通过 trace 检索,不进聚合管线。
Langfuse、OpenLit、Phoenix 三家能力对比
这三家是目前 Agent 可观测性领域最活跃的开源选择,都基于 OpenTelemetry。
| 能力 | Langfuse | OpenLit | Arize Phoenix |
|---|---|---|---|
| 许可协议 | MIT 核心 + 商业云 | Apache 2.0 | Apache 2.0 |
| OTel 原生 | 是,OTLP 接收 | 是,SDK 自动埋点 | 是,OpenInference instrumentation |
| 幻觉评估 | LLM-as-a-Judge、多模态评估器 | LLM-as-a-Judge、程序化评估 | LLM 评估、代码评估、人工标注 |
| 成本追踪 | 按模型和 trace 聚合 | 按模型、provider、request | 按 span 聚合 |
| 独特能力 | 多模态评估、Dashboard as Code、Graph 视图 | Guardrails、Prompt Hub、GPU 监控 | Datasets & Experiments、Span Replay |
| 自托管 | Docker Compose、K8s | Docker Compose、Helm | Docker、K8s |
Langfuse v4 在 2026 年 8 月的更新把 UI 性能提升了最高 165 倍,大规模项目下 dashboard 加载快至少 10 倍。OpenLit 的优势在于零代码埋点和内置的 prompt injection 护栏。Phoenix 的评估工作流最成熟,Dataset 和 Experiment 机制适合做回归测试。
Dynatrace 在 2026 年 9 月完成对 Arize 的收购,交易价值 9.15 亿美元。Phoenix 保持开源,但背后的商业支持从独立公司变成了 Dynatrace。这个变化对选型的影响需要观察,但 Phoenix 的 OpenTelemetry 和 OpenInference 根基不会变。
上线节奏:别想着一步把所有 Agent 都接进来
从我们的实战看,最稳的路径是先挑一个最痛的 Agent 接 OTel GenAI,跑两到四周,把告警阈值和评估器调稳,再铺到其他 Agent。一上来就全量埋点的团队几乎都会被告警风暴淹没,最后不得不关掉所有规则回到黑盒状态。
选第一个接入对象时优先选"用户感知最强、失败最贵"的那个,而不是技术上最容易的那个。技术容易的 Agent 往往失败成本也低,跑了半个月攒不出足够说服管理层的对比数据。挑对的 Agent,一周就能看到幻觉拦截的真实案例。
落地检查清单
- 确认你的 Agent 框架是否在 OpenTelemetry 注册表中。LangChain、LlamaIndex、CrewAI、OpenAI Agents SDK 都有社区埋点,用 opentelemetry-instrumentation 一行包装就能产生 GenAI span。
- 在 Grafana 或你现有的 dashboard 里加三条曲线:按 model 分组的 token 速率、按 agent 分组的 execute_tool 调用计数、chat span 的 p95 延迟。这三条曲线覆盖烧钱、死循环、性能退化。
- 给 invoke_agent span 加上 gen_ai.agent.name 和 gen_ai.agent.version 属性。没有版本维度的数据无法做回归归因。
- 部署一个幻觉检测器,把分数作为 gen_ai.evaluation.result 事件写出。可以从简单的检索-回答一致性对比开始,不需要一开始就上 LLM-as-a-Judge。
- 为每个 Agent 设置 token 速率告警阈值,不是一个总量阈值,是每分钟消耗量的阈值。总量告警响应太慢,速率告警能在循环膨胀前触发。
- 检查 OTel Collector 的配置,确保 GenAI span 的 message content 属性没有被意外丢弃。OTel GenAI 约定把内容字段设为 opt-in 且敏感,默认可能不采集。
- 锁定 semantic conventions 的 schema 版本。GenAI 约定仍在 Development 状态,2026 年 4 月刚调整过 execute_tool 的 span 命名。用 OTEL_SEMCONV_STABILITY_OPT_IN 控制升级节奏。
- 从一个非关键 Agent 开始埋点,跑一周,对比埋点前后的幻觉投诉率和 token 账单。用真实数据说服团队投入可观测性改造。