DSH 中的 pi 组件把 API 错误误判成「上下文超长」,导致不会触发自动重试 #4657
Replies: 1 comment
|
这是我在这个社区见过取证最扎实的一份报告之一——12,238 个 step / 530 次失败 / 81 次压缩 / 压缩后 8 事件内失败只有 12 次(2.3%),这三个数字直接把"压缩导致失败"这个最自然的假设证伪了,而绝大多数报告在第一步就会停在那里。 补三件:一个现在就能用的止血办法、这条属于哪个更大的类、以及修复该落在哪一层。 一、止血:在网关前放一个把"空 body 400"改写成 5xx 的透传代理你已经定位到整条链的转折点是第 3 步——pi-ai 那条为 Cerebras 加的规则(400/413 + 空 body ⇒ 判为上下文超长)。而你自己给出的对照组正好指出了出路:
也就是说:只要那个响应不是 400/413,整条误判链就不成立。 所以在 DSH 和你的网关之间放一个本地透传器,只做一件事——把"状态码 400/413 且 body 为空"的响应改写成 我们仓库里有个可以直接拿来改的透传器( 这条的性质要说清楚:它是绕过,不是修复。 而且它有一个真实代价——如果哪天真的发生了上下文超长,你也会把它改写成可重试的 5xx,于是变成无意义的退避重试。不过按你的数据(6 天内没有任何一次失败带真实的 "context length" 文本、最大成功请求 16.5 万 token 对 256K 窗口),在你的部署里这个代价接近零。加上一行日志记下被改写的次数,如果哪天这个数字异常上涨,就该回头看了。 (好处是它还顺带给你一个东西:你能直接看到那些空 body 400 到底长什么样、来自哪一跳——你原文说网关部署在 AWS ALB 后面,那么"400 是 ALB 返的还是上游返的"这个问题,录一份就有答案了。ALB 在某些情况下会自己返回空 body 的 4xx。) 二、这属于一个反复出现的类:结构化错误信息在中间层被弄坏你这条和另外两帖是同一个形状的三种变体:
三条合起来说的是同一件事:重试决策的质量,完全取决于错误分类的质量;而分类这一步现在既会丢信息(#1215),也会造假信息(你这条)。 造假比丢失更危险,这一点值得你在原帖里点明:
建议在标题或摘要里把这层写出来,比如"pi-ai 的上下文超长启发式会给通用网关的空 body 400 打上 三、修复该落在哪一层:这里有个归属问题值得先说清你已经指出第 3 步在 pi-ai(
第 2 条我觉得特别值得提,因为它不依赖上游,而且它是一条通用的防线:任何"由启发式推断出来的结构化错误码",都应该在有更硬的证据时被推翻。 你这个案例里,"请求只有几万 token 而窗口是 256K"就是那个更硬的证据,而它当时就在手边、没人去看。 (顺带一个相关的版本信息:我们这边把 边界与利益相关我们不修 DSH 自家组件,也不修 pi-ai——错误分类、重试白名单、压缩都在它们各自的仓库里。第一节给的是绕过不是修复;我没有复现过你的场景,也没有读过那条 Cerebras 规则的源码,全部建立在你的取证上。 利益相关:我维护 pi2dsh(Pi 生态兼容层)。这条不推销:第一节那个脚本是独立单文件,拷走就能跑;而且换一套运行时不会改变 pi-ai 的分类逻辑——那条规则在库里,谁调用它都一样。 |
Uh oh!
There was an error while loading. Please reload this page.
问题是怎么发现的
在开发项目过程中,用 DeepSeek Harness 接入第三方 API 跑长对话任务时(第三方 API 自身是否稳定由对方控制,且各家返回的错误格式各不相同),发现一个反复出现的怪现象:
第一反应都会是:压缩了 → 内容还是超长 → 失败。听起来很合理,但冷静下来就觉得不对——压缩的目的就是把上下文变小,压缩完反而更超长? 于是一步步排查。
把现象拆开看,真正的痛点是:DSH 中的 pi 组件(pi-ai)把上游的 API 错误误判成了"上下文超长"(CONTEXT_WINDOW_EXCEEDED),导致该错误不会触发自动重试,模型调用失败后 Harness 直接停止本轮——无论失败发生在压缩前后,只要被定性为不可重试的错误码,任务就会中断,长对话无法继续。
第一步:先看数据 —— 压缩和失败其实没啥关系
把 6 天的会话日志全部扒出来分析(日志是一堆 zstd 压缩包,解开后是逐条 JSONL 事件):
也就是说 97.7% 的失败和压缩没有任何关系。"压缩后必失败"其实是错觉——压缩提示很醒目,之后的失败容易让人误以为是压缩引起的。
第二步:这个报错真的是"上下文超长"吗?
报错自带错误码
CONTEXT_WINDOW_EXCEEDED(上下文窗口超了),但前面的描述是 "400 status code (no body)"——意思是:服务器返回了 400 错误,而且错误详情(body)是空的。把 6 天所有失败翻出来看:
结论:6 天里上下文从未真正超长过,这个错误码与实际不符。
第三步:失败那一刻,请求到底有多大?
以出问题最典型的那个会话为例:压缩 banner 出现后仅过了 5 个事件就失败了。把那一刻发给模型的请求内容重建出来数了数:约 4 万多个字符,折算成真实 token 只有几万。
而模型的上下文窗口是默认的 256K(约 26 万 token)。请求量只有窗口的几十分之一,却被标记为超长,说明问题不在请求体大小。
再补一个数据:历史上最大的一次成功请求是 16.5 万 token,照样成功了。所以更不可能是超长。
第四步:看源码
结合会话轨迹和源码,把完整的调用链梳理出来。从请求发出到本轮失败,实际按以下顺序流转:
400 status code (no body);CONTEXT_WINDOW_EXCEEDED:由于第 3 步判定为超长,这条错误被标记上该错误码;EMPTY_RESPONSE(空响应)、RATE_LIMIT(限流)、SERVER(服务器错误)、TIMEOUT(超时)、TRANSPORT(传输错误)。CONTEXT_WINDOW_EXCEEDED不在其中;补充两点:
RATE_LIMIT(在白名单内),退避重试最多 10 次;5xx 同理归为SERVER。只有 400/413 空 body 这一种,同时触发了超长判定且错误码不在白名单,第一发就直接结束——这就是"调用失败后停止不重试"的直接成因。原因详细说明:上游的问题 × pi-ai 的过度拦截
这个问题的根源来自两头:对方上游的调用流程有问题,而 pi-ai 的错误分类与重试策略设置过于严格,把本可以自愈的问题直接拦截了。
1. 接入第三方 API:自身不稳定,且错误返回设置不统一
问题发生在接入第三方 API 的场景下。我们接入的模型走代理网关(部署在 AWS 反向代理 ALB 后面),调用的正是第三方 API。它有两点直接放大了问题的可能性:
从 6 天的失败数据看:
2. pi-ai 的识别规则与重试策略设置过于严格,把问题直接拦截
让"瞬时异常"升级为"本轮直接失败"的,是 pi-ai 这边的两道设置:
对比:同样是空 body,429(限流)和 5xx(服务器错误)都在白名单内,可退避重试最多 10 次;唯独空 body 的 400/413 没有任何重试机会。对瞬时异常而言,这确实过于严格。
3. 两者叠加的效果:任务卡顿
修复的本质是把这道设置放松一点:让"上下文超长"(误判出来的)错误码也能进入重试流程(退避重试最多 10 次),让上游异常在重试中自行恢复,而不是第一次请求就直接失败、停止本轮。
补充:DeepSeek Harness 内部流程,为什么会导致这一步出问题
在厘清"上游异常 + 误判 + 白名单拦截"后,再看 Harness 每走一步的完整流程,可以解释为什么这个错误会在这一轮暴露。
Harness 每走一步的内部流水线
每推进一个 step,Harness 按固定顺序执行:
400 status code (no body));CONTEXT_WINDOW_EXCEEDED;为什么偏偏是这一步出问题
关键在第 4、5 步只做规则匹配、不做实际校验:
400 status code (no body)即无条件命中;CONTEXT_WINDOW_EXCEEDED不在默认白名单,错误在 0 次重试下直接成为终止原因。若同一异常以"网络/连接错误"或"429"形式出现,则会在白名单内、进入退避重试并大概率恢复;为什么看起来压缩后这一轮更容易出问题
这次的失败恰好发生在压缩之后,属于偶合,但撞上时确实无法自愈:压缩后重建请求刚好碰到网关异常,且该异常被识别为不可重试的超长错误;而同样的异常如果出现在其他步骤,只要以可重试的错误码返回(如 429 限流、5xx 服务器错误),重试机制会自动恢复,无需人工介入。
小结
Harness 流程设计本身合理——真实超长确实不应盲目重试。问题出在 pi-ai 的判定源头过宽(Cerebras 规则误伤)以及超长判定先于网络类判定执行,导致上游瞬时异常被定性为终结性失败,调用失败后停止、不重试。修复即放宽拦截:将
CONTEXT_WINDOW_EXCEEDED加入可重试白名单,使误判的超长也能先重试再决定是否终止。修复:改一行配置就行,不用动代码
既然问题出在"错误码没进重试白名单",那就把它加进白名单——纯配置就能解决。
settings.yaml里模型的重试配置,原来是:改成(把
CONTEXT_WINDOW_EXCEEDED加进白名单,同时必须保留原来的 5 个码,否则限流/5xx 的重试会被一起弄丢):涉及三个走同一个网关的模型配置(具体模型名略去)。
验证:用 Harness 自己的配置解析器跑了三关——YAML 语法、配置校验、重试逻辑模拟全部通过(新码能重试、原来的 5 个码没丢、真实的参数错误仍然不会拿去重试)。
改完之后的效果(预期)
retryableCodes删掉就回到原样。All reactions