网关可观测性怎么做:比错误率更早报警的三个信号
上个月帮一个团队看账单,他们的月度 API 支出比上月涨了将近一半,但监控面板上一切正常:请求量没涨、成功率 99.9%、平均延迟没有明显变化。运维那边翻了两天日志也没找出原因。最后是在网关的花费明细里对出来的——某个后台任务的平均输出 token 数悄悄翻了倍,因为有人把提示词里的「简要回答」改成了「详细说明」,改动本身没有任何问题,也没人觉得需要告诉运维。
这件事很典型。传统的 Web 服务监控,盯住 QPS、错误率、延迟这三样基本就够了,因为服务出问题的方式无非是「挂了」「慢了」「压不住了」。大模型调用不一样:它多了成本、多了输出的不确定性、多了缓存这种会静默失效的东西。这三个维度出问题的时候,错误率往往还是绿的——等到错误率报警,钱已经花出去了,或者用户已经投诉了。
所以这篇不讲怎么装监控软件,讲的是「该采什么量」和「先看哪个信号」。工具层面我尽量少提具体产品,因为各家的技术栈差别太大;但采集口径这件事是通用的,采错了口径,再贵的监控平台也救不了你。
一、必须采的五类量
我把大模型调用需要采的东西分成五类。这个分法不是学术性的,纯粹是按「出事时你会去查哪张图」来划的。
1. 用量:输入、输出、缓存必须分开计
这是最容易做错的一处。很多团队只记一个「总 token 数」,理由是账单也是按总量结的。但总量是个合成指标,它把三件性质完全不同的事糅在了一起:
- 输入 token 反映的是你的提示词设计与上下文管理。它涨,通常是因为你往上下文里塞了更多东西(检索片段变多、历史轮次没截断、系统提示词膨胀)。
- 输出 token 反映的是模型的行为。它涨,说明模型开始变啰嗦,或者陷入了某种重复。
- 缓存命中的 token 反映的是你的提示词前缀是否稳定。
这三个的单位成本通常不一样,优化手段更是完全不同:输入涨了你要去砍上下文,输出涨了你要去调提示词或加长度约束,缓存掉了你要去查前缀被什么打破了。合并成一个数以后,你只能看到「涨了」,看不到「为什么涨」,等于每次都要从头查一遍。
采集上,这几个量在响应里一般都有,网关也会记。唯一的要求是落库的时候别求和,分列存。存储成本可以忽略不计,但省下来的排查时间是实打实的。
2. 成本:按 key、团队、项目、环境归属
成本这一类的核心不是「算出总数」,而是「能切开」。总数每个人都有,切不开就没法做任何决策。
如果你前面接的是 LiteLLM Proxy,这件事它已经替你做了一部分:官方说明里写得很清楚,花费追踪是通过数据库在 key / user / team 三个层级自动进行的,你可以用 /key/info 直接查某个 key 的花费。这意味着只要你发 key 的时候把 user_id 和 team_id 填对了,团队维度的账就是现成的。key 的生成与参数细节可以看 LiteLLM 虚拟 key 怎么用 那篇。
需要提醒的是,这类接口的返回字段和聚合口径各版本可能有出入,接进你自己的报表之前,先用一个真实 key 调一次看看返回长什么样,以官方文档当次为准,别照着别人的博客写解析代码。
3. 质量:格式合法率、空响应率、截断率
这一类是大模型独有的,传统服务里没有对应物。HTTP 200 不代表这次调用是成功的:
- 格式合法率:你要 JSON,模型给了一段带解释文字的 JSON,或者少了个括号。这在结构化输出的场景里是主要故障源。
- 空响应率:返回了,但内容是空的或者只有一句无意义的客套话。
- 截断率:输出因为长度上限被切断了。这个尤其阴险,因为截断的结果往往语法上是合法的,只是内容不完整,下游程序不一定报错。
这三个量的采集位置在你的业务代码里,不在网关里——网关看不出来你要的是不是合法 JSON。做法很简单:在解析响应的那一层,把「解析成功 / 解析失败 / 空 / 被截断」四种结果打成一个计数,带上模型名和调用场景两个维度。这几行代码是我见过投入产出比最高的监控改造,没有之一。
4. 可靠性:分状态码的失败率、重试次数、超时率
失败率必须按状态码分开。把所有非 200 混成一个「错误率」,你会失去最重要的信息:鉴权失败、限速、上游故障、请求本身不合法,这四种失败的处置方式完全不同,混在一起的曲线除了让人心慌没别的用。
重试次数要单独采一个量,而不是只在日志里留痕。原因见下一节,这是本文的重点。
超时率也要单独看,别把它并进失败率。超时和明确的错误返回是两回事:超时意味着你可能已经为这次调用付了钱(上游可能已经生成了内容),而明确的错误码通常不计费。重试与退避的具体策略见 重试与退避怎么设计。
5. 延迟:首 token 与总时长必须分开
在流式场景下,这两个数的体验含义完全不同:
| 指标 | 用户感受 | 变差通常意味着 |
|---|---|---|
| 首 token 延迟 | 「点了没反应」 | 排队、路由绕远、上游冷启动、提示词太长 |
| 总时长 | 「说得慢/说得多」 | 输出变长、解码速度下降 |
只看总时长,你分不清是「等得久」还是「说得多」。首 token 延迟涨而总时长没涨,八成是排队或路由问题;总时长涨而首 token 没涨,八成是输出变长了——这时候该去看的是第三节的第二个信号。
另外,这两个数都要看分布,不要只看平均。平均值会被大量的快请求拉平,长尾问题在平均值上几乎看不出来。这一点在 聚合层日志与可观测性实践 里展开讲过。
二、三个先行信号:比错误率更早暴露问题
前面五类量是「采什么」,这一节是「先看哪个」。下面三个信号的共同特点是:它们变化的时候,错误率还是正常的。这就是它们的价值——你有一段提前量去处理,而不是等用户或账单来通知你。
信号一:重试率上涨
为什么它领先于错误率:重试成功了就不算错误。假设上游有 5% 的请求会瞬时失败,你的客户端重试一次,其中大部分成功了——最终错误率可能只有 0.3%,几乎看不出异常。但真实情况是,你有 5% 的请求付出了双倍的延迟,而且如果失败发生在生成之后(比如流式传输中断),你可能已经为第一次调用付了钱。
上游开始不稳定的时候,重试率一定先动,错误率是重试兜不住之后才动的。中间这段时间就是你的提前量。
怎么采:在发起重试的那一行代码里打点,记录「这是第几次重试」和「上一次失败的状态码」。如果重试是网关做的(比如 LiteLLM 里的 num_retries 这类配置),那就得从网关侧的日志或指标里取,具体字段以官方文档为准。关键是别只记最终结果——只记最终结果的话,重试这件事在数据里是不存在的。
看什么阈值形式:重试率适合看环比,也就是和前一小时、前一天同一时段比。绝对阈值不好定,因为不同上游、不同模型的基线差得很远。我的做法是先跑一周拿到自己的基线,然后按「相对基线翻倍」来报警。
报警后先查什么:
- 按上游/模型切一刀,看是全线上涨还是某一家上涨。某一家上涨就是上游问题,准备切走。
- 按状态码切一刀。如果集中在限速类的失败,那是你自己的并发压上去了或者配额被别的业务吃掉了,不是上游故障。
- 看重试之后的成功率。如果重试也开始失败,说明兜不住了,错误率马上就会跟着涨,这时候该走降级预案。
信号二:平均输出 token 上涨
为什么它领先于用户投诉:模型变啰嗦或者陷入轻微的重复循环时,输出在语法上完全正常,程序不报错,用户可能也只是觉得「话有点多」而懒得反馈。但账单是诚实的——输出 token 通常比输入贵,输出长度涨 30%,这部分成本就实打实涨 30%。
这个信号能抓到的典型问题有三类:提示词被人改动(我开头讲的那个例子)、模型侧的行为变化、以及少数请求陷入重复生成把平均值拉起来。
怎么采:用第一节里分开计的输出 token,按「模型 × 调用场景」两个维度算平均。一定要带调用场景这个维度——全站平均是没用的,因为不同场景的正常输出长度差一个数量级,混在一起谁涨了都看不出来。场景维度可以直接复用你发 key 的维度,或者在业务侧透传一个标识(下一节讲)。
看什么阈值形式:平均输出 token 适合看同比,和上周同一天比。因为它有很强的周内规律——工作日和周末的使用场景分布不一样,环比容易误报。
同时建议加一个分布上的观察:如果平均值涨了而中位数没动,说明是少数请求爆长(很可能是重复生成),该去抓那几条具体请求;如果中位数跟着一起涨,说明是整体行为变了,该去查提示词或模型配置。这个区分能省掉大量排查时间。
报警后先查什么:
- 先看是不是有请求撞上了长度上限(截断率是不是也跟着涨了)。撞上限的请求说明模型是想继续说的,这通常是循环。
- 查这个场景的提示词最近有没有改动。这是最常见的原因,而且往往改动的人不知道会影响成本。
- 抽几条最长的请求出来人眼看一遍。重复生成用眼睛一秒钟就能认出来,用指标反而绕远。
信号三:缓存命中率下跌
为什么它是最隐蔽的:缓存失效之后,功能一切正常。响应是对的,延迟可能只慢一点点,错误率纹丝不动。唯一的变化是成本悄悄涨上去了,而且是持续涨,不是尖峰——这种慢性的成本上升在月度账单里才会显形,那时候距离引入问题的那次提交可能已经过了三周。
命中率下跌的根因几乎总是同一个:前缀被打破了。提示词缓存依赖的是请求前缀逐字一致,而典型的破坏方式包括在系统提示词里插入了当前时间、把用户 ID 或会话 ID 放到了前缀里、给检索结果的排序引入了不稳定因素、或者只是有人在系统提示词开头加了一个空行。这些改动在功能测试里全都是通过的。
怎么采:用第一节里单独计的缓存 token 数,除以输入 token 总数,按场景维度分开算。如果你的上游或网关在响应里给出了命中相关的字段,直接用;字段名各家不同,以官方文档为准。
看什么阈值形式:命中率适合看速率的绝对下跌幅度,而且窗口要短——因为它的正常波动很小,一旦有代码改动打破前缀,掉幅通常是断崖式的,几分钟内就能看出来。我一般设成「相比过去一小时下跌若干个百分点」这种形式,具体数值按你自己的基线定。
报警后先查什么:
- 对时间。命中率的断崖点几乎一定对得上某次发布或配置变更,先去查那个时间点的部署记录。
- 把该场景的一条真实请求前缀 dump 出来,和一天前的对比,逐字符 diff。动态内容一眼就能看见。
- 检查是不是流量分布变了——比如某个新客户的请求模式和存量不一样,这种情况命中率下跌是正常的,不需要修。
三、成本归属怎么落地
前面反复提到「按场景切」,这一节说具体怎么切。原则只有一句话:
你希望账单按什么维度切开,就按什么维度发 key。
这句话听起来像废话,但绝大多数团队一开始都是全公司共用一个 key,等到需要分摊成本的时候才发现数据里根本没有区分的依据,只能靠时间戳和请求特征去猜。
落地上分两层:
网关侧按 key 维度切。 发 key 的时候就把归属信息写进去。用 LiteLLM 的话,user_id 和 team_id 是官方字段,直接对应它的三层花费追踪;metadata 这个对象可以放你自己的业务标识。这样 /key/info 查出来的花费天然就是分好的。
维度怎么定?我的经验是至少要包含这三个正交的方向:
| 维度 | 为什么需要 | 典型取值 |
|---|---|---|
| 环境 | 开发环境跑飞了不该算到业务头上 | 生产 / 预发 / 开发 |
| 团队或项目 | 成本分摊、预算问责 | 按你的组织结构 |
| 场景 | 优化时要知道哪个功能最烧钱 | 摘要 / 客服 / 检索问答… |
三个方向交叉起来 key 的数量会比较多,但这是好事——key 多才切得细。如果你担心管理成本,可以用有效期参数让临时 key 自动过期,参数细节以官方文档为准。
业务侧透传一个请求标识。 光有 key 维度还不够,因为你需要把网关的账和业务侧的日志对上。做法是在业务代码里生成一个请求 ID,一份记进自己的日志,一份通过网关支持的元数据通道带过去。这样出现异常花费时,你能从账单反查到具体是哪一次业务调用,而不是只知道「是客服团队的 key」。
两边能对上账,成本分析才从「知道谁花的」升级到「知道为什么花的」。更细的成本追踪方法可以看 大模型调用成本怎么监控 和 网关用量统计怎么做。
四、自建部分怎么接进来
有个常见的误解是「用了托管服务才有监控,自建就抓瞎」。至少在 vLLM 这件事上不成立。
vLLM 的 OpenAI 兼容服务在基础设施类路径里提供了 /metrics(Prometheus 格式) 和 /health,另外还有 /v1/models。这三个端点分别对应三件事:
/metrics是 Prometheus 格式,意味着你现有的采集体系不需要为它写任何适配代码,配一个抓取目标就能开始收。具体暴露了哪些量、叫什么名字,以你部署的那个版本实际输出的内容为准——直接 curl 一下这个端点看返回,比查任何文档都准,因为不同版本暴露的东西会变。/health直接接进你的健康检查和负载均衡的探活配置,和你对待任何一个后端服务的方式一样。/v1/models可以做轻量探活,同时能验证服务实际对外暴露的模型名是不是你期望的那个。这在配了模型名映射的时候特别有用——映射写错了,服务照样起得来,只有调用的时候才会报模型不存在,而查一下这个端点就能提前发现。
所以自建的可观测性上限并不比托管低,反而在某些方面更高:托管服务只会给你它愿意给的指标,自建你能看到引擎内部的状态。真正的差距在于你得自己维护这套监控,而不是在于数据拿不到。
一个务实的组合是:网关侧负责成本与业务维度(谁花的、花在哪个场景),vLLM 侧的 /metrics 负责引擎与资源维度(排队、显存、吞吐),两边用同一套时间轴对齐。排查的时候先看网关侧确定是哪个场景异常,再看引擎侧确定是不是资源问题,路径很清晰。
五、采样与留存:别把请求体全存下来
最后说存储策略,这是很多团队踩过合规坑的地方。
全量存请求体和响应体,听起来对排查最友好,实际上有两个硬问题:一是成本,大模型的请求体动辄几千 token,全量存一个月的量非常可观;二是合规风险,用户输入里可能包含个人信息、内部资料、甚至凭证,这些东西一旦进了日志系统,就等于扩大了一圈接触面,而日志系统的权限管控通常比业务数据库松得多。
我推荐的是三层做法:
第一层,指标全量。 前面五类量全都是数值型的聚合数据,不含任何内容,存储成本极低,保留期可以拉长到几个月甚至一年——你需要长周期来判断成本趋势和识别季节性。
第二层,样本采样。 请求体和响应体只按比例采样存储,比例按场景定:核心场景高一些,批量任务低一些。另外失败和异常的请求要全量存,因为你要查的就是它们——这个「正常采样、异常全留」的策略能在很小的存储代价下覆盖绝大部分排查需求。样本的保留期设短一点,通常几天到两周就够用了。
第三层,敏感字段脱敏。 存之前过一道脱敏:手机号、邮箱、身份证号、各类 key 和 token 这些有明确特征的东西用规则替换掉。脱敏要在写入日志系统之前做,不要指望在查询侧遮蔽——一旦原文进了存储,你就再也控制不住它会被复制到哪里去。
还有一条经验:指标里绝对不要带高基数的维度。把用户 ID 或者请求 ID 打进指标标签里,看起来很方便,但会让时序数据库的基数爆炸,最后要么把监控系统撑垮,要么被迫丢弃数据。用户级的信息应该在日志和样本里查,不该在指标里查。指标负责告诉你「哪里不对」,日志负责告诉你「具体是哪一条」,两者分工别混。
自查清单
拿这几条对一遍你现在的监控,能过 5 条以上就算比较扎实了:
- 输入 token、输出 token、缓存 token 是分三列存的,不是求和成一个数。
- 有一个「重试次数」的独立指标,而不是只在日志里能翻到重试记录。
- 平均输出 token 是按「模型 × 场景」分维度算的,并且同时看了中位数。
- 缓存命中率有独立的曲线和短窗口的下跌报警,不是只在月底看账单才发现。
- 失败率是按状态码分开的,超时单独一条,没有混成一个笼统的错误率。
- 首 token 延迟和总时长是两条独立的曲线,并且看的是分布不是平均值。
- 每个 key 都能回答「哪个环境、哪个团队、哪个场景」三个问题,业务侧有请求标识能和网关账目对上。
- 请求体是采样存储 + 脱敏的,异常请求全量留,指标标签里没有用户 ID 这类高基数维度。
最后提醒一句:上面提到的所有接口路径与字段,都以你实际使用的那个版本的官方文档和实际返回为准。这类项目迭代很快,接监控之前先拿真实请求跑一次、把返回打出来看看,比照着任何二手资料写解析逻辑都靠谱。