Skip to content

PLAN-FNOS-020 CodeBuddy 瞬时 400 终止整轮修复 ​

字段内容
计划编号PLAN-FNOS-020
计划日期2026-10-19
对应需求FNOS-020 CodeBuddy 瞬时 400 终止整轮修复
本轮功能FNOS-020-01 至 FNOS-020-04
计划状态已完成(两阶段均完成,双客户端已验)

计划目标 ​

让一次偶发的 CodeBuddy 400 不再终止整轮:由 codebuddy 路由自带重试策略,把 INVALID_REQUEST 纳入有界重试,同时保证确定性失败不被误重试。

当前实现与目标设计 ​

领域当前实现目标实现迁移影响
重试策略来源适配器未覆写 providerRetryPolicy(),基类返回 undefined,调用方退回官方默认集合覆写该方法,返回本路由自有策略无:仅影响 codebuddy 路由
可重试错误码官方五码:EMPTY_RESPONSE/RATE_LIMIT/SERVER/TIMEOUT/TRANSPORT官方五码 + INVALID_REQUEST瞬时 400 由「终止整轮」变为「重试后继续」
重试次数官方默认 5 次2 次400 属确定性错误的概率更高,缩短上界
换号逻辑仅 QUOTA/RATE_LIMIT 触发不变无
确定性错误上下文超限 → CONTEXT_WINDOW_EXCEEDED;额度 → QUOTA 换号不变(且刻意排除在重试集合外)无

为什么改在适配器而不是 DSH 上游 ​

DSH 的重试策略按路由归属:llm/src/index.ts 优先取 adapter.providerRetryPolicy(provider),缺失时才退回默认。官方 deepseek provider 正是这样携带自己的策略。因此本修复完全落在本仓库插件内,符合「不修改上游代码」的硬约束。

为什么 INVALID_REQUEST 值得重试 ​

CodeBuddy 对该类失败只回一句 Invalid request parameters,不带业务码,插件无法区分「请求确实不合法」与「网关侧瞬时拒绝」。在无法区分的前提下,选择「有界重试」优于「立即终止」:前者最坏代价是 2 次无效请求,后者代价是整轮中断并要求用户手动继续。

为什么重试是安全的 ​

该错误在 stream() 的 yield* 之前抛出,此时 emitted 恒为 false。适配器已用这个不变量保证:一旦产出过 chunk 就不再重试,避免重复内容、工具重复执行与重复计费。

影响范围分析 ​

需求功能代码模块配置/数据测试文档目标环境
FNOS-020-01plugins/dsh-codebuddy-plugin/src/host/adapter.ts(策略生效路径)无由 FNOS-020-02 的用例间接覆盖排错文档补该错误说明真实使用环境
FNOS-020-02同上:新增 CODEBUDDY_RETRY_POLICY、覆写 providerRetryPolicy()无tests/retry-policy.spec.ts本需求/计划本地构测
FNOS-020-03同上(策略取值)无同上:断言不含确定性错误码—本地构测
FNOS-020-04同上(策略取值)无同上:断言 maxRetries: 2—本地构测

分阶段任务 ​

阶段一:实现与本地验证 ​

任务 ID对应需求/验收修改内容前置条件验证方式
PLAN-FNOS-020-T01-01FNOS-020-02 / AC-01、AC-02新增 CODEBUDDY_RETRY_POLICY:mode: 'normal'、maxRetries: 2、官方五码 + INVALID_REQUEST;用 resolveRetryPolicy 解析以获得合法默认退避无pnpm run typecheck 通过
PLAN-FNOS-020-T01-02FNOS-020-02 / AC-01覆写 providerRetryPolicy() 返回该策略,并在注释里写明「基类默认返回 undefined 会退回官方集合」T01-01单测断言实例返回同一策略对象
PLAN-FNOS-020-T01-03FNOS-020-03 / AC-01、FNOS-020-04 / AC-01新增 tests/retry-policy.spec.ts:纳入、保留官方五码、不混入 CONTEXT_WINDOW_EXCEEDED/QUOTA、maxRetries: 2T01-01、T01-025 条用例全绿
PLAN-FNOS-020-T01-04回归跑插件全量测试,确认未影响既有 909 条用例T01-01~T01-0372 文件 914 条全绿

阶段二:发布后真实环境复验 ​

任务 ID对应需求/验收修改内容前置条件验证方式
PLAN-FNOS-020-T02-01FNOS-020-01 / AC-01发布插件新版本并更新目标 profile,等待偶发 400 出现插件发布、profile 指向新版本会话不再中断;或从事件流确认出现 llm/retry 后继续
PLAN-FNOS-020-T02-02FNOS-020-01 / AC-02同一次恢复中核对输出与工具调用无重复T02-01事件流中无重复 tool/call;界面无重复内容

执行进展 ​

阶段状态说明
阶段一 实现与本地验证已完成策略实现 + 单测 5 条 + 产物层复核 8 项;插件全量 914 条无回归
阶段二 运行时与恢复验证已完成DSH Web:policyKey 确认为 maxRetries: 2 且含 INVALID_REQUEST;首次 400 后自动重试成功;始终 400 时有界失败(1 + 2 次)。DSH Desktop:同样读到新 policyKey,首次 400 后 513ms 重试成功

数据、权限和错误处理 ​

  • 无新增数据结构、无权限变更、无配置项。
  • 错误处理变化仅在 codebuddy 路由的 INVALID_REQUEST 分支:从直接抛出变为外层重试。
  • 重试耗尽的最终行为与修复前一致:仍抛出最后一次失败,由用户可见。

依赖、风险和决策 ​

项说明
依赖@deepseek-ai/dsh-llm 导出的 resolveRetryPolicy 与 ResolvedRetryPolicy(已在 0.2.1-rc.2 中核实存在)
风险一若该 400 实为确定性缺陷(如某段历史确实非法),重试会多耗 2 次请求后才失败。已通过「次数有界」控制代价,且失败信息不变
风险二根因未定位:本修复是缓解而非根治。若后续证明该 400 由读取竞态导致,应另立需求收敛 attemptStream 的读取时序
风险三发布生效链路:运行中的 profile 从 npm 安装插件,源码修改需发布新版本并更新 profile 才对用户生效
决策不改 mode: 'always':无限重试会把确定性失败变成挂死
决策不把 QUOTA 加入重试:该码的正确动作是换号,重试同一账号只会白耗请求

变更记录 ​

日期变更说明
2026-10-19建立 PLAN-FNOS-020拆出两个阶段:阶段一实现与本地验证、阶段二发布后真实环境复验。明确修复落在适配器自带的 providerRetryPolicy(),不改 DSH 上游。
2026-10-19阶段二改用故障注入原计划等网关偶发 400,但该条件不可控。改为本地桩返回同形响应(HTTP 400 + 同文案 + 无业务码)并把活动账号 endpoint 指向桩,从而在 DSH Web 内跑完真实代码路径。附四个必须注意的点:桩需提供合法模型目录、endpoint 覆盖、改配置后须重启、区分标题生成请求。
2026-10-19阶段二 DSH Web 验证完成运行时 policyKey = ["normal",2,[…INVALID_REQUEST…],500,10000,0.1](修复前 maxRetries: 5 且无该码);首次 400 后 459ms 重试成功、轮次「已完成」;始终 400 时主对话恰好 3 次请求后失败。DSH Desktop 未执行(开发态未构建),阶段二未完全收口。
2026-10-20阶段二收口:DSH Desktop 验证开发态 Desktop 复验通过:模型选择器列出 CodeBuddy 分组、消息正常完成;故障注入下首次 400 后 513ms 重试成功,界面「已完成」,运行时 policyKey 与 DSH Web 一致。阶段二完成,计划收尾。

基于 VitePress 构建