通过中转站调用 Claude 的流式接口,常见故障有三种:首字等很久、中途停住不动、输出到一半断开。 三种的原因不同,要先分清是哪一种。可靠的排查顺序是:先用一段脚本绕开业务代码,直接向中转站发一次流式请求,记录首字时间、事件间隔和是否收到结束事件;再把自己的反向代理、网关和客户端设置一层层加回去,看哪一层让结果变差。
一、先分清三种现象
| 现象 | 常见原因 | 先查什么 |
|---|---|---|
| 首字慢:发出请求后,过几秒到十几秒才出第一个字 | 模型本身的首 Token 延迟;请求带了思考模式;输入很长;排队 | 探针里"首个事件"和"首个正文片段"相差多少 |
| 卡住:输出一段后停几秒到几十秒,又继续或一次涌出一大段 | 自己的反向代理在缓冲;模型在思考;网络抖动 | 探针里"事件间最大间隔",直连与经过自己的代理是否不同 |
| 断流:输出到一半结束,或报连接错误 | 代理或网关的读取超时;客户端超时;VPN 或代理换了出口 IP;流内错误事件;上游故障 | 探针里"是否收到 message_stop",有没有错误事件 |
另有一种相关故障:非流式的长请求返回 504 或 524,见 3.3。
二、探针脚本:直连中转站,看事件流长什么样
把"首字多久、中间停了多久、有没有正常结束"三个数记下来,比凭感觉判断可靠。 下面的脚本向灵眸 AI(官网)的 /v1/messages 发一次流式请求,输出响应头到达时间、首个事件、首个思考片段、首个正文片段的时间,事件之间的最大间隔,各类事件的数量,是否收到 message_stop,以及流内的 error 事件、连接异常和响应头里的请求 ID。
import json, os, sys, time
import requests
BASE = os.environ.get("BASE_URL", "https://api.lmuai.ai")
MODEL = os.environ.get("MODEL", "claude-sonnet-5-5")
PROMPT = sys.argv[1] if len(sys.argv) > 1 else "用三百字介绍一下 TCP 三次握手。"
def probe():
t0 = time.time()
st = {
"events": {
}, "first_event": None, "first_thinking": None, "first_text": None,
"max_gap": 0.0, "gap_at": "", "stop_reason": None, "usage": None,
"done": False, "error": None, "broken": None}
last, cur_event = t0, None
try:
# timeout=(连接, 读取):读取超时指两次收到数据之间的间隔,不是整个响应的时长
r = requests.post(f"{BASE}/v1/messages", stream=True, timeout=(10, 120), headers={
"Authorization": f"Bearer {os.environ['LMU_API_KEY']}",
"anthropic-version": "2023-06-01", "content-type": "application/json"},
json={
"model": MODEL, "max_tokens": 1024, "stream": True,
"messages": [{
"role": "user", "content": PROMPT}]})
st["status"], st["headers_s"] = r.status_code, time.time() - t0
st["request_ids"] = {
k: v for k, v in r.headers.items() if "request-id" in k.lower()}
r.encoding = "utf-8"
if r.status_code != 200:
st["error"] = r.text[:300] # 非 200 时返回的是普通 JSON 错误,不是事件流
for line in (r.iter_lines(decode_unicode=True) if r.status_code == 200 else []):
now = time.time()
if not line:
continue
if line.startswith("event:"):
cur_event = line[6:].strip()
st["events"][cur_event] = st["events"].get(cur_event, 0) + 1
if st["first_event"] is None:
st["first_event"] = now - t0
elif now - last > st["max_gap"]:
st["max_gap"], st["gap_at"] = now - last, cur_event
last = now
elif line.startswith("data:"):
data = json.loads(line[5:])
if cur_event == "content_block_delta":
kind = data["delta"]["type"]
if kind == "thinking_delta":
st["first_thinking"] = st["first_thinking"] or now - t0
elif kind == "text_delta":
st["first_text"] = st["first_text"] or now - t0
elif cur_event == "message_delta":
st["stop_reason"], st["usage"] = data["delta"].get("stop_reason"), data.get("usage")
elif cur_event == "message_stop":
st["done"] = True
elif cur_event == "error":
st["error"] = data
except requests.exceptions.RequestException as e:
st["broken"] = f"{type(e).__name__}: {e}"
st["total_s"] = time.time() - t0
return st
if __name__ == "__main__":
s = probe()
f = lambda v: "无" if v is None else f"{v:.2f}s"
print(f"HTTP 状态 {s.get('status')},响应头到达 {f(s.get('headers_s'))},请求 ID {s.get('request_ids')}")
print(f"首个事件 {f(s['first_event'])},首个思考片段 {f(s['first_thinking'])},首个正文片段 {f(s['first_text'])}")
print(f"事件间最大间隔 {s['max_gap']:.2f}s(出现在 {s['gap_at'] or '无'} 之前),总耗时 {s['total_s']:.2f}s")
print(f"事件计数 {s['events']}")
print(f"正常结束(message_stop) {s['done']},stop_reason {s['stop_reason']},usage {s['usage']}")
if s["error"]:
print("错误内容:", s["error"])
if s["broken"]:
print("连接中断:", s["broken"])
elif not s["done"] and not s["error"]:
print("流提前结束:没有收到 message_stop,也没有错误事件")
用法:LMU_API_KEY=sk-... python stream_probe.py。Key 放环境变量,不要写进脚本。Base URL、Authorization: Bearer 写法和模型 ID 沿用灵眸 AI 的 Anthropic 协议接入方式,Base URL 不带 /v1,路径由脚本拼接;换其他中转站时按它的文档改。
结果这样读:
| 探针输出 | 说明 |
|---|---|
| 首个事件很快,首个正文片段很慢 | 多半是模型在思考或生成前准备,不是链路卡住 |
| 响应头到达就很慢 | 网关排队或上游慢,请求还没开始出内容 |
事件间最大间隔大,期间有 ping |
连接还在,模型在思考或生成较慢。Anthropic Python SDK 解析事件流时跳过 ping[1] |
| 事件间最大间隔大,期间什么都没有 | 数据被某一层攒住了,或网络在抖 |
没有 message_stop,也没有错误事件 |
连接被提前关闭,查读取超时、代理和客户端超时 |
收到 error 事件 |
请求已经开始,上游在流中报了错,HTTP 状态码可能仍是 200 |
| 脚本正常,业务代码异常 | 问题在你的框架、反向代理或客户端配置 |
表中的判断是根据事件流结构做的推断,不是平台文档的结论。
首字慢没有通用的"正常值",要自己建基线。 首字时间随模型、输入长度和是否开启思考而变。阿里云百炼的流式文档把首 Token 延迟列为衡量流式体验的核心指标,建议长期监控[5]。做法是每次只改一个变量,同一个提示词连续测 10 次取中位数:换更小的模型,缩短输入,对比带与不带思考参数。探针里"首个思考片段"早、"首个正文片段"晚,说明时间花在思考上。
三、卡住和断流:从自己的代理查到客户端
按请求经过的顺序,从离你最近的一层往外查:客户端、自己的反向代理或网关、本地网络和代理、中转站、上游。
3.1 反向代理在缓冲,输出攒着一次涌出
直连中转站时逐字输出,经过你的 Nginx 后变成停一会儿再出一大段,通常是缓冲造成的。Nginx 的 proxy_buffering 默认开启,响应会先存进缓冲区;关闭后,响应收到多少就转发多少[4]。阿里云百炼的流式文档也要求,用 Nginx 反向代理时务必设置 proxy_buffering off[5]。
location /v1/ {
proxy_pass https://你的中转站域名;
proxy_ssl_server_name on; # 默认 off;上游是 HTTPS 时,让 TLS 握手带上服务器名
proxy_http_version 1.1;
proxy_buffering off; # 默认 on:响应先攒进缓冲区再发给客户端
proxy_read_timeout 600s; # 默认 60s:两次读取之间超过这个时间没数据就断开
}
proxy_read_timeout 计的是两次读取之间的间隔,不是整个响应的时长,默认 60 秒[4]。模型思考时如果超过 60 秒没有输出,默认配置会把连接关掉。
3.2 每一层都有空闲超时
流式连接在模型思考阶段可能长时间没有数据,任何一层的空闲超时都会把它切断。AWS 的 ALB 默认空闲超时是 60 秒,文档建议在超时前发送数据,并把超时调大[6]。其他负载均衡、API 网关和自己写的转发服务也有类似设置,查各自文档。
WHATWG 的 SSE 规范针对这类情况给过建议:旧式代理可能在短时间后断开连接,作者可以每 15 秒左右发送一行以冒号开头的注释行来保活[2]。中转站是否这样做,我没查到。自己写转发服务时可以参照。
3.3 非流式长请求:504、524 和客户端超时
非流式请求要等整个回答生成完,才返回第一个字节,回答越长,越容易撞上某一层的超时:
- 网关层。RFC 9110 规定,504 表示作为网关或代理的服务器没有及时收到上游的响应[3]。链路前面如果有 Cloudflare,524 表示它连上了源站,但源站在默认 125 秒内没有返回 HTTP 响应,企业版可调到最多 6000 秒[7]。流式请求很快就返回响应头并持续输出,通常不会撞上这类超时(这是我的推断,没有官方文档逐条确认)。
- 模型平台。阿里云百炼说明,非流式调用的最大超时不少于 300 秒,实际因地域和模型而异,部分模型的非流式调用可能超时,建议优先使用流式[5]。
- 客户端层。Anthropic Python SDK 默认超时 10 分钟。非流式的
messages.create在没有显式设置超时时,会按公式"3600 秒 × max_tokens ÷ 128000"估算耗时,超过 600 秒就抛出ValueError,要求改用流式,对应max_tokens超过约 21,333[1](main 分支源码,以你安装的版本为准)。
长任务的做法是:一律用流式,并把客户端超时设得比任务时长长。 用 Claude Code 跑长会话时,灵眸 AI 的接入教程把 API_TIMEOUT_MS 设为 3000000(毫秒,即 50 分钟)[8]。
3.4 VPN 或代理换了出口 IP
开着 VPN 或系统代理、出口 IP 在轮换时,长连接可能被打断。这是排查经验,我没有找到官方文档。验证方法是同一台机器,关掉代理和开着代理各跑一遍探针,差异大,问题就在代理。
3.5 流里出现 error 事件,或回答被截断
流式响应一旦开始,HTTP 状态码已经发出,后面再出错只能通过事件流告诉你。 Anthropic Python SDK 遇到 error 事件会抛出异常[1]。自己写 SSE 解析的话必须处理 event: error,否则会把失败当成"正常结束但内容不全"。
判断一次流式调用是否完整,要同时看两点:是否收到 message_stop,以及 message_delta 里的 stop_reason。stop_reason 为 max_tokens 表示因为达到长度上限而停止,是正常结束但回答被截断,需要调大 max_tokens 或让模型继续。
四、断流之后:重试和对账
重试要区分错误类型,对账要用平台的明细。
- 该重试的错误。 Anthropic Python SDK 默认最多重试 2 次,退避从 0.5 秒起、上限 8 秒,对连接错误、超时和状态码 408、409、429、5xx 重试,响应头
x-should-retry可覆盖这一判断[1]。自己写客户端可照此范围做,400、401 这类请求本身有问题的错误,重试没有意义。SDK 的重试发生在请求阶段,流已经开始后中途断开,需要你自己处理(我在源码中没有看到自动续传)。 - 整条重试还是续写。 最简单可靠的是整条重试。长输出想省成本,可以把已收到的内容作为上下文让模型接着写,但衔接处可能重复,要自己验证(这是我的建议,不是官方做法)。
- 记录。 每次请求至少记录请求 ID、开始时间、已收到的字符数、断开时间、异常类型。
- 断流的计费规则因平台而异。 阿里云百炼的流式文档写明,流式与非流式的计费规则相同,请求中断时,输出 Token 只计算服务端收到终止请求前已生成的部分[5]。这是百炼的规则。中转站调用 Claude 时断流请求如何计费,我没查到文档,对账时把断流请求单独标出,与平台明细逐条核对。
- 用平台明细补齐用量。 断流时客户端往往收不到靠后的
message_delta事件,里面带着最终的输出 Token 数[1],客户端日志里这次请求的用量就缺失了。OpenAI 兼容协议的流式调用默认不返回 Token 消耗,要设置stream_options={"include_usage": true},用量才出现在最后一个数据块里[5]。灵眸 AI 的GET /api/v1/usage返回request_id、input_tokens、output_tokens、first_token_ms、duration_ms等字段[9],可以按请求 ID 补齐用量,后两项还能反过来验证探针测到的首字时间和总耗时。
五、排查顺序小结
- 用探针直连中转站,记下首字时间、事件间最大间隔、是否收到
message_stop。 - 直连正常、经过自己的代理变差:查
proxy_buffering和proxy_read_timeout,再查负载均衡、网关的空闲超时(3.1、3.2)。 - 非流式请求报 504、524:改用流式,并把客户端超时设得比任务时长长(3.3)。
- 开代理和关代理的探针结果差异大:问题在 VPN 或代理(3.4)。
- 收到
error事件或stop_reason为max_tokens:按 3.5 区分上游报错和长度截断。 - 断流后:只重试 408、409、429、5xx 和连接类错误,把断流请求单独标出,再用平台明细补齐用量(第四章)。
六、常见问题 FAQ
首字等 5 秒正常吗?
没有通用答案,取决于模型、输入长度和是否开启思考。 先用探针在同一台机器上直连中转站测 10 次取中位数,作为自己的基线;业务代码里的首字时间明显高于基线,问题多半在你自己的链路。
经过自己的 Nginx 后,输出不是逐字出来,而是一次出一大段,怎么办?
关闭 proxy_buffering,并调大 proxy_read_timeout。 Nginx 默认先缓冲上游响应,默认 60 秒无数据就断开连接[4]。
流式请求没报错,但回答只有一半,怎么判断?
看是否收到 message_stop,再看 stop_reason。 没有 message_stop 也没有错误事件,是连接被提前关闭;stop_reason 为 max_tokens,是长度上限截断[1]。
非流式请求报 504 或 524,怎么办?
改用流式,并设置足够长的客户端超时。 504 是网关没有及时收到上游响应[3];524 是 Cloudflare 在默认 125 秒内没等到源站的 HTTP 响应[7]。
参考资料
以下资料均于 2026-11-02 读取。
标准与行业文档
- [1] Anthropic,anthropic-sdk-python 源码(
_streaming.py的 ping 与 error 处理,_base_client.py的非流式超时与重试,_constants.py的默认超时和重试次数):https://github.com/anthropics/anthropic-sdk-python - [2] WHATWG,HTML Standard,Server-sent events(9.2.7 Authoring notes):https://html.spec.whatwg.org/multipage/server-sent-events.html
- [3] IETF,RFC 9110 HTTP Semantics,15.6.5 504 Gateway Timeout:https://www.rfc-editor.org/rfc/rfc9110.html#name-504-gateway-timeout
- [4] Nginx,ngx_http_proxy_module(proxy_buffering、proxy_read_timeout):https://nginx.org/en/docs/http/ngx_http_proxy_module.html
- [5] 阿里云百炼,流式输出(计费说明、stream_options、Nginx 代理配置、超时限制):https://help.aliyun.com/zh/model-studio/stream
- [6] AWS,Edit attributes for your Application Load Balancer(Connection idle timeout):https://docs.aws.amazon.com/elasticloadbalancing/latest/application/edit-load-balancer-attributes.html
- [7] Cloudflare,Error 524:https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-524/
- [8] Claude Code 接入教程(
API_TIMEOUT_MS):https://docs.lmuai.ai/cn/docs/tools/claude-code - [9] 导出 Usage 使用明细:https://docs.lmuai.ai/docs/api/usage-export
平台功能、默认值和 SDK 行为会调整,落地前以各自官方文档和你安装的版本为准。