Skip to content

Provider 超时与重试策略:默认 3 分钟超时,最多重试 5 次 #22

Description

@zhuqingyv

背景

当前 MA 在 DeepSeek 请求阶段可能长时间停在 thinking / 分析结果中。现场诊断确认:本地工具进程空闲,MA 主进程仍保持到 api.deepseek.com 的 443 连接,Node 主线程主要在 uv__io_poll / kevent 等 IO。

现状代码的问题不是本地命令卡死,而是 provider 调用缺少 MA 级超时边界:

  • new OpenAI({ baseURL, apiKey }) 未设置 timeout / maxRetries。
  • OpenAI SDK 默认 timeout 是 10 分钟,交互体验太长。
  • MA 自己的 withRetry 只重试 500/502/503,且只在请求已经抛错后生效。
  • streaming 已建立连接但长时间不吐 chunk 时,目前没有 stream idle timeout。
  • UI 没显示 provider wait / retry attempt,用户只能看到一直 thinking。

目标

  • provider 默认单次 attempt 超时:180_000ms
  • provider 默认最多重试:5 次。按 SDK 语义为失败后最多 retry 5 次,最坏是 1 次初始请求 + 5 次重试。
  • 覆盖 streaming 与 non-streaming 两类 LLM 调用。
  • 用户 ESC / AbortSignal 优先级最高,必须立即中断,不等超时。
  • 重试必须可观测,不能静默等十几分钟。
  • 方案要复用成熟能力,不另造 HTTP client。

非目标

  • 不改 benchmark task 让测试“看起来通过”。
  • 不做 DeepSeek 专用 hack;DeepSeek 只是当前触发问题的 provider。
  • 不在各调用点散落 setTimeout / retry 逻辑。
  • 不对已经执行过工具的 turn 做危险重放。

成熟方案复用

优先复用现有依赖和平台能力:

  • OpenAI SDK 已支持 timeoutmaxRetries、指数退避、Retry-After、408/409/429/5xx 重试。
  • Node 原生 AbortController / AbortSignal 用于用户中断与 deadline 合成。
  • MA 只新增 provider runtime 薄封装,负责统一配置、stream idle watchdog、事件上报和安全重试边界。

不建议引入新 retry 库,除非实现阶段证明 SDK 能力无法覆盖需求。

技术方案

1. 新增统一 Provider Runtime 层

新增模块建议:src/provider/runtime.ts

职责:

  • 创建 OpenAI-compatible client。
  • ModelConfig 解析 provider policy。
  • 统一执行 streaming / non-streaming chat completion。
  • 统一 timeout、retry、abort、错误分类和事件上报。

建议接口形态:

interface ProviderPolicy {
  requestTimeoutMs: number;      // default 180000
  streamIdleTimeoutMs: number;   // default 180000
  maxRetries: number;            // default 5
}

createAgent 不再直接 new OpenAI(...),而是通过 provider runtime 获取 client 与调用函数。

2. 配置扩展,保持兼容

ModelConfig 增加可选字段:

requestTimeoutMs?: number;
streamIdleTimeoutMs?: number;
maxRetries?: number;

默认值:

requestTimeoutMs = 180_000;
streamIdleTimeoutMs = requestTimeoutMs;
maxRetries = 5;

配置来源沿用现有 config.json merge 机制,不破坏旧配置。

3. 复用 OpenAI SDK timeout / retry

client 创建时传入:

new OpenAI({
  baseURL,
  apiKey,
  timeout: requestTimeoutMs,
  maxRetries,
});

每次请求仍传入用户 AbortSignal。实现时注意用户中断必须优先于 timeout,不应被包装成 retryable timeout。

现有 withRetry 应废弃或只保留给非 SDK provider。不要叠加 SDK retry + 自己 retry,避免重复重试次数不可控。

4. 增加 Streaming Idle Timeout

SDK timeout 主要覆盖请求生命周期,但 streaming 场景可能出现连接已建立、长期无新 chunk 的情况。需要在 provider runtime 包装 async iterable:

  • 每收到一个 chunk 重置 idle timer。
  • 超过 streamIdleTimeoutMs 没收到 chunk,abort 当前请求。
  • 抛出明确错误:ProviderStreamIdleTimeoutError
  • 错误信息包含 provider、model、attempt、idleMs。

安全重试规则:

  • 若 timeout 发生在第一个 chunk 前,可自动 retry。
  • 若已经向 UI 输出 token / thinking / tool_call delta,不做静默 retry,避免重复输出或产生不一致上下文;此时中断 turn,并提示用户重试。
  • 若只在内部压缩 summarize 阶段失败,可按现有 compact fallback 处理,但也必须遵守 3 分钟 timeout。

5. 覆盖所有 LLM 调用点

必须统一走 provider runtime:

  • 主 agent streaming chat completion。
  • summarizeContextItems non-streaming 压缩调用。
  • 后续如 benchmark judge / 其他内部 LLM client 可逐步迁移,避免不同路径行为不一致。

当前 benchmark judge 已有网络短重试,但最终也应复用同一套错误分类。

6. UI 与日志可观测性

新增 AgentEvent 建议:

{ type: 'provider:attempt', attempt, maxRetries, timeoutMs }
{ type: 'provider:retry', attempt, maxRetries, reason, nextDelayMs }
{ type: 'provider:timeout', phase: 'request' | 'stream_idle', timeoutMs }

TUI 展示要求:

  • thinking 区显示:等待模型响应 42s / 180s
  • 重试时显示:模型请求超时,重试 2/5
  • 最终失败时显示明确错误,不再无限 thinking。

debug log 记录每次 attempt:model、baseURL host、timeout、retry reason、elapsedMs。不记录 API key。

测试计划

单元测试

  • config 默认值:未配置时 requestTimeoutMs=180000streamIdleTimeoutMs=180000maxRetries=5
  • AbortSignal 用户中断优先,不进入 retry。
  • request timeout 触发后最多 retry 5 次。
  • 408/409/429/5xx/ECONNRESET/ETIMEDOUT 可重试。
  • 400/401/403/invalid schema 不重试。
  • stream idle timeout:fake stream 首 chunk 前卡住 -> retry。
  • stream idle timeout:已经吐 token 后卡住 -> 不静默 retry,返回明确错误。

集成测试

  • fake OpenAI-compatible server:accept 后不返回,3 分钟配置可用小值覆盖为 100ms,验证自动 abort。
  • fake stream server:SSE 打开但不吐 chunk,验证 idle timeout。
  • fake stream server:先吐 token 再停,验证不产生重复 token。
  • ESC 中断测试:用户 abort 后立即结束,不能等 provider timeout。

回归测试

  • npm run build
  • npx tsx --test test/*.test.ts
  • benchmark smoke:至少 L0/L1 关键任务通过。
  • DeepSeek 实测:人为断网或 mock 卡住时,180s 内必须显示 timeout / retry 状态。

验收标准

  • 默认配置下,单次 provider attempt 最多等待 180 秒。
  • 默认最多 retry 5 次,且 UI 清楚显示 attempt 进度。
  • 300 秒无 token 的 DeepSeek 等待不再表现为无边界 thinking。
  • 用户 ESC 可在 provider 等待中立即中断。
  • retry 不会导致重复 token、重复 tool call 或 orphan tool result。
  • 所有 LLM 调用路径使用统一 provider runtime,不再在 agent.ts / summarize.ts 手写各自 timeout/retry。

实施顺序

  1. 新增 provider runtime 与配置默认值,不接入业务。
  2. 给 runtime 补 fake client / fake stream 单测。
  3. 接入主 agent streaming 调用,替换 withRetry
  4. 接入 summarizeContextItems
  5. 增加 UI provider wait / retry / timeout 展示。
  6. 跑单测、benchmark smoke、DeepSeek 实测。

风险与边界

  • maxRetries=5 在最坏情况下会拉长总耗时,因此 UI 必须显示 attempt,用户可 ESC 中断。
  • streaming 已输出内容后不能盲目 retry,否则会污染上下文和 UI。
  • 压缩调用失败不能卡住主任务,应继续使用 compact fallback,但要记录 warning。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions