前言¶
这篇文章介绍一个 DeepSeek Harness 插件,叫 dsh-retry,维护者是 imrcpan。它解决的是智能体开发中一个很具体的问题:模型请求算不上贵,但不稳定,尤其是走中转站(relay)或共享上游池的时候,一次 401 或限流就把整个 turn 带崩了。
DeepSeek Harness 本身在 agent step 边界上有重试能力,自带的 executor 叫 @deepseek-ai/dsh-llm-retry,它只暴露指数退避:initialDelayMs、maxDelayMs、jitterRatio。这套曲线对中转场景并不友好——等一次 401,可能要退避到几十秒,而下一次请求换一个上游账号很可能就成功了。dsh-retry 就是替换掉这条曲线,改用一张固定的时间表。
这是什么¶
一句话定位:dsh-retry 是一个 DSH 插件,它替换系统内置的重试 executor,把模型请求的重试间隔从指数退避改成固定调度,并针对中转端点、共享账号池和不稳定网络做了适配。
维护者:imrcpan。分类:模型推理。许可证:MIT。版本:0.1.0。仓库:https://github.com/imrcpan/dsh-retry ,目录页:https://www.skillhub.cn/plugins/imrcpan/dsh-retry 。
下面是它的重试等待时间表:
| 第几次重试 | 1 | 2 | 3 | 4 | 5 | 6 及以后 |
|---|---|---|---|---|---|---|
| 等待时间 | 2s | 5s | 5s | 5s | 10s | 20s,不限次数 |
前 5 次由 schedule 数组控制,之后统一走 tailDelayMs,每次 20s,不设上限。相比指数退避,这条曲线对短暂抖动反应更快,对永久失败的上游也只是每 20s 重试一次。
核心功能¶
固定间隔的重试调度¶
原 executor 使用指数退避,等待时间会不断翻倍直到上限。dsh-retry 改成显式调度:2s、5s、5s、5s、10s,之后每 20s 重试一次且不设上限。理由是中转站的失败是按账号发生的,不是按请求发生的:某个后端返回 401 或配额耗尽,下一个 attempt 换到另一个账号很可能成功,所以等待应该短而固定,而不是越爬越高。
针对中转端点和不稳定上游的失败分类¶
README 明确说明这个 executor 针对 relay endpoint 和不稳定网络调优,失败码覆盖三类:
- 账号级失败会被重试。
AUTH、QUOTA、ACCOUNT_QUOTA、ACCOUNT_TOKEN_INVALID、INVALID_CREDENTIAL都在默认重试列表里。 - 网关噪声会被重试。Cloudflare 和非代理返回的 5xx 归为
SERVER,包括根本不是合法 JSON 的 HTML 错误页。 - 流损坏会被重试。断裂的 SSE 帧归为
MALFORMED_RESPONSE。
另外 HTTP_* 是前缀规则,覆盖所有未分类的 4xx,所以 HTTP_404 这类错误会被重试而不是直接结束 turn。
重试不产生重复输出¶
被重试的 step 在同一个开放 turn 内、针对同一份 durable history 重新执行。模型看不到重试,它只收到被重试 step 重建后的请求,所以不会有半截答案被重复写一遍。
事件与界面¶
每次计划中的重试会追加一条 durable 的 llm/retry 事件,等待结束后追加 llm/retry-started。Chat 界面会渲染形如 Retrying model request (2/5) . 4s 的一行,可展开查看失败原因和延迟;Trajectory 面板会列出 attempt、错误码和延迟。第 6 次及以后的重试会以 unlimited 形式展示。没有 toast 弹窗,失败尝试的中间输出不会被保留。
安装与启用¶
插件是一个 DSH bundle:package.json 里声明了 dsh.bundle.patch,等于 ./cordis.patch.yml。标准安装会自动挂载新的 executor,并把内置的 llm-retry 下线,不需要手写 patch。
安装命令(来自 README):
dsh plugin --profile web add github:imrcpan/dsh-retry
装完要重启 profile,因为组合关系是在启动时读取的。重启后这一行会出现在 Plugins 下,其中的 config 可以编辑。
安装后插件做了什么¶
挂载的 layer 插入 dsh-retry executor 并禁用 llm-retry:
- insert:
- id: dsh-retry
name: "dsh-retry"
config:
schedule: [2000, 5000, 5000, 5000, 10000]
tailDelayMs: 20000
retryableCodes: [ ... ] # 完整列表见 cordis.patch.yml
- id: llm-retry
disabled: true
禁用 llm-retry 这一步很关键:两个挂载的 executor 会争抢同一个失败,并且各自写重试链状态。如果 profile 覆盖了这个设置,要保持它处于 disabled。
从源码目录安装¶
如果用的是 checkout,需要作为 bundle 安装,这样 manifest 才会生效:
dsh plugin --profile web add /absolute/path/to/dsh-retry
也接受普通的 file: 依赖,但那样不会应用 bundle layer,需要手工把上面那段 patch 复制到 profile 的 cordis.patch.yml:
{
"dependencies": {
"dsh-retry": "file:./plugins/dsh-retry"
}
}
配置¶
config 下有三个键:
| key | 默认值 | 含义 |
|---|---|---|
schedule |
[2000, 5000, 5000, 5000, 10000] |
第 1..n 次重试前的等待毫秒数;数组长度就是有上限的头部 |
tailDelayMs |
20000 |
schedule 用完之后每次重试的等待 |
retryableCodes |
见下 | 值得重试的失败码 |
retryableCodes 里以 * 结尾的条目是前缀规则。HTTP_* 会命中 HTTP_404、HTTP_409、HTTP_422 以及其他未分类状态码。裸 * 会被拒绝,因为它会命中所有失败。
默认会重试的失败码¶
| code | 典型成因 |
|---|---|
EMPTY_RESPONSE |
响应完成但没有内容块 |
RATE_LIMIT |
HTTP 429 或 provider 的限流错误 |
SERVER |
HTTP 5xx,包括网关和 Cloudflare 的 5xx |
TIMEOUT |
请求或流超时 |
TRANSPORT |
连接重置、socket 错误、流被截断 |
PI_AI_ERROR |
pi-ai adapter 无法分类的上游错误 |
AUTH |
HTTP 401 或 403;走共享账号池时换一个账号可能成功 |
QUOTA、ACCOUNT_QUOTA |
余额或账号配额耗尽 |
ACCOUNT_TOKEN_INVALID |
已存账号 token 被 401 拒绝 |
INVALID_CREDENTIAL |
池子里格式错误的凭据条目 |
MALFORMED_RESPONSE |
SSE 帧损坏或事件类型不匹配 |
HTTP_* |
未分类的 4xx,例如 HTTP_404、HTTP_409、HTTP_422 |
默认不重试的失败码¶
| code | 原因 |
|---|---|
CONTEXT_WINDOW_EXCEEDED |
compaction 负责恢复,重试会形成循环 |
INVALID_REQUEST |
请求本身错了,换账号也救不了 |
NO_ADAPTER、UNSUPPORTED_REASONING_EFFORT |
配置错误 |
MISSING_CREDENTIAL |
该路由没有配置凭据 |
ACCOUNT_SIGN_IN_REQUIRED |
需要人工登录 |
ABORTED |
取消必须保持终态 |
UNKNOWN |
外部错误的兜底 code,无上限重试不安全 |
如果部署环境的判断和默认值不一样,可以把这些 code 加进 retryableCodes。
适用场景与注意¶
适合谁:使用中转站、共享账号池,或上游明显不稳定的 DSH 部署。这类失败是按账号分散的随机噪声,固定短间隔重试收益最大。如果走的是稳定直连,内置 executor 已够用,没必要换。
几个必须知道的行为:
- 插件以当前 dsh 进程的权限运行,能读到什么凭据就会按该进程的授权范围使用。安装前建议先检查
cordis.patch.yml、index.js和 LICENSE,确认来源再启用。 - 重试链计数器放在内存里,按 session 一个。host 重启会结束 turn,不会丢失什么。只有 agent loop 的请求会被重试,直接调用
ctx.llm.stream()的地方仍然是单次尝试。 - tail 是真的不限次数。当上游始终不恢复,turn 会一直停在 retrying 状态,直到被手工停止。
- 重试池子里的凭据和配额失败,前提是池子会在下一次 attempt 轮转。如果池子本身永久耗尽,就会每 20s 循环一次。
- executor 自己不会轮换账号,它只重新发起请求,所以中转站必须为下一次 attempt 选中另一个上游。
- 每一次 attempt 都是一次单独计费的 provider 请求。
结尾¶
dsh-retry 的价值不在于发明新的重试机制,而在于用一张固定时间表替换指数退避,并把 relay 场景下最常见的三类失败(账号级、网关级、流损坏)纳进重试范围,同时保证不产生重复输出。想直接看源码和配置,可以打开 https://github.com/imrcpan/dsh-retry ,或在 https://www.skillhub.cn/plugins/imrcpan/dsh-retry 查看目录条目。
生态背景补充一句:DSH 的理念是「一切皆插件」,社区目录是独立站点,与 DeepSeek 或幻方没有官方从属关系,使用时以插件自身来源为准。