这个标题其实问到了很多团队的心坎上。我自己接过不少类似的问题,现象都差不多:本地 Postman 调 DeepSeek、GPT 这类大模型 API,一次就通,返回结果漂漂亮亮;等部署到测试环境甚至生产环境,就开始各种妖蛾子——一会儿超时,一会儿连接被重置,一会儿拿到空内容,日志里躺着一堆看不懂的错误码。更头疼的是,你单看每一次请求似乎又都是"成功"的,可整体服务就是不稳。问题到底出在哪?
先说一个反直觉的结论:"调用成功"这个反馈,本身就是最容易误导人的信号。HTTP 200 只代表你在某个瞬间拿到了一个响应,它不承诺你的系统能持续稳定地拿到正确响应。从"能通"到"能上线稳定跑",中间隔着的是一整套工程问题:超时策略、重试机制、上下文管理、并发控制、流式传输的保活处理,以及可观测性建设。这篇文章就把我实际踩过的坑、排查过的链路和最终落地的方案完整拆开讲,希望能帮正在被"AI API 线上不稳定"折磨的人少走弯路。
1. 先搞清楚一个根本问题:你的"成功"只是开发环境的成功
很多团队在接入 AI API 时,验证成功的标准是"我调用了一次,它返回了结果"。这个标准放在开发环境够用,放在生产环境就是灾难。原因很简单:开发环境是单次请求、低并发、干净网络,而生产环境是连续不断的请求流、共享网络、高并发、恶劣的网络抖动。
1.1 开发环境和生产环境是两个物种
开发环境里,你调用一次 API,网络干净,服务端压力小,模型推理一个简单任务可能就一两秒。生产环境呢?你的服务可能同时有几十上百个请求在调同一个 API,你所在的公司网络出口可能还有防火墙、代理、负载均衡器层层转发。
我遇到过最典型的一个例子:本地调用一个多轮对话接口,响应时间 2 秒,特别稳定。上线后,同一套代码在客户现场动不动就 30 秒才返回,甚至直接断连。最后排查下来,是客户现场的出口网关对 HTTP 连接有闲置超时限制,而我们的客户端没有配置 TCP Keep-Alive,连接池里的连接被网关静默回收了,下次请求还在用这根"死连接",自然频繁报错。这就是典型的开发环境根本遇不到、生产环境天天见的问题。
1.2 单次成功与连续请求流的本质差异
你还要理解一件事:AI API 的响应时间不是稳定的常数。同样一个模型,你问"1+1等于几"可能 500 毫秒就返回,你让它分析一份 5000 字的合同,可能要 30 秒甚至更久。而且模型的负载也在波动——上游 API 服务商高峰期排队时间变长,你的响应时间就会跟着涨。
这就意味着,如果你在开发环境测出"平均 2 秒返回",然后拍脑袋把超时时间设成 5 秒,那线上只要遇到一次复杂请求,或者上游排队,就直接超时失败。你把"均值"当成了"上限",这本身就是不稳定的根源之一。
所以,做线上稳定性第一步,是把心态从"能不能调通"切换到"在不确定的网络和负载环境下,如何保证整体可用"。后面的所有方案,都是围绕这个心态展开的。
2. 线上不稳定最常见的五个根因,按排查优先级排序
我排过不少 AI API 相关的线上事故,总结下来,90% 的不稳定都逃不出下面五个根因。我按出现频率从高到低排,你可以照着这个顺序去排查自己的系统。
2.1 超时设置一刀切,复杂请求必被误杀
这是最常见、也最容易忽视的坑。很多人用 HTTP 客户端请求 AI API 时,超时时间设置得很随意,或者干脆用默认值。但这个"默认值"往往是给普通 HTTP 接口设计的,根本不适合大模型这种"耗时不确定"的接口。
举几个真实参数供参考:
| 类型 | 合理设置 | 说明 |
|---|---|---|
| 连接超时(connect timeout) | 3~5 秒 | 建立 TCP 连接的时间,超过这个基本是网络问题 |
| 读取超时(read timeout) | 非流式 60~120 秒,流式 300 秒以上 | 指等待响应数据的间隔时间 |
| 总超时(request timeout) | 根据业务场景设定,通常 60 秒以上 | 整个请求从发起到完成的总体时间 |
很多人容易把"读取超时"和"总超时"搞混。读取超时是你等响应数据的间隔——比如这个包等了 10 秒还没收到就报错;总超时是整个请求的生命周期上限。对流式接口来说,总超时设长一点没关系,关键是读取超时不能太短,因为模型思考的过程可能有一段时间没有任何数据返回。
踩过的坑:某次线上频繁超时,查了半天,最后发现是 HTTP 客户端的读超时被设成了 10 秒。平时用没问题,一旦用户问的问题复杂一点,模型"思考"时间超过 10 秒,客户端就主动断开了——但模型那边还在正常生成,白白浪费了一次调用。
2.2 重试机制不分青红皂白,反而放大了故障
重试是稳定性手段,但盲目的重试就是灾难。很多人写代码习惯了对所有异常一律重试 3 次,这个习惯在大模型 API 场景下特别危险。
为什么?因为 AI API 的请求比普通接口贵得多——不只是钱,还有延迟。如果你在一个超时请求上机械地重试 3 次,等于把原本 30 秒的请求变成了 90 秒的连环调用,而且三个请求会同时占用上游资源,加重服务商负载。更可怕的是"重试风暴":你的服务一抖动,所有请求同时发起重试,上游 API 直接被你的重试流量打爆,然后返回限流错误(429),你再重试,它再限流……雪崩就是这么来的。
正确的重试策略应该是:
- 哪些错误可以重试:429(限流)、500/502/503(服务端临时故障)、连接超时、连接重置。
- 哪些错误不要重试:400(参数错误)、401(鉴权失败)、403(权限不足)、404(接口不存在)。这些重试一万次结果都一样,纯浪费。
- 重试必须配合退避:每次重试的间隔要指数增长,比如第一次等 1 秒、第二次等 2 秒、第三次等 4 秒,最多重试 2~3 次。
- 重试要考虑业务幂等性:如果你的业务在重试前已经给用户展示了一部分内容,重试后可能会重复计费或重复插入数据。
2.3 上下文管理缺失,token 膨胀拖垮一切
这是 AI API 场景最独特、也最容易被忽略的坑。普通 HTTP 接口,请求体大小基本固定;但大模型 API 的请求体里装着对话历史,而对话历史是不断增长的。
我见过太多团队把用户所有聊天记录一股脑全塞进上下文,不做任何截断。结果就是:用户聊了 50 轮之后,每次请求都要把巨大的上下文重新发送一遍,响应时间越来越慢,token 费用越来越高,最终撞上模型的上下文窗口上限(比如 4096 token 或者 128K token),直接报400 this model's maximum context length is exceeded之类的错误。
这类问题通常不是突然爆发的,而是缓慢劣化——今天用户聊 20 轮没问题,下周聊到 50 轮开始变慢,再下周直接报错。但你很难把它和"线上不稳定"联系起来,因为错误码是 400,看起来像参数问题,实际上是上下文管理问题。
解决思路后面会详细讲,核心是:永远不要无脑把全量历史都发给模型。你需要滑动窗口、摘要压缩、关键信息提取的组合策略。
2.4 并发控制缺失,被上游限流打得措手不及
AI API 服务商基本都有速率限制,而且不止一层。常见的限流纬度包括:
- RPM(Requests Per Minute):每分钟请求数限制。
- TPM(Tokens Per Minute):每分钟 token 消耗量限制,这个最容易被忽略——你请求数不多,但每个请求上下文巨大,照样被限流。
- 并发数限制:同一时间最多允许的并行请求数。
很多团队只关注了 RPM,忽略了 TPM 和并发限制。结果就是:你的请求数可能没超,但某个时刻一个 10 万 token 的请求直接把你的 TPM 配额打满,后续所有请求都被 429。
更隐蔽的问题是客户端自己没做并发控制。假设你的服务通过一个全局的 HTTP 客户端调用 API,并发峰值时 100 个请求同时发出。上游服务商看到你在短时间内打进来 100 个请求,直接给你限流失效。而如果你在客户端做一层"并发信号量"——比如同时最多允许 10 个请求并发,其余排队——不仅不会触发上游限流,还能让整体响应时间更稳定。
2.5 SSE 流式响应的"假死":连接还在,数据不来了
如果你用的是流式接口(SSE),那你还会遇到一个普通 REST 接口完全遇不到的问题:连接一直活着,但数据就是不来。很多人调试时发现,流式接口在模型"思考"较长时间时,中间会出现几十秒没有数据的情况。如果客户端或中间网络设备设置了空闲超时,就会把这条连接干掉,你的请求就"死"了——但它没有报超时错,而是连接被重置,错误信息五花八门,有时候甚至是"连接已关闭"这种莫名其妙的消息。
另外还有一类隐藏很深的坑:SSE 响应被代理服务器或负载均衡器缓冲。如果中间有一层 nginx 开启了响应缓冲,它会等上游攒够足够数据才一次性转发给客户端,那你的流式效果就完全没了——客户端等半天没反应,然后突然收到一大坨数据,体验极差,甚至会因为客户端"长时间没有收到任何数据"而主动断开连接。
3. 一个真实排查案例:从偶发超时到根因定位的完整链路
上面的根因排查看起来很全,但实际排查时很少有人能一眼定位。我分享一个自己处理过的案例,完整还原一下"偶发不稳定"是怎么一步步被查出来的。
3.1 现象描述:批量调用场景下的偶发超时
当时我们有个功能,需要对一批文档做摘要提取,逻辑是循环调用 AI API,每次传一个文档让模型总结。本地测试 20 篇文档全部成功,耗时约 3 分钟。上线后,跑完 20 篇文档,总有 3~5 篇报错,错误信息主要是两种:Request timed out和Connection reset by peer。而且很诡异的是,每次报错的文档不确定,这次是第 3、8 篇,下次可能是第 5、15 篇——没有规律。
3.2 排查过程:从日志到错误的逐步收敛
第一步,看监控。我们把请求耗时、错误码、超时时间都打点记录下来,发现问题集中在两个时段:每天早上 10 点和下午 3 点,正好是业务高峰,API 响应时间从平时的 3 秒涨到 15 秒以上。
第二步,看错误码分布。发现大量 429 和 503。429 说明触发限流,503 说明上游繁忙。这说明问题不只是客户端,上游压力也大。
第三步,这步是关键——我们把每次请求的上下文大小也打点统计了。结果发现,越到后面的文档,累计的上下文越大——因为我们代码里不小心把前几篇文档的摘要也拼进了后面的请求上下文中。也就是说,第 20 篇文档的请求体,比第 1 篇大了好几倍。这是双重打击:上下文越大,请求越慢;请求越慢,越容易撞上上游的繁忙期;一旦撞上繁忙期,上游开始限流,重试机制又火上浇油。
第四步,检查客户端连接池配置。发现连接池最大连接数被设成了 200,但服务实际并发根本用不到这么多。当一批文档循环调用时,前一个请求还没结束,下一个请求又创建了新连接,短时间内在同一台内网机器上打开了几十个到上游的连接。上游检测到这种高频新建连接的行为,直接当成异常流量处理。
3.3 最终定位与修复方案
三层问题叠加,导致了"偶发不稳定"的表象:
- 上下文没有截断,请求体越来越大,响应逐渐变慢。
- 客户端并发没有限制,加上连接池过大,短时间建立大量连接,触发上游限流。
- 重试没有退避,限流后又快速重试,加重了上游负担。
修复方案也不复杂:
- 上下文改成滑动窗口,只保留最近 3 轮对话和最新的文档内容。
- 客户端并发数限制到 10,多出来的请求排队等待。
- 连接池最大连接数从 200 调低到 20。
- 重试策略改为:429 退避 2 秒重试,最多 2 次;503 退避 5 秒重试,最多 3 次;其它错误不重试。
改完之后,同样的 20 篇文档,一次成功,耗时反而从 3 分钟降到了不到 2 分钟。因为减少了很多无谓的重试和排队时间。
4. 让 AI API 调用稳定下来的工程化方案
踩过一遍坑之后,我后来把 AI API 的接入方案沉淀成了一整套可复用的工程模板。这里把最关键的部分分享出来。
4.1 超时与重试的正确姿势
不要手写每个请求的超时逻辑,直接用统一的配置对象管理。核心区分三组时间:连接超时、读取超时、总超时。
以 Java(OkHttp)为例,一个合理的配置长这样:
OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(300, TimeUnit.SECONDS) // 流式接口必须给足够长的读超时 .writeTimeout(5, TimeUnit.SECONDS) .connectionPool(new ConnectionPool(20, 5, TimeUnit.MINUTES)) .build();Python(httpx)版本:
client = httpx.AsyncClient( timeout=httpx.Timeout(connect=5.0, read=300.0, write=5.0, pool=5.0), limits=httpx.Limits(max_connections=20, max_keepalive_connections=10), )重试逻辑建议独立封装,不要散落在业务代码里。伪代码思路:
attempt = 0 while attempt < max_retries: try: return await call_ai_api() except AIAPIRateLimitError: wait = 2 ** attempt # 指数退避 await asyncio.sleep(wait) except AIAPITimeoutError: wait = 1 if attempt == 0 else 3 await asyncio.sleep(wait) attempt += 1 raise AIServiceUnavailableError注意:重试只对"瞬态错误"有效。如果连续 3 次都是同一个错误,大概率是业务代码或网络环境问题,不要再盲目重试了。
4.2 上下文压缩与滑动窗口:治本的关键
管理上下文是 AI API 稳定性里最核心的一环。我推荐三层策略组合:
第一层:滑动窗口。只保留最近 N 轮对话。比如:
def build_context(messages, max_rounds=6): # 保留系统提示 + 最近 6 轮对话 recent = messages[-max_rounds:] if len(messages) > max_rounds else messages return recent第二层:摘要压缩。如果对话确实需要保留更早的信息,就把前面的对话用模型本身总结成一段摘要,作为系统提示的一部分传进去。这比直接截断更智能,也避免信息完全丢失。
第三层:Token 预算控制。在组合请求体时先估算 token 数,如果超过预算,优先压缩较早的对话,而不是最新的。这里可以用简单的字符数/token 数比例估算,或者用模型的 tokenizer 接口精确计算。
def truncate_context_by_token_budget(messages, max_tokens=20000): # 从最旧的消息开始裁剪,直到总 token 数降到预算内 total_tokens = sum(count_tokens(m["content"]) for m in messages) truncated = messages.copy() idx = 0 while total_tokens > max_tokens and idx < len(truncated) - 1: removed = truncated.pop(idx) total_tokens -= count_tokens(removed["content"]) return truncated4.3 熔断与降级:别让 AI API 拖垮整个系统
AI API 是外部依赖,它随时可能故障。你的系统要有能力在它故障时优雅降级,而不是跟着一起挂。
熔断器的逻辑比较简单:统计最近窗口内的错误率,超过阈值(比如 50%)就打开熔断器,后续请求直接走降级逻辑,不再真实调用 AI API。经过一个冷却时间后,放少量请求试探,成功率达到阈值就关闭熔断器恢复流量。
降级策略要看业务形态。常见的有:
- 返回缓存的旧结果:如果用户的问题和之前某次相似,直接把旧答案返回。
- 使用更快的轻量模型:比如主模型 DeepSeek-V3 超时了,降级到 DeepSeek-V4-Flash 之类的快速模型,牺牲一点质量换可用性。
- 简化流程:多轮对话降级为单轮,摘录式总结降级为提取式总结,至少给用户一个"能用的答案"。
这些降级策略在正常情况下不会触发,但在上游故障时会救整个系统一命。
4.4 可观测性:没有监控,稳定性无从谈起
最后但最重要的一步:把一切变成可观测的数据。你无法优化一个看不到的系统。针对 AI API 调用,我建议至少跟踪以下指标:
| 指标 | 含义 | 为什么重要 |
|---|---|---|
| 请求量 | 每秒/分钟调用次数 | 了解峰值流量 |
| 成功率 | 成功请求 / 总请求 | 最直观的稳定性指标 |
| 延迟分位数 | p50/p95/p99 响应时间 | 中位数看不出问题,p99 才能暴露尾延迟 |
| Token 消耗量 | 每请求输入/输出 token 数 | 费用监控 + 上下文膨胀预警 |
| 错误码分布 | 400/401/429/500/503 各自占比 | 快速判断是客户端问题还是上游问题 |
| 上下文长度 | 每请求的输入 token 数 | 发现上下文泄漏、膨胀问题 |
日志方面,除了常规的request_id、model、latency、error_code,我还会打印输入和输出的 token 数,以及请求体的粗略大小。这样一旦线上出问题,可以很快判断是"某个用户的上下文太长"还是"上游整体变慢"。
5. 那些容易被忽略的细节坑,我一个个踩过来的
最后补充几个我在实践中反复踩的坑,它们不在任何官方文档里,但每个都让线上出过事。
5.1 连接池和 DNS 缓存是隐形杀手
连接池配置不当的问题前面提到过。这里再补一个:DNS 缓存。有些 AI API 服务商会做 DNS 负载均衡,同一个域名在不同时段解析到不同的 IP 地址。如果你在客户端缓存了 DNS 结果(Java 默认缓存 30 秒,某些框架会缓存更久),当上游某个 IP 出问题时,你的请求会一直打向那个故障 IP,即使其它 IP 是健康的。
建议:显式设置 DNS 缓存时间,或者定期刷新。Python 的httpx支持自定义 transport 来调整 DNS 解析行为,Java 则通过JVM_DNS_TTL参数控制。
5.2 代理服务器和负载均衡器会"吃掉"你的流式响应
如果你的服务中间有 nginx、网关或负载均衡器,一定要确认它们对流式响应(SSE)的处理行为。很多默认配置会对响应做缓冲(proxy_buffering on),结果就是流式变成了"攒一批发一批"。这在用户量大的时候会导致大量连接被长时间占用,最终引发连接数耗尽——你以为是 AI API 不稳定,其实是自己的网关被拖垮了。
解决方式是在网关层对 SSE 接口关闭缓冲:
location /v1/chat/completions { proxy_buffering off; proxy_read_timeout 3600s; proxy_http_version 1.1; proxy_set_header Connection ""; }5.3 空响应和异常响应的防御性解析
AI API 偶尔会出现 HTTP 200 但响应体里没有内容的诡异情况——模型返回了空字符串,或者只返回了空的 choices 数组。如果你的代码直接拿choices[0].message.content去用,恭喜你,拿到一个undefined或者空指针。
建议对所有 AI API 响应做一层防御性校验:
def extract_content(response): if not response.get("choices"): raise EmptyAIResponseError("No choices in response") message = response["choices"][0].get("message", {}) content = message.get("content") if not content: raise EmptyAIResponseError("Empty content in message") return content这类问题不会频繁出现,但一旦出现,就会导致线上"偶发性返回空数据",非常难排查。
5.4 模型版本漂移:同一个 Prompt 不同时间可能不同结果
这是一个几乎无法"修复"但你必须知道的问题:AI 模型不像传统软件有固定版本行为。就算你锁定了model=deepseek-chat,服务商那边的模型权重也可能在某个节点悄悄更新——你感觉不到,但输出风格、质量、甚至某些输入的响应方式都会变。
这意味着两件事:
- 如果你的业务对输出格式有严格依赖,一定要用 JSON mode 或 function calling,并做好 JSON 解析失败的重试和降级。
- 如果你的业务做的是"一致性"敏感的场景(比如自动生成测试用例、批量生成同一类文案),建议定期跑一组固定的回归用例,确认模型输出没有明显漂移。
5.5 字符编码和特殊字符:最后一个不起眼的大坑
这个是真实发生过的:某个用户上传了一篇文章,内容里有一个特殊符号(不是中文标点,而是某个 Unicode 变体),AI API 的响应因此变成了非法 JSON——里面的引号、反斜杠没有正确转义。你的 JSON 解析器直接崩溃,报了个"语法错误",看起来像是代码 bug,实际上是输入内容触发了模型输出的异常编码。
防御性做法:解析 AI 响应时,如果 JSON 解析失败,不要直接抛异常,先做一层清洗(比如移除非法控制字符、修正未转义引号),再尝试解析。或者使用更宽容的解析器(如json5、demjson),容忍一些不规范但可修复的 JSON。
我一直觉得,AI API 的接入门槛确实低——几行代码就能调通,但真正让它稳定承载业务,比拼的是工程细节。上面这些点,每一条都是拿线上事故换来的经验。如果你现在正被"AI API 调用成功了,线上还是不稳定"困扰,不用急着改代码,先按第二条里列的五个根因,对着自己的系统排查一遍,大概率能找到症结。稳定性不是某个瞬间的正确,而是每个请求的从容。