背景
当前 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 已支持
timeout、maxRetries、指数退避、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=180000、streamIdleTimeoutMs=180000、maxRetries=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。
实施顺序
- 新增 provider runtime 与配置默认值,不接入业务。
- 给 runtime 补 fake client / fake stream 单测。
- 接入主 agent streaming 调用,替换
withRetry。
- 接入
summarizeContextItems。
- 增加 UI provider wait / retry / timeout 展示。
- 跑单测、benchmark smoke、DeepSeek 实测。
风险与边界
maxRetries=5 在最坏情况下会拉长总耗时,因此 UI 必须显示 attempt,用户可 ESC 中断。
- streaming 已输出内容后不能盲目 retry,否则会污染上下文和 UI。
- 压缩调用失败不能卡住主任务,应继续使用 compact fallback,但要记录 warning。
背景
当前 MA 在 DeepSeek 请求阶段可能长时间停在
thinking / 分析结果中。现场诊断确认:本地工具进程空闲,MA 主进程仍保持到api.deepseek.com的 443 连接,Node 主线程主要在uv__io_poll / kevent等 IO。现状代码的问题不是本地命令卡死,而是 provider 调用缺少 MA 级超时边界:
new OpenAI({ baseURL, apiKey })未设置 timeout / maxRetries。withRetry只重试 500/502/503,且只在请求已经抛错后生效。目标
180_000ms。5次。按 SDK 语义为失败后最多 retry 5 次,最坏是 1 次初始请求 + 5 次重试。非目标
setTimeout/ retry 逻辑。成熟方案复用
优先复用现有依赖和平台能力:
timeout、maxRetries、指数退避、Retry-After、408/409/429/5xx 重试。AbortController/AbortSignal用于用户中断与 deadline 合成。不建议引入新 retry 库,除非实现阶段证明 SDK 能力无法覆盖需求。
技术方案
1. 新增统一 Provider Runtime 层
新增模块建议:
src/provider/runtime.ts。职责:
ModelConfig解析 provider policy。建议接口形态:
createAgent不再直接new OpenAI(...),而是通过 provider runtime 获取 client 与调用函数。2. 配置扩展,保持兼容
在
ModelConfig增加可选字段:默认值:
配置来源沿用现有
config.jsonmerge 机制,不破坏旧配置。3. 复用 OpenAI SDK timeout / retry
client 创建时传入:
每次请求仍传入用户
AbortSignal。实现时注意用户中断必须优先于 timeout,不应被包装成 retryable timeout。现有
withRetry应废弃或只保留给非 SDK provider。不要叠加 SDK retry + 自己 retry,避免重复重试次数不可控。4. 增加 Streaming Idle Timeout
SDK timeout 主要覆盖请求生命周期,但 streaming 场景可能出现连接已建立、长期无新 chunk 的情况。需要在 provider runtime 包装 async iterable:
streamIdleTimeoutMs没收到 chunk,abort 当前请求。ProviderStreamIdleTimeoutError。安全重试规则:
5. 覆盖所有 LLM 调用点
必须统一走 provider runtime:
summarizeContextItemsnon-streaming 压缩调用。当前 benchmark judge 已有网络短重试,但最终也应复用同一套错误分类。
6. UI 与日志可观测性
新增 AgentEvent 建议:
TUI 展示要求:
等待模型响应 42s / 180s。模型请求超时,重试 2/5。debug log 记录每次 attempt:model、baseURL host、timeout、retry reason、elapsedMs。不记录 API key。
测试计划
单元测试
requestTimeoutMs=180000、streamIdleTimeoutMs=180000、maxRetries=5。AbortSignal用户中断优先,不进入 retry。集成测试
回归测试
npm run buildnpx tsx --test test/*.test.ts验收标准
agent.ts/summarize.ts手写各自 timeout/retry。实施顺序
withRetry。summarizeContextItems。风险与边界
maxRetries=5在最坏情况下会拉长总耗时,因此 UI 必须显示 attempt,用户可 ESC 中断。