AI 应用的 SLI/SLO 定义——AI 系统怎么量化服务质量
AI 应用的 SLI/SLO 定义——AI 系统怎么量化服务质量
有一次和业务方开需求评审会,产品经理直接问了我一个问题:「你们的 AI 客服能保证准确率多少?」
我当时愣了一秒,下意识想回答「P99 响应时间在 3 秒以内,可用性 99.9%」。后来我意识到她根本不在乎这个。她要的是:这个系统回答用户问题时,有多少比例是靠谱的?错了会怎么样?我们怎么知道它变差了?
这个问题让我意识到,传统 SLI/SLO 体系在 AI 场景有一个根本性的缺陷:它只能描述「系统是否在运行」,无法描述「系统运行得有多好」。
一、传统 SLO 在 AI 场景的失效
先说清楚传统 SLO 的逻辑。
在 Google SRE 的标准框架里:
- SLI(Service Level Indicator):可量化的服务质量度量,比如「P99 响应时间」
- SLO(Service Level Objective):SLI 的目标值,比如「P99 响应时间 < 200ms」
- SLA(Service Level Agreement):和用户或客户签订的承诺,通常比 SLO 宽松
这套体系对于数据库、API 网关、消息队列非常好用。但套到 AI 应用上,出现了几个问题:
问题一:响应时间的尺度完全不同。普通 API P99 < 200ms 是合理目标,但 AI 应用的 P99 可能是 5-10 秒,这不代表服务质量差,是这类应用的固有特性。如果还用 200ms 这个阈值,SLO 永远达不到,失去了意义。
问题二:成功响应不等于有效响应。HTTP 200 只代表「我回复了你」,不代表「我回复得对」。一个持续输出幻觉内容的 AI 服务,从传统 SLO 角度看完全健康。
问题三:用户体验的度量颗粒度不够。流式 AI 应用里,TTFT(首 Token 时间)是 3 秒还是 0.5 秒,对用户感知差异极大,但如果只看总响应时间,这个差异就被淹没了。
二、AI 应用的 SLI 体系
针对上面的问题,我们需要扩展 SLI 的定义范围。AI 应用的 SLI 应该覆盖三个层次:
2.1 可用性 SLI
这一层和传统应用一致,但需要区分「硬错误」和「软错误」:
- 硬错误:HTTP 5xx、超时、网络中断。这类错误直接影响可用性计算。
- 软错误:内容被过滤、Token 超限导致截断。这类错误对用户有影响,但不应该直接记入可用性,而应该单独追踪。
SLI 定义:
可用性 SLI = (总请求数 - 硬错误请求数) / 总请求数2.2 性能 SLI
AI 场景的性能 SLI 至少需要三个:
TTFT SLI(适用于流式场景):
TTFT SLI = TTFT < 阈值的请求比例 / 总流式请求数阈值根据业务场景定:对话类应用通常 2 秒以内是好的体验,超过 5 秒用户就会不耐烦。
总响应时间 SLI:
响应时间 SLI = 总响应时间 < 阈值的请求比例 / 总请求数流式完成率 SLI(容易被忽略):
流式完成率 SLI = 完整完成的流式响应数 / 总流式请求数如果用户开始接收流式输出,但中途连接断了或系统出错导致截断,这对用户体验伤害极大,需要单独监控。
2.3 质量 SLI(最难但最重要)
这是 AI 应用 SLI 体系的核心创新点,也是最难实现的部分。
答案准确率 SLI:
- 离线评估:定期用测试集跑评估,计算准确率
- 在线代理:通过用户反馈信号(点踩、追问)间接估算
引用可靠性 SLI(RAG 场景专属):
引用可靠性 = 能从检索文档中找到明确支撑的答案陈述数 / 答案总陈述数这需要一个「引用验证」步骤,可以通过 NLI(自然语言推断)模型来实现。
语义相关性 SLI:
语义相关性 SLI = 平均检索相关性得分 > 阈值的请求比例三、一个 RAG 客服系统的 SLO 制定过程
理论讲完,说个真实案例。
我们为一家电商公司做了一套 RAG 客服系统,需要和他们的运营团队协商 SLO。整个过程大概经历了三个阶段。
阶段一:初稿(工程师视角)
工程师团队给出的初稿 SLO:
SLO-1: 可用性 ≥ 99.9%
SLO-2: P99 响应时间 < 10 秒
SLO-3: TTFT P99 < 2 秒
SLO-4: RAG 语义相关性平均得分 > 0.75运营团队的反应:「这些数字我看不懂,什么叫语义相关性 0.75,这对用户意味着什么?」
这是很典型的技术指标和业务价值的脱节。
阶段二:翻译成业务语言
第二轮,我们把技术指标翻译成业务指标:
SLO-1: 每月服务中断时间 ≤ 43 分钟(对应 99.9% 可用性)
SLO-2: 90% 的用户在 10 秒内收到完整回答
SLO-3: 95% 的流式回答在 2 秒内开始显示第一个字
SLO-4: 用户对回答「不满意」的比例 ≤ 8%(通过评价按钮采集)
SLO-5: 需要人工转接的比例 ≤ 15%(反映 AI 无法处理的问题比例)这一版运营团队就能理解了,但他们对阈值有异议:「每月 43 分钟还是太多了,能不能更少?」、「15% 转接率太高了,我们的目标是 10% 以内。」
阶段三:对齐阈值,绑定错误预算
第三轮,我们引入了「错误预算」的概念,让业务方理解 SLO 和工程成本的关系:
向业务方解释:「如果 SLO-1 从 99.9% 提升到 99.99%,错误预算从每月 43 分钟变成 4 分钟,这意味着我们需要:多一套冷备、更严格的部署流程、24 小时 on-call 轮值。这些都需要人力和成本。」
最终双方对齐的 SLO:
# 电商客服 RAG 系统 SLO 定义
slos:
- name: "服务可用性"
sli: "1 - (hard_error_count / total_request_count)"
target: 99.5%
window: 30d
error_budget_alert_threshold: 50% # 错误预算消耗超过 50% 时告警
- name: "首字节时间"
sli: "count(ttft < 3s) / count(stream_requests)"
target: 90%
window: 7d
- name: "完整响应时间"
sli: "count(total_latency < 12s) / count(requests)"
target: 85%
window: 7d
- name: "用户满意率"
sli: "1 - (negative_feedback_count / feedback_total_count)"
target: 88% # 不满意率 ≤ 12%
window: 7d
measurement: "用户评价按钮(满意/不满意)"
- name: "人工转接率"
sli: "1 - (human_transfer_count / total_conversation_count)"
target: 88% # 转接率 ≤ 12%
window: 7d
measurement: "客服系统转接日志"阶段四:建立 SLI 采集和 SLO 验证流程
四、SLI 采集的 Java 实现
4.1 SLI 数据收集器
@Service
@Slf4j
public class SliCollector {
@Autowired
private MeterRegistry meterRegistry;
@Autowired
private SloConfigProperties sloConfig;
/**
* 记录一次请求的 SLI 数据
*/
public void recordRequestSli(RequestSliData data) {
String model = data.getModel();
String scene = data.getScene();
// SLI-1: 可用性(硬错误追踪)
if (data.isHardError()) {
meterRegistry.counter("sli_hard_error_total",
"model", model, "scene", scene,
"error_type", data.getErrorType()
).increment();
}
// SLI-2: TTFT(仅流式请求)
if (data.isStream() && data.getTtftMs() > 0) {
boolean ttftMet = data.getTtftMs() <= sloConfig.getTtftThresholdMs();
meterRegistry.counter("sli_ttft_total",
"model", model, "scene", scene,
"met", String.valueOf(ttftMet)
).increment();
}
// SLI-3: 总响应时间
boolean latencyMet = data.getTotalLatencyMs() <= sloConfig.getLatencyThresholdMs();
meterRegistry.counter("sli_latency_total",
"model", model, "scene", scene,
"met", String.valueOf(latencyMet)
).increment();
// 总请求数(分母)
meterRegistry.counter("sli_request_total",
"model", model, "scene", scene
).increment();
log.debug("SLI recorded: model={}, scene={}, ttft={}ms, latency={}ms, hardError={}",
model, scene, data.getTtftMs(), data.getTotalLatencyMs(), data.isHardError());
}
/**
* 记录用户反馈(用于满意率 SLI)
*/
public void recordUserFeedback(String sessionId, String scene, FeedbackType feedbackType) {
meterRegistry.counter("sli_user_feedback_total",
"scene", scene,
"type", feedbackType.name().toLowerCase()
).increment();
}
/**
* 记录人工转接事件
*/
public void recordHumanTransfer(String sessionId, String scene, String reason) {
meterRegistry.counter("sli_human_transfer_total",
"scene", scene,
"reason", reason
).increment();
}
}4.2 SLO 状态计算服务
@Service
@Slf4j
public class SloStatusService {
@Autowired
private PrometheusQueryClient prometheusClient;
/**
* 计算当前 SLO 达标状态和错误预算消耗
*/
public SloStatus calculateSloStatus(String sloName, Duration window) {
switch (sloName) {
case "availability":
return calculateAvailabilitySlo(window);
case "ttft":
return calculateTtftSlo(window);
case "user_satisfaction":
return calculateSatisfactionSlo(window);
default:
throw new IllegalArgumentException("Unknown SLO: " + sloName);
}
}
private SloStatus calculateAvailabilitySlo(Duration window) {
String windowStr = formatWindow(window);
// 查询 SLI 当前值
double hardErrors = prometheusClient.queryScalar(
String.format("sum(increase(sli_hard_error_total[%s]))", windowStr)
);
double totalRequests = prometheusClient.queryScalar(
String.format("sum(increase(sli_request_total[%s]))", windowStr)
);
if (totalRequests == 0) {
return SloStatus.noData("availability");
}
double currentSli = 1.0 - (hardErrors / totalRequests);
double targetSlo = 0.995; // 99.5%
// 错误预算计算
double errorBudgetTotal = 1.0 - targetSlo; // 0.005
double errorBudgetConsumed = 1.0 - currentSli; // 实际消耗
double errorBudgetRemaining = errorBudgetTotal - errorBudgetConsumed;
double errorBudgetPercent = errorBudgetConsumed / errorBudgetTotal * 100;
return SloStatus.builder()
.sloName("availability")
.targetSlo(targetSlo)
.currentSli(currentSli)
.isMet(currentSli >= targetSlo)
.errorBudgetPercent(errorBudgetPercent)
.errorBudgetRemaining(errorBudgetRemaining)
.window(window)
.build();
}
private SloStatus calculateTtftSlo(Duration window) {
String windowStr = formatWindow(window);
double ttftMet = prometheusClient.queryScalar(
String.format("sum(increase(sli_ttft_total{met=\"true\"}[%s]))", windowStr)
);
double ttftTotal = prometheusClient.queryScalar(
String.format("sum(increase(sli_ttft_total[%s]))", windowStr)
);
if (ttftTotal == 0) {
return SloStatus.noData("ttft");
}
double currentSli = ttftMet / ttftTotal;
double targetSlo = 0.90; // 90%
double errorBudgetPercent = (1.0 - currentSli) / (1.0 - targetSlo) * 100;
return SloStatus.builder()
.sloName("ttft")
.targetSlo(targetSlo)
.currentSli(currentSli)
.isMet(currentSli >= targetSlo)
.errorBudgetPercent(errorBudgetPercent)
.window(window)
.build();
}
private String formatWindow(Duration window) {
long days = window.toDays();
if (days > 0) return days + "d";
long hours = window.toHours();
if (hours > 0) return hours + "h";
return window.toMinutes() + "m";
}
}4.3 定时生成 SLO 周报
@Component
@Slf4j
public class SloWeeklyReporter {
@Autowired
private SloStatusService sloStatusService;
@Autowired
private NotificationService notificationService;
@Scheduled(cron = "0 0 9 * * MON") // 每周一上午9点
public void generateWeeklyReport() {
Duration window = Duration.ofDays(7);
List<SloStatus> statuses = List.of(
sloStatusService.calculateSloStatus("availability", window),
sloStatusService.calculateSloStatus("ttft", window),
sloStatusService.calculateSloStatus("user_satisfaction", window)
);
StringBuilder report = new StringBuilder();
report.append("## AI 客服系统 SLO 周报\n\n");
report.append(String.format("统计周期:过去 7 天\n\n"));
for (SloStatus status : statuses) {
String emoji = status.isMet() ? "✅" : "❌";
report.append(String.format("%s **%s**\n", emoji, status.getSloName()));
report.append(String.format(" - 目标:%.1f%% 当前:%.1f%%\n",
status.getTargetSlo() * 100, status.getCurrentSli() * 100));
report.append(String.format(" - 错误预算消耗:%.1f%%\n\n",
status.getErrorBudgetPercent()));
}
// 整体判断
boolean allMet = statuses.stream().allMatch(SloStatus::isMet);
report.append(allMet
? "本周所有 SLO 均已达标。\n"
: "⚠️ 本周有 SLO 未达标,请关注详情。\n"
);
notificationService.sendToChannel("ai-slo-report", report.toString());
log.info("SLO weekly report generated and sent");
}
}五、技术指标与业务价值的对照表
最后整理一个速查表,方便和业务方沟通:
| 技术 SLI | 业务含义 | 推荐目标 |
|---|---|---|
| 硬错误率 < 0.5% | 99.5% 的请求都能得到回复 | 可用性 99.5% |
| TTFT P90 < 3s | 90% 的用户在 3 秒内看到第一个字 | TTFT SLO 90% |
| 流式完成率 > 98% | 98% 的流式回答不会中途截断 | 完成率 98% |
| 语义得分 > 0.75 | 检索的知识点大概率与问题相关 | 语义 SLI 75% |
| 用户负反馈率 < 12% | 88 个用户里有 78 个满意 | 满意率 SLO 88% |
| 人工转接率 < 12% | AI 能独立解决 88% 的问题 | 自助解决 SLO 88% |
总结
AI 应用的 SLI/SLO 体系和传统应用有三点核心差异:
- 性能 SLI 要分层:TTFT、流式完成率、总响应时间是三个不同的维度,不能混为一谈
- 质量 SLI 是核心创新:传统 SLO 不关心内容质量,AI 应用必须关心
- 技术指标要翻译成业务语言:和业务方对齐的 SLO 才是有效的 SLO,看不懂的指标等于不存在
SLO 制定的本质是在「工程能力边界」和「业务期望」之间找到一个双方都能接受的平衡点。错误预算是让这个平衡变得可操作、可量化的工具。
