AI 应用的可观测性设计:从 OpenTelemetry GenAI 语义约定到生产级落地架构
引言:为什么传统 APM 在 LLM 场景会"失明"
把一个大模型调用放进生产环境的第一周,你大概率会收到三种 P0 告警:账单爆表、某个 prompt 让所有用户拿到 5 秒空白回复、模型在某次静默升级后开始胡说八道。Datadog 的 HTTP 平均延迟告诉你"系统很健康",但你已经亏了一天的预算并且流失了用户。这不是 APM 工具不行,而是它们的语义模型从诞生起就没假设过"请求的成本由 token 决定、行为由概率决定、错误不是异常而是分布漂移"。LLM 应用必须有一张独立于传统监控的可观测性图,而这张图的骨架,就是 OpenTelemetry GenAI 语义约定。
一、LLM 系统的五大可观测性陷阱
在进入工具与代码之前,先把"为什么不能复用 Prometheus + Grafana"讲清楚。LLM 系统有五个维度打破了传统监控的假设:
- 非确定性。同一个 prompt 在 temperature=0.7 下会产生不同输出。没有捕获请求时刻的 prompt 全文、模型 ID、温度参数和随机种子,问题就无法复现。传统 APM 给你的是 stack trace,LLM 给你的应该是一条"决策快照"。
- Token 计价的成本模型。一个 128k 长上下文请求的账单可能是普通请求的 100 倍。监控"每秒请求数"完全看不到成本结构,需要的是"每秒 token"和"每次请求 token 分布"。
- 多步 Agent 链。一次用户请求背后可能是 5 次 LLM 调用、3 次工具调用、2 次向量检索。任何一个步骤失败、变慢或变贵都会污染最终体验。传统的 service map 会把这堆调用画成一团乱麻。
- Prompt 敏感性。"helpful assistant"和"You are a helpful assistant"之间可能隔着一次安全策略翻转。Prompt 既是配置又是输入,需要像 Git 一样版本化地追踪。
- 内容合规风险。Prompt 里大概率有 PII、商业机密、医疗信息。直接把它们打到日志后端就是合规事故。可观测性必须从设计上就把"捕获 vs 留存 vs 脱敏"当成一等公民。
二、OpenTelemetry GenAI 语义约定:唯一的"通用语言"
2026 年 6 月,OpenTelemetry 核心仓库 v1.42 把 GenAI 相关约定移到了独立仓库 open-telemetry/semantic-conventions-genai,并把状态标记为 Development。这意味着两件事:约定已经事实上成为跨厂商的事实标准,但字段名还会变,工程上必须做版本快照。
核心属性命名空间统一是 gen_ai.*,下面是生产代码里一定要打的几个:
| 属性 | 类型 | 含义 |
|---|---|---|
gen_ai.system / gen_ai.provider.name |
string | openai / anthropic / aws.bedrock 等 |
gen_ai.operation.name |
string | chat / embeddings / execute_tool / invoke_agent |
gen_ai.request.model |
string | 你请求的模型名 |
gen_ai.response.model |
string | 实际响应的模型名(别名/路由/降级时可能不同) |
gen_ai.request.temperature / top_p / max_tokens |
float / int | 采样参数 |
gen_ai.usage.input_tokens / output_tokens |
int | token 用量 |
gen_ai.response.finish_reasons |
string\[\] | stop / length / tool_calls / content_filter |
需要特别强调的两条工程纪律:
- Prompt 与 completion 内容走 span events,不走 attributes。Attributes 会被所有 backend 索引,超大且含敏感数据。Events 可以在 OTel Collector 里被过滤或丢弃,应用代码完全无感。
gen_ai.request.model和gen_ai.response.model的差值本身就是告警维度。当 OpenAI 把 gpt-4o 后台升级成 gpt-4o-2025-08-06,你的行为可能变化,但部署没动。这个 gap 是监测模型漂移的最便宜信号。
约定还为 Agent 系统定义了 create_agent / invoke_agent / execute_tool 三类 span,分别承载"代理被创建/调用/工具执行"的语义,加上 gen_ai.agent.name 和 gen_ai.tool.name 两个属性,就能在 trace UI 里把一棵决策树完全展开。
三、三大信号支柱:怎么把语义约定变成可查询的数据
OpenTelemetry 用统一的三大信号承载 GenAI 遥测。理解它们各自的角色是设计埋点的前提。
Spans 是 LLM 操作的最小工作单元。一个 chat completion 是一个 span,一个工具调用是一个嵌套 span,一个 Agent 循环是一棵 span tree。Span 是定位"哪一步出问题"的唯一单位。
Metrics 是可聚合的数字。GenAI 的官方 metric 目前都是 Development 状态,包括:
gen_ai.client.token.usage(Histogram,单位 token)gen_ai.client.operation.duration(Histogram,单位 s)gen_ai.client.operation.time_to_first_chunk(Histogram,单位 s,TTFT)gen_ai.client.operation.time_per_output_chunk(Histogram,单位 s)
TTFT 和"每个 chunk 的间隔"是流式场景下拆解延迟的标准切片。一个 latency 退化如果发生在 TTFT,那是 prefill/排队问题(批大小、调度器);如果发生在 chunk 间隔,那是 decode 问题(KV Cache、投机解码)。这两类问题的优化路径完全不同,混在一起看就只能瞎调。
Events/Logs 是高基数内容。Prompt、completion、检索到的 chunk、工具参数和返回值,都应该进 span events 而非 metrics。可以用 gen_ai.system.message、gen_ai.user.message、gen_ai.assistant.message、gen_ai.tool.message 四个事件名规范内容捕获。
四、Agent 系统的层级追踪模型
LLM 应用从"单次 prompt"演化成"Agent 循环"后,可观测性的复杂度跳跃了一个量级。一个生产 Agent 至少有三层 span:
css
agent.run(根 span,invoke_agent)
├─ llm.think(gen_ai.chat span)
│ └─ tool.search(execute_tool span)
│ └─ vector.query(retrieval span)
├─ llm.think(gen_ai.chat span)
│ └─ tool.search(execute_tool span)
└─ llm.answer(gen_ai.chat span,最终输出)
这三层每一层都有自己的成本、延迟和失败模式。把它们画在一棵 trace tree 里,你就能直接回答业务方最常问的几个问题:
- "为什么这次回答这么贵?" → 看哪一步 LLM 调用消耗了 80% 的 token
- "为什么这次回答这么慢?" → 看 TTFT 还是 tool 调用拉垮
- "为什么这个用户没拿到正确答案?" → 看哪一步 retrieve 错了、哪一步工具返回了空
更进一步,给每个 tool span 加上 gen_ai.tool.name 后缀,你就能用一句 PromQL 查询出"上周工具 X 的失败率上升了 30%",而不需要解析日志。
五、主流平台横评:Langfuse / LangSmith / Helicone / Phoenix / Portkey
工具选型本质是问三个问题:你愿意被锁定吗?你愿意为便利付多少钱?你团队有没有平台工程能力?
| 平台 | 定位 | 开源 | 自托管 | 上手时间 | 核心优势 | 关键短板 |
|---|---|---|---|---|---|---|
| Langfuse | 开源生产可观测性 + 评估 | MIT | 是 | ~30min | 完整 span tree、prompt 版本化、LLM-as-judge、社区最大 | 需要 SDK 集成,非代理模式 |
| LangSmith | LangChain 深度集成 | 否 | 仅企业版 | ~30min | LangChain/LangGraph 零配置 trace、dataset 与 eval 成熟 | 离开 LangChain 价值大减 |
| Helicone | 代理模式 + 成本监控 | 否 | 否 | ~2min | 改一行 base URL 即可,100+ 模型成本仪表盘 | 请求级而非 span 级,2026-03 被 Mintlify 收购进入维护模式 |
| Arize Phoenix | OTel 原生 + ML 评测 | MIT | 是 | ~20min | embedding drift、RAG 指标、OTel 一等公民 | 部署复杂,2-4 周才能跑稳生产 |
| Portkey | AI 网关 + 可观测性 | 部分 | 否 | ~15min | 网关级路由、guardrail、语义缓存 | 可观测性是网关附赠,深度有限 |
按 2026 年中最新状态做选型决策:
- 如果你要数据主权、要自托管、要长期不被锁定:Langfuse。2026 年 1 月 ClickHouse 收购 Langfuse 后,MIT 许可覆盖整个核心产品,反而打消了"开源项目被收购就闭源"的担忧。
- 如果你已经全栈 LangChain/LangGraph:LangSmith。零配置 trace 的便利远超其定价溢价。
- 如果你只是想要成本可见性、几小时内就要上线:Helicone。代理模式仍然是最快路径,但要接受"在维护模式"的产品风险。
- 如果你的运维团队已经在用 Datadog/Grafana:用 OTel 直接打到你现有的栈,再叠加一个轻量 LLM 评估工具。不要为 LLM 单独买一套后端。
六、生产级架构:从 SDK 到 Collector 到多后端
一段能跑的 OTel GenAI 埋点代码大致如下:
python
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
import openai
# 应用启动时埋点一次
OpenAIInstrumentor().instrument()
exporter = OTLPSpanExporter(endpoint="otel-collector:4317", insecure=True)
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(exporter))
tracer = trace.get_tracer("ai-service")
# 业务代码
def ask(prompt: str, model: str = "gpt-4o") -> str:
with tracer.start_as_current_span("gen_ai.chat") as span:
span.set_attributes({
"gen_ai.provider.name": "openai",
"gen_ai.operation.name": "chat",
"gen_ai.request.model": model,
"gen_ai.request.temperature": 0.7,
})
span.add_event("gen_ai.user.message", {
"gen_ai.prompt.content": prompt[:500], # 截断长度
})
try:
resp = openai.OpenAI().chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
)
msg = resp.choices[0].message.content
usage = resp.usage
span.set_attributes({
"gen_ai.response.model": resp.model,
"gen_ai.usage.input_tokens": usage.prompt_tokens,
"gen_ai.usage.output_tokens": usage.completion_tokens,
"gen_ai.response.finish_reasons": [resp.choices[0].finish_reason],
})
span.add_event("gen_ai.assistant.message", {
"gen_ai.completion.content": msg[:500],
})
return msg
except Exception as e:
span.record_exception(e)
span.set_status(Status(StatusCode.ERROR))
raise
注意 prompt[:500] 这类截断。生产代码必须做:内容截断、PII 字段名脱敏(regex 替换邮箱/手机/身份证)、按用户控制 opt-in 标志。
OTel Collector 的角色是唯一值得信赖的数据网关,它要承担四个责任:
yaml
receivers:
otlp:
protocols: { grpc: {}, http: {} }
processors:
batch: { timeout: 5s }
attributes/pii:
actions:
- key: gen_ai.prompt.content
action: hash
- key: gen_ai.completion.content
action: hash
filter/noisy:
spans:
drop:
- 'attributes["gen_ai.operation.name"] == "embeddings"'
exporters:
otlp/jaeger:
endpoint: jaeger:4317
otlp/langfuse:
endpoint: langfuse:3000
prometheus: { endpoint: 0.0.0.0:9464 }
service:
pipelines:
traces: { receivers: [otlp], processors: [batch, attributes/pii], exporters: [otlp/jaeger, otlp/langfuse] }
metrics: { receivers: [otlp], processors: [batch], exporters: [prometheus] }
通过 Collector 一份入站、三份出站(Jaeger 看 trace、Langfuse 看 LLM 上下文、Prometheus 看指标),既保留了厂商无关性,又获得了专用 LLM 后端的可视化能力。这是"不锁定"和"有得用"之间的最优解。
七、Token 成本监控与告警策略
可观测性的最大商业价值不是"看 trace",而是"少亏钱"。下面是一套经过验证的成本告警模板:
- 每日预算硬告警 :
sum(rate(gen_ai_client_token_usage_total[5m])) * 单价 > 80% * 日预算,触发后立刻熔断。 - 单请求成本 P99 突增 :
histogram_quantile(0.99, sum(rate(gen_ai_client_token_usage_bucket[1h])) by (le))与昨日同时段对比,偏离 50% 报警。这能抓到"有人在 prompt 里塞了整个 PDF"。 - 按 feature/customer 分桶 :用 span attribute 注入
app.feature、customer.tier、cost.center,成本归属到业务线,才能推动产品方优化 prompt。 - 模型分布漂移 :
gen_ai_request_model的 cardinality 突然出现新值,可能意味着有人在调用没审核过的模型。 - Finish reason 分布变化 :
finish_reasons{filter}占比上升意味着安全过滤更频繁,要么是输入确实变脏,要么是模型升级后变得更保守。
告警一定要带 runbook。一条没有处理指南的"成本上涨 30%" 只会变成告警疲劳。
八、内容隐私与 PII 治理
GenAI 可观测性的最大合规风险是把用户隐私直接打到日志后端。三条铁律:
- 捕获时机脱敏,不存后脱敏。在 SDK 层或 Collector 层做 hash/redact,避免后端成为 PII 数据库。
- 默认 opt-out,敏感租户 opt-in。健康/金融/政企客户默认关闭内容捕获,仅在排查时按工单临时开启。
- 保留期限与访问审计。trace 数据保留 30 天即销毁,跨租户查询需要审批工单,行为进 audit log。
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true 这个环境变量应当被视为"开一次、合规审批一次",而不是默认值。
九、落地清单:上线第一周必须做的事
如果你的 LLM 应用明天就要上生产,按这个顺序加可观测性:
- Day 1:接入 OpenTelemetry + GenAI auto-instrumentation,把 token 用量、延迟、错误率打到你现有的 Prometheus。设置日预算硬告警。
- Day 2:把请求-响应的 trace_id 写入用户反馈系统的关联键,让"用户报问题"→"拉 trace"成为标准流程。
- Day 3 :把 prompt 模板加版本号(hash 或语义版本),所有 LLM span 都带上
prompt.version。没有版本号就没法定位回归。 - Day 4:上线 quality sampling:每 100 个请求采样 1 个跑 LLM-as-judge 评估,指标打点进 trace attribute。
- Day 5:建立周度 trace review 机制,团队花 30 分钟看上周最贵、最慢、最差的各 10 条 trace。这是 LLM 应用真正的 retrospective。
结语
LLM 可观测性的成熟度还远低于传统 APM------GenAI 语义约定仍在 Development 状态,工具生态仍在洗牌(Helicone 进入维护、ClickHouse 收购 Langfuse、Datadog 加 LLM 模块),但工程要求已经定型:用 OTel 语义约定做骨架、用 Collector 做数据治理网关、用专用 LLM 后端做体验层、用版本化 prompt 做回归定位。
把这套架构搭起来之后,你得到的不是一份漂亮 dashboard,而是能在 P0 发生的那一刻,用 trace_id 在 5 分钟内回答"是哪个 prompt、哪次模型升级、哪个用户、哪个 feature 组合触发的"。这是把 LLM 应用从"能跑"推进到"能运营"的工程分水岭。