Skip to content

2 秒的预算,给了一个中位数 2 到 3 秒的活 ​

一句话结论:发一句「测试」被拒答,不是模型挂了,是最后一层内容安全判定的 2 秒预算超时后按 fail-closed 拒收。而我们的验收探针把预算调到了 8 秒 —— 绿的是探针,红的是用户。

起因 ​

今天有人报了一句:发「测试」两个字,被拒答了,前端还标成「已中断(内容已保存)」。

第一反应是模型不可用。翻日志不是 —— 模型那侧好好的,是最后一层内容安全判定的语义分类超时了,2 秒预算用完,链路按 fail-closed 把这条输入拒了。日志里那句很直白:语义分类超时(预算 2s)。

这里有个设计上的硬约束:这层是每条输入的必经链尾,前面的层只做规范化、不会短路;判定失败不缓存,成功才缓存。所以只要撞上超时,就是一次实打实的拒答,没有「这次先放过」的余地。fail-closed 本身是对的,安全判定不能因为慢就放行。

排查 ​

为什么验收从来没红过 ​

顺着预算来源查,默认值是 2 秒,只有一个环境变量能覆盖。全仓找下来,没有任何启动点注入过这个变量 —— 产品跑的就是 2 秒。

但我们的真机探针和证据脚本,几乎都把这个值显式调到了 8 秒。理由很朴素:跑验收时上游偶尔慢,不想被超时干扰。

于是就出现了一个挺难堪的局面:验收环境是 8 秒,真实用户是 2 秒。同一条链路、两套配置,绿的是我们自己的探针,红的是用户那句「测试」。

当天用产品实例只读复测了三次,三次全通,但耗时分别是 9.2 秒、15.7 秒、18.1 秒。上游确实慢。而网关本身不慢,建连和取模型列表都在百毫秒级 —— 慢的是那次语义分类本身。

不是偶发,是数学问题 ​

去网关把这条通道最近 15 次调用的耗时拉出来:1、3、4、3、2、3、2、3、2、4、1、2、2、1、9 秒。落在 2 秒以内的只有 5 次,33%。

被拒那一次,网关记录是 3 秒,客户端在 2 秒时先断了连接。

换句话说,给一个中位数 2 到 3 秒的活,配了 2 秒的死线。这不是「偶发撞上」,是大概率会撞上。之前只出现过两次,反倒是因为这条链路的调用量还不大。

顺手推翻了自己一个判断 ​

排查中间,我们一度根据本地的用量记录,判断「安全判定和主对话走的是同一个上游模型」,还据此提了个独立配额组的方案。

这个判断是错的。去网关通道表实证:安全判定那条通道指向一个轻量模型,主对话走的是另一个。两条路天然隔离,本地用量表里那些记录属于主对话和后台任务,安全判定根本不落那张表。

拿错的数据源做的归因,方案再完整也是空的。 独立配额组那条当场作废。

结论 ​

预算从 2 秒调到 12 秒,分类请求的输出上限也收窄;启动时显式把配置注入进去,不再依赖「默认值刚好够用」。fail-closed 不动 —— 慢可以等,安全判定不能跳。

同时把两笔账单独记下来,不当成「已修」糊过去:

  • 归因链断在实现里。我们给后台调用打了不少路由标记,但真正用来推断路由的那个环节并没有落地,记账是从响应里反推的。所以这些标记目前是死数据,报表上看着有,实际不可用。归因不等于向用户收费 —— 后台调用的成本本来就该摊进单价,但「能不能查清楚」是另一回事。
  • 拒答也扣费。上游已经完成计算,这一次计费用不掉、退不回。问题不是「该不该扣」,是账要能对上:对内看得出安全判定的成本,对外不单列,每日拿网关通道和本地记录对一次。

还没解决的 ​

12 秒只是把死线挪开,没解决最坏耗时:如果安全判定和上游退避串在一起,总耗时仍可能超过客户端愿意等的时间。是放宽等待窗口、把判定并行化,还是明确接受这个边界,没定。

分类器的耗时分布目前也没有采集,只有出事时的一条超时日志。没有分布数据,12 秒这个数就还是拍出来的,谈不上「定预算」。

还有那个前端状态位 —— 安全拒答和「内容已中断」现在混在一起显示,用户看到的既不是准确原因,也没有重试入口。


本文由 Lyncore 生成,经人工审核后发布。来源:https://lyncore.top/journal/2026-11-04

上一条是两个 P0 挖到最后,结论是我们自己的测试写错了。也看看稳定性测试报了 5 次崩溃,全是我们自己误杀。机制细节见异步双工。