Skip to content

探活误判,把全站默认模型换掉了 ​

一句话结论:探测逻辑写得越严,误判概率越高;而默认模型探活这一处的误判代价,是全站换模型。这两件事必须放在一起权衡,不能只优化探测准确率。

起因 ​

为了让"模型不可用时自动换一个",加了一条默认模型探活:定时打一次请求,拿不到正常产出就把 is_default 挪到备用模型上。

逻辑本身不复杂。问题出在一个参数上——探测请求的 max_tokens 设成了 64。

64 够不够?写的时候觉得够:只探活,又不生成正文,几十个 token 足以看出它是活的。

不够。

排查过程 ​

上午的反馈是"对话不说话了"。查下来,默认模型已经不是原来那个了。

链路上每一步看都合理:

  1. 探测请求 max_tokens=64 发出去;
  2. 上游模型要返回一个 tool_call,那段 JSON 本身就超过 64 个 token;
  3. 输出被截断,content 和 tool_calls 两个字段都空;
  4. 探活代码看到"正常结束 + 空产出",判定这个模型不可用;
  5. 触发自动降级,is_default 从 lyncore-flash 改到一个备用模型。

第 4 步是根因。"没有产出"和"产出装不下"是两件事,代码把它们当成了同一件。

更麻烦的是第 5 步选中的那个备用模型,流式返回本身有问题:没有结束标记 [DONE],completion 恒为 0。于是降级不但没兜住,反而把全站从"能用"换成了"不能用"。

怎么修的 ​

三条,都不是"把探测写得更准":

第一,把 max_tokens 提到 512。 让 tool_call 的 JSON 装得下。这是止血。

第二,正常结束但被截断的不算空产出。 判死只认「正常结束且零产出」,FinishReason == "length" 单独归类——有输出装不下,说明模型活着,只是预算不够。这是语义修正,比第一条重要。

第三,降级目标必须与原模型同路由档。 NewAPI 的模型只在 NewAPI 内部降级,直连模型只在直连模型之间降级。这条是事后加的:两次事故都是"从 NewAPI 跳到直连",如果早有限制,都不会发生。

真正的教训 ​

我一开始以为这是个 bug,修完参数就完了。后来发现不是。

探活是一条"决策型"链路。 它读的是一个观测值,写的是全站唯一一个默认模型开关。观测值错一点,写出去的结果是全局的。这类链路的严格程度不是越高越好——探测越严,误判越多,而误判代价是全局的。

所以后来定的原则是:宁可漏判(模型坏了没自动换,用户手动换),不可误判(模型好好的被换掉)。

同一天里,这条链路上还出了另外两次相关事故,其中一次是「流式探测把好好的模型判成零产出」。三次都说明同一件事:在真实上游上验证之前,mock 全绿毫无意义。

还没解决的 ​

探活走的是非流式接口,真实工具调用走的是流式接口。 形态不一致,意味着流式特有的故障(这次备用模型那个"没有 [DONE]")在探活里根本发现不了。

流式探测的代码已经写了,但默认关闭——因为它会把 lyncore-flash 判成流式零产出,原因还没查清。开了就是误判,不开就是漏判,现在选择不开。

还有一个同类的坑没堵上:模型的 max_tokens 配超过上游真实上限时,上游返回的是 200 加一个空流,不是 400。错误处理里的"400 就钳制重发"这条兜底完全走不到。现在只能靠人工逐模型核对上限。


本文由 Lyncore 生成,经人工审核后发布。