典型报错是 requests.exceptions.ConnectTimeout 或者 httpx.ConnectTimeout。含义是域名解析或者三次握手没在设定时间内完成,问题在网络层,常见原因是本机 DNS 出错、代理设置有问题、或者出口被限速。这种超时跟你调用的模型、用的参数完全无关。
典型报错是这样的:
requests.exceptions.ReadTimeout: HTTPSConnectionPool(host='xyuapi.top', port=443): Read timed out. (read timeout=60)
用 OpenAI 兼容 SDK 时是 openai.APITimeoutError: Request timed out.,用 Node 的 undici 时是 UND_ERR_HEADERS_TIMEOUT。这一类占了实际问题的九成:连接已经建立,服务端也在算,只是算完之前你的客户端先放弃了。
返回 504 Gateway Time-out 或者 nginx 的 upstream timed out,说明请求在你和上游之间某一段被掐断了。可能是你自己的反向代理,也可能是链路上的其他环节。这两种情况要改的位置不一样:读超时改客户端,504 改中间层配置。
| 现象 | 典型报错原文 | 断在哪 | 先改哪里 |
|---|---|---|---|
| 连接超时 | ConnectTimeout | TCP 握手 | 本机网络、DNS、出口 |
| 读超时 | Read timed out. (read timeout=60) | 等响应体 | 客户端超时值、流式 |
| SDK 超时 | openai.APITimeoutError | 等响应头 | SDK 的 timeout 参数 |
| 网关超时 | 504 Gateway Time-out | 中间层 | 代理的读等待时间 |
claude-sonnet-4-5-thinking、claude-opus-4-5-thinking 这类模型会先输出一段推理过程,再输出正式答案。这段时间里 HTTP 连接是安静挂着的,一个字节都不返回。客户端如果设了 60 秒读超时,正好卡在这个空窗期上。
max_tokens 是输出上限,模型写满就是最长的生成时间。设成 8192 意味着最坏情况要生成 8192 个 token,非流式请求必须等它全部生成完才返回第一个字节。把上限从 8192 降到 2048,等待时间大致按比例缩短,是最直接的降超时手段。
非流式是把整段回答攒齐了一次性返回。回答越长,攒的时间越长,超时风险越高。流式是生成一个片段就吐一个片段,客户端一直在收数据,读超时被不断重置,同一个请求用流式几乎不会超时。所以顺序应该是先开流式,再考虑调大超时值。
import requests
# (连接超时, 读取超时)
resp = requests.post(
"https://xyuapi.top/v1/chat/completions",
headers={"Authorization": "Bearer " + key},
json={"model": "claude-opus-4-5-thinking",
"max_tokens": 4096,
"messages": [{"role": "user", "content": "写一段 800 字的产品说明"}]},
timeout=(10, 600),
)
连接超时设 10 秒,读超时设 600 秒,是长文本场景比较稳的一组值。写成单个数字 timeout=600 会让连接阶段也等 600 秒,网络有问题时要干等十分钟才知道。
import httpx
from openai import OpenAI
client = OpenAI(
base_url="https://xyuapi.top/v1",
api_key=key,
timeout=httpx.Timeout(connect=10.0, read=600.0, write=30.0, pool=10.0),
max_retries=1,
)
SDK 默认会自己重试。长文本场景下把 max_retries 调到 1 或者 0,避免一次超时变成三次串行等待,总耗时反而翻三倍。
开了流式以后,读超时不再覆盖整段生成时间,只覆盖「两次数据之间的间隔」。所以流式下 600 秒可以降到 120 秒,甚至 60 秒也够用,因为数据在持续到达。这个区别很多人没意识到,于是把流式的超时也设成 600 秒,白白放大了故障时的等待时间。
自己搭了反向代理的话,默认的 proxy_read_timeout 是 60 秒,和很多客户端的默认值撞在一起。哪怕你的代码设了 600 秒,代理也会在 60 秒时先断,客户端拿到的变成 504。三处都要放开:
location /v1/ {
proxy_pass http://gateway;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
proxy_buffering off 这一行对流式特别关键。缓冲开着的时候,代理会把内容攒起来再转发,客户端看不到持续的数据流,流式的意义就没了。
有些边缘节点的空闲连接超时是 30 秒到 100 秒,流式请求在思考阶段长时间没有数据,会被判定为空闲而断开。判断方法:直连和经过 CDN 各测一次同一个长请求,看耗时和断点位置是否不同。
Serverless 环境通常有硬性执行上限,短的是 30 秒。跑长文本生成时,函数还没拿到结果就被平台杀掉了。这种只能改用流式加前端直连,或者换成常驻进程。
在服务器上直接请求本机端口测一次,再走公网域名测一次,两次数值对比。自己代理这一段的问题会在这两个数之间暴露:本机快、域名慢,改代理配置;两边都慢,问题在别处,别在代理上浪费时间。
这两个客户端一般不需要手动改超时,它们对长回答的处理比较宽松。真正卡住时,先看是不是用了不带流式的自定义模型配置,把流式开关打开。
Cline、Cursor 这类插件调用时往往带一大段上下文,生成时间更长。插件设置里有请求超时的选项,把它调到 300 秒以上,同时把单次任务的最大迭代次数压下来,避免一个任务串行发几十次请求。
浏览器 fetch 本身没有默认超时,但服务端和中间层有。前端做流式渲染时要用 TextDecoder 的流式模式,否则中文会被拆成半个字显示乱码,看起来像超时后残缺的响应。
手机应用切到后台时,系统会冻结网络连接,回到前台时流式连接已经断了,看起来像超时。移动端做长回答要在回到前台后检查连接状态,必要时重新发起,并且把已收到的内容保留在界面上。
读超时只说明你的客户端不等了,服务端可能已经把结果算完并发了出来。这种情况下重试会造成第二次生成,按次计费的话就是付两次钱。
| 模型 | 单次价格 |
|---|---|
| gemini-2.5-pro | 0.031 元/次 |
| deepseek-v3.2-thinking | 0.049 元/次 |
| claude-sonnet-4-5-thinking | 0.09 元/次 |
| claude-opus-4-5-thinking | 0.12 元/次 |
单次价格固定,输入长短不影响价格,所以超时重试的代价是可预期的。但这不等于可以无脑重试:一次 600 秒超时加一次重试,用户要等二十分钟。
已经收到部分流式内容的不重试,把已有内容留下;400、401、403 这类明确错误不重试,重试结果一样;同一个请求连续两次超时的不再第三次,说明参数设置本身有问题,先把 max_tokens 和流式开关检查一遍。
连接超时和读超时不要写死在代码里,放到配置文件或者环境变量里。换模型、换场景时只改配置不改代码。排查超时时最耽误时间的往往不是找原因,而是每次都要改代码重启一次。
curl -s -o /dev/null -w "dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n" \
--max-time 300 \
https://xyuapi.top/v1/chat/completions \
-H "Authorization: Bearer sk-你的Key" \
-H "Content-Type: application/json" \
-d '{"model":"claude-sonnet-4-5-thinking","max_tokens":2048,"messages":[{"role":"user","content":"写一段300字说明"}]}'
几个数字的用法:time_namelookup 大说明 DNS 慢;time_connect 大说明网络到网关这段慢;time_starttransfer 是首字节时间,它大而总时间也大,就是生成时间长,属于正常的长文本代价;total 接近 --max-time 就是被掐断。
每次请求记录三个时刻:发出前、收到响应头、收到完整响应体。三个数字一摆,超时发生在哪一段立刻清楚。日志里同时记下模型名和 max_tokens,方便对比不同参数下的耗时。
换 gemini-2.5-pro 发同样的请求。如果它正常,说明问题在长文本生成这一侧,不是网络;如果它也超时,说明是本机网络或者网关配置的问题。这一步能把排查范围砍掉一半。
拿同一个提问,把 max_tokens 分别设成 512、2048、8192 各跑一次,记录耗时。如果耗时随上限明显增长,说明时间花在生成上,这是长文本的正常代价,要改的是超时值而不是网络。如果三次都很短却仍然报超时,问题在链路上,去看中间层的等待时间设置。
把提问换成一句「你好」。如果它秒回,说明连接和鉴权都正常,超时只出现在需要长时间生成的任务上。如果它也要等很久,说明链路上存在固定延迟,跟模型和参数都没有关系。
首字节时间反映服务端多久开始回应,总耗时反映生成用了多久。两个数字一起记,责任一次分清:首字节大是链路问题,总耗时大是生成时长问题。很多人把这两个数混成一个总耗时,于是永远分不清该改哪里。
超时后重试时带上原请求的标记,同一分钟内出现多次相同请求时只保留一份结果,避免把重复生成的答案轮流展示给用户。在没有请求 ID 的场景下,退而求其次的做法是用提问内容的哈希值做标记,同样能挡住大部分重复。
小鱼API 是 AI API 接入平台,走 OpenAI 兼容协议,主入口 https://xyuapi.top/v1,备用 https://xyuai.cc/v1。Chatbox、Cherry Studio、NextChat、Cline 填上地址和令牌即可使用,一个 Key 调全系列模型,按次计费。
| 使用场景 | 连接超时 | 读超时 | 是否流式 |
|---|---|---|---|
| 短问答 | 10 秒 | 60 秒 | 可不开 |
| 长文生成 | 10 秒 | 600 秒 | 建议开 |
| 带思考的模型 | 10 秒 | 600 秒 | 建议开 |
| 前端直连流式 | 10 秒 | 120 秒 | 必须开 |
| 批量离线任务 | 10 秒 | 900 秒 | 建议开 |
超时问题的解法顺序是固定的:先开流式,再按维度设超时,最后才去调中间层。顺序反过来,会在配置里绕很久还得不到结果。