应用代理

AI 编程工具长任务跑到一半就断:流式长连接被代理掐断怎么修

长任务断在一半、短任务却完好,说明被掐断的不是路由,是那条一直不关闭的流。AI 编程工具把模型输出以流式方式(SSE 或 chunked)边生成边推给你,这条 HTTP 连接可能开着 3 分钟、也可能 20 分钟。链路上任何一环只要认为它「闲太久了」就会关掉它,客户端看到的就是输出卡住、connection reset、或者 Codex 那句 stream disconnected before completion。

这和 MCP Server 连不上 是两件事:那篇讲的是握手阶段就连不上或拿不到工具列表,本文讲的是已经在正常输出、跑到中途才被切断。基础代理配置分别见 Claude Code 代理配置、Codex CLI 代理配置、Cursor 代理配置,本文不重复。

先确认症状是不是「长连接被切」

三个特征同时出现,基本可以定性,不用再怀疑代理没配好:

  • 短任务稳定成功。「解释这个文件」「改一个函数」这类几十秒内结束的请求从来不失败。
  • 失败点集中在中后段。它已经吐了几百行、正在跑第五个工具调用,然后停住不动或直接报错。
  • 断开时间有规律。记三次失败的时刻,从任务开始算起的秒数如果都在 60、120、300、600 附近打转,那就是某一层的定时器在起作用,不是随机丢包。

典型报错文案:Claude Code 侧多为 API Error: Connection error 或长时间无输出后中止;Codex CLI 侧是 stream disconnected before completion,后面常跟着请求的 URL;Cursor Agent 侧表现为对话框停在最后一条工具调用上不再更新。这些都只是「流提前结束了」的不同说法,不指向具体哪一层,需要往下逐环排。

一条流会被哪几层掐断

从你的键盘到模型服务器,中间每一环都有自己的超时。按从近到远排:

环节 会怎么掐 可查 / 可改
AI 工具自身 请求超时、流空闲超时到点主动放弃 Claude Code 的 API_TIMEOUT_MS;Codex 的 stream_idle_timeout_ms
本机网络栈 Wi-Fi 切换、休眠唤醒、VPN 抢路由后连接失效 看断开时刻是否与切网重合,见休眠唤醒断网
Clash Verge 内核 节点自动切换、订阅重载、配置重启会杀掉所有在跑的连接 连接页存活时长 + 日志里的切换记录
代理协议层 多路复用会话被回收、QUIC 连接迁移失败 节点配置里的 smux / 传输层设置
出口节点服务端 服务端侧空闲超时、连接数上限触发回收 换同机房另一节点对比;换机场对比
机场落地 / 中转 会话表老化、按连接计费的中转主动回收长连接 只能靠横向对比不同线路
模型服务端 模型长时间不产 token 触发上游 HTTP 超时 换网络仍复现,则不在你这边

注意最后一行。OpenAI 的维护者在 Codex 的 issue 里说明过一种情况:较新的模型有时在两批 token 之间停顿很久,这个停顿本身就可能超过某个 HTTP 超时值,从而触发流断开。也就是说,即使你的代理完全干净,长任务也有一定概率断在上游——这类只能靠重试和拆小任务缓解。

用连接页和一条命令分清断点在哪

Clash Verge 的连接页是这里唯一能直接看到证据的地方。跑长任务时把它开着,按域名过滤(anthropic.com、chatgpt.com 或你的自建网关域名),盯这几列:

  1. 连接存活时长。失败瞬间记下这个数。多次失败都停在同一个整数附近 = 定时器;每次都不一样 = 更像链路质量或上游。
  2. 下载字节是否还在涨。字节停涨但连接还挂着,说明数据不再来了,代理没杀它;连接直接从列表消失,是这一层或更近的一层断的。
  3. 是否出现新连接。同一域名突然多出一条新连接、旧的消失,通常是策略组换了节点,这条流已经废了。
  4. 走的是哪个节点。如果三次失败分别走了三个不同节点,先去关自动切换,再谈别的。

要在不占用 AI 额度的前提下反复复现,用一个能持续慢速吐字节的端点做对照测试。这条命令通过本机混合端口出站,每秒吐 1 字节、总共 600 秒:

curl -N -v --max-time 700 \
  -x http://127.0.0.1:7897 \
  "https://httpbin.org/drip?duration=600&numbytes=600&delay=0" \
  -o /dev/null

这条命令是 Unix 写法,Windows 的 cmd 没有行末续行符、PowerShell 里 \ 也不是续行符,先把几行合并成一行再跑,判读方式不变。

判读标准很简单:它能撑满 600 秒就说明这条链路能保住 10 分钟的流;在第 60 秒或第 300 秒稳定断掉,你就拿到了那个超时值,而且和 AI 工具无关了。把 -x 去掉再跑一次直连(如果直连能到),或者换一个节点再跑,对照结果就能定位到具体是哪一层。端口号以设置页里的实际混合端口为准,见混合端口。公共测试端点会限速限流,只用来看「能活多久」,不要用来测吞吐。

关掉自动切换:它是长任务的头号杀手

策略组用 url-test 时,内核会按 interval 定期测速并可能改选节点。切换发生的那一刻,正在跑的连接被直接断开——你的长任务不是「网络不好」,是被自己的配置杀掉的。负载均衡(load-balance)同理,它会把同一域名的新连接分散到不同出口,重连之后上下文对不上。

给 AI 工具的域名单独走一个固定出口,是收益最大的一步改动:

proxy-groups:
  - name: AI-Stable
    type: select          # 手动选,不自动测速切换
    proxies:
      - HK-01
      - JP-01

  - name: Auto
    type: url-test
    interval: 600         # 其它流量再自动,间隔也别太短
    tolerance: 100
    lazy: true
    proxies:
      - HK-01
      - JP-01

rules:
  - DOMAIN-SUFFIX,anthropic.com,AI-Stable
  - DOMAIN-SUFFIX,claude.ai,AI-Stable
  - DOMAIN-SUFFIX,chatgpt.com,AI-Stable
  - DOMAIN-SUFFIX,openai.com,AI-Stable
  - DOMAIN-SUFFIX,cursor.sh,AI-Stable

改完还有两件事要一起做:跑长任务期间不要点订阅更新,也不要在界面里改配置。订阅重载会重建整个连接表,效果和拔网线一样。托盘里的「更新订阅」和长任务是天然冲突的,养成先跑完再更新的习惯。

桌面端还可以把 TCP keepalive 调密一点,让中间设备不把连接当成空闲:

# mihomo 内核通用配置,写在配置文件顶层
keep-alive-idle: 15       # 空闲多久开始发探测包(秒)
keep-alive-interval: 15   # 探测包间隔(秒)
disable-keep-alive: false

这组值对付「会话表老化」类回收有效,对「服务端硬性 60 秒读超时」无效——探测包不算业务数据,服务端该关还是关。移动端注意:Android 上内核会强制禁用 TCP keepalive 省电,这条改不动。

节点与协议:哪些选择对长连接有害

同一个机场里,不同协议扛长连接的能力差别很大,和它们的测速结果基本无关。

  • 多路复用(smux / mux)要慎用。它把多条业务流塞进一条底层连接省握手开销,代价是这条底层连接一旦被回收,上面所有流一起死。日常网页用感知不到,长任务会被连坐。给 AI 工具选的节点上,优先关掉 mux 或把它的 keepalive 调密。
  • QUIC / UDP 类传输在切网时更脆。它们本身有连接迁移能力,但要中间网络配合;家用 NAT 和公司网络对 UDP 会话的老化时间通常比 TCP 短得多,笔记本从 Wi-Fi 换到热点时断得更彻底。跑长任务时,稳定的 TCP 传输往往比 UDP 更省心。
  • 中转和多跳链路多一层回收方。每加一跳就多一个可能设了空闲超时的设备。同机房的直连落地节点在长任务上通常比「优化中转」表现更好,哪怕后者延迟数字更漂亮。

Tun 模式和系统代理的差别在这里也要说清:Tun 在网络层接管,不依赖进程读环境变量,所以少一类「新开终端没带变量」的假故障,对 Cursor 这种 GUI 进程尤其省事,见给 AI 工具开全局代理。但 Tun 本身不延长任何超时。切换到 Tun 之后长任务依旧断在同一秒数上,说明超时在更远的一层。

工具侧能调的参数,以及怎么把任务拆小

把链路修干净之后,再动工具侧的超时,顺序不要反。

Claude Code 用环境变量或 settings.json 的 env 块。API_TIMEOUT_MS 默认 600000 毫秒(10 分钟),官方文档明确说明「经过代理或网络较慢导致请求超时时提高它」,上限是 2147483647,超过会让底层计时器溢出、请求立刻失败:

// ~/.claude/settings.json
{
  "env": {
    "API_TIMEOUT_MS": "1200000",
    "BASH_DEFAULT_TIMEOUT_MS": "300000"
  }
}

Codex CLI 的三个参数写在 ~/.codex/config.toml 对应的 provider 块里,只在 provider 块内有效,写到顶层会被忽略:

# ~/.codex/config.toml
[model_providers.openai]
name = "OpenAI"
stream_idle_timeout_ms = 600000   # 默认 300000(5 分钟)
stream_max_retries = 10           # 默认 5,上限 100
request_max_retries = 4           # 默认 4,上限 100

把 stream_max_retries 拉高确实能让它自己重连接着跑,社区里有人靠调到 10 才把之前必失败的任务跑完。但代价是每次断开都要试 7、8 次,长任务整体耗时会明显变长。这是止损手段,不是修复手段。Codex 较新版本还带 codex doctor,会检查版本、配置、代理环境、网络可达性和 provider 端点,只读不改,值得在动手前跑一次。

工程侧的做法比调参数更有效:把一个「重构整个模块」拆成五个各自可验收的小任务,每个几分钟内能收尾。长任务被断的概率随时长上升,而且断了之后已经改了一半的文件比失败本身更麻烦。Codex 长会话还有一个额外风险点——上下文自动压缩发生在会话已经很大的时候,这一步失败会让会话卡住,只能重开。把任务切小同时也在控制这个风险。

这些做法基本没用

  • 反复重试同一个长任务。如果断点稳定在同一秒数,重试只是让你再等一遍相同的失败。先测出那个秒数。
  • 换延迟更低的节点。延迟是一次短握手的往返时间,和「这条连接能不能活 15 分钟」没有因果关系。测速榜第一的节点完全可能是空闲连接回收最狠的那个。延迟测试只用来筛掉不可用节点,别拿它判断长连接稳定性。
  • 反复重置配置、重装客户端。短任务能跑就说明配置是对的。这类操作解决的是「完全连不上」,不是「跑到一半断」。
  • 把所有超时都拉到极大。超时拉长只能容忍慢,不能对抗别人主动关连接;而且真出问题时你会卡住二十分钟才知道失败。
  • 切成全局模式碰运气。模式切换本身会重置连接。真要对比,用上面的 curl 命令跑对照,不要拿正在跑的长任务当试验田。

速度慢和连接断是两个问题,不要混在一起治,慢的那类见网速优化;代理开着但请求压根没出去的情况见代理开了却打不开。客户端从下载中心获取。

长任务要稳,出站链路先要能扛住长连接

流式响应是一条几分钟到几十分钟不关闭的 HTTP 连接。到下载中心装 Clash Verge Rev,按本文关掉自动切换、选对协议,再回来跑长任务。