多厂商 AI 调度:统一混乱的供应商生态

3 min read
Zekari
Purikura 项目系统架构AI 调度多厂商管理

单一供应商是脆弱的。

当你的应用依赖某个 AI 厂商的 API,你就把命运交给了他们的稳定性、定价策略和服务质量。厂商 API 超时、限流、下线,你的应用就停摆。

多厂商调度的核心不是贪多,而是分散风险。但它带来了新的复杂度:每个厂商的 API 不同,参数不统一,返回格式各异。如何在这种混乱中建立秩序?

核心挑战:差异性

AI 厂商的 API 差异体现在三个层面。关于分布式 Workers 架构的整体设计,参见 purikura-workers-architecture

参数命名 Fal.ai 用 prompt,KIEAI 用 text,OpenAI 可能用 input。同样的功能,不同的字段名。

参数结构 有的厂商用扁平结构 { width: 1920, height: 1080 },有的用嵌套对象 { resolution: { width: 1920, height: 1080 } }

响应格式 Webhook 回调的数据结构完全不同。Fal.ai 返回 output.video.url,KIEAI 返回 data.result_url

这些差异不是小问题。如果直接在业务代码中处理每个厂商的特殊情况,代码会变成 if-else 地狱。每次添加新厂商,你要改动十几处地方。

有些厂商提供官方 SDK,但这不解决问题。

SDK 只是把 HTTP 调用封装了一层,API 差异依然存在。更重要的是,SDK 增加了依赖。当你需要支持 10 个厂商时,你要安装 10 个 SDK,版本冲突和依赖地狱会让你崩溃。

直接使用 HTTP 请求让你保持控制权。所有厂商用统一的方式调用,统一的方式处理响应。

统一抽象:UniversalModel

解决方案是建立统一的抽象层。所有厂商的差异被封装在适配器(Adapter)中,业务代码只看到统一的接口。

interface UniversalModel {
  id: string
  name: string
  type: 'image' | 'video'
  vendor: string
  parameters: {
    prompt: string
    resolution?: { width: number; height: number }
    duration?: number
    // ... 统一的参数定义
  }
}

前端只需要知道 UniversalModel,不需要关心是哪个厂商。用户选择模型后,系统自动找到对应的适配器,转换参数,调用 API。

适配器的职责

  1. 参数转换 - 把统一参数转成厂商特定格式
  2. 请求构建 - 构造 HTTP 请求(headers、body、auth)
  3. 响应解析 - 把厂商响应转成统一格式
  4. 错误映射 - 把厂商错误码转成统一的错误类型

💡 Click the maximize icon to view in fullscreen

这种设计的好处是扩展性。添加新厂商时,你只需要实现一个新的适配器,不需要修改业务代码。测试也更容易,每个适配器可以独立测试。

关键流程:生成、恢复、退款

AI 生成是异步的、不可靠的、需要容错的。

💡 Click the maximize icon to view in fullscreen

核心点:

  • 立即返回 - 不等待 AI 完成,避免超时
  • 原子扣款 - 积分扣除和任务创建在同一事务中
  • 状态追踪 - 每个状态变化都记录在数据库

关于异步处理的更多思考,参见 queue-reliability-boundaries

但生成可能失败。厂商 API 超时、限流、内部错误。这时需要两个关键机制。

僵尸任务恢复 任务卡在 processing 状态超过 30 分钟,可能是 Webhook 丢失或厂商没有回调。定时任务每 15 分钟扫描一次,向厂商查询真实状态,同步到数据库。

但不能无限重试。如果 24 小时内查询超过 20 次仍未完成,标记为不可恢复,触发退款。

僵尸任务恢复有个困境:厂商 API 本身可能不稳定。

如果每次查询都失败,你是继续重试还是放弃?继续重试可能浪费资源,放弃可能错失真正完成的任务。

目前的策略是:20 次重试 + 24 小时窗口。这个数字是经验值,不是理论最优解。实际运行中需要根据厂商的真实可靠性调整。

自动退款 失败任务自动触发退款,返还用户积分。退款也可能失败(数据库连接问题、并发冲突),所以用指数退避重试(详见 exponential-backoff-timeout)。

💡 Click the maximize icon to view in fullscreen

死信队列(DLQ)是最后的安全网。如果退款重试全部失败,任务进入 DLQ,等待人工处理。DLQ 不是失败的标志,而是对自动化边界的承认。

积分系统:统一的度量

不同厂商的定价模型不同。有的按秒计费,有的按分辨率计费,有的按请求次数计费。如何统一?

答案是积分系统。用户购买积分,生成消耗积分。积分是抽象的货币,屏蔽了厂商定价的差异。

积分计算规则

function calculateCredits(model: UniversalModel, params: GenerationParams) {
  const baseCost = model.base_credit_cost;
  const resolutionMultiplier = getResolutionMultiplier(params.resolution);
  const durationMultiplier = params.duration / 5; // 每 5 秒为基准

  return Math.ceil(baseCost * resolutionMultiplier * durationMultiplier);
}

这个公式不是理论推导的,而是在实际运营中调整的。基础成本基于厂商 API 的实际费用,乘数根据用户接受度和竞争对手定价调整。

积分系统还解决了另一个问题:预扣款。在任务提交时扣除积分,而不是完成时。这避免了用户余额不足导致的任务失败。如果任务失败,退款流程返还积分。

积分比货币灵活。

货币涉及支付通道、税务、退款流程。积分是内部虚拟货币,充值时发生一次真实货币交易,之后都是积分流动。

这让定价调整更灵活。你可以随时修改积分消耗规则,而不需要改动支付系统。你可以给新用户赠送积分,而不需要真实退款。

关于统一积分系统的设计,参见 unified-credit-system-design

可观测性:全链路追踪

分布式系统的调试是噩梦。一个请求经过前端、API Gateway、队列、AI 处理器、厂商 API,最后通过 Webhook 回调。任何一个环节出错,如何定位?

答案是全链路追踪。每个请求生成一个 traceId,它随着调用链传播。

// 前端生成 traceId
const traceId = `client_${Date.now()}_${randomId()}`;

// API Gateway 接收并创建新的 trace 上下文
const serverTraceId = traceManager.createTrace('video_generation', {
  clientTraceId: traceId,
  userId: user.id
});

// 队列消息包含 traceId
await queue.send({
  jobId,
  payload,
  traceId: serverTraceId
});

// AI 处理器继承 trace
const span = traceManager.createSpan(traceId, 'vendor_api_call');

每个关键操作都创建一个 Span,记录开始时间、结束时间、状态、错误信息。Span 之间通过 traceId 关联,形成调用树。

当用户报告问题时,你搜索 traceId,看到整个请求的生命周期:什么时候创建、什么时候入队、什么时候调用厂商、什么时候回调、哪里出错。

全链路追踪有成本。每个 Span 都要写入数据库或日志系统,高并发时会产生大量数据。

权衡方案:

  • 只追踪关键路径(生成、退款、恢复)
  • 采样记录(只记录部分请求)
  • 内存追踪 + 定期持久化(失败时才写数据库)

目前 Purikura 使用内存追踪,只在任务失败或进入 DLQ 时持久化。这在性能和可观测性之间取得了平衡。

关于追踪技术的更多讨论,参见 call-chain-tracing

数据库:单一真相源

所有状态的权威来源是数据库。

ai_vendors - 厂商配置 存储厂商的基本信息、API 端点、认证方式。

ai_models - 模型配置 每个模型关联一个厂商,定义参数、积分成本、支持的功能。

ai_generations - 任务记录 每个生成请求创建一条记录,包含用户、模型、参数、状态、结果。

ai_generation_logs - 事件日志 记录任务的每次状态变化,用于审计和调试。

ai_generation_dlq - 死信队列 记录自动处理失败的任务,等待人工介入。

ai_generation_retry_log - 重试日志 记录每次重试的时间、原因、结果。

这些表不只是存储,更是系统的契约。所有组件通过数据库协调状态,避免了分布式状态不一致的问题。

数据库的角色不只是存储,更是协调者。当多个 Worker 并发操作同一条记录时,数据库的事务和锁确保了一致性。

架构的本质

多厂商调度的复杂度不在技术细节,而在对不确定性的管理。

厂商 API 会超时、会限流、会下线。Webhook 会丢失。网络会分区。这些都是现实,不是异常。

架构设计不是消除不确定性,而是让系统在不确定性中依然可靠。重试机制、僵尸任务恢复、自动退款、死信队列,这些机制不是完美的,但它们定义了系统在混乱中的行为边界。

关于从意图到架构的思考过程,参见 from-intent-to-architecture


好的架构不是一次性设计的,而是在约束条件下持续演化的结果。Purikura 的多厂商调度从单一厂商开始,每次遇到问题就添加新的保护层。现在它支持多个厂商,但代价是更高的复杂度。

这个权衡是否值得?取决于你的业务需要多高的可靠性。如果单点故障不可接受,多厂商是必然选择。

最后更新:2025-11-06

Related Posts

Articles you might also find interesting

分布式 Workers 的解耦设计

3 min read

通过微服务架构和队列系统,实现高可用的 AI 任务处理。从单体到分布式,每个设计决策都是对复杂度的权衡。

Purikura 项目系统架构
Read More

Studio 系统架构:从状态机到端到端流程

3 min read

深入 Studio 系统的状态管理中心、组件协调机制和 AI 生成的完整数据流,理解前后端集成的设计逻辑

Purikura 项目系统架构
Read More

统一积分系统的设计实践

2 min read

从多套积分到单一积分池的架构演进,以及背后的原子性、一致性设计

系统架构数据库设计
Read More
Featured

定价界面优化的三层方法

4 min read

数据诚实、决策引导、视觉精调——三层递进的优化方法。从移除虚假功能到帮助用户选择,再到像素级修复,每一步都在解决真实问题

UI/UX定价策略
Read More

适配器模式:对现实的妥协

4 min read

当 PayPro 要求 IP 白名单而 Stripe 不需要,当一个按秒计费另一个按请求计费,架构设计不是消除约束——而是管理约束。适配器模式不是优雅设计,而是对现实混乱的务实投降。

系统架构支付集成
Read More

离屏渲染:照片捕获为什么需要独立的 canvas

2 min read

实时流与静态合成的本质冲突,决定了系统必须分离。理解这种分离,就理解了架构设计中最重要的原则。

架构设计前端开发
Read More

集中式配置:让 Reddit 组件脱离重复泥潭

2 min read

当同一份数据散落在多个文件中,维护成本呈指数级增长。集中式配置不是技术选择,而是对抗熵增的必然手段。

架构设计React组件
Read More

让文档跟着代码走

2 min read

文档过时是熵增的必然。对抗衰败的方法不是更频繁的手工维护,而是让文档"活"起来——跟随代码自动更新。三种文档形态,三种生命周期。

文档软件工程
Read More

双重导出管道的架构选择

2 min read

在用户生成内容场景中,速度与质量的权衡决定了导出架构。理解两种不同管道的设计逻辑,能够更准确地把握产品体验的边界。

架构设计图像导出
Read More

Purikura的页面系统

3 min read

通过五层分层继承复用架构,实现零代码修改的页面生成系统。从类型定义到页面渲染,每一层专注单一职责,实现真正的数据驱动开发。

架构设计React
Read More

重复数据的迁移实践:从 N 个文件到 1 个真相源

3 min read

当同一份 Reddit posts 配置散落在多个文件中,维护成本以文件数量指数增长。迁移到集中式配置不是技术选择,而是对复杂度的清算。

架构设计配置管理
Read More

在运行的系统上生长新功能

3 min read

扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。

系统设计架构
Read More

分层修复

3 min read

生产问题没有银弹。P0 止血,P1 加固,P2 优化。优先级不是排序,而是在不确定性下的决策框架。

工程实践问题修复
Read More

Studio 前端架构:从画布到组件的设计思考

3 min read

深入 Purikura Studio 前端架构设计,探讨 DOM-based 画布、状态管理和组件化的实践经验

Purikura 项目前端架构
Read More

Context 驱动的认证状态管理

3 min read

认证系统的核心不在登录按钮,而在状态同步。如何让整个应用感知用户状态变化,决定了用户体验的流畅度。

软件设计认证系统
Read More

第三方回调的状态映射完整性

5 min read

KIE.AI 视频生成的三个修复揭示了一个本质问题:回调不只是接收结果,是建立状态映射。没有 vendor_task_id,系统就失去了追溯能力。

Purikura 项目系统设计
Read More