中转站调用 Claude 流式输出卡住、断流、首字很慢怎么办?缓冲、超时、代理和重试的排查方法

简介: 本文详解Claude流式接口三大典型故障——首字慢、中途卡住、输出断流的精准定位与排查方法。提供探针脚本直测事件流,结合首Token延迟基准(如Haiku 5.5约4.4s),分层诊断代理缓冲、超时设置、VPN切换及流内错误,并指导重试策略与用量对账。(239字)

通过中转站调用 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 或让模型继续。

四、断流之后:重试和对账

重试要区分错误类型,对账要用平台的明细。

  1. 该重试的错误。 Anthropic Python SDK 默认最多重试 2 次,退避从 0.5 秒起、上限 8 秒,对连接错误、超时和状态码 408、409、429、5xx 重试,响应头 x-should-retry 可覆盖这一判断[1]。自己写客户端可照此范围做,400、401 这类请求本身有问题的错误,重试没有意义。SDK 的重试发生在请求阶段,流已经开始后中途断开,需要你自己处理(我在源码中没有看到自动续传)。
  2. 整条重试还是续写。 最简单可靠的是整条重试。长输出想省成本,可以把已收到的内容作为上下文让模型接着写,但衔接处可能重复,要自己验证(这是我的建议,不是官方做法)。
  3. 记录。 每次请求至少记录请求 ID、开始时间、已收到的字符数、断开时间、异常类型。
  4. 断流的计费规则因平台而异。 阿里云百炼的流式文档写明,流式与非流式的计费规则相同,请求中断时,输出 Token 只计算服务端收到终止请求前已生成的部分[5]。这是百炼的规则。中转站调用 Claude 时断流请求如何计费,我没查到文档,对账时把断流请求单独标出,与平台明细逐条核对。
  5. 用平台明细补齐用量。 断流时客户端往往收不到靠后的 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 补齐用量,后两项还能反过来验证探针测到的首字时间和总耗时。

五、排查顺序小结

  1. 用探针直连中转站,记下首字时间、事件间最大间隔、是否收到 message_stop。
  2. 直连正常、经过自己的代理变差:查 proxy_buffering 和 proxy_read_timeout,再查负载均衡、网关的空闲超时(3.1、3.2)。
  3. 非流式请求报 504、524:改用流式,并把客户端超时设得比任务时长长(3.3)。
  4. 开代理和关代理的探针结果差异大:问题在 VPN 或代理(3.4)。
  5. 收到 error 事件或 stop_reason 为 max_tokens:按 3.5 区分上游报错和长度截断。
  6. 断流后:只重试 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 读取。

标准与行业文档

平台功能、默认值和 SDK 行为会调整,落地前以各自官方文档和你安装的版本为准。

相关文章
|
4天前
|
JavaScript API iOS开发
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(DSH)是DeepSeek于2026年开源的MIT协议AI Agent框架,支持插件化组装模型、工具、沙箱与UI,兼容主流大模型。本文提供免Node.js的桌面端安装包(Win/macOS),含详细图文教程及热门插件推荐。
|
云安全 安全 网络安全
|
5天前
|
iOS开发 Docker MacOS
【2026实测】Docker Desktop下载、安装、汉化、配置教程(附官网安装包)
Docker Desktop 是 Docker 官方推出的桌面容器管理工具,集成引擎、CLI、图形界面与 Kubernetes,支持 Win/macOS/Linux。2026 最新版 4.88.1 强化 ARM 原生运行、镜像加速及资源可视化,附中文汉化与国内镜像配置指南。
861 1
|
17天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
2971 49
|
11天前
|
JavaScript 安全 前端开发
Nodejs Brotli decoder 代码执行漏洞 (CVE-2026-111497)10629
Node.js发布安全更新修复Brotli解码器高危RCE漏洞(CVE-2026-111497),建议受影响版本用户立即升级。
2100 2
|
5天前
|
安全 算法 定位技术
《高性价比人生指南》完整原版:普通人用最少的资源,过好这一生的底层逻辑~How To Live Better
📖《高性价比人生指南》是一份基于实证、拒绝焦虑的生存手册,倡导以“人生时薪”为视角,重新计算时间、金钱与精力的成本与收益。它不承诺逆袭,只教人算清账:省下的钱若耗掉时间便亏;买体验比买物品更值;睡眠是最高杠杆的健康投资;关系贵在情绪价值而非表面热络;学习重在改变行为而非自我安慰。核心理念:用低成本、高回报的微小行动,换取长期的选择权——不是成为谁,而是掌控自己人生的主动权。
|
10天前
|
人工智能 JSON Linux
【保姆级】ComfyUI部署教程(下载+安装+配置+工作流,2026最新)
ComfyUI 是一款开源节点式AI生成工具,支持图像、视频、音频及3D内容创作。通过拖拽连线组合模型加载、提示词、采样等节点,构建可复用、易修改的工作流,兼容SD1.5、SDXL、Flux、Qwen Image等主流模型。
|
4天前
|
Linux 开发工具 iOS开发
Codex保姆级教程(下载+安装+登录+使用,含所有版本,2026最新)
Codex是OpenAI推出的开源AI编程智能体,本地运行,可理解整个代码库,支持自然语言指令改代码、执行命令、跑测试、修Bug,兼容Win/macOS/Linux。(239字)

热门文章

最新文章