AI 应用的可观测性设计:LLM 调用的监控、追踪与调试方法
AI 应用的可观测性比传统后端更复杂——LLM 调用的延迟波动大、耗材与结果强耦合、黑盒输出难以断言。本文从三个层面拆解 AI 应用的可观测性设计:LLM 调用的指标采集、调用链追踪与上下文还原、以及调试与评估的工作流——适合正在将 LLM 集成到生产系统的团队。
先说结论:AI 可观测性不是”加几个日志”就能解决的问题
把 LLM 调用集成到生产系统后,团队会很快发现:传统后端可观测性的三板斧(日志、指标、追踪)在 AI 场景下远远不够。
LLM 调用有三个特性让传统手段失效:
- 延迟波动大——同一个模型、同一个 Prompt,响应时间可能差 10 倍
- 成本与结果强耦合——响应慢的请求不仅体验差,还更贵(Token 更多)
- 输出无法断言——LLM 的文本输出不能用简单的 schema 验证”对错”
本文从三个层面展开:指标采集 → 追踪与上下文还原 → 调试与评估工作流。
1. 指标采集:LLM 可观测性的基础层
1.1 必须采集的指标
| 类别 | 指标 | 说明 | 聚合方式 |
|---|---|---|---|
| 延迟 | TTFT | 首个 Token 到达时间 | P50/P95/P99 |
| 延迟 | TPOT | 每个输出 Token 的生成时间 | P50/P95/P99 |
| 延迟 | 端到端延迟 | 从请求发起到收到完整响应 | P50/P95/P99 |
| 成本 | 输入 Token | 每次调用的输入 Token 数 | 总和/日均/均次 |
| 成本 | 输出 Token | 每次调用的输出 Token 数 | 总和/日均/均次 |
| 成本 | 单次调用成本 | 按模型单价计算 | 日均/月均 |
| 质量 | 错误率 | 按状态码/错误类型聚合 | 总占比 |
| 质量 | 用户反馈 | 点赞/踩/举报 | 比率 |
| 质量 | 响应长度 | 输出字符数 | 均值/分布 |
1.2 指标采集的实现
// 一个简单的 LLM 调用指标包装器
async function tracedLLMCall(params: {
model: string;
messages: ChatMessage[];
metadata?: Record<string, string>;
}): Promise<LLMResponse> {
const start = performance.now();
const traceId = crypto.randomUUID();
try {
const response = await openai.chat.completions.create({
model: params.model,
messages: params.messages,
stream: true,
});
// 流式响应:记录 TTFT
let ttft: number | null = null;
let outputTokens = 0;
let fullResponse = '';
for await (const chunk of response) {
if (!ttft) ttft = performance.now() - start;
outputTokens += chunk.usage?.completionTokens ?? 0;
fullResponse += chunk.choices[0]?.delta?.content ?? '';
}
const endToEnd = performance.now() - start;
// 上报指标
metrics.record('llm.ttft', ttft, { model: params.model });
metrics.record('llm.e2e', endToEnd, { model: params.model });
metrics.record('llm.output_tokens', outputTokens, { model: params.model });
metrics.record('llm.cost', calculateCost(params.model, inputTokens, outputTokens), { model: params.model });
return { response: fullResponse, traceId, usage: { inputTokens, outputTokens } };
} catch (error) {
metrics.record('llm.error', 1, { model: params.model, errorType: error.code });
throw error;
}
}
1.3 指标聚合的注意事项
- 不要只看平均延迟——LLM 调用的延迟是长尾分布,P95 才有意义
- 按模型和 Prompt 模式分组——不同模型、不同 Prompt 长度,指标差异很大,聚合在一起没意义
- 成本指标必须与延迟指标关联——一个”慢”的请求往往也是”贵”的请求
2. 追踪与上下文还原
2.1 调用链追踪
AI 应用的一个典型调用链:
用户请求 → 网关 → Agent/编排层 → LLM 调用 → 工具调用 → LLM 调用 → 响应
传统追踪工具(如 Jaeger、Zipkin)可以追踪 RPC 调用,但 LLM 的上下文远比 RPC 复杂——我们需要知道:
- 这次调用用了什么 Prompt
- 返回了什么内容
- 消耗了多少 Token
- 是否有工具调用(Function Calling)的中间结果
2.2 实现方案
推荐在 OpenTelemetry 基础上,为 LLM 调用定义自定义 Span 属性:
import { Span, trace } from '@opentelemetry/api';
async function tracedLLM(
model: string,
messages: ChatMessage[],
functions?: FunctionDefinition[]
): Promise<LLMResponse> {
const tracer = trace.getTracer('llm-instrumentation');
const span = tracer.startSpan('llm.call', {
attributes: {
'llm.model': model,
'llm.request.messages': JSON.stringify(messages.map(m => ({ role: m.role, content: truncate(m.content, 500) }))),
'llm.request.functions': functions ? JSON.stringify(functions.map(f => f.name)) : '',
},
});
try {
const response = await openai.chat.completions.create({ model, messages, functions });
const usage = response.usage;
span.setAttributes({
'llm.response.tokens.input': usage?.promptTokens ?? 0,
'llm.response.tokens.output': usage?.completionTokens ?? 0,
'llm.response.total_tokens': usage?.totalTokens ?? 0,
'llm.response.finish_reason': response.choices[0]?.finishReason ?? 'unknown',
'llm.latency_ms': performance.now() - start,
});
// 将完整响应存储到独立 Trace Store(不写入 Span 属性,避免数据过大)
await traceStore.put(span.spanContext().spanId, {
messages,
response: response.choices[0]?.message,
usage,
});
span.setStatus({ code: SpanStatusCode.OK });
return response;
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
span.recordException(error);
throw error;
} finally {
span.end();
}
}
2.3 敏感数据处理
完整的 Prompt 和 Response 可能包含用户隐私数据。处理策略:
| 数据级别 | 示例 | 处理方式 |
|---|---|---|
| 元数据 | Model 名称、延迟、Token 数 | 写入 Span 属性,可聚合 |
| 脱敏内容 | 去除 PII 后的 Prompt 片段 | 写入 Trace Store,保留 7 天 |
| 原始内容 | 完整 Prompt 和 Response | 不写入 Trace Store,仅记录 hash 供审计 |
3. 调试与评估工作流
3.1 在线调试
当用户反馈”AI 回答错了”,传统日志最多告诉你”输入了 X,输出了 Y”——但你要回答的问题是”为什么 AI 给出了这个回答”。
需要的上下文:
- 完整的 Prompt(System Prompt + User Message + 历史对话)
- 模型的原始输出
- Token 消耗和延迟
- 如果有 Function Calling:工具调用链和中间结果
- 温度等参数设置
推荐做法:在 Trace Store 中保留完整的调用上下文,支持按 User ID、Session ID、Trace ID 检索。每次用户反馈问题时,运维人员可以一键还原完整的调用现场。
3.2 回归测试(Golden Dataset)
模型升级或 Prompt 修改后,如何确保输出质量没有回退?
建立 Golden Dataset:
golden-dataset/
test-cases/
- case-001.json # 简单问答
- case-002.json # 多轮对话
- case-003.json # 工具调用
- case-004.json # 边界情况(空输入、超长输入)
expected/
- expected-001.json # 期望输出(或判断标准)
每次变更后,运行回归测试:
# 跑 Golden Dataset 对比
llm-eval run --dataset golden-dataset/ --model gpt-4o-mini --output results/
# 对比与基准版本的差异
llm-eval diff --baseline results/v1.0/ --current results/v1.1/
3.3 自动评估指标
| 评估维度 | 方法 | 说明 |
|---|---|---|
| 回答准确率 | Judge LLM 打分 | 用另一个 LLM 评估”是否准确回答了问题” |
| 幻觉率 | 事实一致性检查 | 输出是否与给定上下文矛盾 |
| 指令遵循 | 命令完成度检查 | 是否按要求格式输出、是否遗漏了必填字段 |
| 安全合规 | 内容安全检测 | 是否有有害内容、是否泄露了系统 Prompt |
| 语义相似度 | Embedding 距离 | 输出与期望输出的向量距离 |
4. 工具链推荐
| 用途 | 工具 | 说明 |
|---|---|---|
| 指标采集 | OpenTelemetry + Prometheus | 标准指标管道的通用方案 |
| LLM 追踪 | OpenTelemetry + 自定义 Span | 标准方案,需要自己定义 LLM Span 属性 |
| 专用 LLM 观测 | Langfuse / Arize Phoenix / LangSmith | 专门为 LLM 设计的观测平台,开箱即用 |
| 评估框架 | deep-eval / Ragas | Golden Dataset 评估与自动评分 |
| 调试 UI | Langfuse / Weights & Biases Prompts | 可视化查看 Prompt、Response、Token 消耗 |
总结
| 层面 | 核心要点 | 常见误区 |
|---|---|---|
| 指标 | 按模型和 Prompt 模式分组,看 P95 不看平均 | 只加了延迟监控,不知道成本和质量 |
| 追踪 | 用 OpenTelemetry Span + 独立 Trace Store | 把完整 Prompt 写入日志(数据泄露风险) |
| 调试 | 保留完整调用上下文,支持一键还原现场 | 只有”用户说错了”的反馈,没有可复现的上下文 |
| 评估 | 建立 Golden Dataset,每次变更跑回归 | 上线后从未评估过输出质量 |
AI 应用的可观测性投入不是”成本”——它是你唯一能在生产环境中回答”为什么 AI 会这样回答”的工具。 没有可观测性,每次用户反馈”AI 回答错了”都是一次盲人摸象式的排查。
相关阅读
- AI Agent 工作流编排实战:从单 Agent 到多 Agent 的架构选型 —— 多 Agent 编排的追踪、评估与失败模式聚类
- 企业私有知识库 RAG 落地实战:从架构选型到检索质量 —— RAG 检索与生成质量的评估闭环设计
- 大模型 Token 多供应商算力撮合:网关架构、路由策略与容错设计 —— LLM 生产系统的另一面:多供应商路由与熔断
- 运维自动化脚本模式:从一次性脚本到可维护工具 —— 可观测性基础设施的自动化运维底座
需要 AI 应用的可观测性方案设计?联系我们,说清你的模型栈与规模,24 小时内回可行性。
常见问题
LLM 调用和普通 API 调用在可观测性上有什么本质区别?
三个区别:① 延迟分布不同——普通 API 调用延迟通常在 10-500ms,LLM 调用在 1-30s,且方差极大(同一个模型的同一次调用可能因上下文长度不同而差 10 倍),所以平均延迟指标毫无意义,需要看百分位分布;② 成本与结果强耦合——每次调用都按 Token 数计费,响应慢的请求不仅体验差还更贵,需要同时追踪延迟和 Token 消耗;③ 输出没法做断言——普通 API 有明确的返回结构可以做 schema 断言,LLM 的文本输出无法用简单规则验证正确性,必须引入额外的评估机制。
LLM 调用的哪些指标是最值得关注的?
三类核心指标:① 服务质量——TTFT(首个 Token 到达时间,反映首屏感知延迟)、TPOT(每个输出 Token 的生成时间,反映流式响应速度)、端到端延迟、错误率(按状态码和错误类型聚合);② 成本——输入 Token 数、输出 Token 数、单次调用成本、日均/月均总成本;③ 质量——用户反馈率(点赞/踩)、语义相似度评分(输出与期望答案的 Embedding 距离)、人工抽检通过率。质量指标最难采集但最重要——没有质量指标的成本优化是盲目的。
怎么追踪 LLM 调用的上下文——Prompt、Response、Token 消耗?
建议为每次 LLM 调用生成一个唯一的 trace ID,在应用层手动构造一个 Span 包裹调用:Span 内记录 system_prompt、user_message、assistant_response、input_tokens、output_tokens、model_name、latency_ms。这些 Span 上报到 OpenTelemetry Collector 或专门的 LLM 观测平台(如 Langfuse、Arize Phoenix)。关键设计:不要将完整的 Prompt/Response 写入日志或指标标签(可能包含敏感数据),而是存储到独立的 Trace 存储中,通过 trace ID 关联。
LLM 输出质量怎么在 CI/CD 中做自动化验证?
三层验证:① 格式层——如果输出要求是 JSON,先验证能否被解析,再验证字段完备性(用 Zod 或 Pydantic schema);② 语义层——用另一个 LLM(Judge LLM)评估输出质量,打分维度包括"是否回答了问题"、"是否有幻觉"、"是否遵循了指令";③ 回归层——维护一组固定的测试用例(Golden Dataset),每次模型升级或 Prompt 修改后跑全量,对比输出与基准输出的语义相似度,下降超过阈值则告警。不需要每次都跑全量,但每次 Prompt 变更和模型版本升级时是必跑的。