Skip to content

refactor(provider): 统一生成重试为“60 秒首包窗口 + 300 秒流空闲超时”,收敛三家 provider 生成语义 #461

@phantom5099

Description

@phantom5099

本 issue 落地一个实现类改动:统一 OpenAI-compatible、Anthropic、Gemini 三家 provider 的生成重试与超时模型,形成一套稳定、可配置、可测试的 provider 级生成尝试语义,先补齐 NeoCode 当前生成链路中“首包等待不稳、长流容易被误杀、三家 provider 行为不一致”这三个质量缺口。

保留一个明确原则:

  • 本次目标不是“让 agent 更智能”,而是让生成链路的重试与超时行为稳定、一致、可验证。
  • 本次不把 provider retry 逻辑重新抬回 runtime
  • 本次不引入 background job、断线恢复、任务持久化等长任务能力。
  • 本次不顺手扩张到 gateway / runtime / tui 协议。
  • 本次只收敛 provider 生成尝试语义,不重构第三方 chat 端口适配层。
  • 本次新增配置字段必须采用“可选字段 + 默认值回填”策略;旧配置缺少新字段时必须仍可直接启动。

目标问题

当前生成链路的重试与超时语义是分裂的:

OpenAI-compatible / Anthropic
  -> 主要依赖 SDK 自带 retry
  -> request timeout / http.Client.Timeout 承担过多职责

Gemini
  -> provider 内部自管 retry
  -> 当前只支持“未开始流输出前重试”
  -> retry 次数与超时语义单独定义

这导致几个直接问题:

  • 首包等待和长流读取被同一个 timeout 粗暴绑定。
  • timeout 太短会误杀正常长流,太长又会拖慢首包失败后的重试。
  • 三家 provider 对相似错误的处理不一致。
  • 当前没有统一定义“什么时候算流已经开始”“什么时候还能重试”。
  • timeout / retry 策略散落在不同 provider 中,后续很难统一治理和测试。

相关实现主要在:

  • internal/provider/openaicompat/*
  • internal/provider/anthropic/*
  • internal/provider/gemini/*
  • internal/config/provider.go

为什么现在做

这不是参数微调,而是一个明确的链路质量问题。

从链路质量看,当前缺口体现在:

  • 首包前坏请求不能稳定快速失败。
  • 首包后正常长流缺少稳定保护。
  • provider 之间 retry 责任不一致,系统行为漂移。
  • 用户很难建立“为什么重试 / 为什么没重试 / 为什么被断流”的稳定预期。

从用户感知看,这个改动应直接带来:

  • 首包迟迟不来的请求更快失败并进入重试。
  • 已开始稳定输出的长流不会因为总时长过长被误杀。
  • 相似错误在不同 provider 上更接近相同行为。
  • timeout / retry 策略可通过 provider 配置统一调整。

实现设计

1. 统一生成尝试模型

本次统一引入两阶段生成语义:

首包前
  -> start timeout
  -> 超时则取消本次尝试并重试

首包后
  -> 不设固定总时长上限
  -> 只要持续有 payload 就允许继续输出
  -> 若连续 idle timeout 没有任何新 payload,则判定流卡死并中断

统一规则直接定死:

  • start timeout 表示“从发请求到收到首个有效流 payload 的最长等待窗口”。
  • idle timeout 表示“首包后,连续没有任何新 payload 的最长空闲窗口”。
  • max retries 表示额外重试次数,不含首次尝试。
  • 只有在“尚未收到首个有效流 payload”时才允许 retry。
  • 一旦收到首个有效流 payload,后续任何流中断或 provider 错误都直接返回,不再 retry。
  • backoff 采用指数退避加抖动。

“首个有效流 payload” 统一定义为首次发出的:

  • text delta
  • tool call start
  • tool call delta

不允许各 provider 自己定义“什么算流已开始”。

2. provider 级配置参数

本次新增 provider 级 YAML 字段,不放入 runtime

generate_max_retries: 5
generate_start_timeout_sec: 60
generate_idle_timeout_sec: 300

对应配置与运行时结构:

type ProviderConfig struct {
    ...
    GenerateMaxRetries      int `yaml:"generate_max_retries,omitempty"`
    GenerateStartTimeoutSec int `yaml:"generate_start_timeout_sec,omitempty"`
    GenerateIdleTimeoutSec  int `yaml:"generate_idle_timeout_sec,omitempty"`
}

type provider.RuntimeConfig struct {
    ...
    GenerateMaxRetries   int
    GenerateStartTimeout time.Duration
    GenerateIdleTimeout  time.Duration
}

默认值与归一化规则直接定死:

  • generate_max_retries <= 0 回填为 5
  • generate_start_timeout_sec <= 0 回填为 60
  • generate_idle_timeout_sec <= 0 回填为 300

本期不支持通过 0 显式关闭重试或 timeout;0 与未配置都表示“使用默认值”。

3. provider 层统一 attempt runner

三家 provider 不允许各自实现一套 retry / watchdog 主流程;必须共用一套 provider-level attempt runner。

统一职责边界:

provider common
  -> attempt lifecycle
  -> start watchdog
  -> idle watchdog
  -> retry loop
  -> retryable decision

provider specific
  -> 构造请求
  -> 消费流
  -> 映射错误

统一执行流程:

  1. 创建一次生成尝试
  2. 启动 start watchdog
  3. 若在 GenerateStartTimeout 内未收到首个有效 payload,则取消本次尝试并进入 retry
  4. 一旦收到首个有效 payload,关闭 start watchdog
  5. 启动 idle watchdog
  6. 每收到一个新的有效 payload,就刷新 idle watchdog
  7. idle watchdog 超时,则终止本次尝试
  8. 仅当“未收到首包 + 错误可重试”时进入下一次 retry

“流已开始”的判定必须统一收敛在 NeoCode 自己的有效事件发射层,而不是直接基于 SDK 原始事件判定。

4. 三家 provider 的统一策略

OpenAI-compatible

  • 显式关闭 SDK 内建 retry,避免双层重试。
  • 生成链路由 NeoCode provider 外层统一管理 retry。
  • 不再依赖 SDK request timeout 作为生成主控制器。
  • http.Client.Timeout 如保留,只能作为宽松保底值,不得抢占主控制权。

Anthropic

  • 与 OpenAI-compatible 一致,显式关闭 SDK 内建 retry。
  • 生成链路由 NeoCode provider 外层统一管理 retry。
  • 不再依赖 SDK request timeout 作为生成主控制器。

Gemini

  • 保留当前 provider 内部流消费与错误映射结构,但接入统一 attempt runner。
  • 删除“固定 2 次重试”的本地语义。
  • retry 次数、首包窗口、空闲窗口全部读取统一运行时配置。

5. 第三方 chat 端口适配边界

本次不修改 OpenAI-compatible 现有第三方 chat 端口适配语义。

保持不变的内容:

  • chat_api_mode
  • chat_endpoint_path
  • /chat/completions/responses 路径推断
  • weak SSE / compatible stream parser
  • typed stream 失败后的 compatible fallback 语义

本次只允许在“完整的一次生成尝试”外层包统一的 start/idle/retry 控制,不允许重构或改写现有端口适配层。

风险与约束

本次最关键的风险点与约束如下:

  • 不允许 SDK retry 与 NeoCode retry 双层叠加。
  • 不允许三家 provider 各自实现一套 retry/watchdog 主流程。
  • 不允许将 SDK 元事件、空 chunk、keepalive 误判为“流已开始”。
  • 不允许 fallback 链路被重复包裹统一状态机。
  • 不允许把 migration 当作旧配置可启动的唯一条件。
  • 不允许让 builtin provider、custom provider、测试默认值出现默认值分叉。
  • 首包后失败不 retry 是有意取舍,用于避免重复文本输出、重复 tool call 和重复副作用。
  • 第三方 chat 端口适配层保持现有行为,不在本 issue 内重构。

任务清单

  • internal/config/provider.go 增加 generate_max_retriesgenerate_start_timeout_secgenerate_idle_timeout_sec 三个字段。
  • 在 provider loader / validate / resolve 链路中接入上述字段。
  • provider.RuntimeConfig 中增加对应运行时字段。
  • 收敛一套统一的 provider-level attempt runner。
  • 固定“首个有效流 payload”的统一定义。
  • 固定“仅首包前且错误可重试时才允许 retry”的统一语义。
  • 固定“首包后仅受 idle watchdog 约束”的统一语义。
  • 改造 OpenAI-compatible 生成链路,显式关闭 SDK 内建 retry。
  • 改造 Anthropic 生成链路,显式关闭 SDK 内建 retry。
  • 改造 Gemini 生成链路,删除固定 2 次重试语义并接入统一 runner。
  • 保持 chat_api_modechat_endpoint_path、weak SSE fallback、compatible parser 现有行为不变。
  • 确保旧配置缺少新字段时仍能直接启动。
  • 视情况补充 provider config migration,但仅用于写回默认值,不作为启动前置条件。
  • 更新文档,明确三项 provider 配置字段的语义、默认值和行为边界。

测试验证

配置测试

  • 未配置三个字段时,默认回填为 5 / 60 / 300
  • 显式配置时,能正确透传到 provider.RuntimeConfig
  • <=0 输入会被归一化到默认值。
  • 旧版 config.yaml 缺少新字段时可直接启动。
  • 旧版 providers/*/provider.yaml 缺少新字段时可直接启动。

Provider 行为测试

  • 首包前连续失败 5 次、第 6 次成功时,总尝试次数为 6
  • 首包后持续稳定输出超过 10 分钟时,不会因总时长被误杀。
  • 首包后若连续 generate_idle_timeout_sec 没有任何新 payload,则会被终止。
  • OpenAI-compatible / Anthropic 的 SDK 内建 retry 已关闭,不会出现双层 retry。
  • Gemini 不再保留固定 2 次重试行为。
  • retry 场景下 usage / token 统计不会重复记账。
  • cancel 与 watchdog 同时触发时,错误归因稳定。
  • start / idle watchdog 不会泄漏 goroutine 或 timer。

第三方 chat 端口适配回归

  • 自定义 chat_endpoint_path 指向非标准 /gateway/chat/completions 时行为不退化。
  • chat_api_mode=responses + 自定义路径时行为不退化。
  • weak SSE 网关继续能通过兼容 parser 正常消费。
  • keepalive / 空行 / 注释行不会误判为首个有效 payload。
  • fallback 分支不会重复触发统一状态机。
  • 慢首包但后续正常的第三方网关可通过调大 generate_start_timeout_sec 解决。

总体验证

  • go test ./internal/provider/... ./internal/config/... 通过。
  • go test ./... 通过。

验收标准

  • 三家 provider 的生成链路统一为“最多 5 次重试 + 60 秒首包窗口 + 300 秒流空闲超时”的默认行为。
  • OpenAI-compatible / Anthropic 不再依赖 SDK 内建 retry 作为主生成策略。
  • Gemini 不再保留“固定 2 次重试”的特殊行为。
  • 首包未到时,不会长时间傻等才失败。
  • 首包已到后,长流不会因为总时长过长被误杀。
  • 流卡死时,系统能在空闲超时后稳定终止。
  • timeout / retry 策略可通过 provider YAML 配置统一调整。
  • 旧配置在未迁移情况下也能直接启动。
  • 现有第三方 chat 端口适配行为不退化。
  • 不新增 runtime/gateway/tui 协议,不恢复 provider retry 事件。

风险与回滚

  • 若统一 attempt runner 没有形成清晰真相来源,整体回滚本次 provider 重试统一实现。
  • 不保留“OpenAI-compatible / Anthropic 继续主要靠 SDK retry、Gemini 主要靠本地 retry”的长期双轨。
  • 若第三方 chat 端口适配回归不稳,优先回滚 OpenAI-compatible 外层统一控制改动,保留现有端口适配语义。
  • 若 migration 首版实现不稳,可整体回滚 migration 子集,但不影响“旧配置可直接启动”这一主目标。

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