Appearance
可观测与链路追踪:知道慢在哪一环
1. 本节产出
一条完整链路的埋点:查询改写 / 检索 / 精排 / 生成 各段耗时与 Token 可分开看,一个 Grafana 面板能看到「慢在哪一环、哪类查询最慢、缓存命中率多少」。
2. 前置依赖
- 02-17 评测体系
- 02-18 成本治理
- Prometheus + Grafana(docker-compose 已含)
3. 为什么 RAG 特别需要可观测
普通服务的链路是 A → B → C,慢了看哪一跳慢就行。RAG 的链路特殊在:
| 特点 | 带来的问题 |
|---|---|
| 有多个外部依赖(向量库、ES、Rerank、LLM) | 任何一环慢都拖垮整体 |
| 各环节耗时差异大(检索 50ms、生成 3s) | 平均延迟掩盖了分布问题 |
| 效果问题表现为速度问题 | 检索召回差 → 模型生成更长 → 更慢 |
| 成本与延迟强相关 | 慢往往意味着贵 |
没有分段埋点时,你只能看到「问答 P95 = 2.8s」,然后开始猜。有了分段埋点,一眼就看出「精排 P95 = 2.1s」,问题在 Rerank 服务。
4. 核心原理
4.1 必须埋的四个维度
| 维度 | 指标 | 用途 |
|---|---|---|
| 延迟 | 各分段耗时(P50/P95/P99) | 定位瓶颈 |
| 流量 | QPS、并发数 | 容量规划 |
| 质量 | 缓存命中率、拒答率、召回率(离线) | 效果监控 |
| 成本 | Token、金额(按租户/类型) | 预算控制 |
四个都要。只有延迟没有质量指标,会出现「系统很快但全是拒答」而你浑然不觉的情况。
4.2 分段计时
一次问答的耗时构成(典型):
┌──────────────────────────────────────┐
│ 改写 60ms ▏ │
│ 向量检索 45ms ▏ │
│ 关键词 30ms ▏ (并行,取最大值) │
│ 精排 280ms ██████ │
│ 生成 2400ms ████████████████████ │
└──────────────────────────────────────┘
总计 ≈ 2.8s这张图是本节最有用的产出。它让所有人立刻明白:优化生成环节(换更快的模型、限制输出长度)的收益远大于优化检索。
4.3 关键指标清单
| 指标 | 类型 | 告警阈值建议 |
|---|---|---|
rag.query.duration | Timer | P95 > 3s |
rag.retrieve.duration | Timer | P95 > 200ms |
rag.rerank.duration | Timer | P95 > 500ms |
rag.generate.duration | Timer | P95 > 4s |
rag.cache.hit_ratio | Gauge | < 20%(缓存可能失效了) |
rag.reject_ratio | Gauge | > 40%(知识库可能有缺口) |
rag.tokens.total | Counter | 按预算设 |
rag.ingest.failed | Counter | > 0 立即告警 |
reject_ratio 这个指标特别有价值:它突然升高通常意味着知识库缺文档或检索出了问题,是用户投诉的先行指标。
4.4 链路追踪 vs 指标
| 工具 | 回答什么 |
|---|---|
| 指标(Metrics) | 「整体 P95 是多少、趋势如何」 |
| 链路(Tracing) | 「这一个请求为什么慢」 |
两者都要。指标用于发现问题和告警,链路用于排查具体个案。只做指标的话,你能知道「慢了」但不知道「为什么慢」。
5. 代码走查
5.1 依赖与配置
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>yaml
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
distribution:
percentiles-histogram:
rag.query: true # 开启直方图才能算分位数
percentiles:
rag.query: 0.5,0.95,0.99提示:不开启
percentiles-histogram的话,Prometheus 只能拿到计数和总和,算不出 P95。这是最常见的配置遗漏。
5.2 分段计时
java
// ch02-rag/src/main/java/com/aitech/rag/metrics/RagMetrics.java
@Component
public class RagMetrics {
private final MeterRegistry meters;
public <T> T timed(String stage, Supplier<T> fn) {
Timer.Sample sample = Timer.start(meters);
try {
return fn.get();
} finally {
sample.stop(meters.timer("rag.stage.duration", "stage", stage));
}
}
public void recordCacheHit(String level) {
meters.counter("rag.cache.hit", "level", level).increment();
}
public void recordReject(String reason) {
meters.counter("rag.reject", "reason", reason).increment();
}
}5.3 接入链路
java
// ch02-rag/src/main/java/com/aitech/rag/chain/ObservableRagChain.java
@Service
public class ObservableRagChain {
public Answer answer(String question, String tenantId, List<String> acl) {
Timer.Sample all = Timer.start(meters);
try {
String q = metrics.timed("rewrite",
() -> rewriter.rewrite(question, history));
List<RetrievalResult> hits = metrics.timed("retrieve",
() -> pipeline.retrieve(q, tenantId, acl));
List<RetrievalResult> top = metrics.timed("rerank",
() -> reranker.rerank(q, hits));
String raw = metrics.timed("generate",
() -> generator.generate(q, top));
return citationEnricher.enrich(raw, top);
} finally {
all.stop(meters.timer("rag.query.duration",
"tenant", tenantId));
}
}
}每一段都用一个 timed() 包起来,简单且不会遗漏。
5.4 链路追踪(OpenTelemetry)
java
// 用 Observation API,Micrometer 自动串联
@Observed(name = "rag.query", contextualName = "rag-query")
public Answer answer(String q) { ... }yaml
management:
tracing:
sampling:
probability: 0.1 # 采样 10%,避免链路数据爆炸
otlp:
tracing:
endpoint: http://otel-collector:4318/v1/traces采样率不要设 1.0。全量采样在高 QPS 下会产生海量链路数据,存储和查询都扛不住。10% 通常足够排查问题。
5.5 Grafana 面板要点
面板分四行:
第一行:总览(QPS、P50/P95/P99、错误率)
第二行:分段耗时堆叠图(改写/检索/精排/生成)
第三行:缓存命中率、拒答率、平均 Token
第四行:按租户的成本 Top10第二行的堆叠图最有价值:一眼看出耗时构成随时间的变化(比如某天精排突然变慢)。
6. 跑起来
bash
git checkout ch02-19-observability
docker compose up -d # 含 Prometheus + Grafana
mvn spring-boot:run
# 产生流量
for i in {1..50}; do
curl -s -o /dev/null -X POST http://localhost:8080/api/ask \
-d '{"q":"年假怎么算","tenantId":"acme"}'
done
# 看指标
curl http://localhost:8080/actuator/prometheus | grep rag_期望输出:
rag_query_duration_seconds{quantile="0.95"} 2.84
rag_stage_duration_seconds{stage="rewrite",quantile="0.95"} 0.061
rag_stage_duration_seconds{stage="retrieve",quantile="0.95"} 0.052
rag_stage_duration_seconds{stage="rerank",quantile="0.95"} 0.288
rag_stage_duration_seconds{stage="generate",quantile="0.95"} 2.410
rag_cache_hit_total{level="exact"} 12
rag_reject_total{reason="low_score"} 3| 检查项 | 通过标准 |
|---|---|
| 分段可见 | 四个阶段各有独立耗时指标 |
| 分位数可用 | 能查到 P95(说明直方图已开) |
| 面板可用 | Grafana 能看到堆叠图 |
| 缓存/拒答可观测 | 有对应计数器 |
| 采样率合理 | Tracing 采样 10%,不过量 |
7. 生产避坑
- 不开
percentiles-histogram就永远看不到 P95。Prometheus 默认只记录计数与总和,平均值会掩盖长尾——而 RAG 的长尾恰恰很严重(生成环节方差极大)。这是上线后最常见的「指标看不到想看的」的原因。 - 不要给高基数标签建指标。把
question(每条都不同)或userId作为标签会导致指标基数爆炸,Prometheus 内存撑不住。标签只放低基数维度:租户、类型、模型、阶段。 - 链路追踪必须设采样率。全量采样在几千 QPS 下会产生 TB 级数据。10% 采样既能排查问题,成本也可控。另外要注意链路里不能记录完整 prompt(含敏感信息),只记 ID 和元数据。
8. 延伸与锚点
- 思考题:指标和评测都有了,接下来要把这些组装成一个可交付的产品。(02C 最后三节:完整项目)
- 代码锚点:
git checkout ch02-19-observability - 下一课时:02-20 完整项目(一):灌库管道与数据源接入
- 对应课件:L02-19 可观测