1. 先想清楚:一个项目为什么要同时接四家翻译引擎
做多语言内容的产品,翻译引擎 API 密钥申请这件事,看着只是"注册个账号拿串字符",实际踩过的坑比写的代码还多。我最近刚把一个内容中台的多语言链路从单引擎切换到四引擎并行,DeepL、火山翻译、有道、百度这四家的密钥都是我一手申请、一手联调、一手收拾烂摊子的。这篇就把从零到跑通的完整流程摊开讲,包括控制台里每一个要点哪、参数怎么填、密钥拿到后怎么存、调用时哪个字段最容易被忽略。
先说结论:没有哪一家引擎在所有语种、所有文体上都赢。DeepL 在欧洲语系的长句和术语一致性上表现稳,尤其是英德法西这类语言,句式重构做得比较自然;火山翻译在中英互译的短句、口语化内容上响应快、延迟低,适合做实时字幕或 IM 场景;有道在中英学术类、词典类场景的词汇准确度上有积累,特别是一些固定搭配;百度的通用翻译覆盖语种多,小语种兜底能力强,价格也相对友好。这四家凑在一起,本质上是在做"能力互补 + 成本分层 + 故障冗余"三件事。
但你要是只为了跑个 Demo,我劝你别折腾四个。密钥管理、签名算法、错误码映射、额度监控,每多接一家就多一套东西要维护。真正该上多引擎的场景有三类:一是内容量级大到需要按语种分流以压成本;二是对可用性有要求,单一引擎限流或抖动时能自动切换;三是需要做翻译质量对比,把同一段文本送进不同引擎做 A/B。如果不符合这三条中的任何一条,选一家跑深就行。
1.1 四家引擎的差异点,决定了你申请密钥时要填什么
密钥申请流程的复杂度,其实和各家 API 的鉴权设计强相关。DeepL 是最简单的,一个 Authorization 头带一个 Key,完事;火山翻译走的是云厂商那套 AK/SK + HMAC-SHA256 签名,请求要自己算签名,坑最多;有道是 appKey + appSecret 双密钥,签名里要拼时间戳;百度是 appid + 密钥,MD5 拼串。这四种鉴权形态直接决定了你后面配置中心里字段该怎么设计,也决定了密钥泄露的风险等级不一样。
我做了个对照,申请前先看一眼,心里有数:
| 引擎 | 鉴权形态 | 密钥数量 | 签名难度 | 控制台概念复杂度 |
|---|---|---|---|---|
| DeepL | Header 单 Key | 1 | 极低 | 低 |
| 火山翻译 | AK/SK + 签名 | 2 | 高 | 中高 |
| 有道智云 | appKey/appSecret | 2 | 中 | 中 |
| 百度翻译 | appid/密钥 | 2 | 低 | 低 |
这张表里最值得注意的不是"谁简单",而是"谁的密钥泄露后果最重"。火山和有道的 Secret 一旦泄露,别人可以直接伪造签名消耗你的额度,而且从日志上看和正常请求几乎没区别;DeepL 的 Key 泄露同样危险,但它至少只有一个字段要轮换。所以后面第七章我会专门讲密钥怎么存、怎么轮换。
1.2 申请前必须先定的三件事
很多人拿到密钥才发现选错了套餐,改不了。所以申请之前,先把这三件事定下来。
第一是账号主体。个人账号和企业账号在额度、发票、实名要求上完全不同。企业账号一般要走对公认证,周期长一点,但额度上限高、支持开票;个人账号开通快,适合先做验证。我的做法是先用个人账号跑通链路,确认要长期用了再迁到企业主体,避免一上来卡在认证流程里。
第二是计费模式。翻译类 API 基本都按字符计费,但"字符"的定义各家不一样:有的是按源文本字符数,有的是按源+目标文本合计,有的对空白字符不计。这个差异在长文本批量场景下能差出十几万字符的月账单。申请之前一定要在控制台的计费说明里确认口径。
第三是字符额度与限流。免费额度只够做验证,别指望上生产。同时要看 QPS 限制(每秒请求数)和单次请求的文本长度上限,这两个数字会直接决定你后面要不要做分批和队列。
2. 申请之前的公共准备:账号、实名与域名白名单
2.1 账号注册这一步最容易卡住的地方
四家的注册流程大同小异,手机号 + 邮箱 + 实名认证。真正卡人的是实名认证的审核时长和材料要求。个人实名一般当天过,企业实名要传营业执照、法人信息,快的一两天,慢的三五天,遇到信息不一致还会打回。我的建议是提前一周开始办,别等到联调前一天才想起来。
另一个容易忽略的是子账号/协作者机制。云厂商那几家(火山引擎、百度智能云)支持主账号下建子账号,给子账号单独授权。这在团队协作里非常重要——不要所有人共用一个主账号的 AK/SK,出了事查不到是谁。正确做法是给每个服务、每个环境(开发/预发/生产)建独立的子账号或独立的密钥,权限最小化,只授予翻译服务相关的读和调用权限。
2.2 密钥命名规范:现在偷的懒,三个月后要还
密钥申请下来第一件事不是复制粘贴到代码里,是起名字。我吃过这个亏:三个项目共用一套密钥,后来其中一个项目被下线,密钥要不要删?删了怕影响另外两个,不删又是个隐患,最后花了半天翻日志才确认调用来源。
现在我的命名规范是这样的:{引擎}-{业务线}-{环境}-{用途},比如deepl-content-prod-glossary、baidu-search-staging-fallback。密钥的备注栏一定要填上负责人和创建日期。四家控制台都支持给密钥加描述,用起来。
注意:控制台里生成的 Secret(尤其是火山的 SecretAccessKey 和有道的 appSecret)通常只在创建时完整显示一次,关掉页面就再也看不到了。申请当场一定要存到密码管理器或配置中心,别截图发聊天软件。截图这个动作本身就是泄露路径。
3. DeepL API:注册、套餐分水岭与第一个请求
3.1 套餐选择是唯一的分水岭
DeepL 的 API 申请流程是四家里最直白的,但有一个决策点必须先搞清楚:Free 版和 Pro 版的接口域名不一样。Free 版走api-free.deepl.com,Pro 版走api.deepl.com。这两个域名填错了,返回的一定是 403,而且报错信息不会告诉你"你域名写错了",只会说鉴权失败。我第一次接的时候在这上面浪费了二十分钟,以为是 Key 没生效。
申请路径大致是:进开发者平台,注册账号,在账号设置里选择 API 套餐。Free 版对每月字符数有上限(具体数值以控制台实时显示为准),单次请求能带的文本条数也有限制;Pro 版按用量付费,额度高、支持更多请求头参数。做验证阶段用 Free 版足够,上生产前记得在控制台完成付费方式的绑定,否则到了额度上限会直接返回 456 错误。
3.2 密钥获取与调用验证
登录后进入账号的 API 密钥管理页,页面顶部会显示你的 Authentication Key。这个 Key 的格式是xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:fx这样的结构,结尾的:fx是 Free 版标记——Free 的 Key 拿到 Pro 域名上调用会被拒,这也是个常见迷惑点。
拿到 Key 之后,别急着写进项目。先在服务器上跑一条 curl 验证连通性和鉴权:
curl -s -X POST "https://api-free.deepl.com/v2/translate" \ -H "Authorization: DeepL-Auth-Key 你的KEY:fx" \ -H "Content-Type: application/json" \ -d '{ "text": ["Hello, world.", "The quick brown fox."], "target_lang": "ZH" }'返回体里是translations数组,顺序和入参text一一对应。这里有个细节:target_lang用的是大写语言代码,中文是ZH,英文目标是EN-US或EN-GB——目标语言为英语时必须带地区后缀,只写EN会报错。源语言source_lang可以省略,DeepL 会自动检测,但在批量场景下建议明确指定,省掉检测开销。
3.3 DeepL 三个高频参数与踩坑点
第一个是tag_handling。做网页或多语言内容时,文本里经常带 HTML 标签或 XML 标签,直接翻译会把标签也翻掉。设tag_handling: "html"后,DeepL 会识别标签结构,只翻标签外的内容。但这个参数和split_sentences一起用的时候要小心:split_sentences: "nonewlines"会让换行符不被当作句子分隔,适合翻译带格式的表格文本。
第二个是glossary(术语表)。企业级场景必须用。你可以在控制台建术语表,导出 ID,然后在请求体里带glossary_id。实测下来,术语表能显著提升品牌名、产品名、专业术语的一致性。但术语表有条数上限,且不是所有语言对都支持,建之前先在文档里确认语言对。
第三个是context。这是个容易被忽略但很好用的参数,用来给翻译提供上下文提示,不参与计费。比如翻译"苹果"这个词,加上 context "这是一篇关于水果的文章",结果就不会翻成品牌名。
实操心得:DeepL 返回的
detected_source_language字段建议落库。做内容中台的时候,这个字段能帮你事后分析"哪些内容源语言被误判了",从而决定要不要强制指定源语言。
4. 火山翻译:AK/SK 签名机制是最大的门槛
4.1 开通服务与密钥创建路径
火山引擎这套东西的门槛不在申请,在签名。申请流程本身是:注册账号 → 实名认证 → 在控制台搜索"机器翻译"或"翻译"服务 → 开通 → 进入访问控制(IAM)创建密钥对。创建密钥对的时候会给你 AccessKeyId 和 SecretAccessKey,Secret 只显示一次。
开通服务这一步要注意:翻译类服务通常是按功能拆开的,文本翻译、文档翻译、图片翻译各自独立计费,你需要什么就开通什么。全开通了不用也会占额度预算,虽然不产生实际扣费,但额度面板会很难看,做成本核算的时候容易搞混。
创建密钥后别急着用主账号的 AK/SK。正确做法是在 IAM 里建一个子用户,只授予翻译服务相关权限,然后给这个子用户创建密钥。主账号的 AK/SK 权限太大,一旦泄露,整个云账号都危险。
4.2 签名算法拆解:为什么你的请求总是 403
火山翻译的接口签名是这套体系里最复杂的,走的是 HMAC-SHA256 的规范签名,需要构造 CanonicalRequest、StringToSign,再逐级派生签名密钥。手工拼几乎一定会错,我的做法是优先用官方 SDK,只在没法引 SDK 的环境(比如某些边缘运行时)才手写。
如果你确实要手写,理解这三个关键点就够了:签名的 Service 名是translate,Region 一般是cn-north-1,请求的Action和Version要放在 query 参数里而不是 body 里。这三点任何一个错了,签名必然不通过。
# 使用官方 SDK 的推荐写法(示意,具体方法名以所用 SDK 版本为准) import os from volcengine.ApiInfo import ApiInfo from volcengine.Credentials import Credentials from volcengine.base.Service import Service from volcengine.ServiceInfo import ServiceInfo service_info = ServiceInfo( "open.volcengineapi.com", {"Accept": "application/json"}, Credentials(os.environ["VOLC_AK"], os.environ["VOLC_SK"]), "POST", 480, 480, ) api_info = ApiInfo( "POST", "/", {"Action": "TranslateText", "Version": "2020-06-01"}, {}, "", ) service = Service(service_info) resp = service.json( "TranslateText", {}, '{"SourceLanguage":"en","TargetLanguage":"zh","TextList":["Hello there"]}', )上面这段里,ServiceInfo的第四个参数(超时)和ApiInfo的 query 字典是最容易写错的地方。我用过的一个笨办法是:先用官方 SDK 跑通一次,把 SDK 里实际发出的请求 URL 和 Header 打印出来,然后拿这个真实请求去比对自己手写的版本,差在哪一眼就看出来了。
4.3 火山翻译的实用参数与额度逻辑
火山的文本翻译请求体里,TextList是个数组,支持一次传多条,这个设计比一家一条友好很多。SourceLanguage可以传auto做自动检测。返回结果里除了译文,还带DetectedLanguage,和 DeepL 一样建议落库。
额度方面,文本翻译一般每月有一定量的免费字符(具体以控制台为准),超出后按字符阶梯计价。这里有个坑:失败请求是否计费。有些情况下请求返回了错误但字符已经计入,做批量任务的时候如果重试逻辑写得不好,会白白烧掉额度。我的处理是重试前先判断错误码类型,只有网络超时、5xx 这类才重试,参数错误和鉴权错误一律不重试。
5. 有道智云:应用创建与实例绑定
5.1 控制台里的两个必填概念
有道的控制台把"应用"和"实例"分成了两层,这是新手最容易懵的地方。你需要先在应用管理里创建一个应用,拿到 appKey 和 appSecret;然后在服务管理里给这个应用绑定翻译实例,选择文本翻译(以及要用的语言对)。没有绑定实例的应用,密钥是能生成但调不通的,会返回无权限类的错误。
创建应用的时候,接入方式要选"API",别选成 SDK 或插件,选错了后续的密钥形态不一样。应用名称建议按前面的命名规范写,备注里填负责人。
5.2 签名算法:v3 版本才是最省事的
有道早期的签名是 MD5,现在官方推荐signType=v3,用 SHA256 保证安全性。v3 的签名串是appKey + input + salt + curtime + appSecret拼起来做 SHA256,其中input的计算有个特殊规则:如果原文长度小于等于 20,input就是原文本身;如果超过 20,input是"前 10 个字符 + 原文长度 + 后 10 个字符"。这个规则不按它来,签名一定对不上。
import hashlib, time, uuid, requests app_key = "..." app_secret = "..." q = "这是一段需要翻译的文本" salt = str(uuid.uuid4()) curtime = str(int(time.time())) truncated = q if len(q) <= 20 else q[:10] + str(len(q)) + q[-10:] sign = hashlib.sha256( (app_key + truncated + salt + curtime + app_secret).encode("utf-8") ).hexdigest() payload = { "q": q, "from": "zh-CHS", "to": "en", "appKey": app_key, "salt": salt, "sign": sign, "signType": "v3", "curtime": curtime, } r = requests.post("https://openapi.youdao.com/api", data=payload, timeout=5) print(r.json())注意from和to的语言代码是有道自己的一套:简体中文是zh-CHS,繁体是zh-CHT,不是通用的zh。这个代码表在文档里有,建议直接抄下来做成常量表,别凭记忆写。
5.3 有道返回体的解析注意
有道的返回是 JSON,errorCode为"0"才是成功,注意是字符串不是数字。成功时译文在translation数组里。有个小细节:有道对多条文本的处理是逐条请求,不像火山支持TextList,所以批量场景要自己做并发控制,并且要注意 QPS 限制,并发太高会被限流。
6. 百度翻译开放平台:AppID 与 MD5 签名
6.1 开发者认证与 AppID 获取
百度翻译开放平台的流程是:进平台 → 注册开发者账号 → 完成实名认证 → 在管理控制台创建应用(或直接获取通用翻译的 AppID)→ 在应用详情里拿到 AppID 和密钥。创建应用时要选择服务类型,通用翻译选"通用文本翻译";如果要做垂直领域(比如生物医药、电子科技),可以选对应的领域版,翻译质量会有提升,但需要单独申请。
百度这边的免费额度是按字符计算的,标准版和高级版的额度差别很大,具体数字在申请页面和控制台里都有说明,这里不写死数字,因为会调整。申请时要勾选对应的服务,没勾选的服务即使有 AppID 也调不通。
6.2 MD5 签名与调用示例
百度的签名相对简单,sign = MD5(appid + q + salt + 密钥),全小写 32 位。注意是"密钥"不是 AppID,两个字段别搞混。
import hashlib, random, requests appid = "..." secret = "..." q = "今天天气不错" salt = str(random.randint(32768, 65536)) sign = hashlib.md5((appid + q + salt + secret).encode("utf-8")).hexdigest() r = requests.get("https://fanyi-api.baidu.com/api/trans/vip/translate", params={ "q": q, "from": "zh", "to": "en", "appid": appid, "salt": salt, "sign": sign, }, timeout=5) print(r.json())百度支持 GET 和 POST 两种方式。短文本用 GET 方便调试,但长文本一定要用 POST,因为 URL 长度有限制,超长会被网关直接截断,返回一个和翻译无关的错误。这个坑我踩过,报错信息完全看不出是 URL 太长导致的。
另外百度的q参数在 GET 场景下必须做 URL 编码,requests的params会自动帮你处理,但如果手拼 URL,中文和特殊符号一定要 encode。
7. 密钥管理与统一接入层的工程化落地
7.1 密钥绝对不能出现在代码里
四家的密钥申请下来之后,最危险的动作是直接写进代码常量然后提交到 Git。我见过不止一次有人在公开仓库里留了带密钥的配置文件,等发现的时候额度已经被刷光了。
正确做法是用环境变量 + 配置中心双层管理。本地开发用.env文件(加进.gitignore),线上从配置中心读取。配置中心里的密钥字段要开启加密存储和访问审计,谁在什么时间读取了哪个密钥都要有记录。
# .env(本地开发用,务必加入 .gitignore) DEEPL_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:fx VOLC_AK=your_ak VOLC_SK=your_sk YOUDAO_APP_KEY=your_app_key YOUDAO_APP_SECRET=your_app_secret BAIDU_APPID=your_appid BAIDU_SECRET=your_secret密钥轮换也要提前设计好。我的建议是每 90 天轮换一次,轮换时用"新旧并存"的方式:先在配置中心加一个新版本密钥,等所有实例都加载完新配置,观察一天没有异常,再禁用旧密钥。别直接删,删了会导致滚动发布期间的实例鉴权失败,形成一段时间的错误率尖峰。
7.2 统一适配器:让业务代码不感知引擎差异
四家的请求格式、签名方式、错误码全不一样,如果业务层直接调各自的 SDK,代码会变成一团乱麻。正确的做法是抽一层统一接口,输入是{文本, 源语言, 目标语言},输出是{译文, 引擎, 检测到的源语言, 耗时},具体用哪家由路由策略决定。
class Translator: def __init__(self, name, translate_fn): self.name = name self._fn = translate_fn def translate(self, text, src, dst): # 统一出口:补全默认值、统一异常、记录耗时 raise NotImplementedError def route(src, dst, biz): # 路由策略示意:欧洲语系走 DeepL,中英短句走火山,兜底走百度 if dst in ("DE", "FR", "ES", "IT") and src in ("EN", "ZH"): return "deepl" if {src, dst} <= {"ZH", "EN"} and biz == "realtime": return "volc" return "baidu"路由策略别写成一堆 if-else 散落在业务里,集中在一个模块里,配置化。这样后面加第五家引擎的时候,只改这一个文件。
7.3 降级、熔断与额度监控
多引擎的真正价值在故障时的自动切换。做法是给每个引擎维护一个滑动窗口的成功率,低于阈值就暂时摘除,请求自动落到备用引擎上;窗口恢复后再加回来。切换到备用引擎时要注意,不同引擎的翻译风格不同,同一个页面上如果一部分内容是一个引擎翻的、另一部分是另一个引擎翻的,风格会不统一。所以降级要按"整篇文档"或"整批内容"为单位,而不是按单句。
额度监控用定时任务拉控制台或直接统计本地调用日志即可。我的做法是每天统计各引擎的字符消耗,达到月度额度的 70% 时告警,90% 时自动把新流量切到备用引擎,避免超额后请求全部失败。
8. 联调阶段的问题排查实录
8.1 错误码对照速查表
联调阶段 80% 的时间花在看错误码上。四家的错误码体系完全不同,我整理了一张常用的对照表:
| 现象 | 可能引擎 | 根因 | 处理方式 |
|---|---|---|---|
| 403 鉴权失败 | DeepL | Free Key 打到 Pro 域名 | 检查api-free/api域名 |
| 456 额度用尽 | DeepL | 当月字符额度耗尽 | 控制台升套餐或切备用引擎 |
| 签名不匹配 | 火山 | Service/Region/Action 拼错 | 打印 CanonicalRequest 逐字段比对 |
| 无权限 / 108 | 有道 | 应用未绑定翻译实例 | 服务管理里补绑实例 |
| 54003 访问频率受限 | 百度 | 超出 QPS | 加队列限速,或申请提额 |
| 52003 未授权用户 | 百度 | AppID 填错或服务未勾选 | 核对应用详情页 |
这张表里最有迷惑性的是火山的签名错误。它通常不会告诉你具体哪个字段错了,只会返回一个通用的签名失败,所以定位全靠打印中间量。我的做法是在签名函数里加一个 debug 开关,打开后把 CanonicalRequest、StringToSign、派生密钥全部打到日志里,一条一条和文档里的示例比对——文档给的示例值是可以直接复现的,用示例值跑通再换成自己的真实参数,这样能快速定位是格式问题还是数据问题。
8.2 字符计数口径不一致导致的额度偏差
这是最容易被忽略的问题。你在本地按len(text)统计的字符数,和按 UTF-8 字节统计的字符数,和引擎实际的计费字符数,三者可能都不一样。中文一个字在len()里算 1,在 UTF-8 字节里算 3;某些引擎还会把空白字符剔除后再计。我做过一次对账,本地统计 82 万字符,实际扣了 94 万,差距全在空白字符和重复请求上。
解决办法是在统一适配器里给每家引擎分别实现一个count_chars方法,按各家的口径预估,并且记录每次请求的预估值和实际配额消耗,做周度对账。对不上就去看是不是有重试导致的重复计费。
8.3 长文本必须自己做分批
四家都有单次请求的文本长度上限。百度是字节限制,DeepL 是文本条数限制,火山是数组长度限制。超限的报错各不相同,但都会让你莫名其妙地失败。
分批的关键是按句子切而不是按字符切。按字符切会把一句话从中间切断,翻译出来是两段语义不完整的内容,质量直线下降。我的做法是先用标点切句,再按顺序累加,超过阈值就开一个新批次,同时保留上下文衔接:把上一批的最后一句作为context参数传给下一批(DeepL 和火山都支持类似的上下文参数),能明显改善跨批次的连贯性。
避坑技巧:分批后的结果拼接时,不要简单地用空字符串连接,要保留原文的段落分隔。我一开始图省事直接 join,结果所有段落挤成一坨,做后续排版的时候又要重新按长度断句,返工成本更高。
9. 我在实际项目里攒下的几点经验
先说密钥申请这件事本身的体会。四家走下来,DeepL 最省心,二十分钟能搞定;火山最费劲,光签名调试就花了大半天;有道和百度居中。但省心不等于好用,DeepL 的免费额度扛不住真实流量,一旦上量就必须付费,所以前期验证阶段别把业务逻辑绑死在它上面,留好切换的余地。
再讲一个我自己的处理方式:把四家的密钥申请做成一份 checklist,写进团队文档,每接一个新引擎就往里加一行,字段包括"申请入口、需要哪些材料、实名时长、密钥字段名、免费额度、单次长度上限、QPS 上限、控制台截图位置"。这份文档看起来土,但新人接手的时候能省下大量的重复摸索,尤其是在密钥轮换和故障切换的时候,直接看表就知道该动哪里。
最后一个实操上容易被低估的点:别在生产环境的代码里做自动重试的无限次叠加。我吃过一次亏,某个引擎抖动,SDK 内部的自动重试加上业务层的重试,一次请求实际打了六次,额度瞬间烧掉一大截,而且日志里看到的是一条业务失败,根本看不出来打了六次。现在的做法是重试只放在一层,并且带指数退避和最大次数,同时在统一适配器里记录"每次业务请求实际发出的引擎请求数",超过阈值就告警。
这些细节没有一条写在官方文档里,但每一条都会在你把服务推上生产之后以某种方式找上门来。密钥申请只是多语言链路的第一公里,后面还有术语管理、翻译质量评估、人工校对回流一堆事,但第一公里走扎实了,后面的路会顺很多。