AI 服务质量定义:SLO 要把推理延迟、可用率和准确率都算进去
一、"99.9% 可用率"不等于用户满意——AI 服务的质量是三维的
传统 Web 服务的 SLO 很简单:可用率。99.9% 的请求返回 2xx,就算达标了。金丝雀发布看错误率,Prometheus 告警绑定 5xx 比例,一切清晰明了。
但 AI 推理服务不是这样。一个请求可能返回 200 OK,但生成的回答完全错误。从 HTTP 状态码的角度看它是"成功"的,从用户的角度看它是"失败"的。如果按传统可用率定义,你的推理服务可能有 99.95% 的可用率——但其中 15% 的请求返回了错误或低质量的答案,而你的监控大盘对此一无所知。
这就是 AI 服务质量定义的独特挑战:质量是多维的。延迟(响应的速度)、可用率(服务的可达性)、准确率(回答的正确性)必须同时纳入 SLO。三个维度中的任何一个不达标,都应该触发告警并阻止上线。
二、三维 SLO 模型的定义与计算框架
将 AI 服务质量定义为三个维度的四组指标:
graph TD
subgraph 请求入口
A[用户请求] –> B{SLO 判定引擎}
end
subgraph 维度1_延迟
B –> C[P50 延迟]
B –> D[P99 延迟]
B –> E[首Token时间 TTFT]
end
subgraph 维度2_可用率
B –> F[HTTP 2xx 率]
B –> G[推理成功率]
B –> H[Token 生成完成率]
end
subgraph 维度3_准确率
B –> I[用户反馈分]
B –> J[自动评估分]
B –> K[安全拒绝率]
end
subgraph SLO_汇总
C –> L[每日 SLO 合规度]
D –> L
E –> L
F –> L
G –> L
H –> L
I –> L
J –> L
K –> L
L –> M{任意维度 < 目标?}
M –>|是| N[触发告警 + 阻断发布]
M –>|否| O[正常]
end
三维 SLO 的具体定义:
- 延迟 SLO:P99 端到端延迟 ≤ 5s(滚动窗口 30 天,达标率 ≥ 99%)。但 P99 还不够——对于流式推理场景,首 Token 时间(TTFT)必须单独设 SLO(如 P95 TTFT ≤ 500ms)。用户在打字时感受到的是"第一句话出现前的等待"。
- 可用率 SLO:HTTP 2xx 率 ≥ 99.9% 且推理成功率 ≥ 99.5%。区分"HTTP 200 但推理失败"和"HTTP 500",前者在传统监控中是不可见的。
- 准确率 SLO:自动评估分(如 Rouge-L、人工评判通过率)的 30 天滑动均值不低于历史基线的 95%。这是最难量化但最重要的维度——它直接定义了用户感知到的质量。
三、三维 SLO 监控的生产级实现
以下 Go 代码实现了 SLO 的实时追踪和燃尽图计算:
package main
import (
"fmt"
"sync"
"time"
)
// SLODimension 定义 SLO 的三个维度
type SLODimension string
const (
DimLatency SLODimension = "latency"
DimAvailability SLODimension = "availability"
DimAccuracy SLODimension = "accuracy"
)
// SLOIndicator 单个 SLO 指标的追踪器
type SLOIndicator struct {
Name string
TargetRate float64 // 目标达标率,如 0.99 表示 99%
Window time.Duration // 滑动窗口大小,如 30*24*time.Hour
}
// SLOTracker 三维 SLO 追踪器(协程安全)
type SLOTracker struct {
mu sync.RWMutex
indicators map[SLODimension][]SLOIndicator
buckets map[string]*TokenBucket // key: "维度_指标_日期"
window time.Duration
alertFunc func(dimension SLODimension, indicator string, current float64, target float64)
}
// TokenBucket 日级 SLO Token 桶,追踪好事件/总事件比率
type TokenBucket struct {
GoodEvents int64
TotalEvents int64
}
// SLOReport 单次 SLO 评估报告
type SLOReport struct {
Dimension SLODimension
Indicator string
Compliance float64 // 当前合规度
Target float64 // 目标合规度
Healthy bool
BurnRate float64 // 燃尽率:消耗错误预算的速率
}
func NewSLOTracker(window time.Duration, alertFunc func(SLODimension, string, float64, float64)) *SLOTracker {
return &SLOTracker{
indicators: map[SLODimension][]SLOIndicator{
DimLatency: {
{Name: "p99_e2e_latency", TargetRate: 0.99, Window: window},
{Name: "p95_ttft", TargetRate: 0.99, Window: window},
},
DimAvailability: {
{Name: "http_2xx_rate", TargetRate: 0.999, Window: window},
{Name: "inference_success_rate", TargetRate: 0.995, Window: window},
},
DimAccuracy: {
{Name: "auto_eval_pass_rate", TargetRate: 0.95, Window: window},
{Name: "user_thumb_up_rate", TargetRate: 0.90, Window: window},
},
},
buckets: make(map[string]*TokenBucket),
window: window,
alertFunc: alertFunc,
}
}
// Record 记录一次请求的三维 SLO 事件
func (t *SLOTracker) Record(latencyOK, availOK, accuracyOK bool) {
t.mu.Lock()
defer t.mu.Unlock()
dateKey := time.Now().Format("2006-01-02")
// 记录延迟维度
t.recordEvent(DimLatency, "p99_e2e_latency", dateKey, latencyOK)
// 记录可用性维度
t.recordEvent(DimAvailability, "http_2xx_rate", dateKey, availOK)
// 记录准确率维度
t.recordEvent(DimAccuracy, "auto_eval_pass_rate", dateKey, accuracyOK)
}
func (t *SLOTracker) recordEvent(dim SLODimension, indicator, dateKey string, good bool) {
key := string(dim) + "_" + indicator + "_" + dateKey
bucket, ok := t.buckets[key]
if !ok {
bucket = &TokenBucket{}
t.buckets[key] = bucket
}
bucket.TotalEvents++
if good {
bucket.GoodEvents++
}
}
// Evaluate 评估当前 SLO 合规度,返回各维度报告
func (t *SLOTracker) Evaluate() []SLOReport {
t.mu.RLock()
defer t.mu.RUnlock()
var reports []SLOReport
cutoff := time.Now().Add(-t.window).Format("2006-01-02")
for dim, indicators := range t.indicators {
for _, ind := range indicators {
// 聚合滑动窗口内的所有日桶
var totalGood, totalEvents int64
for key, bucket := range t.buckets {
if !t.matchesIndicator(key, string(dim), ind.Name, cutoff) {
continue
}
totalGood += bucket.GoodEvents
totalEvents += bucket.TotalEvents
}
compliance := 1.0 // 无数据时默认达标
if totalEvents > 0 {
compliance = float64(totalGood) / float64(totalEvents)
}
burnRate := 0.0
errorBudget := 1.0 – ind.TargetRate
if errorBudget > 0 {
burnRate = (ind.TargetRate – compliance) / errorBudget
}
report := SLOReport{
Dimension: dim,
Indicator: ind.Name,
Compliance: compliance,
Target: ind.TargetRate,
Healthy: compliance >= ind.TargetRate,
BurnRate: burnRate,
}
reports = append(reports, report)
if !report.Healthy && t.alertFunc != nil {
t.alertFunc(dim, ind.Name, compliance, ind.TargetRate)
}
}
}
return reports
}
func (t *SLOTracker) matchesIndicator(key, dim, indicator, cutoff string) bool {
// key 格式: "latency_p99_e2e_latency_2026-07-01"
expected := dim + "_" + indicator + "_"
if len(key) < len(expected) {
return false
}
if key[:len(expected)] != expected {
return false
}
dateStr := key[len(expected):]
return dateStr >= cutoff
}
// —- 使用示例 —-
func main() {
tracker := NewSLOTracker(30*24*time.Hour, func(dim SLODimension, ind string, cur, tgt float64) {
fmt.Printf("[ALERT] %s/%s 合规度 %.2f%% < 目标 %.2f%%\\n",
dim, ind, cur*100, tgt*100)
})
// 模拟记录请求事件
tracker.Record(true, true, true) // 全面达标
tracker.Record(false, true, true) // 延迟超标
tracker.Record(true, false, true) // 可用性超标
tracker.Record(true, true, false) // 准确率超标
reports := tracker.Evaluate()
for _, r := range reports {
status := "HEALTHY"
if !r.Healthy {
status = "UNHEALTHY"
}
fmt.Printf("[%s] %s/%s: %.2f%% (目标 %.2f%%) 燃尽率: %.2f\\n",
status, r.Dimension, r.Indicator, r.Compliance*100, r.Target*100, r.BurnRate)
}
}
设计考量:
- 燃尽率(Burn Rate):这是 Google SRE 的核心概念。燃尽率 = (目标 – 当前) / 错误预算。燃尽率 > 1 意味着正在以超过预算的速度消耗错误配额。燃尽率 > 14.4 意味着一天就烧掉了一个月 2% 的错误预算——这应该触发 P1 告警。
- 无数据默认达标:一个新上线的服务缺乏历史数据,不能因为没有数据就判定 SLO 失败。这个默认值应该随着服务成熟逐步收紧。
- 原子记录:单次请求的三个维度通过一次 Record 调用原子记录,避免维度间的数据不一致。
四、SLO 定义的陷阱:不要把 SLO 写成 SLA
SLO 与 SLA 的本质区别。SLO 是内部目标,没达到说明需要改进。SLA 是外部合同,没达到意味着赔钱。很多团队直接把"99.9% 可用率"从 SLO 写成 SLA 对外承诺,这是给自己挖坑。准确率的 SLO 更应该保守——自动评估分的 95% 基线不是法律承诺,是一个需要持续优化的内部目标。
准确率 SLO 的量化困境。你怎么知道模型的回答是准确的?自动评估分(如 Rouge-L、GPT-Eval)只是近似值。人工标注成本极高。一个务实的方案是:用自动评估分做日常 SLO 追踪,月度随机抽样 200 条做人工标注作为"真值校准",修正自动评估的系统性偏差。
SLO 的"好天气偏误"。在流量平稳、无突发事件时,SLO 看起来总是绿的。但真正检验 SLO 的是异常时刻:模型升级、依赖服务故障、突发流量。建议在 SLO 报告旁边附上"压力测试下的 SLO 表现"——在混沌实验期间的三维达标率,比平静期的达标率更有说服力。
多租户差异化。不同客户的 SLO 期望不同。付费用户的延迟容忍度远低于免费用户,对准确率的要求也更高。SLO 追踪器应该支持按租户、按 API Key 分组统计,而不是只看全局平均。
五、总结
AI 服务的质量不是一维的。将延迟、可用率和准确率同时纳入 SLO 体系,才能在监控大盘上看到服务质量的完整画像。
落地三步:第一步,定义延迟(P99 ≤ 5s)、可用率(≥ 99.9%)、准确率(≥ 基线 95%)的 SLO 基线;第二步,在推理服务的中间件层接入本文的 SLOTracker,记录每次请求的三维事件;第三步,在 Grafana 上构建燃尽率大盘,设置燃尽率 > 10 时的 P1 告警。
基础设施不需要漂亮话。它需要的是当你看着监控大盘时,每一个绿色的指标背后都有一个经得起推敲的定义。
