Spring AI 学习篇(十五)| Spring AI应用的可观测性
- 一、本章核心学习目标
- 二、前置知识准备
- 三、为什么AI应用的可观测性比传统应用更重要?
- 四、可观测性的三大支柱:日志、指标、链路追踪
- 五、日志体系:从零散到体系化
-
- [1. AI应用日志的四层分级策略](#1. AI应用日志的四层分级策略)
- [2. 模型调用日志的完整实现](#2. 模型调用日志的完整实现)
- [3. RAG检索日志](#3. RAG检索日志)
- [4. Agent执行日志](#4. Agent执行日志)
- 六、指标监控:让数字说话
-
- [1. 核心监控指标体系设计](#1. 核心监控指标体系设计)
- [2. 在业务代码中埋点](#2. 在业务代码中埋点)
- [3. Prometheus配置与Grafana看板](#3. Prometheus配置与Grafana看板)
- 七、告警机制:不要等到用户投诉才知道出问题
-
- [1. AI应用告警规则设计](#1. AI应用告警规则设计)
- [2. 告警通知多渠道实现](#2. 告警通知多渠道实现)
- 八、用户行为分析与反馈闭环
-
- [1. 用户行为埋点](#1. 用户行为埋点)
- [2. 反馈数据驱动优化](#2. 反馈数据驱动优化)
- 九、企业级最佳实践
-
- [1. 日志分级策略](#1. 日志分级策略)
- [2. 监控看板分层设计](#2. 监控看板分层设计)
- [3. 成本控制三板斧](#3. 成本控制三板斧)
- [4. 告警分级与响应](#4. 告警分级与响应)
- 十、常见坑与解决方案
- 十一、本章总结与下章预告
- 十二、课后练习
一、本章核心学习目标
学完本章,你将能够:
- 深刻理解AI应用可观测性的三大支柱与核心价值
- 实现模型调用的全链路日志追踪与性能监控
- 精准统计每次调用的token消耗、耗时与成本
- 建立向量数据库的性能监控与调优体系
- 实现用户行为分析与反馈闭环
- 搭建Prometheus + Grafana企业级AI监控看板
- 建立告警机制,第一时间发现和定位线上问题
二、前置知识准备
- 已经完成前14篇的学习,拥有一个完整的智能办公Agent项目
- 了解Spring Boot Actuator和Micrometer基础
- 熟悉日志框架(SLF4J/Logback)的基本使用
- 了解Prometheus和Grafana的基本概念
三、为什么AI应用的可观测性比传统应用更重要?
传统Web应用出问题,通常是接口报错、数据库超时这类确定性故障,排查路径清晰。但AI应用完全不同------它的大部分问题都是"说不清哪里不对,但效果就是不好"。
AI应用特有的三大观测难题
- 模型调用是黑盒:你不知道大模型内部发生了什么,只能通过输入输出来判断。一次调用耗时从200ms到30s都有可能,完全取决于模型当时的负载。
- 成本不可见:每次调用消耗的token数量不同,如果没有精确统计,月底收到账单时可能会吓一跳。一个用户一天问100个问题,可能消耗几十万token,也可能消耗几百万token。
- 效果无法量化:传统应用的"正确"是二元的(接口返回200就是正确),但AI应用的"正确"是模糊的------回答准确吗?有没有幻觉?用户满意吗?这些都无法通过简单的错误码来判断。
一句话总结:传统应用的可观测性告诉你"系统有没有挂",AI应用的可观测性告诉你"系统有没有在好好工作"。后者远比前者复杂。
四、可观测性的三大支柱:日志、指标、链路追踪
在AI应用中,我们需要同时关注三个维度:
┌─────────────────────────────────────────────────────────┐
│ AI应用可观测性体系 │
├─────────────────┬─────────────────┬─────────────────────┤
│ 日志(Logs) │ 指标(Metrics) │ 链路追踪(Traces) │
├─────────────────┼─────────────────┼─────────────────────┤
│ 发生了什么 │ 趋势和数量 │ 整个调用链路 │
│ 模型输入输出 │ token消耗趋势 │ 用户→API→模型→返回 │
│ 错误堆栈 │ 延迟分布 │ RAG检索链路 │
│ 审计记录 │ 成功率 │ Agent工具调用链 │
│ 用户反馈 │ 成本曲线 │ 向量检索耗时分布 │
└─────────────────┴─────────────────┴─────────────────────┘
五、日志体系:从零散到体系化
1. AI应用日志的四层分级策略
ERROR → 模型调用失败、工具执行异常、向量数据库连接断开
WARN → token消耗异常、响应时间过长、检索结果为0、重试触发
INFO → 每次模型调用的输入输出摘要、token消耗、耗时
DEBUG → 完整的模型输入输出内容、向量检索详细结果、提示词内容
2. 模型调用日志的完整实现
我们先实现一个模型调用日志切面,自动记录每次AI调用的完整信息:
java
package com.example.ai.monitoring.logging;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
@Aspect
@Component
public class AiCallLoggingAspect {
private static final Logger aiLogger = LoggerFactory.getLogger("AI-MODEL-CALL");
private static final Logger costLogger = LoggerFactory.getLogger("AI-COST-REPORT");
@Around("execution(* org.springframework.ai.chat.client.ChatClient.call(..))")
public Object logAiCall(ProceedingJoinPoint joinPoint) throws Throwable {
Instant start = Instant.now();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
try {
// 记录输入摘要
if (args.length > 0 && args[0] != null) {
aiLogger.info("[AI调用开始] 方法: {}, 输入长度: {} 字符",
methodName,
args[0].toString().length());
}
Object result = joinPoint.proceed();
Instant end = Instant.now();
long elapsed = Duration.between(start, end).toMillis();
// 提取并记录token使用情况
if (result instanceof ChatResponse response) {
Usage usage = response.getMetadata().getUsage();
if (usage != null) {
aiLogger.info("[AI调用完成] 方法: {}, 耗时: {}ms, " +
"输入token: {}, 输出token: {}, 总计token: {}",
methodName, elapsed,
usage.getPromptTokens(),
usage.getGenerationTokens(),
usage.getTotalTokens());
// 成本日志(精确到分)
// DeepSeek: 输入 0.001元/1K tokens, 输出 0.002元/1K tokens
double inputCost = usage.getPromptTokens() / 1000.0 * 0.001;
double outputCost = usage.getGenerationTokens() / 1000.0 * 0.002;
double totalCost = inputCost + outputCost;
costLogger.info("[AI成本] 调用: {}, 输入费用: {} 元, 输出费用: {} 元, 总费用: {} 元",
methodName,
String.format("%.6f", inputCost),
String.format("%.6f", outputCost),
String.format("%.6f", totalCost));
}
}
return result;
} catch (Throwable e) {
Instant end = Instant.now();
long elapsed = Duration.between(start, end).toMillis();
aiLogger.error("[AI调用失败] 方法: {}, 耗时: {}ms, 错误: {}",
methodName, elapsed, e.getMessage(), e);
throw e;
}
}
}
3. RAG检索日志
RAG系统有自己的日志需求------你需要知道每次检索召回了多少结果、每条结果的相似度分数、以及最终选择了哪些分块送入大模型:
java
@Component
public class RagRetrievalLogger {
private static final Logger ragLogger = LoggerFactory.getLogger("RAG-RETRIEVAL");
public void logRetrieval(String queryId, String query, List<Document> rawResults,
List<Document> rerankedResults, List<Document> finalResults) {
ragLogger.info("[RAG检索] 查询ID: {}, 查询语句: {}", queryId, query);
ragLogger.info("[RAG检索] 原始召回: {} 条", rawResults.size());
// 记录Top 5召回结果的相似度分数
rawResults.stream().limit(5).forEach(doc ->
ragLogger.debug("[RAG检索] 召回结果: 分数={}, 来源={}, 摘要={}",
doc.getMetadata().getOrDefault("score", "N/A"),
doc.getMetadata().getOrDefault("source", "N/A"),
truncate(doc.getContent(), 100))
);
ragLogger.info("[RAG检索] 重排序后: {} 条, 最终使用: {} 条",
rerankedResults.size(), finalResults.size());
// 当检索结果为0时发出警告
if (rawResults.isEmpty()) {
ragLogger.warn("[RAG检索告警] 查询召回为0! 查询ID: {}, 查询语句: {}", queryId, query);
}
// 当重排序后有效结果过少时发出警告
if (finalResults.size() < 3) {
ragLogger.warn("[RAG检索告警] 有效结果不足! 查询ID: {}, 最终结果: {} 条", queryId, finalResults.size());
}
}
private String truncate(String content, int maxLength) {
return content.length() > maxLength ? content.substring(0, maxLength) + "..." : content;
}
}
4. Agent执行日志
Agent的执行链路比普通模型调用复杂得多------它包含多轮思考、工具调用和结果处理。我们需要记录每一步的执行情况:
java
@Component
public class AgentExecutionLogger {
private static final Logger agentLogger = LoggerFactory.getLogger("AGENT-EXECUTION");
public void logAgentStart(String sessionId, String task) {
agentLogger.info("[Agent开始] 会话ID: {}, 任务: {}", sessionId, task);
}
public void logReActStep(String sessionId, int stepNumber, String thought, String action) {
agentLogger.info("[Agent步骤{}] 会话ID: {}, 思考: {}, 行动: {}",
stepNumber, sessionId, truncate(thought, 200), action);
}
public void logToolCall(String sessionId, String toolName, String params, long elapsedMs, boolean success) {
if (success) {
agentLogger.info("[工具调用成功] 会话ID: {}, 工具: {}, 参数: {}, 耗时: {}ms",
sessionId, toolName, params, elapsedMs);
} else {
agentLogger.error("[工具调用失败] 会话ID: {}, 工具: {}, 参数: {}, 耗时: {}ms",
sessionId, toolName, params, elapsedMs);
}
}
public void logAgentComplete(String sessionId, int totalSteps, long totalTimeMs, boolean success) {
agentLogger.info("[Agent结束] 会话ID: {}, 总步骤: {}, 总耗时: {}ms, 状态: {}",
sessionId, totalSteps, totalTimeMs, success ? "成功" : "失败");
}
private String truncate(String content, int maxLength) {
return content != null && content.length() > maxLength
? content.substring(0, maxLength) + "..." : content;
}
}
六、指标监控:让数字说话
日志告诉你"发生了什么",指标告诉你"趋势是什么"。日志查问题,指标发现问题。
1. 核心监控指标体系设计
我们使用Micrometer + Prometheus来采集和暴露指标:
java
package com.example.ai.monitoring.metrics;
import io.micrometer.core.instrument.*;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class AiMetricsCollector {
private final MeterRegistry meterRegistry;
// 模型调用计数器(按提供商和模型分组)
private Counter modelCallTotal;
private Counter modelCallSuccess;
private Counter modelCallFailure;
// Token消耗计数器
private Counter inputTokensTotal;
private Counter outputTokensTotal;
// 模型调用耗时分布
private Timer modelCallLatency;
// RAG检索计数器
private Counter ragQueryTotal;
private Counter ragQueryEmptyResult; // 检索结果为0的次数
private Timer ragRetrievalLatency;
// Agent执行计数器
private Counter agentTaskTotal;
private Counter agentTaskSuccess;
private Counter agentTaskFailure;
private DistributionSummary agentStepsDistribution;
// 成本累计
private Counter totalCostInFen; // 总成本,以分为单位
public AiMetricsCollector(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@PostConstruct
public void init() {
// 模型调用指标
this.modelCallTotal = Counter.builder("ai.model.call.total")
.description("模型调用总次数")
.tag("provider", "deepseek")
.register(meterRegistry);
this.modelCallSuccess = Counter.builder("ai.model.call.success")
.description("模型调用成功次数")
.register(meterRegistry);
this.modelCallFailure = Counter.builder("ai.model.call.failure")
.description("模型调用失败次数")
.register(meterRegistry);
this.inputTokensTotal = Counter.builder("ai.tokens.input.total")
.description("输入token总数")
.register(meterRegistry);
this.outputTokensTotal = Counter.builder("ai.tokens.output.total")
.description("输出token总数")
.register(meterRegistry);
this.modelCallLatency = Timer.builder("ai.model.call.latency")
.description("模型调用耗时")
.publishPercentiles(0.5, 0.95, 0.99) // P50, P95, P99
.register(meterRegistry);
// RAG检索指标
this.ragQueryTotal = Counter.builder("ai.rag.query.total")
.description("RAG查询总次数")
.register(meterRegistry);
this.ragQueryEmptyResult = Counter.builder("ai.rag.query.empty_result")
.description("RAG查询返回空结果次数")
.register(meterRegistry);
this.ragRetrievalLatency = Timer.builder("ai.rag.retrieval.latency")
.description("RAG检索耗时")
.register(meterRegistry);
// Agent执行指标
this.agentTaskTotal = Counter.builder("ai.agent.task.total")
.description("Agent任务总数")
.register(meterRegistry);
this.agentTaskSuccess = Counter.builder("ai.agent.task.success")
.description("Agent任务成功数")
.register(meterRegistry);
this.agentTaskFailure = Counter.builder("ai.agent.task.failure")
.description("Agent任务失败数")
.register(meterRegistry);
this.agentStepsDistribution = DistributionSummary.builder("ai.agent.steps")
.description("Agent任务步骤数分布")
.publishPercentiles(0.5, 0.95)
.register(meterRegistry);
this.totalCostInFen = Counter.builder("ai.cost.total.fen")
.description("AI调用总成本(分)")
.register(meterRegistry);
}
// 记录模型调用成功
public void recordModelCallSuccess(String provider, String model,
int inputTokens, int outputTokens, long latencyMs) {
modelCallTotal.increment();
modelCallSuccess.increment();
inputTokensTotal.increment(inputTokens);
outputTokensTotal.increment(outputTokens);
modelCallLatency.record(latencyMs, TimeUnit.MILLISECONDS);
// 成本计算(精确到分,1分=0.01元)
double costInFen = inputTokens / 1000.0 * 0.1 // 输入: 0.001元/1K = 0.1分/1K
+ outputTokens / 1000.0 * 0.2; // 输出: 0.002元/1K = 0.2分/1K
totalCostInFen.increment(costInFen);
}
// 记录模型调用失败
public void recordModelCallFailure() {
modelCallTotal.increment();
modelCallFailure.increment();
}
// 记录RAG检索
public void recordRagQuery(int resultCount, long latencyMs) {
ragQueryTotal.increment();
if (resultCount == 0) {
ragQueryEmptyResult.increment();
}
ragRetrievalLatency.record(latencyMs, TimeUnit.MILLISECONDS);
}
// 记录Agent任务
public void recordAgentTask(boolean success, int steps) {
agentTaskTotal.increment();
if (success) {
agentTaskSuccess.increment();
} else {
agentTaskFailure.increment();
}
agentStepsDistribution.record(steps);
}
}
2. 在业务代码中埋点
先定义一个标记注解 @AiMonitored:
java
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AiMonitored {
String provider() default "deepseek";
String model() default "deepseek-chat";
}
然后用 AOP 切面自动采集指标:
java
@Aspect
@Component
public class ModelCallMetricsAspect {
private final AiMetricsCollector metricsCollector;
public ModelCallMetricsAspect(AiMetricsCollector metricsCollector) {
this.metricsCollector = metricsCollector;
}
@Around("@annotation(aiMonitored)")
public Object monitor(AiMonitored aiMonitored, ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long elapsed = System.currentTimeMillis() - start;
if (result instanceof ChatResponse response) {
Usage usage = response.getMetadata().getUsage();
if (usage != null) {
metricsCollector.recordModelCallSuccess(
aiMonitored.provider(),
aiMonitored.model(),
usage.getPromptTokens(),
usage.getGenerationTokens(),
elapsed
);
}
}
return result;
} catch (Throwable e) {
metricsCollector.recordModelCallFailure();
throw e;
}
}
}
使用方式:在想监控的方法上加 @AiMonitored(provider = "deepseek", model = "deepseek-chat") 即可。
3. Prometheus配置与Grafana看板
在application.yml中暴露Prometheus端点:
yaml
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
metrics:
export:
prometheus:
enabled: true
tags:
application: ${spring.application.name}
推荐的AI应用Grafana监控面板布局:
┌──────────────────────────────────────────────────────────────┐
│ 第一行:核心KPI │
│ [总调用次数] [成功率%] [P99耗时] [今日成本] [活跃用户数] │
├──────────────────────────────────────────────────────────────┤
│ 第二行:趋势图 │
│ [模型调用QPS曲线] [Token消耗趋势] [成本累计曲线] │
├────────────────────────────┬─────────────────────────────────┤
│ 第三行:分布图 │ 第四行:RAG与Agent │
│ [调用耗时分布(P50/P95/P99)]│ [RAG检索召回数分布] │
│ [Token消耗分布] │ [Agent任务成功率] │
│ [错误率按错误类型] │ [Agent步骤数分布] │
└────────────────────────────┴─────────────────────────────────┘
七、告警机制:不要等到用户投诉才知道出问题
1. AI应用告警规则设计
与传统应用不同,AI应用需要关注一些特殊的告警指标:
java
package com.example.ai.monitoring.alerting;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
@Component
public class AiAlertRules {
private final MeterRegistry meterRegistry;
private final AlertNotifier alertNotifier;
public AiAlertRules(MeterRegistry meterRegistry, AlertNotifier alertNotifier) {
this.meterRegistry = meterRegistry;
this.alertNotifier = alertNotifier;
}
@PostConstruct
public void registerAlertChecks() {
// 这里简化展示告警逻辑,生产环境建议使用Prometheus AlertManager
}
/**
* 检查模型调用成功率,低于95%告警
*/
@Scheduled(fixedDelay = 60000) // 每分钟检查一次
public void checkModelSuccessRate() {
double successRate = getSuccessRate();
if (successRate < 95.0) {
alertNotifier.sendAlert(AlertLevel.WARNING,
"模型调用成功率低于95%",
String.format("当前成功率: %.2f%%,请检查模型服务状态", successRate));
}
if (successRate < 80.0) {
alertNotifier.sendAlert(AlertLevel.CRITICAL,
"模型调用成功率低于80%",
String.format("当前成功率: %.2f%%,模型服务可能宕机", successRate));
}
}
/**
* 检查P99延迟,超过10秒告警
*/
@Scheduled(fixedDelay = 60000)
public void checkP99Latency() {
double p99Latency = getP99Latency();
if (p99Latency > 10000) { // 10秒
alertNotifier.sendAlert(AlertLevel.WARNING,
"模型调用P99延迟超过10秒",
String.format("当前P99延迟: %.0fms,可能影响用户体验", p99Latency));
}
}
/**
* 检查每日成本,超过预算告警
*/
@Scheduled(fixedDelay = 3600000) // 每小时检查一次
public void checkDailyCost() {
double dailyCost = getDailyCost();
if (dailyCost > 100) { // 超过100元
alertNotifier.sendAlert(AlertLevel.WARNING,
"AI调用日成本超过100元",
String.format("今日成本: %.2f元,请检查是否有异常调用", dailyCost));
}
}
/**
* RAG检索结果为空率过高告警
*/
@Scheduled(fixedDelay = 300000) // 每5分钟检查
public void checkRagEmptyRate() {
double emptyRate = getRagEmptyRate();
if (emptyRate > 30.0) {
alertNotifier.sendAlert(AlertLevel.WARNING,
"RAG检索结果为空比例过高",
String.format("空结果比例: %.2f%%,请检查知识库内容和检索配置", emptyRate));
}
}
private double getSuccessRate() {
double total = meterRegistry.get("ai.model.call.total").counter().count();
double success = meterRegistry.get("ai.model.call.success").counter().count();
return total > 0 ? (success / total) * 100 : 100;
}
private double getP99Latency() {
return meterRegistry.get("ai.model.call.latency").timer().takeSnapshot().percentileValue(0.99);
}
private double getDailyCost() {
// 简化的日成本统计,实际应通过时间窗口计算
return meterRegistry.get("ai.cost.total.fen").counter().count() / 100.0;
}
private double getRagEmptyRate() {
double total = meterRegistry.get("ai.rag.query.total").counter().count();
double empty = meterRegistry.get("ai.rag.query.empty_result").counter().count();
return total > 0 ? (empty / total) * 100 : 0;
}
}
2. 告警通知多渠道实现
java
@Component
public class AlertNotifier {
private static final Logger log = LoggerFactory.getLogger(AlertNotifier.class);
public void sendAlert(AlertLevel level, String title, String detail) {
// 1. 日志记录(最基础)
if (level == AlertLevel.CRITICAL) {
log.error("[AI告警-严重] {} - {}", title, detail);
} else {
log.warn("[AI告警-警告] {} - {}", title, detail);
}
// 2. 企业微信通知(推荐)
sendWecomNotification(level, title, detail);
// 3. 邮件通知(严重级别)
if (level == AlertLevel.CRITICAL) {
sendEmailNotification(title, detail);
}
}
private void sendWecomNotification(AlertLevel level, String title, String detail) {
// 发送企业微信机器人消息
// 实际代码需要调用企业微信Webhook API
}
private void sendEmailNotification(String title, String detail) {
// 发送邮件通知给运维和开发团队
}
}
enum AlertLevel {
WARNING, CRITICAL
}
八、用户行为分析与反馈闭环
可观测性不只是看系统指标,还要看用户行为。用户的行为数据是最真实的"效果评估"。
1. 用户行为埋点
java
@Component
public class UserBehaviorTracker {
private static final Logger behaviorLogger = LoggerFactory.getLogger("USER-BEHAVIOR");
/**
* 记录用户提问
*/
public void trackQuery(String userId, String sessionId, String query, String intent) {
behaviorLogger.info("[用户行为] 类型: 提问, 用户: {}, 会话: {}, 意图: {}, 问题: {}",
userId, sessionId, intent, query);
}
/**
* 记录用户对回答的操作(关键指标)
*/
public void trackFeedback(String userId, String sessionId, String queryId,
FeedbackType type, String comment) {
behaviorLogger.info("[用户行为] 类型: 反馈, 用户: {}, 查询ID: {}, 反馈: {}, 评论: {}",
userId, queryId, type, comment);
// 负面反馈需要重点关注
if (type == FeedbackType.THUMBS_DOWN || type == FeedbackType.REPORT_INACCURATE) {
behaviorLogger.warn("[用户行为告警] 负面反馈! 用户: {}, 查询ID: {}, 类型: {}, 评论: {}",
userId, queryId, type, comment);
}
}
/**
* 记录用户复制了答案(正向信号)
*/
public void trackCopy(String userId, String queryId) {
behaviorLogger.info("[用户行为] 类型: 复制答案, 用户: {}, 查询ID: {}", userId, queryId);
}
/**
* 记录用户追问(回答不够好,需要追问)
*/
public void trackFollowUp(String userId, String sessionId, String originalQueryId) {
behaviorLogger.info("[用户行为] 类型: 追问, 用户: {}, 会话: {}, 原始查询ID: {}",
userId, sessionId, originalQueryId);
}
enum FeedbackType {
THUMBS_UP, // 点赞
THUMBS_DOWN, // 点踩
REPORT_INACCURATE, // 举报不准确
REPORT_IRRELEVANT // 举报不相关
}
}
2. 反馈数据驱动优化
用户反馈不只是"看看",而是要形成闭环,驱动系统持续优化:
java
@Service
public class FeedbackDrivenOptimizer {
private final FeedbackRepository feedbackRepository;
private final KnowledgeBaseService knowledgeBaseService;
/**
* 每日分析负面反馈,生成优化建议
*/
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void analyzeDailyFeedback() {
// 1. 统计昨日负面反馈
List<Feedback> negativeFeedbacks = feedbackRepository
.findByTypeInAndCreatedAtAfter(
List.of("THUMBS_DOWN", "REPORT_INACCURATE"),
LocalDate.now().minusDays(1).atStartOfDay()
);
if (negativeFeedbacks.isEmpty()) {
return;
}
// 2. 按查询分类聚合
Map<String, List<Feedback>> groupedByQuery = negativeFeedbacks.stream()
.collect(Collectors.groupingBy(Feedback::getQueryCategory));
// 3. 找出高频问题的知识缺口
for (Map.Entry<String, List<Feedback>> entry : groupedByQuery.entrySet()) {
if (entry.getValue().size() >= 3) { // 同一类问题出现3次以上负面反馈
String category = entry.getKey();
log.warn("[反馈分析] 类别 '{}' 昨日收到 {} 次负面反馈,建议补充相关知识库文档",
category, entry.getValue().size());
// 4. 自动生成知识库补充建议
generateKnowledgeGapReport(category, entry.getValue());
}
}
}
private void generateKnowledgeGapReport(String category, List<Feedback> feedbacks) {
// 生成知识缺口报告,发送给知识库管理员
StringBuilder report = new StringBuilder();
report.append("## 知识缺口报告\n\n");
report.append("**类别**: ").append(category).append("\n");
report.append("**负面反馈数**: ").append(feedbacks.size()).append("\n\n");
report.append("**用户原始问题**:\n");
feedbacks.forEach(f -> report.append("- ").append(f.getOriginalQuery()).append("\n"));
report.append("\n**建议**: 请补充该类别相关的文档到知识库中");
// 发送报告给管理员
knowledgeBaseService.sendGapReport(report.toString());
}
}
九、企业级最佳实践
1. 日志分级策略
- 开发环境:开启DEBUG级别,记录完整的模型输入输出和检索细节
- 测试环境:开启INFO级别,重点验证监控指标是否正确
- 生产环境 :开启INFO级别,WARN和ERROR重点关注。绝对不要记录完整的用户输入和大模型输出到生产日志(数据安全风险)
2. 监控看板分层设计
- 运维看板:服务可用性、延迟、错误率(关注系统层面的健康)
- 业务看板:活跃用户数、问题解决率、用户满意度、成本趋势(关注业务价值)
- AI专项看板:模型调用QPS、token消耗趋势、RAG召回率、幻觉率(关注AI效果)
3. 成本控制三板斧
- 设置每日成本上限:超出预算自动降级或熔断
- 缓存高频问题:相同问题在短时间内直接返回缓存结果
- 路由策略:简单问题用便宜的小模型,复杂问题才用大模型
4. 告警分级与响应
| 级别 | 定义 | 响应时间 | 通知方式 |
|---|---|---|---|
| P0-紧急 | 模型服务完全不可用 | 5分钟内 | 电话+企微+邮件 |
| P1-严重 | 成功率低于80%或P99延迟>30s | 15分钟内 | 企微+邮件 |
| P2-警告 | 成功率低于95%或日成本超预算 | 1小时内 | 企微 |
| P3-关注 | 检索空结果率升高或用户负面反馈增加 | 次日处理 | 日报 |
十、常见坑与解决方案
坑1:在生产环境记录了完整的用户问题和大模型输出
后果 :日志中包含大量敏感信息,一旦日志泄露,后果严重。
解决:生产环境INFO级别只记录token数量和摘要,绝不记录完整内容。敏感日志使用专门的脱敏工具处理。
坑2:模型调用的"假成功"
现象 :模型返回了HTTP 200,但内容是"抱歉,我无法回答这个问题"。
后果 :监控指标显示成功率100%,但用户实际上没有得到有效答案。
解决:除了监控HTTP状态码,还需要监控输出内容的长度和关键模式。例如输出为空、只有拒绝回答的模板、与输入高度重复等,都应该标记为"效果异常"。
坑3:token计数不准确
现象 :自己统计的token消耗与API服务商的账单对不上。
原因 :提示词模板在运行时会被渲染,实际发送的token比模板多。而且不同模型的tokenizer不同,计费标准也不同。
解决 :始终使用Usage对象返回的真实token数,不要自己估算。DeepSeek、智谱、OpenAI的令牌计算方式不一样,计费单价也不同,确保分别统计。
坑4:Agent的步骤数爆炸
现象 :Agent陷入无限循环,一个简单任务执行了50步还没结束。
解决 :始终设置maxIterations上限(建议10-15),超过上限强制终止并告警。同时监控Agent平均步骤数,如果突然上升,说明模型可能出了问题。
十一、本章总结与下章预告
本章总结
- AI应用的可观测性比传统应用更复杂,因为它关注的不只是"系统有没有挂",更是"系统有没有在好好工作"
- 三大支柱:日志告诉你"发生了什么",指标告诉你"趋势是什么",链路追踪告诉你"整个调用路径是什么"
- 模型调用的核心监控指标:成功率、P99延迟、token消耗、成本
- RAG特有的监控:检索召回数、空结果率、重排序有效率
- Agent特有的监控:任务成功率、步骤数分布、工具调用成功率
- 用户行为数据是最真实的"效果评估",必须建立反馈闭环
- 告警要分级,成本要设上限,日志要脱敏
下章预告
本章我们学习了如何"看"清AI应用的运行状态,但还有一个更重要的问题没有解决------安全。下一章我们将学习AI应用的安全与合规,包括如何防止Prompt注入攻击、如何对敏感数据进行脱敏、如何满足国内AI应用的合规要求。这些都是企业级AI应用上线的必要条件。
十二、课后练习
- 为你的智能办公Agent项目添加本章的日志切面和指标收集器
- 配置Prometheus端点,搭建一个本地的Grafana看板
- 设计你的AI应用的告警规则,至少包含3条告警
- 思考:你的应用中有哪些用户行为值得追踪?如何利用这些行为数据来优化AI效果?