最近帮同事排查了一个问题,搞得我一度怀疑是 Codex 本身抽风:对话框里一直显示“正在思考”,转圈转个没完,可等上十几分钟也不见一个 token 出来。翻日志发现请求全部卡在/responses这个端点上,再往下追,问题出在 WebSocket 长连接一直握手失败。最后把传输方式从 WebSocket 回退到 HTTPS 短连接,前后不过几分钟,一切恢复正常。
这篇文章把这次完整的排障过程记录了下来。包括哪些日志值得看、为什么“正在思考”会骗人、怎么用手边的命令确认 WebSocket 被网络链路“暗杀”,以及如何配置 HTTPS 回退。如果你也在用 Codex 做 AI 编程,碰到过类似的无响应、卡顿、转圈不结束的问题,这篇文章估计能帮你省下好几个小时。
1. 先还原现场:Codex“正在思考”卡住时我看到了什么
1.1 现象:不是崩溃,而是无限等待
那天的现象很典型。我在某个项目的自动化代码生成流程里用 Codex,大约上午十点开始,开发机上所有对话都不正常:输入问题后,界面上的状态变成了“正在思考”,然后就没有然后了。
最折磨人的是它不报错。不是那种红色错误提示,也不是超时弹窗,就是安静地转圈。你点取消倒是能取消,但重新发送一遍,还是一样卡住。我试过把问题从一句话扩写成一段详细需求,没用;换成简单到不能再简单的“1+1等于几”,也没用。看起来跟输入内容无关,是通信链路的问题。
1.2 第一手资料:日志里藏着真正原因
一开始我按老套路来,先重启 Codex。这里要多说一句:重启确实能解决一些临时性的进程问题,但如果是网络链路的问题,重启一百遍也没用。
我翻到了 Codex 的本地日志目录,在~/.codex/logs/下,按日期滚动。用的命令很简单:
tail -f ~/.codex/logs/codex.log | grep -iE "websocket|wss|upgrade|responses"日志里很快就刷出了关键信息:
[ERROR] websocket dial: wss://api.example.com/v1/responses: connection reset by peer [WARN ] transport fallback skipped: timeout waiting for 101 Switching Protocols [WARN ] retry after 5s, attempt 2/5 ...看到这里,我基本锁定方向:WebSocket 握手失败了。第二行的101 Switching Protocols是 WebSocket 握手成功的标志,没有 101,就意味着连接没有升级成功。
1.3 影响范围:不是所有请求都失败
为了确认到底是 Codex 的问题还是后端模型服务的问题,我用普通 HTTPS 请求打了一下同一个服务的普通接口:
curl -sS --max-time 15 https://api.example.com/v1/models | head -20结果返回正常,HTTP 200,数据完整。
这时候我得出一个初步结论:后端服务本身没有问题,出问题的是流式传输通道。于是我把现象做成了一张简单的对照表:
| 检查项 | 结果 |
|---|---|
| 普通 HTTPS 请求 | 正常返回,无超时 |
| WebSocket 握手请求 | 连接被重置,无 101 响应 |
| Codex 本地转发进程健康检查 | 进程存活,但转发失败 |
| 模型服务状态 | 日志显示服务端已收到请求,但无法建立流式通道 |
这张表说明:链路的前半段通了,后半段断了。接下来要重点排查的就是 WebSocket 这条长连接链路。
2. 排障思路:从“卡住”到锁定 WebSocket 的每一步
2.1 为什么 Codex 偏爱 WebSocket?
先解释一个基础问题:为什么 Codex 不老老实实用普通 HTTPS,非要优先尝试 WebSocket?
因为 AI 编程助手的回复是流式的。你在界面里看到的一句句代码、一段段解释,其实是一个个 token 随着生成过程陆续推送到客户端的。WebSocket 是全双工长连接,服务端生成一个 token 就能推一个,客户端立刻展示,体验类似“打字机”效果。如果用普通 HTTPS 请求,客户端就得等服务端把整段内容生成完,一次性拿到完整结果,体验会差很多。
一句话概括:WebSocket 像打电话,两边随时可以说话;HTTPS 请求-响应更像寄快递,你发一个单子,对方把东西打包好再寄回来。
但 WebSocket 对网络链路的要求也更高。中间任何一个网络设备不支持协议升级,或者对长连接做了强制超时,就会出现“看着正常、实际不通”的情况。
2.2 本地代理在这条链路里的角色
Codex 在本地通常会启动一个转发进程,也就是日志里经常提到的 local proxy。这里先澄清一下,别把它理解成上网代理,它是 Codex 内部做请求转发的小进程:Codex 界面先把请求发到本机的某个端口,再由这个端口转发到模型服务。
完整链路是这样的:
Codex 界面 -> 本地转发进程 -> 公司出口网关 -> 模型服务
日志里出现这样一句话时,基本可以确定本地转发进程在处理/responses这个端点时出了问题:
ERROR switch local proxy failed while handling codex endpoint /responses. provider: ...意思是:客户端已经生成了发送请求的任务,但本地转发进程切换目标时失败了。因为/responses是流式响应的核心端点,一旦这里卡住,界面上就会永远停在“正在思考”。
检查本地转发进程是否存活,我用的命令:
# 找到 Codex 相关进程监听的端口 lsof -iTCP -sTCP:LISTEN -P | grep -i codex # 对本机端口做健康检查,端口号以上一步输出为准 curl -s --max-time 5 http://127.0.0.1:17365/health如果本地进程已经死了,或者端口被别的程序占用,转发肯定失败。但这次发现进程还活着,健康检查也返回了 OK,说明问题不在本地。
2.3 排查命令清单:一步步缩小范围
我把整个排查过程整理成了一个清单,方便照着做:
- 查日志:
grep -iE "websocket|wss|upgrade" codex.log,确认有没有握手失败记录。 - 模拟握手:用 curl 发送 WebSocket Upgrade 请求,观察服务端是否返回 101。
- 查环境变量:
env | grep -iE "proxy|codex|transport",确认没有配置导致流量绕路的错误参数。 - 查本地转发进程:用
lsof和curl确认端口存活。 - 换传输方式:在配置里强制关闭 WebSocket,改用 HTTPS。
这五步做完,基本能把问题缩小到“WebSocket 链路被阻断”这个结论上。
3. 深入根因:为什么握手会失败,回退机制为什么没生效
3.1 WebSocket 握手的原理和脆弱点
WebSocket 的握手实际上是基于一次普通 HTTP 请求完成的。客户端发送一个带有特殊头部的 HTTP 请求,比如:
curl -i --max-time 10 \ -H "Connection: Upgrade" \ -H "Upgrade: websocket" \ -H "Sec-WebSocket-Key: c2VjcmV0LWtleQ==" \ -H "Sec-WebSocket-Version: 13" \ https://api.example.com/v1/responses如果服务端同意升级,会返回:
HTTP/1.1 101 Switching Protocols看到101,才算握手成功。如果中间某个节点没把这个 Upgrade 请求放行,它可能直接吞掉后续数据包、返回 502,或者干脆一直不响应。客户端这边没有收到明确的失败信息,就会一直等。
这次的情况属于典型的“静默丢弃”。请求到达了公司出口网关,但网关对Upgrade: websocket这条请求没有放行,导致后续所有数据都进不来。客户端看着连接还挂着,实际已经是一个“假连接”了。
3.2 “正在思考”的假象是怎么来的
很多人遇到“正在思考”会以为是模型推理慢,其实不一定。Codex 界面上只要请求没有被终止,就会一直显示“正在思考”。问题在于,TCP 连接被中间设备静默切断时,客户端进程并不一定会立即感知。
WebSocket 协议本身有心跳机制,也就是 ping/pong 帧,用来探测连接是否存活。但如果握手都还没完成,客户端压根还没进入正常的 ping/pong 循环,它只能一直等那个永远不会来的101 Switching Protocols。
我用一个比喻解释给同事听:就像你打电话,拨出去之后对方一直没接,但听筒里也没有“暂时无法接通”的提示,你就只能一直举着电话干等。Codex 的界面就是在“举着电话干等”的状态。
3.3 复现与确认:用客户端开关验证根因
判断问题到底是不是 WebSocket 造成的,最直接的办法是强制关掉 WebSocket,让它改走普通 HTTPS。
在 Codex 的配置文件里,一般会有一个传输方式的配置项。我这边使用的字段是transport,修改之前先备份:
cp ~/.codex/config.toml ~/.codex/config.toml.bak然后改成:
transport = "https"重启 Codex。重启后发同样的问题,如果“正在思考”立刻消失,内容正常输出,根因就确认了。
这里提醒一句:每次只改一个变量。不要同时改网络配置和传输配置,也不要在改完配置的同时重启了出口网关,否则你根本不知道是哪一步起的作用。
4. 回退 HTTPS 的落地配置与效果验证
4.1 快速回退操作
如果你现在也遇到了同样的问题,最快的方式是设置环境变量。不同版本的 Codex 支持的变量名可能不太一样,可以用帮助命令确认:
codex --help | grep -i transport codex config list | grep -iE "transport|websocket"我这边可用的环境变量是CODEX_TRANSPORT,设置方式:
export CODEX_TRANSPORT=https如果想更明确地禁用 WebSocket,也可以尝试:
export CODEX_DISABLE_WEBSOCKET=1但要注意,不是所有版本都支持这两个变量,不支持的版本会直接忽略,然后继续走 WebSocket。所以改完之后一定要看一眼日志,确认配置真的生效了。
配置文件的方式更持久。在~/.codex/config.toml里加上:
transport = "https"保存后重启 Codex。重启后日志里如果能看到using https transport或者类似的字样,说明回退成功。
4.2 验证连接恢复正常
配置生效后,我做了几组简单对比:
- 清空日志文件,方便观察最新输出:
> ~/.codex/logs/codex.log - 在 Codex 里发一个中等复杂度的编码任务,例如“写一个 Python 脚本,批量重命名某个目录下的所有文件”。
- 观察首 token 响应时间。
结果差异非常明显:
| 模式 | 首次 token 响应 | 日志表现 | 连续 20 次请求成功率 |
|---|---|---|---|
| WebSocket(默认) | 无限等待 | 大量websocket dial和101 timeout错误 | 约 15% |
| HTTPS(回退) | 约 3~5 秒 | 无 WebSocket 错误 | 100% |
我还顺手测了一个比较复杂的代码生成场景,HTTPS 模式下整体生成时间没有明显变长。原因很简单:模型生成 token 本身才是最耗时的部分,传输方式带来的额外握手开销基本可以忽略。
4.3 长期方案:让 WebSocket 也能稳定工作
回退 HTTPS 只是避开了问题,并没有消除问题。如果你希望以后还能用 WebSocket 获得更顺畅的流式体验,可以考虑下面几个方向:
- 调整网络出口设备,允许
443端口上的Upgrade: websocket请求通过。公司网络如果由统一网关管理,这一步需要网络管理员配合。 - 配置
NO_PROXY,让模型服务的请求绕过本地转发进程,直接走直连出口,减少一层转发干扰。 - 升级 Codex 到新版本,有些版本对“等待 101 超时后自动回退”的逻辑做得更好,不会傻等十分钟。
- 如果只是临时在办公网里用,保持 HTTPS 回退是最省心的方案。
注意:不要同时设置
transport、use_websocket和CODEX_TRANSPORT多个配置,如果它们互相冲突,客户端可能启动的时候直接报配置错误。
5. 避坑清单与常见问题速查
5.1 排障速查表
我把这次排查以及之前几次类似问题归纳成了一张速查表,遇到相同症状可以直接查:
| 症状 | 可能原因 | 快速排查方法 | 处理方式 |
|---|---|---|---|
| 一直“正在思考”,无错误提示 | WebSocket 握手失败,回退未触发 | 日志搜websocket、101 | 强制切换到 HTTPS 传输 |
日志出现local proxy failed | 本地转发进程端口冲突或状态异常 | lsof查端口,curl查健康接口 | 重启 Codex,检查端口占用 |
| 偶发卡顿,重启后恢复 | WebSocket 长连接被网络设备空闲超时断开 | 日志搜timeout、keepalive | 缩短心跳间隔,或改用 HTTPS |
| 普通 HTTPS 请求正常,只有对话卡住 | 中间设备不支持 Upgrade 头 | 用 curl 模拟握手,观察是否返回 101 | 关闭 WebSocket,强制走 HTTPS |
| 环境变量改了没生效 | 配置字段名写错,或当前版本不支持 | codex config list确认字段名 | 按当前版本支持的变量名修改 |
5.2 我在这次排障里踩过的三个坑
第一个坑是过度依赖重启。前 20 分钟我一直在重启 Codex,状态确实偶尔能好一下,但下次请求又卡住。后来想明白了:重启只是把未完成的连接全部清掉,相当于强行挂断那通没人接的电话,但下次拨号还是走同样的失败路径。
第二个坑是只测了普通接口,没测 WebSocket 握手。刚开始我用 curl 请求了一次/v1/models,看到返回正常就以为后端没问题,差点把方向带偏。实际上普通的 HTTPS 短连接和 WebSocket 长连接在网络链路上完全是两套命运,必须针对性地验证101响应。
第三个坑是改完配置之后没看日志。我在配置文件里加了一行transport = "https",重启后发现还是卡住,一度以为这个方法无效。后来才发现是环境变量里残留了一个旧配置,优先级更高,直接覆盖了配置文件。所以改完配置后,一定要去看日志确认实际加载的参数是什么。
5.3 给同样被“正在思考”折磨的朋友一句总结
我个人在多次处理这类问题后的体会是:当你看到“正在思考”四个字,不要第一反应去怀疑模型变笨了,先确认链路还通着。尤其是 Codex 这类重度依赖流式传输的工具,WebSocket 握手成功与否,直接决定你能不能收到内容。学会看101 Switching Protocols这个状态码,比会敲多少花哨命令都实在。
如果你也碰到类似情况,优先按这个顺序处理:看日志,确认是 WebSocket 还是普通 HTTPS,再决定要不要回退。大部分时候,让 Codex 老老实实走 HTTPS 回退,是最不折腾人的方案。