大模型应用的可观测性:日志、指标与追踪
大模型应用出问题时,"哪里错了"往往比传统服务更难定位——可能是检索没召回、可能是 prompt 有歧义、也可能是模型本身的随机性。可观测性决定了你能不能快速找到原因。
一、为什么大模型应用更难排查
传统服务出了 bug,通常有明确的线索:异常堆栈、错误码、SQL 慢查询日志。链路是确定的,同样的输入必然得到同样的输出。
大模型应用不一样:
| 特点 | 带来的排查困难 |
|---|---|
| 概率性输出 | 同样的问题两次回答不同,无法靠"重放"复现 |
| 链路长 | 检索 → 重排 → 组装 → 生成,任何一环都可能出问题 |
| 失败很隐蔽 | 不报错,只是"答得不对"——而系统认为它成功了 |
| 输入不可控 | 用户可能问任何问题 |
| 外部依赖 | 模型 API 延迟波动、限流、内容过滤 |
最麻烦的是第三点:传统监控看错误率,但大模型应用的"答非所问"不会体现在错误率里。
二、三类可观测性数据
| 类型 | 回答什么问题 | 关键内容 |
|---|---|---|
| 日志(Logs) | 这次请求到底发生了什么 | 完整 prompt、检索结果、模型输出 |
| 指标(Metrics) | 整体健康度如何 | 延迟、token 消耗、成功率、成本 |
| 追踪(Traces) | 时间花在哪一环 | 各阶段耗时分解、调用链 |
三者互补:指标发现异常 → 追踪定位环节 → 日志看清细节。
三、必须记录的日志字段
一次大模型请求,至少要能还原出这些信息:
public class LlmCallLog {
private String traceId; // 链路 ID,贯穿全流程
private String userId; // 谁问的
private String sessionId; // 会话 ID,用于还原上下文
private String scene; // 场景:导购/客服/文案生成
// 输入侧
private String userQuery; // 原始问题
private String rewrittenQuery; // 改写后的查询(如果有)
private List<String> retrievedChunkIds; // 召回了哪些片段
private List<Double> retrievedScores; // 相似度分数
private List<String> rerankedChunkIds; // 重排后留下的
private int inputTokens; // 实际输入 token 数
// 模型侧
private String modelName;
private String promptVersion; // prompt 模板版本
private int outputTokens;
private long latencyMs; // 模型调用耗时
private String finishReason; // stop / length / content_filter
// 输出侧
private String answer;
private boolean success;
private String errorMessage;
}
几个容易被忽略但很关键的字段
| 字段 | 为什么重要 |
|---|---|
promptVersion |
prompt 改动后效果变化,没有版本号就无法回溯 |
retrievedScores |
判断是"没召回到"还是"召回了但没用上" |
finishReason |
length 说明被 max_tokens 截断,答案不完整 |
rerankedChunkIds |
对比召回和重排结果,判断重排是否丢了有用信息 |
inputTokens |
定位 token 消耗异常 |
finishReason 特别值得注意:如果大量请求的 finishReason 是 length,说明 max_tokens 设小了,答案被截断——但用户看到的是一个"看起来正常"的不完整回答,不会报错。
敏感信息处理
日志里会包含用户输入和模型输出,可能含个人信息。处理原则:
| 数据 | 处理方式 |
|---|---|
| 手机号、身份证、银行卡 | 脱敏后存储 |
| 用户完整对话内容 | 加密存储,限制访问权限,设保留期限 |
| 模型输入输出 | 采样存储(不必全量) |
全量存 prompt 和输出成本很高(一次 RAG 请求的 prompt 可能几千 token)。常见做法是全量存元数据 + 采样存完整内容(比如 10%),异常请求单独标记并全量保留。
四、关键指标
| 指标 | 计算方式 | 异常含义 |
|---|---|---|
| 首字延迟(TTFT) | 请求发出 → 第一个 token 返回 | 用户感知的响应速度 |
| 总延迟 | 请求发出 → 生成完成 | 端到端耗时 |
| 检索耗时 | 向量库查询时间 | 检索性能下降 |
| 模型耗时 | 模型 API 调用时间 | 上游性能波动 |
| 输入/输出 token | 按请求统计 | 成本异常 |
| 检索命中率 | 有结果的请求占比 | 知识库覆盖不足 |
| 空回答率 | 返回空的请求占比 | 模型或 prompt 问题 |
| 拒答率 | 模型拒绝回答的比例 | prompt 过严或问题越界 |
| 重试率 | 重试次数 / 总请求 | 上游不稳定 |
| 用户反馈率 | 点赞/点踩占比 | 质量指标 |
首字延迟是最重要的体验指标
| 指标 | 用户感受 |
|---|---|
| 首字延迟 < 1s | 流畅 |
| 1~3s | 可接受 |
| 3~10s | 明显卡顿 |
| > 10s | 以为卡死,大概率刷新 |
优化首字延迟的方法:流式输出、减少检索片段、用更快的模型做首轮响应。
延迟要分阶段统计
只知道"总延迟 8 秒"没法优化,必须拆开看:
总延迟 8.2s
├─ 查询改写 0.3s
├─ 向量检索 0.4s
├─ 重排 0.8s
├─ 上下文组装 0.1s
├─ 模型首 token 1.2s ← 关键
└─ 生成剩余内容 5.4s ← 关键
大多数情况下,模型生成占大头。如果检索环节占了 5 秒,那是检索的问题;如果模型首 token 就要 3 秒,那要考虑换模型或减少输入。
五、追踪:串联整条链路
大模型应用的调用链通常跨越多个组件:
用户请求
↓
业务服务 ──→ 检索服务 ──→ 向量库
↓ ↓
│ 重排模型
↓
模型网关 ──→ 大模型 API
↓
输出处理 ──→ 敏感词过滤
每个环节都要带上同一个 traceId,这样才能:
- 看到一个请求完整的耗时分布
- 定位是哪一环慢或出错
- 把散布在各服务里的日志串起来
与模型调用链的对接
部分模型平台支持返回自己的调用 ID,建议一并记录:
String platformTraceId = response.getHeader("x-request-id");
log.info("traceId={}, platformTraceId={}, latency={}ms",
traceId, platformTraceId, latency);
当怀疑是平台侧问题时,可以拿这个 ID 去平台查询,避免"双方都说不是自己的问题"。
六、质量监控:最难也最重要的一环
传统的可用性监控(错误率、超时)只能发现"服务挂了",发现不了"答得不好"。
三类质量信号
| 信号 | 采集方式 | 时效 |
|---|---|---|
| 用户反馈 | 点赞/点踩、连续追问比例 | 实时 |
| 自动评估 | 用模型或规则自动打分 | 准实时(采样) |
| 人工抽检 | 定期抽样人工评估 | 离线 |
用户反馈最直接但覆盖低(只有小部分用户会点赞踩),自动评估覆盖高但可能不准,人工抽检最准但成本高。三者配合使用。
自动评估的可落地方案
| 方法 | 做法 | 成本 |
|---|---|---|
| 规则检查 | 检查答案是否为空、是否含引用、长度是否异常 | 极低 |
| 关键词检查 | 是否包含不该出现的内容 | 极低 |
| 模型打分 | 用小模型判断答案是否基于检索内容 | 中 |
| 一致性检查 | 同一问题问两次,答案差异是否过大 | 中 |
规则检查应该全量跑,因为它几乎零成本,却能抓住大量明显问题(空回答、超长、被截断)。
一个实用的采样策略
全量:元数据 + 规则检查 + 用户反馈
10%:完整 prompt 和输出
100%:异常请求(超时、空格、被截断、用户点踩)
七、告警该怎么设
大模型应用的告警容易走向两个极端:要么太吵(误报多被忽略),要么太迟钝(出问题没人知道)。
| 告警项 | 阈值建议 | 说明 |
|---|---|---|
| 错误率(5xx) | > 1% | 传统告警 |
| 首字延迟 P95 | > 5s | 体验劣化 |
| 空回答率 | 环比翻倍 | 可能是模型或 prompt 问题 |
| 拒答率 | 环比翻倍 | 可能是 prompt 过严 |
| token 消耗 | 日消耗超预算 80% | 成本预警 |
| 检索命中率 | 低于基线 20% | 知识库或向量库问题 |
| 上游调用失败率 | > 5% | 模型平台不稳定 |
告警必须带上下文:只说"空回答率上升"没有价值,要能直接跳到对应 traceId 的日志。
小结
- 大模型应用排查更难的原因:概率性输出、链路长、失败隐蔽(答得不对但不报错)
- 三类数据互补:指标发现异常 → 追踪定位环节 → 日志看清细节
- 必记字段里的关键项:
promptVersion、retrievedScores、finishReason、rerankedChunkIds finishReason = length说明答案被截断,但不会报错,必须主动监控- 日志含用户输入,要脱敏 + 采样存储(全量存 prompt 成本很高)
- 首字延迟(TTFT)是最重要的体验指标,> 10 秒用户会以为卡死
- 延迟必须分阶段统计,否则无法定位优化点
- 全链路共用
traceId;记录平台侧调用 ID,避免责任不清 - 质量监控三类信号:用户反馈(实时但覆盖低)、自动评估(覆盖高但可能不准)、人工抽检(准但贵)
- 规则检查应该全量跑——零成本,能抓住空回答、超长、截断等明显问题
- 告警要带上下文,能直接跳到对应 traceId,否则没人会认真看
大模型应用的可观测性,本质上是把"黑盒"变成"可追溯"。当用户说"它答错了"时,你能否在几分钟内定位到是检索没召回、prompt 有歧义,还是模型理解偏差——这决定了系统能不能持续迭代。
留下点什么
评论基于 GitHub Discussions(giscus)。在项目根目录配置.env.local填入 NEXT_PUBLIC_GISCUS_* 四个变量后自动开启。