等级 L3 · 每天 40 分钟 · 场景:调API→一、调用链路的工程问题,控成本→五、成本控制落地
一句话:把 LLM 接进业务系统,难点不在「怎么调通」——那是十行代码的事——而在超时、重试、并发、成本、可观测这些老本行。Demo 到生产之间隔的全是你熟悉的东西。
学习地图
| 你的问题 | 看哪一节 |
|---|---|
| 「调通了但一上量就出问题」 | 一、调用链路的工程问题 |
| 「超时怎么设、能不能重试」 | 二、超时与重试 |
| 「首字延迟太长用户等不了」 | 三、流式返回 |
| 「返回的 JSON 解析老是失败」 | 四、结构化输出 |
| 「账单比预估高一个数量级」 | 五、成本控制落地 |
| 「线上出问题查不出来」 | 六、可观测 |
前置:大模型基础概念里的 token、上下文窗口、成本三节。
一、调用链路的工程问题
LLM 调用和你熟悉的 RPC 调用,差异在这几处:
| 维度 | 普通 RPC | LLM 调用 |
|---|---|---|
| 延迟 | 几十毫秒 | 几秒到几分钟 |
| 响应大小 | 可预期 | 不可预期(取决于生成多少) |
| 失败模式 | 超时 / 5xx | 超时 / 限流 / 内容不符合预期 |
| 幂等 | 通常可重试 | 重试结果不一定相同 |
| 成本 | 忽略不计 | 按量计费,是主要成本项 |
这五条差异决定了后面每一节。最反直觉的是延迟量级——按 RPC 的思路设 3 秒超时,会发现几乎全部超时。
别把 LLM 调用放在同步主链路里
如果一次调用要几秒到几十秒,放在用户请求的同步路径上会拖垮整条链路。常见的三种处理:
- 流式返回:用户能立刻看到内容在生成(见第三节)
- 异步 + 轮询/推送:提交任务返回 taskId,前端轮询或走 WebSocket
- 离线批处理:不要求实时的场景(内容审核、批量打标)走批量接口,通常有大幅折扣
选哪个取决于用户要不要立刻看到结果,不取决于技术偏好。
二、超时与重试
超时要按分钟设,不是按秒
各家 SDK 的默认超时通常在 10 分钟量级。你要根据场景收紧,但别收到秒级。
一个容易踩的坑:不同 SDK 的时间单位不一样。Python、Ruby 用秒,TypeScript 用毫秒。写 timeout: 30 在 TS 里是 30 毫秒,不是 30 秒——请求还没发出去就超时了。
重试的三个坑
坑 1:超时也会被重试,实际耗时是乘法关系
SDK 通常默认重试 2 次,且超时本身也在重试范围内。这意味着最坏情况的墙钟时间是:
最坏耗时 = 超时时间 × (重试次数 + 1)
设 10 分钟超时 + 默认 2 次重试 = 最坏 30 分钟才返回。上游的超时必须大于这个值,否则上游先断,你这边还在重试。
坑 2:不是所有错误都该重试
| 状态码 | 含义 | 该不该重试 |
|---|---|---|
| 400 | 请求格式/参数错 | ❌ 重试多少次都一样 |
| 401 / 403 | 认证/权限 | ❌ |
| 404 | 模型 ID 写错了 | ❌ |
| 413 | 请求太大 | ❌ 要减小输入 |
| 429 | 限流 | ✅ 按响应里的 retry-after 等待 |
| 500 / 529 | 服务端错误 / 过载 | ✅ 指数退避 |
SDK 一般已经内置了对 429/5xx 的自动重试。先确认默认行为,别重复造一层——两层重试叠加会把重试次数放大成乘积。
坑 3:重试不幂等
同样的输入,重试后的输出不保证相同。所以:
- 别用「重试直到解析成功」来兜底格式问题——该用结构化输出(第四节)
- 有副作用的场景(重试会重复扣款、重复发消息),要在你这层做幂等控制,不能指望模型
限流要自己扛一层
各家都有 RPM(每分钟请求数)和 TPM(每分钟 token 数)限制。TPM 比 RPM 更容易先撞上——一个长上下文请求可能顶几十个短请求。
生产上建议在自己这侧加一层限流/排队,理由和你给下游服务加限流一样:让排队发生在你能控制的地方,而不是把 429 直接透传给用户。
三、流式返回
长输出必须用流式。这不只是体验问题——非流式请求在输出量大时会撞上 HTTP 超时,直接失败。
一个经验值:输出预期超过一万多 token 就该流式;流式下可以把上限放得更宽。
流式的工程代价
流式解决了首字延迟,但引入三个新问题:
- 错误可能发生在中途——已经吐了一半内容才失败。你的前端和落库逻辑要能处理「不完整的结果」
- 网关/代理要支持——Nginx 的缓冲要关(
proxy_buffering off),否则它会攒完再一次性吐给客户端,流式白做 - 取消要能传递——用户关掉页面时,要能把取消传到上游,否则你还在为没人看的内容付费
落库时机
流式场景下常见的做法是:边推给前端,边在内存里拼完整结果,结束时一次落库。不要每个分片都写一次库——那是自找的写放大。
四、结构化输出
要用程序解析模型输出时,「在提示词里请求返回 JSON」是最弱的方案。三个层次:
| 方案 | 可靠性 | 说明 |
|---|---|---|
| 提示词里写「只返回 JSON」 | 低 | 可能带前言、带 ```json 包裹、字段名漂移 |
| 结构化输出(传 JSON Schema) | 高 | 服务端保证输出符合 schema,首选 |
| 工具调用 / Function Calling | 高 | 本来就要触发动作的场景更自然 |
用 schema 时要知道的限制
各家对 JSON Schema 的支持都是子集,常见限制:
- 不支持递归 schema
- 不支持数值范围(
minimum/maximum)、字符串长度(minLength)这类约束 - 对象通常要求显式写
additionalProperties: false
这些约束要在你代码里补校验——schema 只保证结构合法,不保证值合理。模型完全可以返回一个结构完美但年龄是 -5 的对象。
另外,新 schema 第一次请求会有编译开销(后续会被缓存一段时间)。压测时注意区分冷启动和稳态。
无论用哪种,解析都要容错
生成有随机性,不要假设两次调用返回完全相同的字符串。留好解析失败的降级路径和告警,别让一次解析异常打挂整个接口。
五、成本控制落地
成本的算法在大模型基础概念讲过,这里讲怎么落地。
按投入产出排序,依次做
① Prompt caching(性价比最高,优先做)
共享的长前缀(系统提示、文档、few-shot)缓存后按约 0.1 倍计价。要点:
- 缓存是前缀匹配:稳定内容放前面,易变内容放后面。时间戳、请求 ID、用户 ID 拼在系统提示开头 = 缓存 100% 失效
- 有最小长度门槛,太短的前缀静默不缓存(既不报错也不生效)。门槛值各模型不同,而且不随代际单调下降——换模型后要重新确认,别沿用旧假设
- 断点数量有限(通常 4 个),要放在稳定性的分界处
- 并发请求第一次都吃不到缓存:缓存要等第一个响应开始返回后才可读。扇出场景先发 1 个,等它开始返回再发其余的
② 按任务分流模型
分类、抽取、格式化这类高频简单任务用轻量档,能省下数倍成本。别一律用旗舰档。
③ 用批量接口
不要求实时的场景走批量接口,通常有 50% 左右折扣。适合:离线打标、批量摘要、数据清洗。
④ 压缩输入
删掉提示词里的冗余说明、长文档先摘要、多轮对话定期压缩历史。放在最后做——收益不如前三项,改动却更大。
一个必须设的东西:预算熔断
按 token 计费意味着一个 bug 可能烧掉一个月预算——死循环调用、异常输入导致超长上下文、被恶意刷接口。
至少要有:单请求 token 上限(max_tokens 别设太大)、单用户/单租户配额、总量告警 + 自动熔断。这和你给下游服务设限流是同一个道理,只是这次超了要付钱。
六、可观测
没有 token 账本,前面所有优化都是盲调。 这是接 LLM 时最该先建的东西,很多团队上线几个月才想起来。
每次调用至少记这些:
| 字段 | 为什么记 |
|---|---|
| 输入 token / 输出 token | 成本核算的基础 |
| 缓存命中 token / 缓存写入 token | 判断 caching 有没有真生效 |
| 模型 ID | 分流策略的效果对比 |
| 耗时(首字 + 总时长) | 流式体验的关键指标 |
| stop_reason / 结束原因 | 见下 |
| 业务标识(场景、租户、traceId) | 出问题能定位到是哪个功能 |
两个最值得盯的指标
缓存命中率:如果重复请求下缓存命中 token 一直是 0,说明有东西在破坏前缀——时间戳、随机 ID、没排序的 JSON 序列化、每次都变的工具列表。这是上线后第一个该加的监控。
注意统计口径:响应里的「输入 token」通常只是未命中缓存的那部分,不是总输入量。总量要三项相加,否则你会以为成本降了其实只是统计漏了。
结束原因分布:模型停止生成的原因通常有几类——正常结束、达到 token 上限、要调用工具、内容被拒。「达到 token 上限」的占比是个健康度指标:占比高说明你的输出被截断了,用户看到的是半句话。
常见追问
-
该用哪家的 API? 先看三个硬条件:数据能不能出境/出内网、有没有企业合同和 SLA、账单能不能按部门拆分。技术差异通常不是决定因素——能力有差距但都在快速变化,合规和采购流程才是真门槛。
-
要不要做多厂商适配层? 看你有没有真实的切换需求。没有需求就别做——各家的能力差异(工具调用格式、结构化输出、缓存机制)没法完全抹平,抽象层会退化成最小公倍数,把各家的优势都磨掉。真要做,抽象在「业务能力」层(如”意图识别”),不要抽象在「API 调用」层。
-
本地部署的开源模型呢? 数据不能出内网时是唯一选择。代价:能力通常落后一代、要自己搞 GPU 和推理服务、要自己做扩缩容和高可用。先用商用 API 验证需求成立,再评估自建——反过来容易在基建上耗几个月然后发现需求不成立。
-
多轮对话的历史怎么存? API 是无状态的,会话状态由你维护。存 Redis 就行,和普通会话没区别。要注意的是历史会无限增长——必须有截断或压缩策略,否则迟早撑爆上下文窗口,而且每轮都在为越来越长的历史付费。
-
怎么做压测? 三个和普通压测不同的点:要按 token 而不是按 QPS 算容量(TPM 通常先于 RPM 撞限);要区分缓存冷热(冷启动的成本和延迟完全不同);要压长输入长输出的极端场景,那才是最容易暴露超时问题的地方。