AI 系统的可观测性——比普通应用多追踪哪些维度
AI 系统的可观测性——比普通应用多追踪哪些维度
有一次开周会,产品问我:"我们的 AI 助手质量怎么样?"
我打开 Grafana,指着几个图表说:"你看,P99 响应时间 3.2 秒,错误率 0.3%,可用性 99.9%。"
产品沉默了一下,说:"这些我知道。我是说 AI 的回答质量怎么样?用户有没有得到有用的答案?AI 有没有乱说?"
我哑口无言。
我的监控体系是按传统 Web 服务搭的:延迟、错误率、QPS、可用性。这些指标对于 "HTTP 服务好不好用" 有意义,但对于 "AI 应用好不好用" 完全不够。
AI 系统的可观测性需要额外的维度。这篇文章我来说清楚那些额外的维度是什么,以及怎么用 Micrometer 实现轻量级的自定义采集。
AI 特有的可观测性维度
传统应用的可观测性,通常用 Metrics(指标)、Logs(日志)、Traces(链路追踪)三个维度,俗称 "三大支柱"。
这个框架对 AI 系统同样适用,但每个维度都需要 AI 专属的内容。
1. 幻觉率(Hallucination Rate)
这是 AI 系统特有的质量问题。AI 可能"自信地"输出错误信息,这在普通应用里不存在。
怎么检测幻觉?
对于 RAG 系统,可以检测"引用一致性":AI 的回答里引用的事实,是否真的出现在检索到的文档里?如果 AI 说"根据文档,X 的价格是 100 元",但检索到的文档里根本没有提到价格,那就是幻觉。
@Component
public class HallucinationDetector {
/**
* 检测 RAG 响应是否存在幻觉(基于引用一致性)
*/
public HallucinationCheckResult check(String aiResponse, List<String> retrievedDocs) {
// 从 AI 响应中提取声明性陈述
List<String> claims = extractClaims(aiResponse);
int totalClaims = claims.size();
int unsupportedClaims = 0;
List<String> unsupportedDetails = new ArrayList<>();
for (String claim : claims) {
boolean supported = isClaimSupportedByDocs(claim, retrievedDocs);
if (!supported) {
unsupportedClaims++;
unsupportedDetails.add(claim);
}
}
double hallucinationRate = totalClaims > 0
? (double) unsupportedClaims / totalClaims
: 0.0;
return HallucinationCheckResult.builder()
.totalClaims(totalClaims)
.unsupportedClaims(unsupportedClaims)
.hallucinationRate(hallucinationRate)
.unsupportedDetails(unsupportedDetails)
.hasHallucination(hallucinationRate > 0.3) // 超过 30% 不支持的声明认为存在幻觉
.build();
}
private boolean isClaimSupportedByDocs(String claim, List<String> docs) {
// 简化实现:检查 claim 中的关键词是否出现在文档中
// 生产环境建议用语义相似度模型
String[] keywords = extractKeywords(claim);
for (String doc : docs) {
int matchCount = 0;
for (String keyword : keywords) {
if (doc.contains(keyword)) matchCount++;
}
if (matchCount >= keywords.length * 0.6) {
return true; // 60% 关键词出现在文档中,认为支持
}
}
return false;
}
}2. 引用准确率(Citation Accuracy)
对于 RAG 系统,AI 引用的来源是否准确?
@Component
public class CitationAccuracyChecker {
public CitationCheckResult checkCitations(String aiResponse,
List<RetrievedDocument> retrievedDocs) {
// 从响应中提取引用标记,如 [1], [来源1], [Source: xxx]
List<String> citations = extractCitations(aiResponse);
int validCitations = 0;
int invalidCitations = 0;
for (String citation : citations) {
boolean valid = validateCitation(citation, retrievedDocs);
if (valid) validCitations++;
else invalidCitations++;
}
double accuracy = citations.isEmpty() ? 1.0
: (double) validCitations / citations.size();
return new CitationCheckResult(citations.size(), validCitations, accuracy);
}
}3. Token 利用率(Token Utilization)
即 Prompt 里的 Token 有多少真正"有用"——是否有大量冗余的上下文?
public class TokenUtilizationAnalyzer {
/**
* 分析 Token 利用率
* 利用率 = 核心内容 Token / 总 Token
*/
public TokenUtilizationResult analyze(String systemPrompt,
List<String> retrievedDocs,
String userQuery,
int totalInputTokens) {
// 估算各部分 Token 数
int systemTokens = estimateTokens(systemPrompt);
int queryTokens = estimateTokens(userQuery);
int docTokens = retrievedDocs.stream()
.mapToInt(this::estimateTokens)
.sum();
// 核心内容(用户查询 + 相关文档)
int coreTokens = queryTokens + docTokens;
double utilization = (double) coreTokens / totalInputTokens;
return TokenUtilizationResult.builder()
.totalTokens(totalInputTokens)
.systemPromptTokens(systemTokens)
.queryTokens(queryTokens)
.documentTokens(docTokens)
.utilizationRate(utilization)
.isEfficient(utilization > 0.7) // 70% 以上认为高效
.build();
}
private int estimateTokens(String text) {
// 简单估算:中文约 1.5 字/token,英文约 4 字符/token
if (text == null) return 0;
return (int) (text.length() / 1.5);
}
}4. Prompt 命中率(Prompt Cache Hit Rate)
很多模型支持 Prompt Caching(对相同的 system prompt 缓存,减少重复计费)。监控命中率可以发现优化机会。
5. 用户满意度代理指标
用户很少主动评分,但用户行为能反映满意度:
- 二次追问率:问完 AI 还来追问人工,说明 AI 没答好
- 会话放弃率:用户开始了 AI 对话后直接关闭,说明没有帮助
- 复制率:用户复制了 AI 的回答,说明内容有用(对代码生成场景尤其重要)
- 重试率:用户点了"重新生成",说明对第一次结果不满意
自建轻量级可观测体系
下面用 Micrometer 实现 AI 专属指标采集。
自定义指标注册
@Component
@Slf4j
public class AiObservabilityMetrics {
private final MeterRegistry registry;
// 计数器
private final Counter totalCallsCounter;
private final Counter hallucinationCounter;
private final Counter citationErrorCounter;
// 分布摘要(适合记录分布,如 Token 数、耗时)
private final DistributionSummary inputTokenSummary;
private final DistributionSummary outputTokenSummary;
private final DistributionSummary tokenUtilizationSummary;
// 直方图(用于计算 P50/P90/P99)
private final Timer firstTokenLatencyTimer;
private final Timer totalLatencyTimer;
// Gauge(当前值,如并发数)
private final AtomicInteger concurrentRequests = new AtomicInteger(0);
public AiObservabilityMetrics(MeterRegistry registry) {
this.registry = registry;
// 注册计数器
this.totalCallsCounter = Counter.builder("ai.calls.total")
.description("Total AI calls")
.register(registry);
this.hallucinationCounter = Counter.builder("ai.hallucination.detected")
.description("Detected hallucinations in AI responses")
.register(registry);
this.citationErrorCounter = Counter.builder("ai.citation.errors")
.description("Invalid citations in AI responses")
.register(registry);
// 注册分布摘要
this.inputTokenSummary = DistributionSummary.builder("ai.tokens.input")
.description("Input token counts per request")
.baseUnit("tokens")
.publishPercentiles(0.5, 0.75, 0.90, 0.99)
.register(registry);
this.outputTokenSummary = DistributionSummary.builder("ai.tokens.output")
.description("Output token counts per request")
.baseUnit("tokens")
.publishPercentiles(0.5, 0.75, 0.90, 0.99)
.register(registry);
this.tokenUtilizationSummary = DistributionSummary.builder("ai.token.utilization")
.description("Token utilization rate (0-1)")
.publishPercentiles(0.25, 0.5, 0.75)
.register(registry);
// 注册计时器
this.firstTokenLatencyTimer = Timer.builder("ai.latency.first_token")
.description("Time to first token in streaming responses")
.publishPercentiles(0.5, 0.90, 0.99)
.register(registry);
this.totalLatencyTimer = Timer.builder("ai.latency.total")
.description("Total AI call latency")
.publishPercentiles(0.5, 0.90, 0.99)
.register(registry);
// 注册 Gauge
Gauge.builder("ai.requests.concurrent", concurrentRequests, AtomicInteger::get)
.description("Current concurrent AI requests")
.register(registry);
}
/**
* 记录一次完整的 AI 调用指标
*/
public void recordCall(AiCallObservation observation) {
// 标签(用于分维度查询)
Tags tags = Tags.of(
"feature", observation.getFeatureKey(),
"model", observation.getModel(),
"tenant", observation.getTenantId()
);
// 计数
totalCallsCounter.increment(1, tags);
// Token 指标
inputTokenSummary.record(observation.getInputTokens(), tags);
outputTokenSummary.record(observation.getOutputTokens(), tags);
// Token 利用率
if (observation.getTokenUtilizationRate() > 0) {
tokenUtilizationSummary.record(observation.getTokenUtilizationRate(), tags);
}
// 延迟
if (observation.getFirstTokenLatencyMs() > 0) {
firstTokenLatencyTimer.record(observation.getFirstTokenLatencyMs(), TimeUnit.MILLISECONDS, tags);
}
totalLatencyTimer.record(observation.getTotalLatencyMs(), TimeUnit.MILLISECONDS, tags);
// 幻觉检测
if (observation.isHallucinationDetected()) {
hallucinationCounter.increment(1, tags);
}
// 引用错误
if (observation.getCitationErrorCount() > 0) {
citationErrorCounter.increment(observation.getCitationErrorCount(), tags);
}
}
public void incrementConcurrent() { concurrentRequests.incrementAndGet(); }
public void decrementConcurrent() { concurrentRequests.decrementAndGet(); }
}AOP 整合所有 AI 可观测性采集
@Aspect
@Component
@Slf4j
public class AiObservabilityAspect {
@Autowired
private AiObservabilityMetrics metrics;
@Autowired
private HallucinationDetector hallucinationDetector;
@Autowired
private CitationAccuracyChecker citationChecker;
@Autowired
private TokenUtilizationAnalyzer tokenAnalyzer;
@Around("@annotation(aiObservable)")
public Object observe(ProceedingJoinPoint joinPoint, AiObservable aiObservable) throws Throwable {
String featureKey = aiObservable.featureKey();
long startTime = System.currentTimeMillis();
metrics.incrementConcurrent();
Object result = null;
Exception exception = null;
try {
result = joinPoint.proceed();
return result;
} catch (Exception e) {
exception = e;
throw e;
} finally {
metrics.decrementConcurrent();
long latencyMs = System.currentTimeMillis() - startTime;
if (result instanceof RagResponse) {
RagResponse ragResponse = (RagResponse) result;
// 幻觉检测
HallucinationCheckResult hallucinationResult = null;
if (aiObservable.checkHallucination()) {
hallucinationResult = hallucinationDetector.check(
ragResponse.getContent(),
ragResponse.getRetrievedDocuments()
);
}
// 引用准确率
CitationCheckResult citationResult = null;
if (aiObservable.checkCitations()) {
citationResult = citationChecker.checkCitations(
ragResponse.getContent(),
ragResponse.getRetrievedDocuments()
);
}
// Token 利用率
double utilizationRate = 0;
if (ragResponse.getTokenUsage() != null) {
TokenUtilizationResult utilResult = tokenAnalyzer.analyze(
ragResponse.getSystemPrompt(),
ragResponse.getRetrievedDocuments().stream()
.map(RetrievedDocument::getContent)
.collect(Collectors.toList()),
ragResponse.getUserQuery(),
ragResponse.getTokenUsage().getInputTokens()
);
utilizationRate = utilResult.getUtilizationRate();
}
// 记录所有指标
metrics.recordCall(AiCallObservation.builder()
.featureKey(featureKey)
.model(ragResponse.getModelVersion())
.tenantId(getTenantId())
.inputTokens(ragResponse.getTokenUsage() != null
? ragResponse.getTokenUsage().getInputTokens() : 0)
.outputTokens(ragResponse.getTokenUsage() != null
? ragResponse.getTokenUsage().getOutputTokens() : 0)
.totalLatencyMs(latencyMs)
.firstTokenLatencyMs(ragResponse.getFirstTokenLatencyMs())
.tokenUtilizationRate(utilizationRate)
.hallucinationDetected(hallucinationResult != null && hallucinationResult.isHasHallucination())
.citationErrorCount(citationResult != null ? citationResult.getInvalidCitations() : 0)
.success(exception == null)
.build());
}
}
}
}AI 可观测性指标体系
Grafana 看板设计
有了 Micrometer 采集的指标,用 Grafana 搭一个 AI 专属看板。我的看板分四个区域:
区域1:工程健康度
- QPS 折线图
- P50/P99 延迟折线图
- 错误率折线图
- 并发请求数折线图
区域2:AI 质量看板
- 幻觉率趋势(RAG 场景)
- 引用准确率趋势
- Token 利用率分布(饼图)
- 各功能的响应质量对比
区域3:成本看板
- 日成本趋势
- 各功能成本占比(饼图)
- Token 消耗 Top 10 用户
- 单次调用 Token 数分布
区域4:用户体验看板
- 二次追问率趋势
- 会话完成率
- 用户评分分布
告警规则配置
# Prometheus AlertManager 配置(Micrometer 导出到 Prometheus 后生效)
groups:
- name: ai_quality_alerts
rules:
# 幻觉率超过 15%
- alert: HighHallucinationRate
expr: rate(ai_hallucination_detected_total[5m]) / rate(ai_calls_total[5m]) > 0.15
for: 10m
labels:
severity: warning
annotations:
summary: "High hallucination rate detected for {{ $labels.feature }}"
description: "Hallucination rate is {{ $value | humanizePercentage }}"
# Token 利用率持续低于 40%(说明 Prompt 有大量无用上下文)
- alert: LowTokenUtilization
expr: histogram_quantile(0.5, ai_token_utilization_bucket) < 0.4
for: 30m
labels:
severity: info
annotations:
summary: "Low token utilization for {{ $labels.feature }}"
description: "Median token utilization is {{ $value | humanizePercentage }}, consider optimizing prompts"
# 首 Token 延迟 P99 超过 10 秒
- alert: HighFirstTokenLatency
expr: histogram_quantile(0.99, ai_latency_first_token_seconds_bucket) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "High first-token latency for {{ $labels.model }}"和 Langfuse 的关系
有人问:已经用了 Langfuse,还需要这套吗?
两者不冲突,定位不同:
Langfuse:专注 LLM 调用的全链路追踪,适合调试(某次调用用了哪个 Prompt、检索了哪些文档、每步耗时多少)。粒度细,但存储成本高,不适合做聚合统计。
Micrometer + Prometheus/Grafana:做聚合指标监控,适合运营(过去一周幻觉率是多少、哪个功能成本最高)。粒度粗,但查询快,适合看趋势。
我们两者都用:日常运营看 Grafana,出问题排查用 Langfuse 追踪具体链路。
最后
那次被产品问到哑口无言之后,我花了大概两周时间搭建了这套 AI 专属的可观测体系。
现在再开周会,我能说:
"这周幻觉率 8%,比上周降了 3 个百分点,主要来自 RAG 检索策略的优化;Token 利用率提升到 72%,通过精简 system prompt 节省了 15% 的成本;用户二次追问率从 23% 降到 18%,说明回答质量在改善。"
这才是 AI 系统的可观测性应该给你的信息。
