服务端什么都没返回,卡住的是你这一端的等待逻辑,典型原文是这几条:
httpx.ReadTimeout: The read operation timed outrequests.exceptions.ReadTimeout: HTTPSConnectionPool(host=..., port=443): Read timed out. (read timeout=60)openai.APITimeoutError: Request timed out.curl: (28) Operation timed out after 30001 milliseconds with 0 bytes received关键字是 ReadTimeout 和 timed out,后面括号里的数字就是你自己设的超时值。看到 60 秒这个数字,基本能确定是默认值没改。
{"error": {"code": 504, "message": "Deadline exceeded", "status": "DEADLINE_EXCEEDED"}}
Deadline exceeded 说明请求已经送到末端一跳,是处理时间超过了限定的截止时间。它和客户端超时不是一回事:前者要放宽的是服务端与网关的时间预算,后者要改的是你代码里那个数字。
开了流式之后,常见的是先在终端看到几十个 chunk,然后突然报 Connection reset by peer 或者 RemoteProtocolError: peer closed connection without sending complete message body。这类多半是中间层的缓冲或超时把长连接掐了,而不是模型本身挂了。
gemini-3-pro-preview、gemini-3.1-pro-preview 这类带推理的模型,收到问题后先在内部推演,再吐出首字。一道复杂的分析题,内部推理几分钟属于正常现象。
不开流式时,客户端从发请求到收到响应之间一直是静默的,中间没有任何数据回来,正好命中 read timeout。把默认的 60 秒用在推理模型上,几乎必然超时。
塞进去几万字的文档做总结,服务端要先读完再答,首字节时间自然被拉长。这跟输入长度不影响单次价格不冲突——计费按次数算,但处理时间确实随输入变长。
连接超时管的是握手,读取超时管的是等响应,总时长管的是整次请求。三个混成一个,就会出现连接一直没有响应的请求把线程占很久的情况。经验值参考:
| 参数 | 建议值 | 说明 |
|---|---|---|
| 连接超时 | 10 秒 | 握手失败要立刻换路,别耗着 |
| 读取超时(非流式) | 300 秒 | 推理模型要留足思考时间 |
| 读取超时(流式) | 60 秒 | 每两个 chunk 之间不该等 60 秒 |
| 单请求总时长 | 600 秒 | 兜底,防止极端情况挂死 |
| 重试次数上限 | 2 次 | 按次计费,重试是重复花钱 |
短问答给 60 秒足够,代码生成给 180 秒,长文档总结和深度分析给 300 到 600 秒。全项目用一个超时值,要么短任务被拖慢,要么长任务被误杀。
设成 0 或者不限制,一次网络抖动就能把工作线程长期占住,几十个这样的请求就足以让整个服务排队卡死。宁可让它失败重试,也不要让它一直等下去。
给每个请求加上总时长兜底还有另一个好处:长尾请求不会一直堆在工作队列里。超时后被及时释放,后面的任务才能按时开始,整体吞吐反而更高。
超时值也不建议一步拉到极限。先给一个保守值跑一周,统计真实耗时分布,再按 P99 往上留三成余量,比一开始就设成 900 秒更合理,也更容易发现真正的慢任务。
import httpx
from openai import OpenAI
client = OpenAI(
api_key="你的平台KEY",
base_url="https://xyuapi.top/v1",
max_retries=2,
timeout=httpx.Timeout(connect=10.0, read=300.0, write=30.0, pool=10.0),
)
resp = client.chat.completions.create(
model="gemini-3-pro-preview",
messages=[{"role": "user", "content": "把这份合同拆成条款清单"}],
)
print(resp.choices[0].message.content)
httpx.Timeout 支持分段传值,这一段比一个孤零零的数字好用得多。
长任务调用时单独传一次更大的超时,其余调用仍然维持短超时,避免为了个别慢任务把全局阈值抬高:
resp = client.chat.completions.create(
model="gemini-3.1-pro-preview",
messages=[{"role": "user", "content": long_text}],
timeout=600,
)
from openai import APITimeoutError, APIConnectionError
try:
resp = client.chat.completions.create(model="gemini-2.5-pro", messages=msgs)
except APITimeoutError:
# 超时通常可以安全重试:这类请求是只读的
resp = client.chat.completions.create(model="gemini-2.5-pro", messages=msgs, timeout=600)
except APIConnectionError as e:
print("连接层问题:", e)
把超时和连接错误分开处理,能避免所有网络异常都被塞进同一个重试分支里反复重试。
开了流式,服务端每产出一段就发一段,客户端不会一直静默等待,超时阈值可以从 300 秒降到几十秒,用户体验也从「转圈两分钟」变成「字一个个往外冒」。
stream = client.chat.completions.create(
model="gemini-3-pro-preview",
messages=[{"role": "user", "content": "写一份周报模板"}],
stream=True,
timeout=60,
)
buf = []
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
buf.append(delta)
print(delta, end="", flush=True)
print("\n总字数:", len("".join(buf)))
中断后不能续传,只能记录已收到的内容,然后决定是重发一次还是直接采用半成品。判断依据看已经收到的内容有没有完成主体结构。
打印和写文件都要轻,别在循环里做大数据量运算,否则会把接收缓冲拖满,反而触发断流。
还有一点:流式断连后不要悄悄吞掉。至少记录断在第几个分片、已经收到多少字、当时耗时多少。这些数据攒起来就能看出是固定时长断还是固定字数断,两种情况的处理方向完全不同。
同一台机器上多个任务同时开流式时,注意连接池大小。默认池子偏小时,新请求会排队等连接,表现成「明明没超时却卡住」,把池子调大或者限制并发数都能解决。
自建反代时,proxy_read_timeout 默认 60 秒,模型还在思考,nginx 已经先返回 504 了。正确配置是把读取超时放宽,并关掉缓冲,让流式数据实时透传:
location /v1/ {
proxy_pass http://127.0.0.1:5010;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_buffering off;
chunked_transfer_encoding on;
}
浏览器到 CDN、CDN 到网关、网关到服务、服务到上游,这四段各有自己的超时。只改一层,其余层照旧掐断,表现就是「明明设了 600 秒还是 60 秒报错」。
容器编排平台的探针、云负载均衡的空闲超时同样会断开长连接。排查时把链路画出来,逐段核对自己的配置,比反复改客户端超时有效。
排查这类问题有个省事的办法:拿一个短请求和一个长请求各测一遍。短请求正常、长请求固定在第 N 秒断开,那个 N 基本就是某一层的超时值,照着这个数字去各层配置里搜,很快就能找到对应的那一行。
如果链路里还有 CDN,注意它的空闲连接回收往往比网关更激进。开流式并保持每几秒有数据往返,是稳妥的保活方式。
改完配置记得验证生效:把长请求重跑一次,看能不能稳定越过原来的断点。只确认文件改过了,不算验证。
对话类请求是只读操作,不会产生副作用,超时后重发是安全的。但要控制次数:按次计费的模式下,每次重试都是一次实打实的调用。
一批请求同时超时、同时重发,很容易接着撞配额上限。重试间隔加上随机抖动,并且重试也走同一个并发信号量,两个问题一起解决。
| 模型 | 价格 | 处理时长预期 | 建议超时 |
|---|---|---|---|
| gemini-2.5-pro | 0.031 元/次 | 5 到 30 秒 | 120 秒 |
| gemini-3-pro-preview | 0.05 元/次 | 10 到 90 秒 | 300 秒 |
| gemini-3.1-pro-preview | 0.09 元/次 | 30 到 240 秒 | 600 秒 |
| claude-sonnet-4-5-thinking | 0.09 元/次 | 20 到 180 秒 | 300 秒 |
| NanoBanana-Pro(生图) | 0.27 元/次 | 10 到 60 秒 | 180 秒 |
按任务实际需要的模型选型,比统一用贵模型更划算:能一次搞定的简单抽取给 gemini-2.5-pro,几百条数据跑下来成本差得很明显。
| 现象 | 报错关键字 | 根因位置 | 处理方向 |
|---|---|---|---|
| 等满 60 秒报错 | ReadTimeout | 客户端超时值 | 按任务分别放宽阈值 |
| 约 60 秒收到 504 | Deadline exceeded | 网关或反代 | 放宽 proxy_read_timeout |
| 流到一半断 | Connection reset | 中间层缓冲或超时 | 关缓冲、缩短空闲 |
| 一直没响应也不报错 | 无 | DNS 或连接被丢弃 | 加连接超时并换线路 |
| 偶发超时、重试即过 | APITimeoutError | 上游瞬时负载 | 有限重试加抖动 |
高峰期偶发的超时是上游瞬时负载造成的,重试一次通常就过。只有当超时集中在固定时段、固定任务类型上,才说明配置需要调。
先 curl -w 看各阶段耗时,再确认网关日志里有没有对应记录,之后才怀疑客户端代码。反过来查,往往在代码里来回改半天,问题其实在 nginx 那行默认值上。
curl -s -o /dev/null -N -w 'dns:%{time_namelookup} connect:%{time_connect} tls:%{time_appconnect} first:%{time_starttransfer} total:%{time_total} code:%{http_code}\n' \
--connect-timeout 10 --max-time 300 \
https://xyuapi.top/v1/chat/completions \
-H "Authorization: Bearer $XYU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gemini-2.5-pro","messages":[{"role":"user","content":"ping"}]}'
time_starttransfer 明显偏大而 time_connect 很小,说明慢在处理阶段,要放宽的正是读取超时。
每次调用记录模型名、输入长度、耗时、是否超时、重试次数。四个字段凑齐之后,一眼就能看出是哪个模型、哪类输入在拖时间。
把连续两次超时的输入单独存下来,作为回归用例。改完参数复跑一遍,确保没有把别的任务拖慢。
主入口 https://xyuapi.top/v1,备用 https://xyuai.cc/v1,另支持 Gemini 原生 /v1beta 路径,OpenAI 兼容协议,Chatbox、Cherry Studio、NextChat、Cline 这类客户端填上地址和 key 即可使用。
一次请求固定价,输入多长都不影响单价。超时重试会重复计费,所以正确做法是先把超时阈值按任务类型设对,把重试次数压到两次以内,而不是放任重试。