多引擎翻译 API 架构实战:适配器、路由、故障转移与可观测性
从统一适配器与能力筛选,到超时预算、错误分类、熔断恢复和上线验证,逐步设计可追踪、可控制成本的多引擎翻译工作流。

给产品接入第一家翻译服务时,一个 HTTP 请求往往就够了。真正的复杂度通常出现在后面:某种语言的质量不符合预期、上游额度耗尽、接口变慢,或者业务希望更换供应商,而调用代码已经散落在多个系统里。
多引擎架构的价值,是把供应商差异收敛到可维护的适配器,再用明确规则管理路由、故障和成本。增加一个备用引擎只是起点;备用是否满足语言、格式和数据处理要求,才决定切换后能否继续完成任务。
本文介绍应用侧的架构设计。按本站当前引擎可用性说明,TranslateHub 已完整实现并验证 DeepL,其他适配器正在接入。下面的候选引擎、熔断和路由策略是实现方法,不表示每个账号已经开通所有能力。
一、先判断什么时候需要多引擎
先列出语言方向、内容类型、日常字符量和可接受等待时间。帮助中心可以在构建时翻译,客服消息却需要在交互时间内返回;前者可以暂停队列,后者必须有明确的失败出口。没有这些约束,很难判断备用服务究竟解决了什么问题。
如果一家供应商已经满足需求,可以先把它封装成适配器,暂不启用自动切换。每增加一个引擎,都要维护语言代码、鉴权、质量基线和计费规则。多接几家并不会自动降低运维工作量。
建议为调用定义三种清楚的结果:首选引擎成功、经过允许的备用引擎成功、任务失败且可以恢复。不要把原文装进成功响应冒充译文,否则故障可能直到错误语言出现在页面上才被发现。
二、统一业务契约,把差异留在适配器
业务系统应表达“需要翻译什么”,适配器负责“如何调用这家供应商”。内部请求可以包含下列信息:
| 内部字段 | 作用 |
|---|---|
requestId | 把一次业务调用与各次上游尝试关联起来 |
tenantId | 选择凭证、额度和数据处理范围 |
text、sourceLang、targetLang | 描述原文与语言方向 |
contentType | 区分纯文本、结构化内容等实际支持的格式 |
policyRevision | 记录本次使用的路由与质量规则版本 |
allowFallback | 说明业务是否允许使用已批准的备用引擎 |
这些是应用内部字段,不是让你向 TranslateHub 请求体添加未经文档确认的参数。公开端点与可用字段应以开发者文档为准。
适配器需要完成语言代码映射、凭证注入、请求序列化、响应校验、错误分类和取消处理。返回值至少应包含译文与实际使用的引擎。供应商提供追踪标识时可以保留;没有返回计费字符数时,就不要自己编造一个“实际用量”。
三、先筛能力,再谈价格和速度
路由之前,应检查目标语言方向、内容格式、必需的术语能力、允许的数据处理区域、可用额度和运行状态。一个延迟很低但无法保留所需格式的引擎,不应进入本次候选列表。
把条件分成硬约束和偏好会更容易维护。数据区域、格式支持属于硬约束;价格和延迟可以作为合格候选之间的排序依据。不能因为某个服务便宜,就绕过租户对处理范围的要求。
还要约定“指定引擎”的含义。如果用户选择某个引擎是为了复现结果,静默切换会改变实验条件。应用应单独记录是否允许降级,并在一次请求开始后固定候选顺序,避免处理途中因配置更新而改变行为。
四、错误要分类,不能全部重试
不同供应商的错误码需要由各自适配器解释。以 DeepL 的错误处理文档为例,参数错误、权限问题、限流和额度耗尽需要不同处理:
| 故障例子 | 建议动作 |
|---|---|
400:输入无效 | 修改请求,不原样重复提交 |
403:授权或权限失败 | 核对密钥与权限,定位凭证问题 |
413:请求过大 | 按端点限制拆分内容 |
429:受到限流 | 遵循适用的重试指引,降低调用压力 |
456:额度耗尽 | 解决用量限制,不持续重试同一账号 |
| 临时服务错误或网络故障 | 在预算内有限重试,或使用合格备用 |
备用引擎可以缓解某家服务不可用,却无法修复错误原文或应用鉴权。租户预算耗尽也不等于供应商故障:如果额度是用户主动设置的成本上限,切换供应商不能成为绕过上限的方式。
超时尤其需要单独记录。请求超时只说明调用方没有及时拿到结果,并不能证明上游没有完成工作;再次调用另一家服务,仍可能产生额外处理量和费用。
五、给整个调用设置总时间预算
主引擎等一秒、重试再等一秒、备用引擎继续等一秒,最终等待时间可能远超页面设计。应先设总截止时间,再分配每次尝试的时间;退避、响应校验和返回组装也要留出空间。
下面是一个顺序故障转移的 JavaScript 示例。候选列表应在调用前完成能力与权限筛选;适配器负责响应取消信号,并把自身故障转换为内部错误类型。时间参数是设计示例,不是本站性能实测。
export async function translateWithFallback(request, eligibleAdapters, { budgetMs = 2400, attemptMs = 1000 } = {}) {
const deadline = performance.now() + budgetMs;
const candidates = request.allowFallback ? eligibleAdapters : eligibleAdapters.slice(0, 1);
let lastError = new Error('No eligible translation engine');
for (const adapter of candidates) {
const remaining = Math.floor(deadline - performance.now());
if (remaining < 100) break;
try {
const result = await adapter.translate(request, {
signal: AbortSignal.timeout(Math.min(attemptMs, remaining)),
});
if (typeof result.text !== 'string' || result.text.length === 0) {
throw new Error('Invalid translation response');
}
return { ...result, engine: adapter.id };
} catch (error) {
lastError = error;
const retryable =
error instanceof Error &&
(error.name === 'TimeoutError' || ['rate_limited', 'unavailable'].includes(error.kind));
if (!retryable) throw error;
}
}
throw lastError;
}这段代码展示核心循环,没有实现供应商冷却、重试队列和调用方取消。适配器必须把信号传给实际网络请求;如果忽略信号,循环无法替你强行结束上游调用。上线前用一个故意变慢的测试服务验证,而不能只检查超时参数是否写在代码里。
限流引擎的等待时间如果超过本次预算,可以按策略返回待重试状态,或选择另一家合格服务。不要为了追求一次请求成功,让多个重试层各自重复尝试。
六、用熔断避免反复访问已知故障
当一个引擎持续不可用时,每次请求都先等它超时,会把故障传播给全部用户。熔断器的作用,是暂时阻止新的无效尝试,再通过有限探测判断是否恢复。
闭合状态允许正常调用,并统计适用的故障;打开状态在冷却期内跳过候选;半开状态只允许少量恢复探测。探测成功后恢复正常路由,再次失败则重新打开。
统计边界也要准确。一家供应商整体故障,与一个租户密钥失效不是同一件事;后者不应导致所有租户都停止使用该引擎。低流量下单次失败会显著影响比例,高流量下过长的观察窗口又可能反应迟缓,阈值应结合自己的调用量确定。
七、保证切换后的术语与缓存一致性
不同引擎的术语、标点和语气可能不同。同一份技术文档如果中途切换供应商,局部译文可读,整篇却可能出现两套术语。对一致性要求高的任务,应固定引擎和术语版本;发生降级时记录受影响片段,并决定是否重新审校。
缓存也必须区分引擎与策略版本。否则一次备用结果可能在后续请求中被当作主引擎输出复用。缓存键、失效与并发去重的完整实现见翻译 API 缓存实战。
启用新路由前,用同一组业务原文评估两家服务,覆盖数字、否定词、占位符和行业术语。保存样本版本和评审规则,后续升级才有可比较的基线。可以配合翻译质量评估框架建立审校表。
八、把上游尝试与业务成功分开统计
日志至少记录请求 ID、引擎、尝试序号、耗时、错误分类和是否切换。常规日志不应包含完整客户正文或密钥;供应商提供的追踪标识可以单独保留,用于定位具体调用。
同时观察业务端到端的 P50/P95 延迟、各引擎尝试耗时、最终失败率、故障转移比例,以及每个成功请求实际产生了几次上游调用。备用引擎可能提高完成率,也可能增加延迟和字符用量,只看成功率会漏掉这些变化。
对批量任务,再区分预计提交字符和供应商实际报告用量。一次超时加一次备用成功,可能比最终保存的一份译文消耗更多资源。费用应结合批量翻译成本优化方法和实际账单核对。
九、按一条路由逐步上线
- 先将现有引擎封装到适配器,保持调用结果与原流程一致。
- 验证备用的语言、格式、质量与数据处理条件。
- 注入参数错误、无效凭证、限流、慢响应和全部上游不可用等故障。
- 确认禁止降级时不会调用备用,慢请求能在预算内结束。
- 只对一个工作负载或小范围已批准流量启用,联合观察质量、用量与延迟。
- 保留无需修改调用方代码即可恢复旧路由的开关。
一次有效的演练,应同时核对用户最终看到的结果、实际上游调用次数和故障恢复后的状态。只确认“备用被调用了”,还不足以发现重复重试与恢复阶段的流量尖峰。
十、常见问题
每次失败都应该切换备用吗?
不应该。先明确哪些故障允许降级,再筛选备用候选。输入无效、租户预算限制和供应商不可用需要分别处理。
同时请求两家,谁快用谁,会不会更好?
并行竞速可能降低部分长尾等待,也会把同一份文本发送给多个服务,并可能产生多次计费。只有处理范围、预算和实际测量结果都支持时,才值得采用。
全部引擎失败时应该返回什么?
返回明确的可恢复错误,或将批量任务保留为待重试状态。如果界面展示原文,应说明它是原始内容,避免让用户误认为翻译已经成功。


