跳到主内容
星链API

大模型 API 超时排查:Python 日志区分连接、首字、流式中断

人工智能4,213
大模型 API 超时排查:Python 日志区分连接、首字、流式中断

调用大模型 API 时,最难处理的情况往往不是收到明确的错误码,而是请求一直等待,或者文字输出到一半突然停止。

这些现象看起来都是 “超时”,发生的位置却可能完全不同。有的请求没有建立连接,有的正在等待上游返回,还有的已经生成内容,却在代理转发或客户端读取时中断。

如果直接把所有超时时间调大,可能只是让失败发生得更晚。更有效的做法是:先记录请求走到了哪一步,再判断应该检查网络、应用配置,还是模型服务。

本文围绕一条诊断路径展开:标记时间点,区分结束状态,再用最小请求逐层验证。

一、先把 “请求超时” 拆成三个阶段

一次流式模型调用,可以简化为:
发起请求 → 收到响应头 → 收到有效内容 → 持续输出 → 正常结束。

根据最后到达的位置,排查方向可以分成三类。

表格

现象优先检查什么不能直接得出的结论
迟迟没有收到响应头,随后连接异常地址、网络、TLS、代理、连接池及上游等待模型一定没有收到请求
收到响应头,但没有有效内容心跳、排队、推理过程、解析逻辑及读取时限模型已经开始生成答案
已经收到文字,随后停止结束标记、流内错误、代理读取时限和客户端取消输出一定已经完成

这里有一个容易忽略的区别:收到响应头,不等于收到模型正文;收到心跳,也不等于收到模型生成的内容。

同样,客户端没有拿到结果,也不能证明服务端没有执行。请求可能已经被接收,只是返回链路出了问题。这个区别会直接影响后续能否安全重试。

二、用三个时间点,让日志具有定位价值

仅记录 “请求耗时超过 30 秒” 通常不够。建议至少保留三个时间点:

T0:请求开始。
T1:收到响应头。
T2:收到第一段有效内容。

此外,还应记录请求结束时间、结束原因、HTTP 状态码、模型名称、异常类型,以及服务端返回的请求 ID。

由这些信息,可以计算:
响应头等待时间 = T1 − T0
首段有效内容等待时间 = T2 − T0
总耗时 = 请求结束时间 − T0

这些都是客户端观察到的耗时。它们可能包含网络传输、代理处理、服务排队与推理过程,不能直接当成模型内部计算时间。

第一段有效内容也要定义清楚。对于普通问答,可以记录首次出现非空文本的时间;对于工具调用,应另行记录首次工具调用增量。若模型返回独立的推理内容,还应区分 “首次推理内容” 和 “首次答案内容”。

日志中不要输出完整密钥。涉及用户资料时,也应避免默认保存完整提示词与回答。排查通常先需要时间、状态与请求标识,而不是整份业务数据。

三、读取超时,不等于请求总时限

开发者经常把一个 timeout 参数理解成 “这次请求最多运行多久”。实际含义取决于所用客户端。

HTTPX 官方文档区分了连接、读取、写入和连接池等待四类超时。其中,读取超时针对等待下一块数据的时间,并不等同于整个请求的总耗时。文档还说明,其默认行为是在网络无活动达到 5 秒后触发超时;SDK 或业务代码可能另行覆盖该设置。

因此,排查前应该先回答两个问题:
当前报错的是哪一种超时?
这个限制由哪一层设置?

例如,连接池等待超时可能来自本地并发过高。此时继续增加重试请求,反而可能加剧拥堵。连接超时则应先检查网络和地址,而不是调整模型输出长度。

生产应用还需要独立的总时限。即使服务端持续发送少量数据,业务也不能无限等待。

四、为什么连接还在,却一直没有答案?

大模型接口可能在等待过程中发送保活内容。

DeepSeek 官方文档说明:等待响应期间,非流式请求可能收到空行,流式请求可能收到 SSE 保活注释。若请求在 10 分钟后仍未开始推理,服务端会关闭连接。这是该服务公开描述的条件,不是所有 API 的统一规则。

这带来两个诊断要点。

第一,心跳说明链路仍有数据传输,不能证明已经产生答案。如果程序把心跳当成有效文本,就会错误地报告 “首字已经返回”。

第二,读取超时可能没有触发,但业务等待已经过长。只要保活内容持续到达,某些读取计时器就可能不断获得新的数据。因此,业务总时限需要单独管理。

SSE 使用事件流格式。以冒号开头的行属于注释,不能当作 JSON 解析。事件还可能包含多行 data:,通常通过空行划分事件边界。自行处理流式响应时,应按协议解析,而不是把每条网络数据块都视为一个完整事件。

五、Python 诊断示例:先看请求停在哪里

下面的示例面向返回 Chat Completions 风格 SSE 数据的接口,记录响应头、第一段答案文本及结束信号。它不是通用于所有模型协议的解析器。

运行环境为 Python 3.11 及以上,需安装 httpx。通过环境变量提供完整请求地址、密钥和模型 ID,避免把敏感信息写进代码。

代码中的连接 10 秒、读取 30 秒、总时限 90 秒,仅用于演示不同计时器的分工,不是厂商建议值。

import asyncio
import json
import os
import time
import httpx

async def diagnose():
    started = time.perf_counter()
    first_text = None
    finish_reason = None
    done = False
    phase = "等待响应头"

    def log(event, **fields):
        print(json.dumps({
            "elapsed_s": round(time.perf_counter() - started, 3),
            "event": event,
            **fields,
        }, ensure_ascii=False))

    async def events(response):
        parts = []
        async for line in response.aiter_lines():
            if line == "":
                if parts:
                    yield "\n".join(parts)
                    parts = []
            elif line.startswith(":"):
                continue
            elif line.startswith("data:"):
                value = line[5:]
                parts.append(value[1:] if value.startswith(" ") else value)

    timeout = httpx.Timeout(
        connect=10.0, read=30.0, write=10.0, pool=5.0
    )

    try:
        async with asyncio.timeout(90.0):
            async with httpx.AsyncClient(timeout=timeout) as client:
                async with client.stream(
                    "POST",
                    os.environ["LLM_CHAT_URL"],
                    headers={
                        "Authorization": f"Bearer {os.environ['LLM_API_KEY']}"
                    },
                    json={
                        "model": os.environ["LLM_MODEL"],
                        "messages": [
                            {"role": "user", "content": "请简短介绍你自己。"}
                        ],
                        "stream": True,
                    },
                ) as response:
                    log(
                        "response_headers",
                        status=response.status_code,
                        request_id=response.headers.get("x-request-id"),
                    )
                    response.raise_for_status()
                    phase = "已收响应头,等待有效文本"

                    async for data in events(response):
                        if data == "[DONE]":
                            done = True
                            break

                        chunk = json.loads(data)
                        if chunk.get("error"):
                            log("stream_error")
                            break

                        for choice in chunk.get("choices", []):
                            delta = choice.get("delta") or {}
                            if delta.get("content") and first_text is None:
                                first_text = time.perf_counter() - started
                                phase = "已收到答案文本"
                                log("first_text")

                            if choice.get("finish_reason") is not None:
                                finish_reason = choice["finish_reason"]

                    log(
                        "stream_end",
                        done_marker=done,
                        finish_reason=finish_reason,
                    )
    except httpx.TimeoutException as exc:
        log("http_timeout", kind=type(exc).__name__, phase=phase)
    except TimeoutError:
        log("total_deadline", phase=phase)
    except httpx.HTTPStatusError as exc:
        log("http_error", status=exc.response.status_code)
    except (httpx.RequestError, ValueError) as exc:
        log("transport_or_parse_error", kind=type(exc).__name__, phase=phase)

asyncio.run(diagnose())

HTTPX 提供了异步流式读取接口;外层总时限使用 Python 的 asyncio.timeout (),与网络读取时限分开设置。

使用这段代码时,还要注意三个边界:
没有 x-request-id 不代表请求异常,不同平台可能采用其他标识字段。
示例仅把 delta.content 计为答案文本,未测量推理内容或工具调用的首次到达时间。
示例没有调用真实账户进行模型实测,实际接口是否支持该结构,应以对应平台文档为准。

诊断时先保存日志,再结合异常和结束状态判断。不要从一条 “超时” 日志直接跳到服务商故障结论。

六、连接阶段异常:先缩短请求链路

如果问题发生在收到响应头之前,应先检查请求地址、端口、TLS 及网络代理。若明确报出连接池等待异常,还要检查本地连接是否被正确释放。

建议准备一个最小请求:短输入、单次调用、无文件上传、无工具执行。然后在相同环境中逐步测试:

使用独立脚本调用已配置的服务地址。
加入自己的后端转发服务。
加入反向代理或网关。
最后接入前端和完整业务流程。

每次只增加一层。哪一步开始出现明显变化,就优先检查该层新增的配置与处理逻辑。这种对照只能缩小范围,仍需日志验证。

使用 星链 API 这类 API 中转站时,同样应区分客户端到中转服务、中转服务到模型上游两段链路。提交问题时提供脱敏请求 ID、发生时间与模型名称,比单独描述 “接口很慢” 更容易定位;具体端点和模型 ID 应按平台当前文档填写。

七、已经输出却中断:先确认是正常停止还是故障

文字停止出现,并不一定意味着超时。

模型可能已经正常结束,也可能因为输出上限停止;连接还可能在结束标记到达之前中断。因此,不能仅凭 “没有新文字” 判断任务完成。

对于采用相应 Chat Completions 流式约定的接口,应结合结束标记、finish_reason、流内错误和传输异常检查。不同协议可能采用不同终止事件,不能把一种接口的完成标记套到另一种接口上。

还要区分协议结束与业务完成。即使流正常终止,输出也可能因长度限制而不完整,或者只产生了需要后续执行的工具调用。

如果存在 NGINX 反向代理,可以检查读取时限和缓冲配置。官方文档列出的 proxy_read_timeout 默认值为 60 秒,限制的是两次连续读取操作之间的间隔,并非整个响应只能持续 60 秒。proxy_buffering 则关系到上游响应如何被缓冲和传递。

这些配置应在明确路径后调整。不能看到流式输出慢,就直接关闭所有缓冲,或把所有超时统一改成很大的数值。CDN、负载均衡器和应用服务器可能还有各自的限制。

八、哪些官方数值可以引用,哪些不能混用?

技术文章里的数字必须带上适用条件。

例如,阿里云百炼错误码文档在 RequestTimeOut 条目中列出调用大模型请求超时的 300 秒说明,并提供流式输出等处理建议;同一条目还对千问模型当前返回行为作出补充。这些描述属于对应服务的具体机制,不能扩大成 “所有大模型 API 都在 300 秒超时”。

同样,平台首页展示的响应速度,若没有定义测量起点、终点、模型、输入长度和并发条件,就不应直接作为本篇排障的首字时间标准。

本文保留官方数值,是为了帮助识别限制来自哪一层,而不是建立一套适用于所有场景的超时模板。

九、重试之前,先判断会不会重复执行

请求超时后的重试,需要考虑任务是否已经发生。

普通文本生成可以按业务规则决定是否重新生成,但第二次调用可能产生额外费用,也可能返回不同结果。涉及订单、邮件、数据库修改等工具操作时,风险更高:第一次请求可能已经触发执行,只是客户端没有收到确认。

因此,重试策略至少应包含:
最大尝试次数。
包含重试在内的总时限。
带随机抖动的退避。
业务操作去重与执行状态查询。

API 本身是否支持幂等键,需要查看具体文档,不能默认所有模型接口都具备该能力。

对于已经输出部分内容的请求,也不要直接把新回答接到旧文本后面。重新生成可能重复开头或改变结论。应先决定是完整重做、明确续写,还是向用户展示中断状态。

十、修复是否有效,要用同一任务复测

把超时时间调大后,一次请求成功,不能说明问题已经解决。

更可靠的验证方式是保持模型、输入、输出要求与并发条件一致,分别记录修改前后的成功率、首段有效内容时间、总耗时和中断位置。

如果只改变客户端读取时限,结果就明显不同,可以优先检查客户端等待策略;如果独立脚本正常,经过业务代理后才中断,就应继续核对代理日志。若问题只在较高并发下出现,则需要检查连接池、请求队列与服务配额。

这些结果都是定位线索。最终结论应能回答三个问题:在哪一层发生、什么证据支持、修复后如何复现验证。

接入 星链 API 或其他服务时,建议从第一次调用就保留这套诊断记录。它不会替代平台日志,却能让跨团队排查建立在相同的时间线之上。

大模型 API 超时的排查起点,是把 “没返回” 拆成可观察的阶段。先分清连接、等待和生成中断,再调整对应限制,才能避免用更长等待时间掩盖真正的问题。

大模型APIAPI超时流式输出SSE首字延迟代理超时安全重试开发者

Related

相关文章推荐