DeepSeek Harness PI_AI_ERROR:瞬时中断不重试、限流误判与 400 锁死会话

故障排查发布于 2026-10-17作者: DeepSeek Plugin 插件市场
DeepSeek HarnessPI_AI_ERRORllm-pi-airetryableCodes重试策略INVALID_REQUESTopencode-go
DeepSeek Harness 走 pi-ai 路由时,报错被兜底成 PI_AI_ERROR 就不再重试:上游 HTTP/2 流中断、Codex 服务端过载等瞬时故障、网关限流被 401/403 正则抢走、无 body 的 400 让长会话永久锁死。本文给出分类器机制、判据与 retryableCodes 配置。

如果你在 DeepSeek Harness(DSH)里用 pi-ai 类路由(Codex、OpenRouter、opencode-go、自建网关等),很可能会遇到一种「什么都没坏但就是不重试」的失败:一轮对话突然结束,报一个笼统的 PI_AI_ERROR,日志里连一条 llm/retry 都没有。 原因不在网络,而在错误分类器:classifyPiAiError 是一串按顺序匹配的无锚文本正则,首个命中即返回,认不出就落到末尾兜底 PI_AI_ERROR;而重试轮只处理 EMPTY_RESPONSE、RATE_LIMIT、SERVER、TIMEOUT、TRANSPORT 五类。于是本批讨论里反复出现的四类上游瞬时故障全部落进兜底:HTTP/2 流中断(upstream_http2_stream_error)、Codex 服务端过载与缓冲限制(server_error / exceeded request buffer limit)、限流被鉴权正则抢走(同一句里出现 401 与 429 时被判成 AUTH),以及无 body 的 400 被判 INVALID_REQUEST 之后长会话进入不可自愈的吸收态(#9589、#9397、#9502、#9564、#9636、#9352)。

DeepSeek Harness 机制:一个纯文本正则分类器 + 五类白名单

一句话结论:分类靠「文本长什么样」而不是「上游给的码是什么」,所以只要上游措辞不在正则表里,就会被归成兜底码 PI_AI_ERROR,而兜底码没有重试资格——这就是「瞬时故障不重试」的全部原因。 两处实现细节决定了所有症状(#9589):

  1. 分类器:classifyPiAiError() 是一串无锚(不带 ^/$)的文本正则,按顺序匹配、首个命中即返回;末尾是 catch-all 兜底 PI_AI_ERROR;
  2. 重试轮:只重试 EMPTY_RESPONSE / RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT 这五类,PI_AI_ERROR 不在其中。

DeepSeek Harness 本批被报告的具体缺口

上游返回期望归类实际归类原因
upstream_http2_stream_error: Upstream HTTP/2 stream failedTRANSPORTPI_AI_ERROR分类器只认旧措辞 HTTP2 request did not get a response(#9564、#9508)
server_error: Our servers are currently overloaded…SERVERPI_AI_ERRORSERVER 分支只认 \b5\d\d\b,而 Responses 路线生成的是 ${code}: ${message},不带数字(#9502)
exceeded request buffer limit while retrying upstreamSERVERPI_AI_ERROR同上,纯文本无状态码(#9397)
Codex 服务端过载(无状态码文案)SERVERPI_AI_ERROR同上;另外 \bsocket\b 匹配不到 WebSocket(无词边界)(#9589)
同一句里同时含 401/403 与 429RATE_LIMITAUTH401|403 这一条排在 429 之前,首个匹配胜出(#9636)
400 status code (no body)——INVALID_REQUEST\b400\b 分支对「只有状态码、无 body」的字符串断言「请求内容非法」(#9352)

讨论里给过一个很有说服力的量化:约有一半 pi-ai 可重试措辞对 harness 是不可见的——也就是说「上游认为可重试」与「DSH 认为可重试」两张表在漂移(#9397)。

DeepSeek Harness 判据:怎么确认你踩的是这一族

看两件事就够:turn/end 有没有带 assistant/message,以及有没有 llm/retry 事件。

  1. 一轮被 PI_AI_ERROR 打断,日志里 turn/end 显示错误结束、没有 assistant 产出;
  2. 全程没有任何 llm/retry 事件(说明这一次根本没进重试轮);
  3. 换成 TRANSPORT 类故障时,能看到重试 5/5 之类的记录——这个对照最能说明问题不在网络而在分类(#9564)。

DeepSeek Harness 处置一:把缺失的码补进重试白名单(配置层)

一句话结论:retryableCodes 是整体替换语义,所以补的时候必须把默认五类一起写全。 配置写在与当前会话模式一致的 profile patch 文件(cordis.patch.yml)里,不是过时的 settings.yaml(#9694)。

  1. 先确认要补哪个码。看日志里 turn/end 的 reason.error.code,确认是 PI_AI_ERROR 还是 INVALID_REQUEST(#9694)。
  2. 在 profile patch 里写全列表(把默认五类一起列出,避免整体替换把它们挤掉):
yaml
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- patch:
    id: llm-pi-ai
    config:
      retryPolicy:
        retryableCodes:
          - EMPTY_RESPONSE
          - RATE_LIMIT
          - SERVER
          - TIMEOUT
          - TRANSPORT
          - PI_AI_ERROR        # 本次要补的兜底码
  1. 重启 DSH 让配置生效,然后重跑之前失败的那条请求。
  2. 验证:日志里这次应当出现 llm/retry 事件;若仍没有,说明这个码在更早的地方就被拦下(见下一节的 400 情形)(#9694)。
  3. 知道代价:PI_AI_ERROR 是兜底码,把它加进白名单等于「重试所有未识别的错误」,包括那些本来就不该重试的。所以这是临时缓解,不是长期方案(#9694)。

一个容易困惑的点:为什么在设置界面里找不到这个选项?因为 pi-ai 适配器的 retryPolicy 没有标记为可在设置界面编辑(deepseek 适配器才显示),所以只能手写 patch(#9694)。

DeepSeek Harness 处置二:无 body 的 400 为什么是「吸收态」

一句话结论:INVALID_REQUEST 不在默认可重试集合,而压缩溢出的自动恢复只认 CONTEXT_WINDOW_EXCEEDED,两者对不上——所以这个状态既不会被重试、也不会被自动恢复,长会话就此锁死。 症状是「突然每条请求都报 400 status code (no body)」,此后连 /compact 也救不回来(#9352)。

还有一条相关的量级问题值得一起看:某些网关的错误体只有 31 字节,即「有状态码、没有有效消息」,于是 DSH 只能按「裸状态码」处理;opencode-go 上的真实触发条件是 prompt_tokens + max_tokens > 2^20——请求的 max_tokens 取自目录声明的输出上限(例如 384000),而声明的 1M 窗口大于上游实际的 2^20 限制(#9434)。

DeepSeek Harness 三步处置

  1. 先把请求体量降下来:给该 provider/model 显式压 maxTokens(讨论中给出的一个可行值是 65536),或手动 /compact 缩短历史(#9434):
yaml
providers:
  <provider>:
    modelOverrides:
      <model>:
        maxTokens: 65536
  1. 把 INVALID_REQUEST 加进 retryableCodes(写法同上一节,注意整体替换语义),让这类 400 有机会重试成功(#9352)。
  2. 或使用把无 body 4xx 改判为可重试错误的社区插件(讨论中提到有同类插件把「无 body 4xx」改判为 EMPTY_RESPONSE,从而让默认策略接手)(#9352)。
  3. 验证:重放同一条请求,预期这次要么成功、要么至少出现一次重试记录;若依旧立刻失败,说明还有别的原因(例如窗口声明与实际不符),回到第 1 步继续压体量。

DeepSeek Harness 处置三:别期待「整体放开 PI_AI_ERROR」能一劳永逸

一句话结论:把整个 PI_AI_ERROR 放宽到重试,会把「本不该重试」的错误一起拉进来无限重试。 讨论里多次强调这条边界(#9397、#9502):

  1. 会连累的错误:某些状态类错误(例如运行中、待处理一类)本来就不该重试,重试反而放大成本;
  2. 语义相反的相邻分支:靠近 413 的分支与「缓冲超限」语义相反,泛化匹配 buffer / too-large 会把两者混为一谈(#9397);
  3. 更稳的方向:窄修——把上游的 code 前缀整族映射到 TRANSPORT 或 SERVER(例如 upstream_http2_* 整族、server_error 单独一条),并且保持分支顺序、把限流分支提到鉴权分支之前(#9502、#9636);
  4. 上游有现成能力:讨论中提到 pi-ai 自身导出了可重试判定(isRetryableAssistantError),DSH 侧改成调用它可以避免维护第二份会漂移的措辞表(#9589)。

DSH插件 排查注意事项

这一族的排查口诀是:先看「有没有 llm/retry」,再看「turn/end 的 code 是什么」,最后才看上游原文。 九条要点:

  1. 没有 llm/retry 就说明没进重试轮,不要把力气花在「重试次数不够」上(#9564)。
  2. PI_AI_ERROR 的字面含义是「分类失败」,不是一种具体故障(#9589)。
  3. 两张重试表在漂移:上游认为可重试的不等于 DSH 认为可重试的,量级差异可达一半(#9397)。
  4. 限流被误判成鉴权很常见,判据是「密钥明明有效却报 AUTH」(#9636)。
  5. INVALID_REQUEST 是吸收态:不重试 + 不自动恢复,必须人工介入(#9352)。
  6. 错误体被截断(如只剩 31 字节)时,DSH 只能按裸状态码处理,排查很难,先解决「错误体为什么丢了」(#9434)。
  7. retryableCodes 是整体替换:写它就必须把默认五类一起写全(#9694)。
  8. 设置界面里找不到这个选项是正常的:pi-ai 适配器的 retryPolicy 没有暴露到 UI(#9694)。
  9. 修复状态如实:分类器缺口的窄修、INVALID_REQUEST 的可重试性、以及摘要与上游 code 的映射,在讨论所指版本里都未修;本文给出的均是用户侧配置与插件缓解,不是官方修复。

排查这类错误时,先把「插件层」变量排掉往往能省一半时间——毕竟重试策略、视觉附件、计价守卫都是插件形态。如果你想在界面上把当前环境的插件与日志一眼看清,与其在各处手敲命令,不如用 DSH Plugin Hub 这类可视化插件市场:已安装列表、装卸历史与系统日志都在一个面板里。

系统日志

来源:Discussion #9589、Discussion #9397、Discussion #9502、Discussion #9564、Discussion #9508、Discussion #9636、Discussion #9434、Discussion #9352、Discussion #9694。

常见问题

DeepSeek Harness 的 pi-ai 路由没有 llm/retry 就报 PI_AI_ERROR,为什么?

DeepSeek Harness 报 PI_AI_ERROR 且不重试,是因为它是分类器认不出时的兜底码,而不在默认可重试集合里。pi-ai 适配器用一串文本正则给错误分类,首个匹配胜出;上游措辞一旦不在正则覆盖范围内,就会落到末尾兜底 PI_AI_ERROR,而不重试的重试轮只处理 EMPTY_RESPONSE、RATE_LIMIT、SERVER、TIMEOUT、TRANSPORT 这五类,于是这一轮直接结束。判据是 turn/end 没有 assistant/message 且全程没有 llm/retry 事件。

DeepSeek Harness 对 server_error 或 HTTP/2 流中断为什么不自动重试?

DeepSeek Harness 不重试这两类,是因为它们的措辞都不带数字状态码。分类器的 SERVER 分支只认 5 开头的三位数字,而 Responses 路线生成的字符串是 code: message 的形状、没有数字,所以匹配不上;HTTP/2 那条也只认旧措辞,新码同样落入兜底。可行的缓解是把这些码显式加进重试白名单,或把上游的 code 前缀整族映射到 TRANSPORT 一类,但要注意别整体放开 PI_AI_ERROR。

错误信息里同时出现 401 和 429 时,DeepSeek Harness 说密钥无效但密钥其实是好的,怎么解释?

DeepSeek Harness 把 401/429 同现判成密钥无效,是正则顺序问题。分类器全是无锚文本正则、首个匹配胜出,而 401|403 这一条排在 429 之前,所以同时出现时会被判成 AUTH 而不是 RATE_LIMIT,AUTH 又不在默认可重试集合里,于是既不重试也给出错误的归因。修法是把限流分支提到鉴权分支之前,或给该路由的 retryableCodes 补上对应码。

DeepSeek Harness 长会话突然每条请求都报 400 status code (no body),连压缩也救不回来,怎么办?

DeepSeek Harness 长会话一直报无 body 的 400,是无 body 的 400 被判成 INVALID_REQUEST 造成的吸收态:该码不在默认可重试集合,而压缩溢出的自动恢复只认 CONTEXT_WINDOW_EXCEEDED,两者对不上,所以怎么都不会恢复。可用的处置是把 INVALID_REQUEST 显式加进该 profile 的 retryableCodes,或使用把无 body 4xx 改判为可重试错误的社区插件。

DeepSeek Harness 把 PI_AI_ERROR 加进 retryPolicy.retryableCodes 要写在哪里、会不会覆盖默认五类?

DeepSeek Harness 的 retryableCodes 写在 profile 的 patch 文件里(cordis.patch.yml 那一层),不是过时的 settings.yaml。要注意 retryableCodes 是整体替换而不是追加:如果你只写 PI_AI_ERROR,默认的 EMPTY_RESPONSE、RATE_LIMIT、SERVER、TIMEOUT、TRANSPORT 会全部丢掉,所以必须把想要的码一起列全。另外 pi-ai 适配器的 retryPolicy 没有标记为可在设置界面编辑,因此你在设置页里找不到这个选项。

相关术语

PI_AI_ERROR
PI_AI_ERROR 是 DeepSeek Harness 的 pi-ai 适配器在错误分类器无法识别上游错误时使用的兜底错误码,表示「分类失败」而不是某种具体故障。它不在默认可重试集合里,因此一轮遇到它通常会直接结束。— https://github.com/deepseek-ai/deepseek-harness/discussions/9589
classifyPiAiError
classifyPiAiError 是 pi-ai 适配器里把上游错误文本映射成内部错误码的函数,实现方式是一串按顺序匹配的文本正则,首个命中即返回。它不解析上游的稳定错误码,因此上游措辞变化会导致分类漂移,最终落入 PI_AI_ERROR 兜底。— https://github.com/deepseek-ai/deepseek-harness/discussions/9636
retryPolicy.retryableCodes
retryPolicy.retryableCodes 是重试策略里声明「哪些错误码允许自动重试」的列表,属于插件配置。它是整体替换语义:patch 里写什么就只剩什么,未写出的码会失去重试资格。— https://github.com/deepseek-ai/deepseek-harness/discussions/9694
upstream_http2_stream_error
upstream_http2_stream_error 是上游在 HTTP/2 流被中断时返回的错误码,属于瞬时传输故障,语义上应当可重试。它当前没有被分类器归到 TRANSPORT 一类,因此会落入 PI_AI_ERROR 兜底而失去重试。— https://github.com/deepseek-ai/deepseek-harness/discussions/9564

来源