ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

大模型API线上稳定性排查指南:从超时到流式输出的完整方案

大模型API线上稳定性排查指南:从超时到流式输出的完整方案 上个月我负责的服务接入了大模型 API上线前所有测试都过了我甚至用脚本压了 50 个并发本地表现一直很理想。结果真正放量之后用户反馈接踵而至回答到一半突然断了、页面转圈十几秒、多问几轮就开始报错。后台看监控成功率 99% 以上错误日志也干干净净完全对不上号。后来我才想明白“AI API 调用成功”和“线上稳定”是两码事。前者只说明在某个时间点、某条网络路径、某个参数组合下服务端返回了 200后者要求整条链路上每一个环节都在各种并发和网络条件下稳定发挥。这篇文章就把我这次从“调通”走向“稳住”的完整过程写下来涉及现象分类、根因定位、重试策略、流式连接细节以及线上监控和降级方案。内容比较长但每一条都是我实际踩过的坑希望能帮你少走一些弯路。1. 先分辨你遇到的“不稳定”其实是四种不同的问题很多人一看到“线上不稳定”就习惯性地怀疑模型服务商然后一头扎进代码里调超时、加重试。但如果不先分清现象很可能白忙一场。我自己的经验是把问题分成四类排查思路会清晰很多。1.1 偶发超时接口偶尔需要消耗 30 秒以上表现形式是前端请求挂起直到浏览器或网关报超时。这类问题要重点观察规律性是集中在某个业务高峰时段还是集中在某个地域的节点或者只出现在特定长度和类型的请求上。如果规律不明显很可能不是模型本身慢而是网络链路、代理配置或容量规划出了问题。1.2 流式输出中断回答生成到一半就断掉流式输出SSE是 AI 对话场景最常用的交互方式但它比普通 JSON 请求脆弱得多。生成过程可能持续几十秒反向代理默认的读超时只有 60 秒中间任何一环掐断连接用户看到的就是“答到一半没下文”。这种情况错误日志往往不记录因为 HTTP 连接是被端侧断开服务端并不一定产生了 5xx。1.3 间歇性错误码200、429、500 交替出现如果日志里出现 429 限流、500 服务端错误、401 token 过期等状态码并且是间歇性出现就需要逐类分析。429 说明触发了配额限制500 可能是服务商过载或模型推理服务异常401 则可能是 token 过期但旧的缓存仍在使用。这三类问题的处理方式完全不同混在一起做重试会非常危险。1.4 请求成功但“内容质量”不稳定还有一种更隐蔽的不稳定接口始终返回 200但生成结果时好时坏或者到了某个时间点后回答质量明显下降。这种往往不是因为基础设施问题而是上下文超长被截断、模型解码参数被重置、或者同一个 key 在不同会话之间共享了上下文。我们后面会细说。为了方便定位我习惯把这四种现象整理成一张表现象典型根因方向排查手段常见误区偶发超时网络链路、代理缓冲、容量不足抓取全链路耗时观察分位点盲目调大超时流式断连反向代理读超时、缓冲、心跳缺失检查代理配置抓包看字节流只改前端重试交替错误码限流、token 过期、上游故障按状态码分类统计用统一点重试逻辑处理所有错误内容不稳上下文截断、参数漂移记录请求体、token 使用量误以为是模型效果问题2. 一条完整链路拆解从客户端到模型服务的五个检查点确定现象类型后下一步是把从用户浏览器到模型服务端的整条链路过一遍。很多时候问题根本不在模型 API而在我们自己搭建的中间层。2.1 本地能通不代表线上网络路径能通本地开发机通常有比较特殊的网络配置而生产服务器是纯净的公网环境。我遇到过一次很典型的案例本地请求第三方模型服务一切正常线上却频繁超时。排查了半天发现是生产服务器所在云厂商的出口 IP 被服务商风控系统标记请求被静默丢弃。如果你也遇到“本地好、线上差”的情况先检查三件事生产服务器的出口 IP 是否在服务商的允许名单里DNS 解析是否正常服务器到 API 网关的连通性是否稳定。可以在服务器上用 curl 反复请求同一个接口观察响应时间和返回码分布这是最快的手段。2.2 反向代理的超时配置是第一个坑很多团队会在应用前面挂一层 Nginx 或网关。Nginx 的proxy_read_timeout默认是 60 秒对于普通 API 够用但对于大模型生成接口来说远远不够。一个稍复杂的 prompt模型推理时间很容易超过 30 秒如果再加上排队时间连接很可能在生成完成前就被代理断掉。我建议按照实际业务场景配置proxy_connect_timeout 10s; proxy_send_timeout 300s; proxy_read_timeout 300s;同时设置proxy_buffering off避免 Nginx 把 SSE 的流式内容暂时缓冲起来导致前端拿不到增量数据。这个配置对流式接口尤其重要后面讲 SSE 时会详细展开。2.3 网关层的并发参数决定了一个“坏请求”能影响多少人当多个请求同时打到模型 API 时网关连接池的大小、worker 进程数、keepalive 参数都会成为瓶颈。默认情况下每个 Nginx worker 能维护的连接数是有限制的而模型 API 的响应慢导致连接长时间被占用新请求只能排队等待。这不是让你盲目调大worker_connections而是要根据服务商给的 RPM每分钟请求数和模型平均耗时来换算并发上限。我曾经踩过连接池耗尽导致整个服务雪崩的坑后来统一了网关 keepalive 配置问题才解决。2.4 容器网络与 DNS 缓存会带来“幽灵”超时如果你的服务跑在 Kubernetes 里还会遇到一层容器网络的复杂性。CoreDNS 解析失败、节点之间网络策略限制、Service 端口映射配置错误都可能表现为间歇性超时。最典型的例子是 DNS 超时应用第一次调用 API 时解析域名DNS 请求卡住整个调用被阻塞第二次调用时命中本地缓存又恢复正常。这类问题的排查逻辑是先在 Pod 内部直接 curl API 域名观察是否稳定再检查应用的 DNS 缓存配置最后看 CoreDNS 的监控指标。把链路一层层剥开不要上来就怀疑模型服务。2.5 SDK 和客户端库版本不一致也会导致行为差异本地调试用的 Python requests 是最新版生产环境却锁在某个旧版本本地用 Node 18线上是 Node 14这些版本差异都可能带来超时行为、连接池管理、TLS 握手方式上的细微区别。我现在的习惯是把 AI API 调用的客户端版本也纳入依赖锁文件并在测试环境完全复刻生产版本后再做上线前验证。不要总觉得“代码一样就没问题”底层基础库的差异经常是线上不稳定的隐藏变量。3. 第一个真正的元凶token 上下文失控把链路和代理层排查干净之后我开始关注请求内容本身。结果发现真正的元凶之一是 token 上下文管理失控。3.1 一次间歇性 400 背后的规律某个用户连续对话多轮后接口开始随机返回 400错误信息类似“model maximum context length is XXXXX tokens”。我最初以为这是个偶发 bug后来发现规律很明显对话轮数越多越容易出现。也就是说随着对话进行请求体里的历史消息越来越长最终超过了模型的上下文窗口上限。这类错误不是每次请求都会触发而是在用户某个特定轮次才突然爆发所以日志里看起来是“间歇性”的。如果业务方不做任何处理用户可以靠刷新页面暂时缓解但下一次又要从头开始对话体验非常糟糕。3.2 为什么“调用成功”之后还会出现这种错误很多接入方在原型阶段只做单轮问答觉得模型返回正常就万事大吉。但真实用户会连续提问对话历史会无限累积。如果不做裁剪每个请求都会携带从第一轮开始到当前轮的全部消息。这里有个被忽略的点max_tokens只是限制本次生成的输出长度并不能阻止你把超长请求体发给模型。模型的 context window 限制的是“输入 输出”的总 token 数因此在组装请求体时就必须预留足够的输出空间。3.3 token 到底怎么数别凭感觉估算不同模型的 tokenizer 不一样同一个中文字符可能对应 1 到 2 个 token。为了在生产环境精确控制请求体大小我强烈建议使用官方 tokenizer 库或 API 返回的用量信息。以 OpenAI 兼容接口为例响应里通常会有usage.prompt_tokens和usage.completion_tokens。每次调用结束后把这两个值记录下来用来动态调整下一次请求的历史数量。如果要离线估算可以用这个简单逻辑中文大约 1.5 字符对应 1 个 token英文大约 4 个字符对应 1 个 token。但只用于估算真正的预算管理还是要靠 tokenizer 计算。3.4 滑动窗口和摘要压缩两个最实用的裁剪方案我的做法是把上下文管理抽象成一个独立的模块核心策略是滑动窗口加摘要压缩。滑动窗口的思路很简单保留最近 N 轮对话更早的历史直接丢弃。这样虽然可能丢失一部分早期信息但在业务场景中用户最关心的往往是最近几轮的内容。如果业务不能接受完全丢弃可以每隔几轮把之前的对话交给模型做一次摘要并把摘要作为压缩后的历史消息参与后续请求。这一招特别适合客服、法律咨询这类需要长期记忆的场景。代码层面大概是这样def build_messages(history, max_input_tokens6000, reserve_output1000): # 从后往前遍历历史累积 token 数 messages [] token_count 0 for item in reversed(history): message {role: item[role], content: item[content]} tokens estimate_tokens(message) if token_count tokens max_input_tokens - reserve_output: break messages.append(message) token_count tokens return list(reversed(messages))这段逻辑不复杂但能把“按轮数粗剪”变成“按 token 精剪”稳定性提升非常明显。3.5 在业务层建立 token 预算我还会在业务层给每个会话预设一个 token 预算比如输入不超过 6000输出不超过 1000。这样即使用户疯狂输入很长的内容也不会把整个请求体撑爆。除了输入侧控制输出侧也要设置好max_tokens。有些模型支持动态调整最大输出长度如果不显式设置默认行为可能导致输出被截断用户以为是服务不稳定其实是 token 预算用完了。4. 第二个真正的元凶限流与重试风暴另一个上线后才会逐渐暴露的问题是限流和重试机制。这块处理不好会直接把一次小故障放大成雪崩。4.1 429 限流并不只有“每分钟请求数”一种大多数模型服务商至少有三类限流维度RPM每分钟请求次数适合短请求场景。TPM每分钟 token 消耗量适合长 prompt、长回答场景。并发数同时进行的 in-flight 请求数量。只监控 RPM 很容易忽略 TPM 限流。比如你设置了每分钟 100 次请求但每次请求都携带 5000 token 的上下文很可能请求数没超token 数先爆了。因此日志里要把请求成功、限流、token 用量全部记录下来综合判断。4.2 无脑重试让故障扩散很多人的第一反应是“遇到 429 就重试”这恰恰是最危险的做法。限流说明你已经超过了服务商的配额继续重试只会让配额占得更满其他正常业务也被拖下水。更极端的情况是重试风暴打满服务商网关导致你没有配额可用的时间里所有请求都以 429 返回形成恶性循环。4.3 正确的重试策略指数退避加抖动重试只能用于暂时性错误比如网络抖动、503、短暂过载。对于 429如果在同一秒内重试几乎必然失败。标准的做法是指数退避加随机抖动import random import time def call_with_retry(func, max_retries4): for attempt in range(max_retries): try: return func() except RateLimitError: if attempt max_retries - 1: raise delay (2 ** attempt) random.uniform(0, 1) time.sleep(delay)难点在于拿到 429 响应头里的Retry-After字段。服务商让你等多久就等多久这个优先级高于任何退避算法。如果响应头里没有该字段再用指数退避兜底。4.4 客户端本地限流避免把请求全堵在网络层除了重试还要在客户端实现本地限流。我常用信号量控制最大并发数超过即排队或直接返回一个明确的错误码而不是把所有请求都塞到模型 API 面前。import threading semaphore threading.Semaphore(5) def limited_call(func): with semaphore: return func()这能让系统在高峰期保持稳定而不是被一小波突发流量打挂。4.5 缓存和合并请求也是降峰手段对于高频但内容相同的问题缓存模型回答是性价比最高的方案。我遇到过一个客服场景大量用户问“退款政策是什么”模型每次都要跑一遍推理费时又费钱。后来加了 Redis 缓存命中率超过 80%限流压力骤减。如果请求参数相似但结果不允许缓存还可以在业务层做请求合并同一用户 5 秒内的相同请求只往上游发一次其他请求等待结果返回后直接复用。5. 流式输出SSE在线上特别容易踩的坑对话类应用基本都会用 SSE 做流式输出但这部分在本地调试时很难暴露问题只有到了真实网络环境里才会花样百出。5.1 数据到了代理层被缓冲前端“卡死”SSE 的目的是让用户尽快看到第一个 token。但反向代理默认开启缓冲比如 Nginx 会把上游内容攒到一定大小再整体发给客户端导致前端迟迟收不到数据看起来就像卡住了。解决方法是显式关闭代理缓冲并增加响应头proxy_buffering off;在后端响应头里也需要标注X-Accel-Buffering: no Cache-Control: no-cache Content-Type: text/event-stream Connection: keep-alive这样 Nginx 和浏览器都不会对事件流做缓冲用户才能第一时间看到内容在生成。5.2 连接保持与空闲超时对抗SSE 连接一旦建立可能长时间维持但并不是每时每刻都在传输数据。模型在推理时可能是若干秒后才输出一个 token此时连接处于空闲状态。如果代理层的proxy_read_timeout设置得太小空闲时间一长就会断开连接前端表现为“答到一半断了”。我的做法是把proxy_read_timeout设置到 300 秒以上并在应用层主动发送心跳注释行比如每隔 15 秒发送一个: keep-alive。注释行对 SSE 协议来说是合法的能有效防止中间网络设备因为空闲而掐断连接。5.3 前端也需要心跳和自动重连机制即使服务端做了心跳前端的处理也不能马虎。我见过不少项目用原生 EventSource服务端一断连接就直接白屏没有任何提示。更稳妥的做法是用 fetch 自己读取流并在连接异常时按策略重连async function connectSSE(url, onMessage) { while (true) { try { const response await fetch(url, { headers: { Accept: text/event-stream } }); const reader response.body.getReader(); const decoder new TextDecoder(); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); // 解析 SSE 事件并调用 onMessage } } catch (e) { // 指数退避重连 await new Promise(resolve setTimeout(resolve, 1000)); } } }这里要注意不要用EventSource直接通信因为很多服务商要求通过自定义 Header 携带 token而EventSource不支持自定义 Header。fetch 流式读取是目前最通用的方案。5.4 服务端要避免一次性把整段文本塞进事件有些开发者图省事让模型完整生成完后一次性通过 SSE 发出去。这虽然也是合法 SSE但完全失去了流式输出的意义用户体验和普通 POST 请求没有区别还白白占用长连接。正确的做法是模型每产出一小段 token就立即包装成data:格式发送到客户端。这样首字节时间可以从好几秒降到几百毫秒用户感知到“响应很快”。6. 稳定性不是靠运气监控、预警与降级解决完上面几个根因后线上已经稳定很多。但我特别想强调稳定不是靠一次排查就一劳永逸的必须有监控和预案兜底。6.1 必须埋点的四个指标我建议最少埋四个指标指标采集方式建议告警阈值请求成功率统计非 2xx 与总请求数之比低于 99% 预警P50 / P95 / P99 延迟记录每个请求的总耗时计算分位点P95 超过 10 秒预警token 用量从响应 usage 字段获取TPM 达到配额 80% 预警限流次数单独统计 429 状态码连续 5 次预警尤其是延迟不要只看平均值。平均值非常容易被少数慢请求拉高但也会隐藏大量快速请求。P99 能反映最差的一批用户体验这才是线上稳定性最重要的参考。6.2 用 trace_id 打通全链路日志当某个请求慢到不可接受时如果没有链路追踪你很难判断时间花在了哪一段。我的方案是在入口处生成一个trace_id然后在网关、应用层、模型 API 调用层、日志系统里都打印这个字段。这样定位问题时只需要查一个trace_id就能看到请求在队列里等了多久、反向代理转发花了多久、模型 API 返回用了多久、业务处理消耗了多久。没有 trace_id 的日志在分布式环境里基本等于没有意义。6.3 降级方案不能等到故障发生才设计模型 API 是第三方服务谁也无法保证永远稳定。我强烈建议提前设计好降级策略至少有这三级第一级简单问题返回预置答案比如高频业务问答可以走缓存或本地规则。第二级切换备用模型服务商虽然效果不一定完全一致但至少保证业务可用。第三级关闭流式输出退化为完整返回摘要或者提示用户稍后再试。降级策略的关键是自动触发而不是人工介入。监控系统检测到连续多次 5xx 或超时就应该自动切换流量到备用通道。6.4 容量规划公式你需要的并发没你想的那么多计算并发需求的公式很简单最大并发 每分钟允许的请求数 × 平均请求耗时秒 / 60假设服务商给你 60 RPM平均每个请求耗时 20 秒那么理论上只需要 20 并发就能打满配额。但如果有 100 个用户同时点击你需要在客户端做排队而不是全部一股脑发出去。部署模型 API 网关时我会预估峰值流量并留出 1.5 倍余量同时在网关层配置超时熔断避免一个慢接口拖垮整个服务。7. 生产环境里的经验复盘三个我真实踩过的坑到这里技术层面的排查思路已经讲完。最后分享三个我在生产环境里遇到的具体故障每一个都花了不少时间才定位到根因。7.1 故障一连接池配置太大反而惹祸某个服务用 Java 的 RestTemplate 调用模型 API平时一切正常。某天大促流量上来后突然出现大量连接超时进程 CPU 也没高日志里全是“Connection pool timeout”。排查后发现是连接池最大连接数设得很高但模型 API 的平均响应时间很长导致连接全部被占用新任务拿不到连接只能干等。解决方法是把连接池大小与模型 API 的并发限制对齐同时给连接获取操作设置明确的等待超时避免线程无限阻塞。这个问题以前在普通 API 上不明显因为普通 API 响应快、连接周转快但大模型 API 动辄十几秒连接池设计思路完全不同。7.2 故障二只监控平均值P99 恶化到 40 秒才发现有段时间用户频繁反馈“特别慢”但后台监控显示平均延迟只有 3 秒。我一直不理解问题出在哪直到把延迟按分位点拆开才发现 P99 已经接近 40 秒P50 只有 1.5 秒。也就是说大部分请求很快但每 100 个用户里就有 1 个体验极差这种用户虽然不多但在社交平台上吐槽的往往就是他们。从那以后我把所有外部调用的监控全部改成“平均值 分位点”双看尤其关注 P99。对 AI API 这种强依赖第三方服务的场景只看平均延迟完全是在自欺欺人。7.3 故障三连接复用后出现大量 TIME_WAIT某个服务开启 keepalive 后网关机器的连接数反而暴涨。用ss -s一看大量连接处于 TIME_WAIT 状态。原因是我们虽然复用了连接但模型 API 每次返回后主动关闭连接且我们这边没有正确复用导致每次请求都新建一条 TCP 连接最后积压了大量 TIME_WAIT socket。解决方式是调整客户端连接池的 keepalive 时间同时确认服务器端也启用了 HTTP keepalive保证一条连接能够服务多个请求。这个问题在长耗时接口上尤其明显因为连接占用时间长复用率低TIME_WAIT 的数量会呈指数级增长。如果你也正准备上线 AI 相关功能我的建议是先别急着迭代产品功能花一天时间把上面这些链路检查一遍。我在这次排查里最大的收获不是修好了某一个 bug而是建立了一套“稳定性基线”成功率、P95、限流次数、token 用量全部量化任何指标异常都能第一时间定位到具体环节。这套方法放到任何一家模型服务商身上都通用毕竟外部 API 永远会有波动真正决定线上体验好坏的是你自己在周边系统里打下了多深的基础。
返回列表