大模型应用可观测性:日志、Tracing 与问题复盘
一次线上事故让我重新理解可观测性
去年我负责一个 RAG 客服系统,上线第三周接到运营反馈:「最近回答质量变差了,好多用户在骂。」我去查日志,只看到最终的用户输入和模型输出——中间检索了什么片段、Prompt 拼了什么上下文、模型版本是什么时候切的,全部缺失。排查了两天,最后发现是向量库索引重建后 Top-K 从 5 悄悄变成了 3,检索质量下降导致模型「没料可答」。
这件事让我意识到,大模型应用的可观测性不是「锦上添花」,而是「没有它你就没法debug」。传统后端服务的日志、Metrics、Tracing 三板斧在大模型场景下依然成立,但需要重新设计采集粒度和字段结构。这篇文章把我这两年踩过的坑和整理的方案写下来,希望对正在做 LLM 应用可观测性的团队有帮助。
大模型应用到底需要观测什么
传统 Web 服务的请求链路是「请求 → 路由 → 数据库 → 响应」,每一步延迟可控、结果确定。大模型应用的链路要复杂得多:一次用户请求可能经历意图识别、上下文组装、向量检索、重排序、Prompt 渲染、模型调用、工具调用、后处理、安全过滤等十几个步骤。任何一个环节出问题,最终输出都会偏离预期。
我把需要观测的信号分成三层:
Trace 层:记录一次请求的完整生命周期。每个请求分配一个 trace_id,下面的每个步骤是一个 span。span 之间形成父子关系,可以还原出请求的完整执行树。
Log 层:记录每个 span 内部的详细信息。包括 Prompt 内容、模型参数、检索结果、工具调用参数和返回值。日志需要带 trace_id 和 span_id,才能跟 Trace 关联。
Metrics 层:从 Trace 和 Log 中聚合出来的统计指标。比如 P50/P95 延迟、Token 消耗、单次调用成本、错误率、重试率、用户采纳率。
这三层不是独立存在的。Trace 是骨架,Log 是血肉,Metrics 是仪表盘。没有 Trace,Log 就是一盘散沙;没有 Log,Trace 就是个空壳;没有 Metrics,你就只能一个一个翻 Trace,没法发现系统性问题。
OpenTelemetry GenAI 语义约定:行业标准来了
2024 年之前,大模型应用的可观测性基本靠各团队自己定义字段。OpenTelemetry 社区在 2025 年发布了 GenAI 语义约定(Semantic Conventions for Generative AI),定义了一套标准化的 span 类型和属性名,让不同厂商的工具可以互通。
核心 span 类型有三个:
| Span 类型 | 含义 | 典型场景 |
|---|---|---|
invoke_agent | 顶层 span,代表整个 Agent 交互 | 用户发起一次对话请求 |
chat | 子 span,代表一次 LLM 调用 | 调用 GPT-4o 生成回答 |
execute_tool | 子 span,代表一次工具调用 | 调用搜索 API 获取实时信息 |
标准化属性方面,关键的有这些:
| 属性名 | 说明 | 示例 |
|---|---|---|
gen_ai.request.model | 调用的模型名称 | gpt-4o |
gen_ai.usage.input_tokens | 输入 Token 数 | 1024 |
gen_ai.usage.output_tokens | 输出 Token 数 | 512 |
gen_ai.response.finish_reasons | 结束原因 | stop、tool_calls |
gen_ai.system_instructions | 系统 Prompt(可选) | 完整 system message |
gen_ai.input.messages | 输入消息(可选) | 用户和助手消息列表 |
gen_ai.output.messages | 输出消息(可选) | 模型响应和工具结果 |
需要注意的是,内容类属性(system_instructions、input.messages、output.messages)默认不采集,因为可能包含敏感数据。只有显式开启 captureContent 才会记录完整内容。这个设计很务实——既保护了隐私,又保留了排查问题时需要的信息。
标准化还定义了两个核心 Metrics:
gen_ai.client.operation.duration:LLM 调用延迟的直方图,可以按模型过滤做性能对比。gen_ai.client.token.usage:Token 消耗的直方图,可以按 input/output 类型拆分。
如果你的团队正在从零设计可观测性方案,我建议直接对齐 OTel GenAI 语义约定。这样后续不管换什么观测平台,数据格式都不用大改。
一次 LLM 请求的完整 Trace 结构
把一次典型的 RAG 请求拆开看,Trace 的树形结构大致是这样的:
对应到 Trace 数据,每个节点都是一个 span。我给一个简化的 JSON 结构:
{
"traceId": "abc123",
"spans": [
{
"spanId": "span-001",
"name": "invoke_agent",
"attributes": {
"gen_ai.request.model": "gpt-4o",
"user.id": "u-7890",
"session.id": "sess-456"
}
},
{
"spanId": "span-002",
"parentSpanId": "span-001",
"name": "vector_search",
"attributes": {
"query": "如何退货",
"top_k": 5,
"results_count": 5,
"latency_ms": 45
}
},
{
"spanId": "span-003",
"parentSpanId": "span-001",
"name": "chat",
"attributes": {
"gen_ai.request.model": "gpt-4o",
"gen_ai.usage.input_tokens": 1024,
"gen_ai.usage.output_tokens": 512,
"gen_ai.response.finish_reasons": ["stop"],
"latency_ms": 1200
}
}
]
}这个结构的关键在于:每个 span 都有 parentSpanId,形成一棵树。你可以从顶层 invoke_agent 往下遍历,看到请求经过了哪些步骤、每步花了多久、用了什么参数。
主流观测平台对比
2026 年的 LLM 可观测性市场已经相当成熟。我整理了一张对比表,覆盖三个主流平台和两个传统方案:
| 维度 | Langfuse | LangSmith | Arize Phoenix | 自建 ELK | 自建 OTel + Grafana |
|---|---|---|---|---|---|
| 开源协议 | MIT | 闭源 | Apache 2.0 | 开源 | 开源 |
| 部署方式 | 自托管 / Cloud | Cloud / 企业自托管 | 自托管 / Cloud | 自托管 | 自托管 |
| 框架绑定 | 无绑定 | LangChain 生态优先 | 无绑定 | 无绑定 | 无绑定 |
| Trace 模型 | Trace → Observation 层级 | Run 层级 | Span 树 | 日志搜索 | Span 树 |
| Prompt 管理 | ✅ 内置 | ✅ 内置 | ❌ 需外接 | ❌ 需自建 | ❌ 需自建 |
| 评估(Eval) | ✅ 内置 | ✅ 内置 | ✅ 内置 | ❌ 需自建 | ⚠️ 需插件 |
| OTel 支持 | OTLP 端点接收 | 原生支持 | 原生支持 | 需适配 | 原生支持 |
| 成本追踪 | ✅ Token × 单价 | ✅ Token × 单价 | ⚠️ 基础支持 | ❌ 需自建 | ⚠️ 需配置 |
| 定价模式 | 按 Observation 数 | 按 Trace 数 + 座位数 | 按数据量 | 基础设施成本 | 基础设施成本 |
| 适合团队 | 重视隐私、多框架 | LangChain 深度用户 | 需要 ML 评估能力 | 已有 ELK 栈 | 已有 OTel 基础设施 |
再看一张更细粒度的对比,聚焦三个最主流的专业平台:
| 能力 | Langfuse | LangSmith | Laminar |
|---|---|---|---|
| Trace 结构 | Trace → Observation 嵌套 | Run 层级 + 会话聚类 | Span 因果链 |
| Agent 支持 | 多步推理 Trace | LangGraph Studio IDE | 浏览器 Agent 回放 |
| 评估集成 | 内置 + 自定义 LLM Judge | 内置 + 人工标注 | 内置自动评估 |
| 实时性 | 近实时(秒级) | 实时 | 实时 + 会话回放 |
| 免费额度 | 50,000 Observations/月 | 5,000 Traces/月 | 1 GB 数据/月 |
| 付费起步 | Hobby 免费,Pro $59/月 | Plus $39/座/月 | Pro $50/月 |
| 自托管 | ✅ 完全免费 | ⚠️ 仅企业版 | ✅ Docker Compose |
选择建议很直接:如果你的团队深度使用 LangChain/LangGraph,LangSmith 的集成体验最好;如果你需要自托管、多框架支持或者对数据隐私有要求,Langfuse 是首选;如果你做的是浏览器自动化 Agent,需要会话回放来排查问题,Laminar 有独特优势。
三个真实场景的 Trace 设计
场景一:RAG 系统的检索质量追踪
RAG 系统最容易出问题的环节是检索。用户问「怎么退货」,系统检索到的文档片段不相关,模型就算再强也答不好。我在检索环节加了这些 span:
# 检索 span
with tracer.start_as_current_span("vector_search") as span:
span.set_attribute("query", user_query)
span.set_attribute("top_k", 5)
span.set_attribute("embedding_model", "text-embedding-3-small")
results = vector_store.similarity_search(user_query, k=5)
span.set_attribute("results_count", len(results))
span.set_attribute("scores", [r.score for r in results])
span.set_attribute("latency_ms", elapsed_ms)关键点:记录 scores 字段。当用户反馈「回答不对」时,你可以回溯 Trace 看检索分数——如果最高分只有 0.6,那问题大概率在检索侧,不是模型的问题。
场景二:多轮工具调用的因果链
Agent 场景下,一次请求可能触发多次工具调用。我需要看到完整的调用链和每次工具调用的输入输出:
# Agent 主循环
with tracer.start_as_current_span("invoke_agent") as agent_span:
agent_span.set_attribute("gen_ai.request.model", "gpt-4o")
agent_span.set_attribute("user.id", user_id)
for step in agent_steps:
if step.type == "llm_call":
with tracer.start_as_current_span("chat") as llm_span:
llm_span.set_attribute("gen_ai.request.model", step.model)
llm_span.set_attribute("gen_ai.usage.input_tokens", step.input_tokens)
llm_span.set_attribute("gen_ai.usage.output_tokens", step.output_tokens)
llm_span.set_attribute("gen_ai.response.finish_reasons", [step.finish_reason])
elif step.type == "tool_call":
with tracer.start_as_current_span("execute_tool") as tool_span:
tool_span.set_attribute("tool.name", step.tool_name)
tool_span.set_attribute("tool.arguments", json.dumps(step.args))
tool_span.set_attribute("tool.status_code", step.status_code)
tool_span.set_attribute("tool.response_size", len(step.response))这段代码的关键设计:tool_call span 记录了 tool.name、tool.arguments、tool.status_code 和 tool.response_size。当某个工具调用失败时,你可以直接定位是哪个工具、什么参数、返回了什么错误码。
场景三:成本异常监控
有一次我发现某天的 API 账单突然翻了三倍。查了 Metrics 才发现,有个内部测试接口在循环调用 GPT-4o,每次传 10 万 Token 的上下文。如果没有 Token 级别的监控,这种问题可能要到月底才被发现。
我设置的告警规则:
alerts:
- name: token_usage_spike
metric: gen_ai.client.token.usage
condition: "sum(input_tokens) > 100000 per minute"
severity: warning
- name: high_cost_request
metric: gen_ai.client.operation.duration
condition: "duration > 30s AND input_tokens > 50000"
severity: critical
- name: retry_loop
metric: gen_ai.client.operation.duration
condition: "same trace has > 5 chat spans in 60s"
severity: critical第三条规则专门抓重试循环——同一个 Trace 里 60 秒内出现超过 5 次 chat span,大概率是 Agent 陷入了工具调用死循环。这种问题不看 Trace 根本发现不了。
代码方案对比:从简单到完整
不同阶段的团队需要的可观测性方案复杂度不同。我把四种方案放在一起对比:
方案一:结构化日志 + trace_id(最小可行方案)
适合早期项目,用 Python 标准库就能搞定:
import logging
import uuid
logger = logging.getLogger(__name__)
def handle_request(user_query: str, user_id: str):
trace_id = str(uuid.uuid4())
logger.info("request_start", extra={
"trace_id": trace_id,
"user_id": user_id,
"query_length": len(user_query),
})
# 检索
contexts = retrieve(user_query)
logger.info("retrieval_done", extra={
"trace_id": trace_id,
"results_count": len(contexts),
"top_score": contexts[0].score if contexts else 0,
})
# 模型调用
response = llm_call(user_query, contexts)
logger.info("llm_done", extra={
"trace_id": trace_id,
"model": "gpt-4o",
"input_tokens": response.usage.input_tokens,
"output_tokens": response.usage.output_tokens,
"latency_ms": response.latency_ms,
})
return response优点:零依赖,五分钟接入。缺点:没有层级关系,查询靠 grep。
方案二:Langfuse SDK(开源平台方案)
当项目进入正轨,引入专业的 LLM 观测平台:
from langfuse import Langfuse
langfuse = Langfuse()
def handle_request(user_query: str, user_id: str):
trace = langfuse.trace(
name="rag_request",
user_id=user_id,
metadata={"source": "web"}
)
# 检索 span
retrieval_span = trace.span(
name="vector_search",
input={"query": user_query},
metadata={"top_k": 5}
)
contexts = retrieve(user_query)
retrieval_span.end(output={"results_count": len(contexts)})
# 模型调用 span
generation = trace.generation(
name="llm_call",
model="gpt-4o",
input=[{"role": "user", "content": user_query}],
metadata={"contexts": [c.text for c in contexts]}
)
response = llm_call(user_query, contexts)
generation.end(
output=response.content,
usage={
"input": response.usage.input_tokens,
"output": response.usage.output_tokens
}
)
return response优点:自动关联、有 UI 可以看 Trace 树、支持评估和 Prompt 管理。缺点:依赖外部服务,需要评估数据量成本。
方案三:OpenTelemetry 原生接入(标准化方案)
如果你的团队已经在用 OTel 做基础设施监控,直接复用现有链路:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
provider = TracerProvider()
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317"))
)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("llm-app")
def handle_request(user_query: str, user_id: str):
with tracer.start_as_current_span("invoke_agent") as span:
span.set_attribute("user.id", user_id)
span.set_attribute("gen_ai.request.model", "gpt-4o")
# 检索
with tracer.start_as_current_span("vector_search") as search_span:
contexts = retrieve(user_query)
search_span.set_attribute("results_count", len(contexts))
# 模型调用
with tracer.start_as_current_span("chat") as llm_span:
llm_span.set_attribute("gen_ai.request.model", "gpt-4o")
response = llm_call(user_query, contexts)
llm_span.set_attribute("gen_ai.usage.input_tokens", response.usage.input_tokens)
llm_span.set_attribute("gen_ai.usage.output_tokens", response.usage.output_tokens)
llm_span.set_attribute("gen_ai.response.finish_reasons", [response.finish_reason])
return response优点:与现有 OTel 基础设施无缝集成,可以导出到任何支持 OTLP 的后端。缺点:需要自己搭建或维护 OTel Collector 和存储后端。
方案四:异步批量上报(生产级优化)
生产环境下,可观测性不能影响主链路性能。把上报改成异步批量:
import queue
import threading
from collections import defaultdict
class AsyncTraceExporter:
def __init__(self, flush_interval=5, batch_size=100):
self._queue = queue.Queue()
self._flush_interval = flush_interval
self._batch_size = batch_size
self._worker = threading.Thread(target=self._run, daemon=True)
self._worker.start()
def emit(self, span_data: dict):
self._queue.put(span_data)
def _run(self):
batch = []
while True:
try:
item = self._queue.get(timeout=self._flush_interval)
batch.append(item)
if len(batch) >= self._batch_size:
self._flush(batch)
batch = []
except queue.Empty:
if batch:
self._flush(batch)
batch = []
def _flush(self, batch: list):
# 批量发送到后端
exporter.export(batch)
exporter = AsyncTraceExporter()
# 使用
def handle_request(user_query: str, user_id: str):
exporter.emit({
"type": "request_start",
"user_id": user_id,
"query_length": len(user_query),
})
# 主逻辑不受阻塞这种模式的核心思想是:Trace 事件先写入本地队列,后台线程批量刷新。主链路只做内存操作,不阻塞。Langfuse 和大多数专业平台内部也是这么做的。
Trace 数据该怎么存、存多久
这个问题没有标准答案,取决于你的业务场景和合规要求。我整理了一个参考:
| 数据类型 | 建议保留期 | 存储位置 | 说明 |
|---|---|---|---|
| Trace 元数据(span 属性、时间戳) | 30-90 天 | 热存储(ES / ClickHouse) | 日常排查和 Dashboard 用 |
| 完整 Prompt / Response 内容 | 7-30 天 | 热存储 → 冷存储 | 涉及隐私,需要脱敏策略 |
| 聚合 Metrics | 1 年+ | 时序数据库(Prometheus / VictoriaMetrics) | 趋势分析和容量规划 |
| 用户反馈关联数据 | 永久 | 数据库 | 用于评估模型迭代效果 |
| 审计日志 | 按合规要求 | 冷存储(S3 / OSS) | 金融、医疗行业可能有法规要求 |
一个实用的建议:把 Trace 元数据和内容数据分开存。元数据体积小、查询频率高,放热存储;内容数据体积大、主要是排查问题时按需查看,可以放到对象存储,需要时再拉取。
上线前检查清单
可观测性方案落地后,我用这个清单做最终检查:
Trace 结构
- 每次请求有唯一 trace_id,且贯穿整个链路
- 每个 span 有 parentSpanId,形成完整的树形结构
- span 名称统一使用团队约定的命名规范(如
vector_search、chat、execute_tool) - 时间戳精确到毫秒级
字段覆盖
- LLM 调用 span 记录了模型名称、input_tokens、output_tokens、finish_reason
- 检索 span 记录了 query、top_k、results_count、top_score
- 工具调用 span 记录了 tool_name、arguments、status_code、response_size
- 请求 span 记录了 user_id、session_id、task_type
数据质量
- 敏感字段(用户手机号、身份证、密码)已脱敏
- 大字段(超长 Prompt、Base64 图片)已截断或只存引用
- 没有重复上报的 span
性能影响
- 上报是异步的,不阻塞主链路
- 批量上报间隔和大小合理(建议 5 秒或 100 条)
- 本地队列有上限,防止内存爆炸
告警和 Dashboard
- P50/P95 延迟有 Dashboard 和告警
- Token 消耗和成本有趋势图,异常时自动告警
- 错误率按类型拆分(接口失败 / 内容质量 / 业务规则)
- 用户采纳率或满意度有追踪
可维护性
- 字段对齐 OTel GenAI 语义约定,方便后续切换平台
- 有文档说明每个 span 的含义和字段定义
- 新成员看完文档后能在 10 分钟内查到一次请求的完整 Trace
写在最后
可观测性这件事,越早做成本越低。我见过太多团队在早期觉得「先跑起来再说」,等功能上线、用户量起来之后,发现线上问题根本没法排查,回头补可观测性的成本是最初的十倍。
早期项目不需要一上来就搭完整的观测平台。结构化日志 + trace_id 就够了,关键是字段设计稳定,能按用户、功能、模型版本筛选。等功能进入付费阶段、接入企业客户或自动化流程后,再引入 Langfuse 或 LangSmith 这样的专业平台。
但有一条底线:至少要能看到每次请求「用了什么模型、传了什么上下文、花了多少 Token、花了多少时间」。做不到这四点,你的大模型应用就是在裸奔。
参考资料
- OpenTelemetry GenAI Semantic Conventions — OTel 官方 GenAI 语义约定规范
- Inside the LLM Call: GenAI Observability with OpenTelemetry — OTel 官方博客,详解 GenAI 可观测性实践
- Langfuse Observability & Application Tracing — Langfuse 官方文档,开源 LLM 可观测性方案
- Laminar vs Langfuse vs LangSmith: LLM Observability Compared — 2026 年三大平台横向对比
- Observability in LLM Workflows: Metrics, Traces & Logs — TrueFoundry 的 LLM 工作流可观测性指南
- The Complete Guide to LLM Observability for 2026 — Portkey 的 LLM 可观测性完整指南
- Datadog LLM Observability natively supports OpenTelemetry GenAI — Datadog 对 OTel GenAI 语义约定的原生支持
- MLflow GenAI Semantic Conventions — MLflow 的 GenAI 语义约定文档