你的 AI 功能在开发环境能跑通,如何让它在生产环境里真正可靠

3 min read

工作流编辑界面,展示主模型、备用模型、缓存窗口和绑定的结构化输出

大多数 AI 功能出问题,并不是因为 prompt 写得差

真正的问题往往是:你直接把模型 API 当成了可上线的生产基础设施。

在开发环境里,最顺利的路径看起来很完整。你调用 OpenAI,拿到结果,渲染出来,然后心里会觉得:“好,这一块做完了。”

接着,真实用户来了。

某个 provider 开始限流。延迟突然变高。同一个请求不断被重复发送。某一次响应格式稍微变了一点,你的解析逻辑就崩了。再接着,某个晚上出了问题,你盯着日志开始反复问自己:到底是我们的代码有问题,provider 有问题,网络有问题,还是刚好撞上了某个奇怪的响应?

在 ModelRiver 出现之前,我在做 AI 产品时反复遇到的就是这类问题。一开始我总把它们当成一次性的 bug。多加一次重试,多打一行日志,再补一个解析分支。后来才不得不承认:真正反复出问题的,通常不是 prompt,而是 prompt 周围那层架构。

这些失败模式,其实非常可预测

一个 AI 功能一旦从 demo 阶段进入真实流量,通常很快就会遇到四类问题:

  • Provider 故障。你的应用绑在一个 provider 上,所以对方的事故很快就会变成你的事故。
  • 重复成本。同样的请求一遍又一遍进来,而你每次都要重新付一遍 Token 成本。
  • 缺少可见性。请求失败了,但你很难快速知道到底是哪段 payload、哪次重试、哪条 fallback 链路出了问题。
  • 响应契约不稳定。不同 provider 不一样,不同模型也不一样。测试时还能工作的解析逻辑,到生产环境里就开始随机出错。

这不是边缘情况。这几乎就是“直接从应用代码调用单一 provider”之后最自然会出现的结果。

我们在做这个之前,先试过什么

我们并不是一开始就想清楚了“应该做一个抽象层”。如果真是这样,那只能说明我们当时比实际更聪明。

我们先试的是所有人都会先试的方案:在应用代码里自己补重试逻辑,发现某个 provider 状态不稳定时手动切换,打一些临时日志,再在解析层多加几个 if 去兼容输出格式波动。这里补一个 timeout,那里加一个 fallback 标记,再在 parser 里多写一个分支,因为某个 provider 这周返回的字段和上周稍微不一样了。

这些东西多少有点帮助,但也让整个集成越来越难维护。基础设施层面的问题,开始在代码库里到处被零散地解决。到最后,大家都不太想碰那段重试逻辑了,因为它已经慢慢长成了一团谁都不想承担风险的边界情况集合。

真正的问题,是耦合太紧

大多数 AI 应用一开始的架构都很像这样:

TEXT
你的应用 -> OpenAI SDK -> 单一 provider -> 响应

做原型时,这完全没问题。但一旦进入生产环境,它会变得很脆弱,因为一切都耦合在同一个外部系统上:

  • provider 可用性
  • provider 延迟
  • provider 响应格式
  • provider 定价
  • provider 自己的调试工具链

真正缺的是一层位于你的应用和 AI provider 之间的中间层。

这个模式:AI 路由层

更稳妥的做法,是在应用和底层模型之间加上一层 AI 路由层。

TEXT
你的应用
-> AI 路由层
-> 主 provider / 模型
-> 备用 providers / 模型
-> 精确匹配缓存
-> 请求日志与时间线
-> Schema 校验

这一层负责处理:

  • Routing:请求应该发到哪个 provider、哪个模型
  • Failover:主 provider 超时或失败时自动切换
  • Caching:相同请求不再重复消耗 Token
  • Observability:每一个请求都可追踪
  • Response contracts:下游拿到的是经过校验的结构化响应,而不是一段“希望它能被解析”的文本

这就是 “demo 能跑” 和 “这个东西我敢上线然后去睡觉” 之间的差别。

代码里到底是什么样

如果你的应用本来就在用 OpenAI SDK,接入改动其实可以非常小。

之前

PYTHON
from openai import OpenAI
 
client = OpenAI(api_key="OPENAI_API_KEY")
 
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a support assistant."},
{"role": "user", "content": "Summarise this support ticket."}
]
)

之后

PYTHON
from openai import OpenAI
 
client = OpenAI(
base_url="https://api.modelriver.com/v1",
api_key="mr_live_YOUR_API_KEY"
)
 
response = client.chat.completions.create(
model="support_summary", # 工作流名
messages=[
{"role": "system", "content": "You are a support assistant."},
{"role": "user", "content": "Summarise this support ticket."}
]
)

你的 prompts 不用改。消息格式不用改。流式逻辑和响应解析逻辑也可以保持不变。真正变化的主要只有三点:

  • base_url 指向 ModelRiver
  • api_key 换成 ModelRiver 的 key
  • model 不再是某个 provider 的原始模型名,而是一个 workflow 名称

这一套方式同样适用于 Python OpenAI SDK、Node.js OpenAI SDK、LangChainLlamaIndex 以及 Vercel AI SDK

这基本就是 ModelRiver 的核心:一个兼容 OpenAI 的 AI 路由层,而 workflow 是控制单位。你在控制台里配置 provider、模型、备用链路、缓存窗口和响应契约;你的应用继续调用同一个接口。说实话,刚开始做的时候,我们以为这种分离只是“看起来更优雅”。后来才发现,它几乎就是让生产环境变得可控的关键。

在实际里会发生什么变化

不需要自己维护重试胶水代码的自动故障转移

当主 provider 开始返回 429 或直接超时时,请求不会直接变成用户看到的错误,而是继续尝试下一个已配置好的备用模型。用户拿到的依然是结果,而不是报错。你第二天再看日志时,就能清楚知道到底发生了什么。

请求日志时间线,展示主模型失败后切换到备用 provider 成功返回

我知道这听起来像个小细节。但第一次你需要在生产配置里手动做 failover 决策时,它就不会再显得“小”了。fallback 逻辑是那种团队迟早都会自己在应用代码里重复实现一遍的东西,直到大家都被维护烦为止。我们就是烦透了。

用精确匹配缓存处理重复请求

不是所有请求都适合缓存,但有些请求显然值得缓存。

如果同一个请求被重复发送,ModelRiver 可以直接返回缓存结果,而不是再次打到 provider。这样带来的就是更低的成本和更低的延迟。

关键点在于:这不是模糊缓存,而是 精确匹配缓存。prompts、system messages 和模型设置都必须完全一致。只要不一致,请求就会绕过缓存,按正常流程执行。

工作流缓存看板,展示命中率、节省延迟和重复请求活动

这会让行为变得“无聊”,而这种无聊恰恰是好事。因为当输出结果要进入产品代码时,你最不想看到的就是不确定性。说到底,缓存这一层的意义就是:别再为同一个答案反复付费。

真正能拿来排查问题的可观测性

AI 生产问题里,最糟的未必总是彻底失败的请求。有时候更烦的是:以前 2 秒就能返回的请求,现在突然变成 8 秒;或者某条代码路径不小心在循环里重复调用 provider,一周下来成本悄悄翻倍。我们两种情况都遇到过。

这类问题通常都藏在你的应用和 provider 之间的某一段链路里,而且经常没有一个单一入口可以直接看清楚。

请求日志的价值就在这里。你能直接看到:

  • 最终由哪个 provider 处理了请求
  • 请求耗时
  • 是否触发了 fallback
  • token 使用量和成本
  • 原始请求体与响应体

在这套东西出现之前,排查一次生产环境里的 AI 问题,往往意味着你要来回看应用日志、provider dashboard,最后还得带着猜测下判断。现在你打开一条日志,整个生命周期都在那里。它不是一个多么“革命性”的想法,只是事实证明,大多数团队并不想自己从头造一遍,包括我们自己。

用响应契约替代“希望它能被解析”

让 AI 功能变脆弱的最简单方式之一,就是因为测试时 JSON 看起来还行,就默认 provider 返回的一定是可信结构。

更稳妥的方式,是先定义一次响应契约,然后让每次响应都经过校验。

在 ModelRiver 里,这件事是通过 structured outputs 配置的。但真正重要的并不是这个 UI 名字,而是架构上的意义:你的前后端依赖的是一个经过验证的 schema,而不是一段自由格式的文本。

结构化输出编辑器,展示 JSON Schema 与 sample response contract

这会让你的应用接口在底层 provider 变化时依然保持稳定,整个系统的“心态”也会平静很多。你不会再继续写那种其实偷偷依赖某个模型怪癖的代码。

不需要 redeploy 的控制能力

这是那种一开始很容易被低估、后来却会经常用到的能力。

你可以直接把一个 workflow 切到另一个模型、加一个 backup provider,或者换成更严格的响应契约,而不需要改集成代码。应用仍然调用同一个 workflow 名称,真正变化的是下面那层路由逻辑。

这比把各种 provider 假设散落在代码库里健康得多。它也意味着像“把这个 workflow 切到另一个模型”这种产品决策,不会再自动变成一项工程发布任务。

什么情况下你其实不需要它

有很多场景里,直接调用 provider 仍然是对的:

  • 周末项目和原型。你大概率还不需要再加一层。
  • 流量很低的应用。如果 uptime 和成本都还不是问题,就先保持简单。
  • Provider 特有能力。有些 OpenAI 参数在兼容层里不会被完整传递,比如 logprobs 不会被透传,而 n > 1 会被明确拒绝。
  • 事件驱动异步工作流。如果你需要 webhook 和 callback 编排,就直接使用原生的 ModelRiver async flow,而不是兼容层 endpoint。

它应该是生产阶段的工具,而不是一个必须从第一天就引入的抽象层。

更大的变化

真正变的并不是代码,而是我们对这个问题的看法。

很长一段时间里,我总把 AI 集成看成一种“特殊情况” - 和数据库不一样,和普通 API 也不一样,因为它输出不确定、失败模式也看起来更怪。但后来我意识到,它并没有那么特殊。它本质上还是“依赖外部服务并在规模下运行”会遇到的那些老问题:可用性、延迟、成本、响应格式、调试。

当我不再把 AI 调用当成某种“魔法能力”,而是开始把它当作基础设施来处理时,很多设计决策一下子就简单了。

如果你现在正处在这个阶段

如果你的 AI 功能已经能在开发环境里工作,但你也开始遇到“为什么它又坏了”的生产版问题,那这个模式值得你认真看看。我们也是在这个阶段,才从打补丁转向真正去做这层基础设施。

你可以从这些文档开始:

接入只需要几分钟。整件事的核心也很简单:你不应该只是因为真实用户来了,就被迫重写半套 AI 集成逻辑。