大模型 API 调用工程实践:流式输出、重试、限流与结构化返回

这篇文章从 构建一个生产级ai对话框 的实际业务出发,解析了一些开发者可能遇到的问题,并尝试解答。

1 LLM 的调用

1.1 LLM 调用大致流程

流程如下:

  1. 客户端请求,服务器鉴权:用户(客户端)提出访问请求,发给服务器(开发者的),服务器需要确认该请求的用户、套餐、配额,确认是否允许此次访问;

  2. 服务器组装 prompt:若访问合法,则服务器会将 system prompt、历史信息、RAG 知识、MCP 工具包、用户 prompt、输出格式等信息组装成需要 llm 处理的 prompt;

  3. 服务器 token 估算:组装好的 prompt 会被 Tokenizer 拆分为 token,同时计算 token 输入量,估算需要给输出留多少 token 量,并决定是否压缩上下文、裁切上下文、是否可以换用更加契合需求的模型;

  4. 模型网关路由:选择模型、供应商、区域等路由工作。此外模型网关还肩负这一个重要职能:全权确保大模型的输出稳定,所以还包含了超时参数、重试策略、限流桶等;

  5. 供应商 LLM 工作:服务器将请求发往供应商,请求调用大模型,于是在供应商的服务器中 LLM 就会用于推理,将输出流式或同步地发往服务器;

  6. 模型网关解析相应:还是模型网关,前面提到模型网关另一大职能是确保大模型的输出稳定,供应商将 LLM 的输出返回服务器后,需要模型网关来处理响应,例如残缺的结构化输出要补全、异常的大模型调用要重试、过多的输出要做限流等;

  7. 模型网关封装输出:大模型响应解析完毕后,保存完整回答、增量片段、Token 用量、调用成本、失败原因和业务状态;

  8. 模型网关检测&告警:记录 traceId、providerRequestId、TTFT、总耗时、重试次数、429 次数、解析失败率。

1.1 模型网关为什么这么重要?

模型网关与常规的代理,例如 nginx 这种 只负责路由代理,不负责实际业务 的组件有着本质不同,大模型的返回与传统 api 输出来说相当不稳定:1.由于返回量大,很有可能中途截断,或者输出过多;2.大模型的网络状况复杂,响应时间太长。。。。。。一旦这些问题直接被返回给业务服务器,不仅会加剧服务器负担,还会要求服务器具备给大模型输出兜底的能力,又是一笔人员开发成本。

所以,我们要求所有关于大模型的问题,在模型网关这个组件中(当然,模型网关也属于开发者服务器)就一次性解决。根据业务选择合适的模型、网络路由、解析大模型响应、失败重试、流量管控、检测告警等操作,都需要在模型网关内完成。

2. 流式输出

2.1 流式返回 vs 同步返回

一般而言,调用 llm 后,服务器等待 llm 将答案全部生成完毕后,再将答案一口气返回给客户端,这就是 同步返回

流式返回 则是:llm 每生成一个 token,服务器就将该 token 同步返回给客户端,当 llm 生成结束时,服务器也差不多把答案返回完成了。

流式返回降低 TTFT Time To First Token

因为不需要等待 llm 生成完毕了。

增量片段

而在谈到大模型的增量输出时,往往针对的是 流式输出 ,我们知道流式输出是 llm 生成多少,服务器就响应给客户端多少,每一段原子时间内的响应就是增量片段。服务器接受到增量,直接将其拼接到上一个时间拼接起来的全量后。如下表所示:

时间步 (Time) 供应商服务器实际吐出的增量(Delta) 客户端/网关拼出来的全量(Full)
T1
T2 今天
T3 今天气
T4 今天天气好

而在同步返回中,直接一次性返回全量输出。

2.2 流式返回的三种协议

看不懂,先不学

3 流式输出异常怎么办

3.1 用户取消

用户主动中断大模型调用时,后端应该同步关闭:

  • 到供应商服务器的请求;

  • 如果已经拿到了 llm 响应,那就直接丢弃;

  • 相关后续任务

3.2 超时

三种超时:

  • 连接超时:连不上供应商。

  • TTFT 超时:连接上了,但迟迟没有第一个事件。

  • 总时长超时:一直有输出,但超过业务可接受时间。

没啥解决办法,只能做好日志记录。

3.3 断流

比如输出太长超过了 max_tokens,导致输出被截断,这个时候绝对不能把它直接给客户端,要在模型网关内检查是否有 finish_reason 等字段,如果确认 interrupted 了,前端可以显示 已中断,请重新尝试

3.4 重连

大模型的调用重连,在多数情况下,无法真正从 Token 级别续上。

4 调用大模型时重试?

大模型最大的问题是:同样的上下文,同样的提示词,每次预测的输出都不一样,所以在没有确认肯定是连接到供应商服务器的网络中断这一原因时,尽量不要重试

有关重试,java guide 自己总结了一些:

类型 示例 是否建议重试 处理方式
网络瞬断 连接重置、DNS 抖动、读超时 可以 指数退避 + 抖动,限制最大次数
供应商 5xx 500、502、503、504 可以 短暂重试,超过阈值切换模型或降级
供应商过载 Anthropic 529、类似 overloaded 错误 可以 慢重试,必要时熔断该供应商
429 限流 RPM、TPM、RPD、并发限制超出 谨慎 优先看 Retry-After 和限流头,排队或降级
流式中断 未收到正常结束事件 视场景 用户可见任务不自动重试,后台任务可幂等重试
400 参数错误 Schema 不合法、字段缺失、上下文超限 不建议 修请求,不要重试同一 payload
401/403 鉴权错误 API Key 无效、权限不足 不建议 告警并停用对应 Key
安全拒答 内容策略拒绝 不建议 进入业务拒答流程
解析失败 JSON 不完整、字段类型错误 可有限重试 带失败原因二次修复,最多 1-2 次

简单来说:

  • 网络问题的可以重试,这个时候还没有连接到供应商,或者供应商的大模型还没真正处理请求;

  • 供应商拒绝响应请求时,例如额度耗尽、api key 过期或者内容涉嫌安全问题时,供应商主动拒绝,所以再重试也没有意义;不过有一个例外是 429 限流,有可能真的只是供应商压力大了呢?可以等待一下然后再重试。

  • 大模型输出有质量问题,这个时候一般来说不建议重试了,因为我们要极力避免大模型重复处理同一个请求。不过要做好了日志记录,比如增量片段序列,可以少量重试,尝试修复质量问题。

4.1 指数退避

OpenAI 建议在 rate limit error 时使用随机指数退避进行重连,即重试时:第 1 次失败等一小会儿,第 2 次失败等更久,第 3 次再更久,直到达到最大等待时间或最大重试次数。

4.2 抖动

抖动是防止大量重试时,因为请求拥挤造成的系统爆炸。核心:给请求重试加一个随机时间:

1
sleep = min(maxDelay, baseDelay * 2^retryCount) + random(0, jitter)

4.3 硬约束

  • 最大重试次数 = 2:重试两次即可,再多没意义;

  • 总体截止时间:从用户发请求开始算起,一直到得到响应的时间限定在一个时间内

4.4 幂等性

有重试就必须考虑幂等性,而幂等性在后端里面都快做烂了:给请求加 key,发送请求去掉重复请求即可。

  • 如果初次请求已经成功,后续直接读结果;

  • 如果初次请求正在生成,返回后续一个结果生成的地址,等生成结束后在这个地址读结果;

  • 如果初次请求失败且正在尝试重试,后续尝试发请求,但是 key 不变;

  • 如果初次请求失败,且不允许重试,则直接返回失败结果。

4.5 重试后响应内容有重复怎么办

比如有时候大模型响应时间太长,导致服务器认为响应挂了,又重试了一遍,但此时响应完成了输出又返回服务器了,然后重试的输出也返回服务器了,这样一来两份内容对应同一个请求,内容大概率有冲突、重复、重叠。

对聊天类应用,建议把一次用户消息下的多次模型调用区分为:

  • message_id:业务消息 ID,对用户可见。
  • attempt_id:模型调用尝试 ID,对系统可见。
  • provider_request_id:供应商请求 ID,用于排查。
  • stream_sequence:增量片段序号,用于去重和补发。

增量片段序号补发

我们默认供应商服务器是没有问题的,供应商每次提供流式输出的增量片段可能因为网络原因到不了我们的业务服务器,用序号要求供应商服务器进行补发就行。

5 限流

5.1 429

429错误:API Error: Request rejected(429),超出限额了

前面说了,这篇文章是以 开发 AI 聊天框 为出发的,一旦报错 429 ,说明我们的 api key 限额完全用光了,一个 api key 需要共享给多用户调用,假如我们没有其他的 api key,那么服务直接就崩溃了,相当严重。所以这里 java guide 说

1
很多团队的限流意识,是从收到第一个 429 开始的。这已经晚了。 等供应商把你拦住,说明你的系统里根本没有容量管理。 供应商的 429 是最后一道墙——如果你把它当容量规划工具用,迟早会在流量尖峰时被连续打脸。

5.2 四层限流架构

层级 限制对象 核心目的 常见策略
用户级 单个用户或账号 防止滥用、误用和脚本刷写接口 每分钟请求数、每日 Token 上限
租户级 企业、团队、项目 控制套餐成本和公平性 月度配额、并发上限、优先级队列
模型级 某个模型或模型族 避免热门模型被打满 模型维度令牌桶、降级到备用模型
供应商级 OpenAI、Anthropic、Gemini 等 保护外部依赖和 API Key 全局 RPM、TPM、并发、熔断

RPM: Request per Minute

TPM: Tokens per Minute

令牌桶:就是并发锁,比如我们将 claude opus 4.7 的最大请求并发数设置为 100,这就是一个令牌桶,只有拿到并发锁的请求才能成功调用,不然就反复重试尝试拿到并发锁。

具体怎么做?为每个层级下的所有个体进行 token 用量统计 & 并发监控,超过一定程度就进行降级、限流,直至停止服务,力求绝不能因为单一个体的异常影响其他个体的。例如对于一个请求,我们可以假设这样一条限流关卡:

1
2
3
4
1. 查 User ID:你今天或者眼下短时间内用超额了吗?(没有,直接放行;超额了,拒绝请求,等待额度回归)
2. 查 Tenant ID:你们公司欠费了吗?又或者并发太高?(没有,直接放行;欠费了,拒绝;并发太高,做限流)
3. 查 Model:你请求的 GPT-4 现在拥挤吗?(拥挤,稍微排队1秒,放行)
4. 查 Provider:我们公司在 OpenAI 的额度快满了吗?(没满,放行,最终路由给供应商服务器)

5.3 调用大模型相比传统 API 调用必须考虑 TPM

传统的 API 调用,只负责计算调用数量,也就是 QPS,而大模型除了考虑 QPS 或 RPM,TPM 也必须考虑,因为在 QPS 上均只占一次的两条请求,token 花销甚至可能在十倍以上,显然只计算 QPS 是不对等的。

另外,用户真正调用的时候,先扣额度,再发请求

5.4 限流策略

策略 适合场景 优点 缺点
固定窗口 简单后台任务、管理接口 实现简单,容易统计 窗口边界容易突刺
滑动窗口 用户级请求限制 边界更平滑 实现和存储成本更高
令牌桶 模型调用、Token 预算 支持一定突发,工程上常用 参数需要调优
漏桶 严格平滑出流量 输出稳定,适合保护供应商 突发体验差
并发信号量 流式生成、长任务 能限制同时占用连接 不控制单个请求 Token 成本
优先级队列 多租户、多套餐 能保护高优先级请求 需要处理饥饿和超时

上面四层限流架构里用的都是以上策略

6 结构化输出

什么是结构化输出?就是将模型本身输出的自然语言封装成结构化的语言,比如 JSON、XML 等。举个例子,原本模型的输出是:

1
你好,我叫李建国,今年三十五岁了。我老家是山东的。我大学是在哈工大读的,学的是机械工程。我会用 AutoCAD,也会一点 SolidWorks。过去五年我一直在比亚迪干结构工程师。哦对了,我现在不想找新工作,就是想在你们平台登记一下。我的邮箱是 jianguo_li@example.com,手机号暂时停机了就不留了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"personal_info": {
"full_name": "李建国",
"age": 35,
"hometown": "山东"
},
"education": {
"university": "哈尔滨工业大学",
"major": "机械工程"
},
"skills": [
"AutoCAD",
"SolidWorks"
],
"work_experience": [
{
"company_name": "比亚迪",
"job_title": "结构工程师",
"duration_years": 5
}
],
"contact": {
"email": "jianguo_li@example.com",
"phone_number": null
},
"is_actively_looking": false
}

结构化输出不是面向人类的,而是面向服务的,对于这种 JSON 输出,后端服务器可以直接转化成对象,易读性极高。

6.1 怎么强制化结构化输出

为了实现结构化输出,大模型一般有三种输出模式:JSON Mode、JSON Schema 和 Structured Output

方式 约束强度 工程价值 风险
普通自然语言 几乎没有 适合展示型回答 不适合程序解析
Prompt 要求 JSON 简单、跨模型 容易混入解释文本或缺字段
JSON Mode 通常能保证语法是 JSON 不一定符合业务字段 Schema
JSON Schema 明确字段、类型、必填、枚举 不同供应商支持子集不同
Structured Output 更强 供应商在解码或 SDK 层增强约束 受模型、SDK、Schema 子集限制
Function Calling / Tool Use 面向动作 适合让模型选择工具和参数 不是最终自然语言答案的万能替代

6.2 未能结构化输出怎么办

这种情况下,需要后端服务器来兜底,分成四级:

  • 本地校验:用 JSON Schema、Jackson、Bean Validation 校验字段和类型。
  • 轻量修复:只让模型修复格式,不重新生成业务内容。
  • 降级 Schema:复杂对象拆成多个小对象,或先分类再抽取字段。
  • 人工或规则兜底:高价值订单、金融、医疗、法务场景不要完全依赖自动修复。

6.3 流式输出结构化输出

前面提到,模型有可能无法输出完整的结构化输出,若后端服务器一拿到增量片段就贸然转成对象,百分比报错,那么就需要后端先拼接成完整输出,进行校验无误后再转成对象;如果有误再转到兜底策略。

7 调用 LLM 的注意事项

必须要做好模型网关,分散业务不要直接调用供应商的 SDK,必须走模型网关;

先扣额度再发请求;

没有指标就没有稳定性,必须记录好每一次调用的详细信息。
一些需要记录的指标:

1
2
3
4
5
6
7
8
9
10
11
>指标|	含义	|用途
>---|---|---
>TTFT| 首个 Token 返回时间| 判断排队、上下文过长、供应商抖动
>E2E Latency| 端到端完成时间| 判断用户体验和 SLA
>Input Tokens|输入 Token |成本分析、上下文膨胀排查
>Output Tokens|输出 Token| 成本分析、异常长回答排查
>Retry Count |重试次数| 识别供应商不稳定或策略过激
>429 Rate |限流比例| 判断配额和限流桶是否合理
>Parse Failure Rate |结构化解析失败率| 判断 Schema、Prompt、模型适配问题
>Cancel Rate |用户取消比例| 判断响应太慢或生成太长
>Provider Error Rate |供应商错误率| 路由、降级、熔断依据

日志需要记录的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>trace_id
>tenant_id
>user_id
>conversation_id
>message_id
>attempt_id
>model
>provider
>prompt_version
>input_tokens
>output_tokens
>ttft_ms
>latency_ms
>retry_count
>finish_reason
>error_type
>provider_request_id

幂等性在同一个请求 id 里进行,绝对不要将同一个请求拆成两个 id。

同步输出与流式输出分别处理,流式输出要记录号增量序列,利于补发重传。

流式输出结构化输出,不能同步到客户端,必须全部接受完后在服务器进行校验。

8 面试题

1.一次完整的 LLM 调用流程是什么?

1
2
3
4
5
6
7
8
客户端发起请求 
=> 服务器鉴权,决定是否放行
=> 服务器内拼接 prompt,包括系统提示词、mcp tool、历史上下文、用户提示词等
=> 服务器内的模型网关根据请求信息,估算 token 用量,然后决定路由到哪个供应商的哪个模型
=> 供应商服务器内接受请求,鉴权通过后让 LLM 处理请求,并通过同步或流式输出供应商服务器将输出响应给业务服务器
=> 服务器接受响应后,交给模型网关进行本地校验,同时记录请求的相关信息,增量片段、失误
=> 输出没有问题则响应给客户端,有问题则进入解决策略
=> 记录日志

2.流式输出为什么能提升用户体验?

因为 TTFT 减少了,用户能够更早得到响应,体验自然就提升了。

但是从开发者角度来说,流式输出在后端还需要经过复杂的校验流程,反而更加麻烦。

3.SSE 和 WebSocket 怎么选

暂时不懂。

4.哪些大模型 API 错误可以重试

可以重试的主要是因为某些原因引发的供应商未能成功处理的问题,因为此时请求并未真正到达供应商服务器,又或者确实到达了但是大模型没能进入处理流程,此时可以重试。

不能重试的主要有供应商本身拒绝应答的问题,比如额度耗尽、鉴权不过、内容涉嫌安全问题等,此时再重试也没意义。

除此之外,还有另外的一些特殊情况:

  1. 429 限流:供应商并发太高暂时限流,可以等待一下然后有限重试。

  2. 结构化输出不符合完整格式:可以少量重试,尝试让大模型自行解决这个问题,不过前提是开发服务器内做好了日志记录,特别是增量片段要做好记录。

5.为什么大模型调用必须做幂等

因为我们要极力避免大模型重复处理同一个相同的请求,最主要的原因是大模型无法保证相同上下文输出的内容也相同,重复处理同一个请求,几乎一定会造成输出重复、冲突。另外一个原因是浪费 token。

6.限流为什么不能只按 QPS

因为有 token 限制。

7.JSON Mode 和 Structured Outputs 有什么区别

JSON Mode 主要关注模型是否输出合法的 JSON 格式;

JSON Schema 关注模型在能够输出合法 JSON 格式的基础上,能否符合业务需求,例如定义了一个 Java 类的 JSON Schema,模型是否能够完全按照该类的字段输出 JSON 文本。

Structured Outputs 则在此之上,假设模型的输出是不可控的,即使是加上了 JSON Schema 也有可能输出不合法 JSON,所以 Structured Outputs 是供应商干的活,把模型的输出先行截获,然后强制转为合法 JSON ,这样同样能够保证输出的一定是 JSON,而且比纯 JSON Schema 约束还要强。

而 Function Calling/Tool Use 这种同样也是格式化输出,但是是面对特定工具、方法的输入。比如 SQL 数据库,模型就必须输出合法的 sql 语句以控制数据库;比如 elasticsearch,模型就必须输入合法的 es 查询语句。

8.流式结构化返回怎么处理

后端要做好校验,确保结构化输出完全合法。