Cursor/Codex 长对话卡死在 Planning,根因是运营商对国际出口持续大流量上行做 QoS。记录从 Hysteria2 到套 Cloudflare 反向代理,最终把上行限速问题彻底解决的完整过程。
Cursor 新开一个对话总是好的,问几句、跑几个工具调用,一切正常。但只要这个对话进行得够久,几乎必然卡死在 Planning next moves,一动不动。换个新对话又好了。一开始我以为是模型的问题,后来才发现是代理的上行撑不住。
长对话必卡,新对话没事
Cursor 的 Agent 每次请求要把整段对话上下文 POST 到 agent.api5.cursor.sh。新对话上下文只有几 KB,卡不出问题;对话进行到几轮、带上文件内容之后,上下文涨到几百 KB 到几 MB,请求就经常在发送阶段直接卡死或者报连接断开。
日志里翻不出业务错误,只有传输层的异常:Client network socket disconnected before secure TLS connection was established,或者干脆超时。这类现象最容易被误判成"模型不稳定"或者"Cursor 服务又抽风了",但换个新对话立刻恢复正常这个细节其实已经在暗示:跟对话长度、也就是请求体大小有关,不是模型的锅。
直连能治好,但会砍掉一半模型
最直接的排查方式是对照测试:同样大小的请求体,走代理和不走代理(curl --noproxy '*')分别测一次上传耗时。
结果很干净:
| 请求体大小 | 走代理(VLESS) | 直连 |
|---|---|---|
| 128 KB | 6.5 KB/s,20 秒超时失败 | 1.9 秒完成 |
| 512 KB | 26 KB/s,超时失败 | 1.9 秒完成 |
| 1 MB | 29 KB/s,超时失败 | 2.6 秒完成 |
直连稳定在 2 秒内完成,走代理的三组全部超时。把 Clash 里 Cursor 相关域名的规则从 PROXY 改成 DIRECT,长对话立刻不卡了。
但直连引出一个新问题:Cursor 会按客户端出口 IP 限制模型列表,国内 IP 直连会砍掉 Claude 等一批模型。换句话说,"不走代理"不是一个能用的方案——问题从"怎么解决卡顿"变成了"怎么让代理本身扛得住"。
上行和下行完全是两个世界
在换协议之前,先把现象钉死,不然容易在错误的方向上瞎折腾。分别测代理链路的下载和持续上传:
- ping 到 VPS:约 135ms,大包零丢包
- 走代理下载 20MB:约 8.7 MB/s,正常
- 走代理持续上传:约 6~110 KB/s,随时段波动,晚高峰更差
延迟正常、下载正常,只有持续大流量上传慢得离谱。这是运营商对国际出口做 QoS 的一个典型指纹:小包、短请求、下载方向都放行,专门卡的是"持续时间长 + 体积大 + 出境方向"的流量——而 Cursor 长对话每次要上传几百 KB 到几 MB 上下文,四个特征全占。
顺手排除了协议本身的问题:同一条隧道下载 8.7MB/s 说明链路是通的,不是 VPS 或证书配置的问题,问题精确定位在"上行"这一个方向上。
先加一条 Hysteria2,把 Cursor 单独摘出来
原来的隧道是 VLESS + Reality(TCP,伪装成访问 www.cloudflare.com 的正常 TLS 流量)。TCP 在这种"看起来通、大流量被限速"的场景里天然吃亏:一旦丢包,拥塞窗口需要慢慢爬升,RTT 越大恢复越慢,在中美 200ms+ 的往返延迟下这个恢复过程会被无限拉长。
Hysteria2 走 UDP + QUIC,拥塞控制策略更激进,对这类 QoS 限速的抵抗力明显更强。选它而不是升级 VPS 线路或者加一台国内中转机,主要是成本考虑:升级线路要花钱且不一定能测出效果,加中转机要多一台机器、架构复杂;装 Hysteria2 只是在同一台 VPS 上多开一个端口,免费,几分钟能验证有没有效果。
具体做法是在服务器上加装 Hysteria2(UDP 443),Clash 配置里新增一个节点,只把 Cursor / OpenAI 相关域名指过去,其余流量继续走原来的 VLESS:
proxies:
- name: "US1-HY2"
type: hysteria2
server: "<VPS_IP>"
port: 443
password: "<PASSWORD>"
sni: www.cloudflare.com
skip-cert-verify: true
udp: true
rules:
- DOMAIN-SUFFIX,cursor.sh,US1-HY2
- DOMAIN-SUFFIX,cursor.com,US1-HY2
- DOMAIN-SUFFIX,cursorapi.com,US1-HY2
- DOMAIN-SUFFIX,cursor-cdn.com,US1-HY2效果立竿见影:
| 请求体大小 | 改前(VLESS) | 改后(HY2) |
|---|---|---|
| 128 KB | 6.5 KB/s,超时 | 237 KB/s,0.55s |
| 512 KB | 26 KB/s,超时 | 618 KB/s,0.85s |
| 1 MB | 29 KB/s,超时 | 1.1 MB/s,0.95s |
| 4 MB | 超时 | 1.65 MB/s,2.5s |
原来的 VLESS 节点没动,日常浏览、其他网站流量完全无感;只有 Cursor/OpenAI 这几个域名切到了新隧道。这个方案的好处是可回退——真出问题,把规则改回 PROXY 或者干脆删掉 HY2 节点,一分钟内恢复原状。
没几天,HY2 也顶不住了
修好之后正常用了一周,某天上行又开始变差,从 1.6MB/s 一路掉到 20~37KB/s,长对话又开始卡。
第一反应是协议参数没调好,换了端口、开了 Hysteria2 的 Brutal 抗丢包模式(固定码率、无视丢包硬发)。结果是帮倒忙——在一条本来就高丢包的链路上用固定 30Mbps 硬发,产生了海量无效重传,直接把家里的上行带宽堵死,连原来能用的 VLESS 都开始超时。回滚 Brutal、服务端改回 BBR 之后才恢复到"能用但不快"的状态。
这时候意识到光测上层协议的吞吐已经不够了,得绕开协议本身,直接测物理链路。用裸 TCP(不走任何代理协议)测本机到 VPS 的上下行:
| 方向 | 结果 |
|---|---|
| 裸 TCP 上行(IPv4) | 129 Kbit/s,8 秒内 110 次重传 |
| 裸 TCP 上行(IPv6) | 0.9 Mbit/s,703 次重传 |
| 裸 TCP 下行 | 211 Mbit/s,零重传 |
| 直连上传到 Cloudflare 测速点 | 6.5 Mbit/s,正常 |
这组数据把可能性排除干净了:家里宽带正常(上传 CF 测速点没问题),VPS 正常(下行 211Mbit/s),IPv6 也没能绕开限速。唯独"本机到这台 VPS"的上行方向,无论 TCP 还是 UDP,都在被限速和大量重传。问题不在协议、不在服务器,在"移动出境到这台 VPS"这条物理路径本身——运营商的路由拥塞或者 QoS 策略本身在这几天恶化了,换协议只是在同一条烂路上换车,路彻底堵死后照样没用。
顺手把这组 iperf 数据整理成工单发给了 VPS 服务商,附上"上行 129Kbps、110 次重传,下行 211Mbps"的对比,说明这是路由层面的问题,不是配置问题。这条路留着当第二手准备,但没指望它能马上解决。
把长途连接拆成两段
真正的解法是不再让"中国到这台 VPS"成为一条完整的长途连接,而是把它拆成两段:中国到 Cloudflare 边缘,和 Cloudflare 到 VPS。
你的电脑 ──① TLS,443──> Cloudflare 边缘 ──② 境外内部网络──> VPS ──③──> 真实目标(cursor.sh 等)
第①段:连的是 Cloudflare 的 Anycast IP,到 CF 的上行实测一直正常(几百 KB/s 到几 MB/s,不受那条"到 VPS 直连"路径的限速影响)。第②段:CF 边缘到 VPS 走的是境外网络内部转发,不经过中国出口。相当于把最难走的那一段外包给了 CF。
需要一个托管在 Cloudflare 的域名(我用的是自己已有的 niuzj.org),在 DNS 里加一条 A 记录,指向 VPS 真实 IP,代理状态打开(橙色云):
| Type | Name | Content | Proxy status |
|---|---|---|---|
| A | us1 | <VPS_IP> | Proxied(橙云) |
这一步之后,全世界查询 us1.niuzj.org 得到的都是 Cloudflare 的 IP,不是 VPS 真实 IP。
关键的坑在协议选择上:不能直接把 Reality 隧道套在这条 CF 记录上。Cloudflare 的橙云代理是七层 HTTP 反向代理,会解开 TLS、读取 HTTP 语义再重新建连;Reality 伪装成 TLS 握手但本质是 TCP 层协议,一旦被 CF 解开就不再是合法的 HTTP 流量,CF 直接报错(我们最初测的时候拿到的就是 521)。要走 CF,必须换成 WebSocket,因为 WebSocket 是标准的 HTTP Upgrade,CF 能正常处理:
proxies:
- name: "US1-CF"
type: vless
server: us1.niuzj.org
port: 443
uuid: "<UUID>"
network: ws
tls: true
udp: true
servername: us1.niuzj.org
client-fingerprint: chrome
ws-opts:
path: /vless-ws
headers:
Host: us1.niuzj.org服务器这边在 Xray 里新增一个 VLESS + WebSocket + TLS 的入站,监听 443,原有的 Reality(8443)和 Hysteria2 都不动,三套协议并存。Cloudflare 的 SSL/TLS 模式要设成 Full(不能用 Flexible,否则源站证书校验会有问题)。
顺带一提,Hysteria2 这条路走不通——它是 UDP,Cloudflare 免费版的橙云只代理特定端口上的 TCP/HTTP(S) 流量,不转发任意 UDP,所以 HY2 只能继续直连,享受不到这层中转。
配好之后只把 AI 相关域名切到这条新隧道,其余流量还走原来的直连(省一跳延迟):
rules:
- DOMAIN-SUFFIX,cursor.sh,US1-CF
- DOMAIN-SUFFIX,cursor.com,US1-CF
- DOMAIN-SUFFIX,cursorapi.com,US1-CF
- DOMAIN-SUFFIX,cursor-cdn.com,US1-CF
- DOMAIN-SUFFIX,openai.com,US1-CF
- DOMAIN-SUFFIX,chatgpt.com,US1-CF实测上行:
| 请求体大小 | 速度 | 耗时 |
|---|---|---|
| 128 KB | ~53 KB/s | 2.5s |
| 1 MB | ~287 KB/s | 3.7s |
| 4 MB | ~925 KB/s | 4.5s |
比起前几天 HY2 掉到 20~37KB/s、大文件必超时的状态,这个结果够长对话用了。
我一开始的解释是错的
方案跑起来之后,很自然地想解释一下"为什么套 CF 会快"。我最初给出的版本是:Cloudflare Anycast 会把你路由到最近的边缘节点(国内用户通常落在香港、东京这类亚洲机房),中国到亚洲只有 30-60ms、丢包低,这一段短距离连接撑起了整体速度。
这个解释是错的,而且错得挺离谱。真去测的时候发现,us1.niuzj.org 实际落地的 CF 机房是圣何塞(美国西海岸),不是亚洲——本机到这个边缘节点的 RTT 反而有 200ms+,用 ICMP 测出来丢包率 15%,比直连 VPS 的 133ms/零丢包还要差。用"就近接入、短距离低丢包"来解释一个跨太平洋、延迟更高的路径,这套说辞根本立不住。
真正的原因要靠一次严格对照实验才验证出来:把 Clash 的节点选择在 US1-VPS(直连)和 US1-CF(套 CF)之间切换,用同一个测速点、同样 20MB 的文件、同一个时刻分别测上传,两条路径的最终出口 IP 都是同一台 VPS:
| 线路 | 路径 | 上行速度 | 20MB 耗时 |
|---|---|---|---|
| 直连 VPS | 中国 → VPS | ≈ 53 KB/s | 120 秒未传完,超时 |
| 走 CF | 中国 → CF → VPS | ≈ 3.2~4.1 MB/s | 5~6 秒 |
两条路径终点相同、时间相同,唯一的变量是"从中国到这台 VPS 这一段怎么走"。差距 66 倍,而且这次测试是在非高峰的上午做的,排除了"只是赶上晚高峰"这个干扰因素。
结论只能是:限速不是针对"某个协议"或者"某种流量特征",而是精确打在"中国到这一个具体的、孤零零的小众 VPS 的持续大上传"这条流量上。Cloudflare 那个巨大的 IP 段承载着海量正常网站流量,和国内运营商之间有大容量的对等互连,这一条通路本身就更宽、更不容易被单独限速——你的流量混在千万个访问 Cloudflare 的普通 HTTPS 连接里,运营商的 QoS 策略很难把你单独拎出来。这跟距离、延迟、地理位置几乎没关系,跟"你连的是谁的 IP 段"关系更大。
这段纠错过程比方案本身更值得记下来:第一直觉的解释往往是最顺耳、最像教科书的那个版本,但只有拿真实数据做受控对照,才知道它到底站不站得住。
现在留了几层线,也留了几个没解的坑
最终的分流规则收敛成这样:Cursor、OpenAI、Anthropic、GitHub、npm、PyPI 这类"经常有大体积上传"的服务统一走 CF 中转;普通境外浏览、下载还走原来的 VLESS 直连,因为这部分流量以下行为主,直连反而少一跳延迟。Hysteria2 没有删,作为直连之外的另一条备用道留着。
这套方案没有免费午餐,几个代价是明确的:
- 这几个域名的下载也被迫绕了一道 CF(Clash 按域名匹配,没法只对上传方向生效)。走 CF 下载也有 3-4MB/s,影响不大,但确实多了一跳。
- GitHub 如果用 SSH 推代码,走的是
ssh.github.com:443,这条流量默认不经过 Clash 代理,所以这条分流规则对git push不生效,要单独配 SSH 的ProxyCommand才能覆盖。 - 源站的 443(CF 入口)和 8443(Reality 直连)两个端口都还暴露在公网,Cloudflare"隐藏源站真实 IP"这个卖点在这套方案里基本没用上——真想要这一层保护,得配防火墙只放行 CF 的 IP 段,或者换成 Cloudflare Tunnel 让源站完全不开公网端口。这个我目前还没做。
这条排查线最后落地的认知是:换协议能解决"这个协议在这条路上不行"的问题,但解决不了"这条路本身被针对性限速"的问题。真正的解法从来不是找一个更强的协议去硬扛限速,而是换一条不会被限速的路——哪怕这条路要绕一圈。
按上面这套方案改完,Cursor 和 Codex 长对话卡顿这块算是彻底解决了,用到现在没再复发。我这台是 DMIT 的美国 VPS,本身线路不差,问题都出在国际出口这一段。如果你现在用的机场本来线路质量就一般,换协议大概率也救不回来——不如直接自己买一台 VPS,照这套思路搭一遍,至少限速这块能自己说得上话。