说明:本文讨论的是 AI 应用的服务等级目标怎么定与怎么维护,属于 AI 运维话题,不涉及具体模型版本与价格。AI 领域版本迭代极快,凡涉及版本号、价格、可用性,请以你阅读时的官方页面为准。文中代码为结构示意,请按自己的技术栈调整后再上生产。
一、可用性在 AI 应用上会给出错误答案
传统服务的可用性口径只有两个量:请求成功率和响应时间。对读写业务库的服务,这个口径大体够用,返回成功就等于业务做成。AI 应用不满足这个前提:返回成功只说明请求被受理、产出了内容,内容对不对,可用性指标不知道。
1.1 可用性的定义域里没有对错这个概念
可用性回答的是服务有没有接住请求,不回答接住之后给的东西能不能用。这两个问题在传统业务里高度重合,在 AI 应用里可以完全脱钩。
于是会出现一个看起来矛盾的组合:可用性长期停在 99.9%,用户侧却持续反馈答案不能用。两者不冲突,量的是不同的东西。延迟分位同理,它量的是等待时长,不是等待的价值——一个 200 毫秒返回的错答案,在延迟指标上比一个 3 秒返回的好答案好看得多。
结论先行:可用性要保留,但它不能当 AI 应用的唯一目标。至少并行维护三类:质量、延迟、成本。
1.2 六种在监控上看不出来的失败
| 失败形态 | 监控上长什么样 | 用户侧实际感受 | 归属目标 |
|---|---|---|---|
| 答非所问 | 200,输出长度正常,延迟正常 | 内容不可用 | 质量 |
| 结构崩掉 | 200,但下游消费方解析失败 | 流程中断 | 质量 |
| 静默截断 | 200,结束标记正常,内容不完整 | 结论缺一半 | 质量 |
| 依赖兜底 | 200,实际走了降级分支 | 结果明显变差 | 质量 |
| 变慢 | 200,延迟分位上移 | 等待过长 | 延迟 |
| 变贵 | 200,用量上移 | 无感,事后对账才发现 | 成本 |
对照传统口径:只有后两行会反映到可用性与延迟指标上,前四行在可用性、响应时间、错误率上全是绿的。这就是可用性骗人的机制——定义域覆盖不到这些失败。
1.3 三类目标与一条前置条件
质量、延迟、成本三类各自独立度量,互不替代。把三者压成一个综合分,是这套体系里最常见的第一处错误,第 2 章讲为什么。
还有一条前置条件:每个目标都要能被自动测量,测量结果要能落进同一个固定窗口的口径里。做不到自动测量的目标会退化成口号——写进文档,没人能判断有没有达成,也就没人会为它做取舍。
二、质量 SLO:从可自动判定的代理指标起步
质量的定义本身是主观的:同一条回答,换个用户、换个场景,判定可能相反。所以质量目标不能直接定,要先找到与质量相关、又能自动算出来的代理指标。
2.1 什么是合格的代理指标
一个代理指标要同时满足三个条件,缺一不可:
- 与质量单调相关:指标变差时用户感知的质量大概率也变差,不能反过来。
- 可自动判定:判定过程不需要人在环,否则做不成门禁。
- 有稳定基线:同一批流量正常情况下的波动范围有限,波动能被识别成异常。
常见错误是把代理指标当成质量本身。它的作用是发现质量下降,不是证明质量合格——这个定位要提前跟消费这个指标的人说清楚,否则质量出事后一定有人追问为什么指标没报警。
2.2 四个可以起步的代理指标
| 代理指标 | 怎么算 | 能抓到什么 | 抓不到什么 |
|---|---|---|---|
| 解析成功率 | 返回能被下游结构消费的请求数除以总请求数 | 结构崩掉、空返回 | 内容对不对 |
| 拒答率 | 明确拒答与转人工的数量除以总请求数 | 可用性收缩 | 拒答是否合理 |
| 引用可解析率 | 引用能定位到真实来源的条数除以总引用条数 | 来源被编造 | 来源是否支撑结论 |
| 人工抽检一致率 | 抽检样本中人工判定合格的数量除以抽检样本数 | 内容层面的整体质量 | 单条问题的归因 |
四个指标分工互补:解析成功率和拒答率覆盖能不能用,引用可解析率覆盖依据是否真实,人工抽检一致率覆盖内容层面。引用可解析率怎么校验是另一个话题,这里只把它当一个可自动算的比值。
拒答率要双向看:它升高时质量类风险下降,但同时说明服务在收缩——用户没拿到东西,等价于一次失败。所以拒答率的目标是区间,不是单边上下限。
2.3 为什么不能只用一个总分
把四个代理指标加权合成一个 0 到 1 的总分,看着好看,实际会引入三个问题:
- 方向相反被抵消:拒答率上升拉低可用性感知,却可能拉高某些质量项,加权后互相抵消,总分纹丝不动。
- 量级不对等被掩盖:解析成功率涨 1 个点,可以掩盖引用可解析率跌 5 个点,后者对用户信任的破坏更严重。
- 归因失效:总分掉下来不知道该动哪一段,工程上只能靠猜。
做法是把总分降级成看板排序依据,不进发布门禁。门禁逐项读:任何分项低于目标值都拦住发布,或要求随发布附一份降级方案。总分只回答哪个方向先看,不回答能不能发。
⚠️ 代码待验证
# 质量代理指标计算示意:分项各自统计,不合成单一总分defquality_proxies(window_events):total=len(window_events)iftotal==0:returnNoneparsed_ok=sum(e["downstream_parsed"]foreinwindow_events)refused=sum(e["is_refusal"]foreinwindow_events)ref_total=sum(e["ref_count"]foreinwindow_events)ref_ok=sum(e["ref_resolvable"]foreinwindow_events)return{"parse_success":parsed_ok/total,# 解析成功率"refusal_rate":refused/total,# 拒答率,看区间不看单边"ref_resolvable":(ref_ok/ref_total)ifref_totalelseNone,# 引用可解析率"sample_size":total,# 样本量必须一起返回}# 判据:样本量低于阈值时只记录趋势,不出结论,避免小样本噪声进发布门禁MIN_SAMPLE=200# 示意值,按自己的流量规模与窗口长度调整三、延迟 SLO:首字与端到端分开定
延迟是三类目标里最好量的一类,也是最容易被一个均值糊弄过去的一类。定延迟目标之前,先要确定量的是哪一段。
3.1 首字延迟与端到端延迟是两种体感
首字延迟指用户看到第一个字的时间,端到端延迟指任务完成的时间。流式输出把两者彻底分开:首字可能很快,用户觉得响应及时;也可能首字很慢,后端还在正常生成,用户已经在找关闭按钮。
影响因素也不同:首字主要受排队位置、输入长度与预填充影响;端到端还叠加输出长度、工具调用次数与重试次数。优化手段不同就不能合成一个目标——合成后首字变慢会被端到端变快抵消,而用户感知恰恰是变差的。
判据:一个改动让首字延迟下降、端到端延迟上升,算不算改进取决于场景是交互式还是批处理。这一点要写在目标定义里,不能留给事后讨论。
3.2 分位数选哪个
均值在延迟上几乎没用:一小撮很慢的请求被大量快请求摊平,均值看着正常,那一小撮用户却已经把体验做坏了。用分位数,并且分层用:
- 50 分位:看典型体感,回答多数请求大概等多久。
- 95 分位:主要目标值,覆盖到足够多的用户,样本量也足够稳定。
- 99 分位:只做观察,不进发布门禁。样本少、抖动大,作为门禁会频繁误伤发布,把团队逼到调阈值而不是改问题。
这里只引用分位数的概念,怎么测出来、压测怎么设计属于另一套话题,不在本文展开。
3.3 长任务用完成时间,不用请求时间
异步与批处理任务上,请求时间这个量直接失效:请求早返回了,真正的工作在后面慢慢跑。这时要看两个分开的量:排队时长和执行时长。
| 任务类型 | 该看什么 | 不该看什么 | 理由 |
|---|---|---|---|
| 流式对话 | 首字与端到端分开 | 只报端到端 | 首字决定等待体感 |
| 单轮问答 | 端到端 95 分位 | 均值 | 均值掩盖长尾 |
| 工具链多轮 | 单次工具往返加全链路 | 只报首字 | 大部分时间花在工具等待 |
| 异步批任务 | 排队时长与执行时长分开 | 请求时长 | 请求早已返回,量不到真实等待 |
排队时长要单独定目标,它受并发水位影响,执行时长受任务本身影响。合成一个完成时间目标,就会出现队列积压拉高完成时间、执行环节却没问题的误判。
⚠️ 代码待验证
# 延迟目标定义示意:首字与端到端分开,异步任务拆排队与执行latency_slo:-item:first_token# 首字延迟,交互式场景的主要体感channel:streamingquantile:95window:28dgate:true# 进发布门禁,阈值按测量窗口的基线分位定-item:end_to_endchannel:streamingquantile:95window:28dgate:true# 与首字分开统计,超阈值计数互不冲抵-item:end_to_endchannel:streamingquantile:99window:28dgate:false# 只观察,样本少、抖动大,进门禁会误伤发布-item:async_completechannel:batchsplit:[queue_time,exec_time]# 排队与执行分开,避免队列问题算成执行问题window:28dgate:true四、成本 SLO:按每完成一件任务定目标
成本目标最容易定错的地方是分母。分母选错,目标就会奖励错误的行为。
4.1 每次调用与每件任务是两个不同的分母
按每次调用定成本,数字看起来干净,但它奖励的是削减单次调用,不是削减完成任务的代价。重试、工具调用、多轮改写都会让单次调用便宜、整体更贵。
按每完成一件任务定,口径是:窗口内的总用量除以窗口内成功完成的任务数。失败任务和重试产生的用量必须留在分子里,否则成本会被成功样本稀释,看起来越来越便宜,实际是失败被藏进了另一个统计口径。
判据:某一周成功任务数下降、单次调用成本不变时,按每件任务算出的成本一定上升。这个上升是正确的信号,不该被平滑掉。
4.2 成本与质量互相牵制
成本和质量的张力不是意外,是结构性的。下表列出四类常见动作对三个目标的方向影响,用于在定目标时提前说清取舍:
| 动作 | 质量影响 | 成本影响 | 延迟影响 |
|---|---|---|---|
| 增加检索轮次 | 通常上升 | 上升 | 上升 |
| 加大上下文 | 边际上升,很快趋平 | 明显上升 | 上升 |
| 失败重试 | 上升 | 上升 | 上升,长尾更明显 |
| 裁剪上下文降级 | 下降 | 下降 | 下降 |
这张表逼着定目标的人在纸上先选一次,而不是等出事时现场吵。降级动作要提前定义触发条件:质量分项未跌破下限、成本分项连续两个窗口超目标时触发并记录;质量一旦跌破下限,降级立即停止。不要让降级自己决定要不要降级。
4.3 目标值怎么给
先测基线,再定目标,顺序不能反。做法是:
- 上线后先只观测,跑一个完整的测量窗口,本文按 28 天示意。
- 按任务类型分别取基线分布,记下 50 分位与 75 分位。
- 目标值定在基线与更优之间,留一段缓冲,不拍整数。
- 目标值与基线一起记录,注明定值时间,方便复核。
成本目标要区别任务类型。把短问答和长文档处理混在一个目标里,长任务拉高目标,短任务的实际消耗被掩盖。账单归因与分账是另一套话题,这里只定目标值与门禁。
五、错误预算:怎么算、烧完之前做什么
三类目标定好之后,还需要一个把它们和发布节奏绑定的东西。错误预算就干这个:它把目标值翻译成一份可以消耗的额度。
5.1 预算怎么算
预算的定义很简单:目标值的补集。质量目标允许失败的样本数、延迟目标允许超阈值的请求数、成本目标允许超支的额度,都是预算。
有一条换算必须遵守:预算要换算成绝对量,不要只留百分比。百分比在决策时没用——它不告诉你还剩多少。换算完再把额度按时间摊到窗口上,才有可比的消耗速度。
窗口用滚动窗口,本文按 28 天示意,好处是预算会自然回补,不会因为月初一次事故把整月判死。
5.2 烧毁速率与动作分档
只有剩余比例不够用,还要看消耗速度。同一个剩余比例,平稳消耗和三天烧掉一半是两个完全不同的事件。烧毁速率就是当前消耗速度与允许速度的比值。
| 烧毁速率区间(示意值) | 判断 | 动作 |
|---|---|---|
| 小于 1 | 消耗慢于允许速度 | 不动,继续观察 |
| 1 到 2 | 消耗偏快,尚未失控 | 记入发布评审,暂停非必要变更 |
| 2 到 5 | 明显异常,存在持续劣化 | 冻结发布,定位窗口内最近的变更 |
| 大于 5 | 预算将在窗口内耗尽 | 回滚最近变更,同时启动保底降级 |
四档动作里只有最后一档改运行态,前三档只影响发布节奏。这个设计是刻意的:预算管节奏,不管故障处置。
5.3 预算与发布门禁怎么挂钩
门禁读分项预算,不读综合分,规则可以简化成三条:
- 所有分项剩余高于阈值,正常发布。
- 剩余低于阈值,但最近一次变更与该分项无关,可发布,需附降级方案与回滚点。
- 剩余低于阈值,且最近一次变更与该分项直接相关,拦住,先复核再发。
还有一条容易忽略:短窗烧得快、长窗没动时先别动门禁,去查那个短窗里发生了什么。短窗波动经常来自流量结构变化,不是真劣化。
⚠️ 代码待验证
# 错误预算与烧毁速率:分项各算一份,不合成总分defburn_rate(consumed,allowed,window_fraction):ifallowed<=0:returnNoneexpected=allowed*window_fraction# 该时间点允许消耗的量ifexpected<=0:returnNonereturnconsumed/expected# 短窗看突变,长窗看趋势;长短窗都要看,避免被单次抖动带偏SHORT_WINDOW="1h"# 示意值LONG_WINDOW="6h"# 示意值BUDGET_WINDOW="28d"# 示意值,滚动窗口,预算会自然回补完整版资料清单:本文用到的错误预算计算示例与烧毁速率对照表都整理在里面了,扫码即可获取:
六、SLO 与告警的分工
SLO 和告警都盯着同一批信号,但它们回答的问题不同。混着用,两个都会失效。
6.1 两者看的时间尺度不同
SLO 看趋势,尺度在天到周,回答的是这个服务最近是不是在变差、要不要调整工程节奏与发布计划。告警看短窗口,尺度在分钟到小时,回答的是现在有没有东西需要立刻处理。
两种误用都会出问题:
- 拿 SLO 当告警:预算还在正常范围,服务在缓慢劣化,因为没到阈值,没人知道。
- 拿告警当 SLO:短窗一抖就触发,天天有通知,长期趋势却平稳,团队被训练成忽略通知。
6.2 边界怎么划
| 维度 | SLO | 告警 |
|---|---|---|
| 时间尺度 | 天到周 | 分钟到小时 |
| 依据 | 目标值与错误预算 | 单个或少数几个窗口内的信号 |
| 输出 | 要不要改变发布与工程节奏 | 现在要不要有人介入 |
| 影响范围 | 发布与排期 | 即时响应 |
| 定错的代价 | 要等一个窗口才发现,纠正慢 | 反复打扰,降低响应意愿 |
这张表只划边界,告警怎么分级、怎么抑制重复属于另一套话题。这里强调的是不要把预算趋势接进即时通知:它变化慢,接进分钟级通道只会变成噪声,稀释真正需要即时响应的信号。
6.3 两个方向的抬升判断
边界划清之后,还有两个方向要显式判断,否则两边会各自以为是对方的问题:
- 告警反复触发、预算没动:告警阈值与当前流量结构不匹配。回看短窗的触发原因分布,而不是去调 SLO 目标值。
- 预算持续下降、告警没响:覆盖缺口,这条劣化路径没有对应的短窗信号。补一条能反映该分项的短窗判据,并验证它能被触发。
每轮预算复盘时对一次表:把预算下降的时段与告警触发记录并排放,看有没有跨窗口的空白段。
⚠️ 代码待验证
# SLO 与告警的分工:两条链路读不同的窗口与数据源slo:window:28d# 趋势判断,服务于发布门禁source:budget_ledgerfeeds:release_gate# 输出给发布门禁,不产生即时通知alerting:window:5m# 即时判断,只看短窗口source:short_window_metricsfeeds:incident_flowexcluded_sources:# 预算趋势不进即时通道,避免噪声稀释信号-budget_trend七、目标值怎么定与怎么维护
最后一章讲这套东西怎么落地,以及上线之后怎么维护。
7.1 先测量再定值
上场就定一个目标值,是最常见的返工来源。定值前必须先有一个测量窗口:只观测、不出结论、不设门禁,跑满一个完整窗口(本文按 4 周示意)。
测量窗口结束之后,按下面三步定值:
- 按任务类型分别取分项指标的分布,记下 50 分位与 75 分位。
- 目标值取在基线的偏优一侧,留出缓冲,并把缓冲大小写进定义。
- 每个目标值都注明定值时间与当时的基线值,方便下次复核时对照。
不要拍整数。一个看起来整齐的目标值往往离真实基线很远,结果要么长期不达标、门禁形同虚设,要么长期轻松达标、什么也拦不住。
7.2 分层:按档位、按租户、按任务类型
一个目标值管不了全部流量。分层的常用维度有三个:
- 按档位:不同服务等级对应不同的质量与延迟目标。档位之间要有明确判定条件,否则会成为扯皮入口。
- 按租户:租户之间用量结构差异大,混在一起会让大租户拉偏整体目标。分租户看,也要有一套整体口径对外说明。
- 按任务类型:长任务与短任务的质量、延迟、成本分布差别明显,分开定是基本要求。
分层的陷阱是层级太细:一个目标底下只剩很少的样本量,指标抖动就会淹没真实变化,门禁变成随机拦截。判据:某层在一个窗口内的样本量低于最小阈值时,该层只观测不设门禁,向上合并到上一层。
7.3 变更之后必须重定并复核
目标值不是一次定完就不动的。触发重定的变更有三类,要重看的指标也不同:
- 模型换代:质量与延迟都要重看,成本通常同时变化。
- 提示词或流程大改:重点看质量分项,尤其是拒答率与引用可解析率。
- 依赖换代:重点看延迟与成本,质量可能滞后一个窗口才体现。
复核动作固定成一条:变更上线后重测一个完整窗口,把新基线与旧目标值对照,决定调目标还是改实现。不要把复核做成一次性动作,它要进发布流程、和变更绑定,否则一定被跳过。
⚠️ 代码待验证
# 目标值维护要看的观测项(示意,指标名按自己的监控系统改写) slo_objective{target=quality, item=proxy_name} # 当前目标值,按层拆开 slo_actual{target, item} # 窗口内实际值 slo_gap{target, item} # 目标与实际的差值 error_budget_remaining_ratio{target, item} # 剩余预算比例 burn_rate{target, item, window=short} # 烧毁速率,长短窗各一份 slo_layer_sample_size{layer=tier} # 分层样本量 slo_review_total{reason=model_change} # 复核触发计数 # 复核清单:变更上线后重测一个完整窗口,再决定目标值是否要调整。 # 分层样本量低于最小阈值时,该层只观测不设门禁,向上合并到上一层看。完整版资料清单:本文用到的三类 SLO 目标模板与错误预算复盘清单都整理在里面了,扫码即可获取:
附表 A:关键取舍一览
| 取舍 | 本文结论 | 判断依据 | 位置 |
|---|---|---|---|
| 可用性能不能当 AI 应用的唯一目标 | 不能 | 定义域里没有内容对错的判定 | 第一章 |
| 质量目标用什么承载 | 可自动判定的分项代理指标 | 质量主观,无法直接定值 | 第二章 |
| 拒答率的目标形态 | 区间,不是单边上限 | 升高说明服务在收缩 | 第二章 |
| 质量分项要不要合成总分 | 不合成,总分只看板排序 | 合成会抵消、掩盖、破坏归因 | 第二章 |
| 首字与端到端能否合成一个目标 | 不能 | 影响因素不同,合成会互相抵消 | 第三章 |
| 延迟目标取哪个分位 | 主要取 95 分位 | 样本量与稳定性兼顾 | 第三章 |
| 异步任务看请求时长还是完成时长 | 看完成时长,且拆排队与执行 | 请求早已返回,量不到真实等待 | 第三章 |
| 成本的定值分母 | 每完成一件任务 | 按次调用会奖励削减单次开销 | 第四章 |
| 失败与重试的用量要不要计入分子 | 要 | 不计入会让成本被成功样本稀释 | 第四章 |
| 错误预算按百分比还是绝对量管 | 绝对量 | 百分比不告诉你还剩多少 | 第五章 |
| 预算趋势要不要接即时通知 | 不要 | 变化慢,进分钟级通道只会变噪声 | 第六章 |
| 目标值先拍还是先测 | 先测一个完整窗口再定 | 拍出来的值离基线远,门禁失效 | 第七章 |
| 分层样本量不足怎么办 | 只观测不设门禁,向上合并 | 噪声会淹没真实变化 | 第七章 |
| 目标值要不要随变更重定 | 要,与变更绑定 | 不绑定一定被跳过 | 第七章 |
附表 B:术语速查表
| 术语 | 含义 |
|---|---|
| SLO | 服务等级目标,一个可测量的服务质量目标值与窗口的组合 |
| 代理指标 | 与质量相关、可自动判定、有稳定基线的间接指标,用于发现质量下降 |
| 解析成功率 | 返回结果能被下游结构消费的请求占总请求的比例 |
| 拒答率 | 明确拒答与转人工的请求占总请求的比例,通常按区间定目标 |
| 引用可解析率 | 给出的引用能定位到真实来源的条数占引用总条数的比例 |
| 首字延迟 | 从请求发出到用户看到第一个输出片段的时间 |
| 端到端延迟 | 从请求发出到任务完成的时间,与首字延迟分开统计 |
| 完成时间 | 异步任务从入队到执行结束的时间,通常拆成排队时长与执行时长 |
| 错误预算 | 目标值的补集,换算成绝对量后可用于消耗的额度,按滚动窗口回补 |
| 烧毁速率 | 当前错误预算消耗速度与允许速度的比值,分短窗与长窗各算一份 |
| 发布门禁 | 发布前逐项检查错误预算与目标值的规则集合,读分项不读总分 |
| 分层目标 | 按档位、租户或任务类型拆分的独立目标值,样本量不足时向上合并 |
写在最后:这篇用到的资料
写这篇文章时,我把几个模型的官方文档、参数表和实测记录都对了一遍,顺手整理成几份配套的东西:
- 大模型学习路线图:从 LLM 基础到 Agent 开发,各阶段该学什么、用什么资料
- 大模型全套教程:按主题分好的视频与文档清单
- 大模型实战好书:24 本,附每本适合的阶段
资料是我自己整理的,放在下面这个码上,扫码即可获取:
添加时备注「大模型」,优先通过。
拿到之后建议先看学习路线图那一份,先定位自己在哪个阶段,再决定学什么,比一上来就啃框架效率高得多。