# 一个 API 密钥，多个租户：我们如何在不同客户间隔离 DeepL 翻译

> Rasepi 对所有租户使用一个 DeepL API 密钥。以下是我们如何处理每个客户的词汇表、风格规则、缓存翻译和块级隔离，而不泄露任何信息。

2026-04-18 · Tim Cadenbach · https://www.tcdev.de/zh/blog/one-api-key-many-tenants-deepl-isolation/

---

每次我向其他开发人员解释 Rasepi 的翻译架构时，他们都会提出一个问题："等等，你们的所有租户都共享一个 DeepL API 密钥？你如何保证他们的词汇表和风格规则不会相互泄露？

这个问题很有道理。答案涉及的设计工作比你想象的要多。

我们在上一篇文章中介绍了[完整翻译流水线](https://www.tcdev.de/zh/blog/inside-the-translation-engine-glossaries-style-rules-and-smart-retranslation/)，包括块级散列、协调器以及从文档保存到翻译输出的整个流程。本篇文章将深入探讨多租户的具体问题。如何使用没有租户概念的第三方 API 并在其基础上构建租户隔离。

## 问题：DeepL 不了解您的客户

DeepL 的 API 使用单个 API 密钥进行验证。在该密钥下创建的所有内容、词汇表、样式规则列表、翻译历史都属于同一个账户。DeepL 没有 "此词汇表属于租户 A "的概念。

当您调用 `GET /v2/glossaries`时，您将获得来自 ** 个租户的 ** 份词汇表。创建样式规则列表时，该列表与其他租户的样式规则位于同一命名空间。API 是扁平的。

对于自托管产品来说，每个客户都使用自己的 DeepL 密钥运行自己的实例，这样就可以了。对于我们管理基础设施的多租户 SaaS 而言？你需要一个隔离层。

## 数据库是真相之源

我们的核心设计决策：**数据库拥有所有词汇表内容和样式规则配置。DeepL 是运行时的执行目标，仅此而已。**

每个 `TenantGlossary` 和 `TenantStyleRuleList` 实体都实现了 `ITenantScoped`，这意味着 EF Core 全局查询过滤器会自动将所有读取范围限定为当前租户。在租户 A 的请求上下文中查询词汇表，永远不会返回租户 B 的条目。这也是我们在 Rasepi 中随处使用的隔离模式，在 ORM 层级强制执行。

有趣的地方就在这里。当租户编辑词汇表术语时，我们不会立即调用 DeepL。我们会更新数据库行并设置 `IsDirty = true`。就是这样。实际的 DeepL 词汇表会在下一次翻译需要它之前懒散地创建（或重新创建）。

___codeblock_0__

`TenantGlossaries`_上的查询过滤器会进行隔离。_`IsDirty`标志实现了懒同步。命名约定（_`rasepi-{glossary.Id}`）仅用于在 DeepL dashboard 中调试，没有任何功能用途。

为什么是懒同步？因为 [DeepL v2 词汇表是不可变的](https://developers.deepl.com/docs/api-reference/glossaries)。您无法编辑它们。任何更改都意味着删除和重新创建。如果一个团队导入了包含 200 个术语的 CSV，然后修改了一个词条中的一个错字，我们不希望两次删除和重新创建 DeepL 词汇表。我们只需两次都设置 `IsDirty`，下一次翻译运行时就会进行一次重新创建。

### 样式规则：相同的模式，不同的应用程序接口

[DeepL's样式规则](https://developers.deepl.com/docs/api-reference/translate/openapi-spec-for-text-translation) 是较新的（v3 API），实际上是可变的，这一点更好。你可以使用 `PUT /v3/style_rules/{style_id}/configured_rules` 就地更新已配置的规则，还可以单独添加或删除自定义指令。

不过，我们仍然使用相同的 `IsDirty` 模式。一个 `TenantStyleRuleList` 有一个 `DeepLStyleId` 映射到 DeepL 的运行时标识符，另外还有 `ConfiguredRulesJson` 用于格式化规则，以及一个 `TenantCustomInstruction` 条目集合用于自由文本翻译指令。

真正的威力在于这些自定义指令。每条指令都是最多 300 个字符的纯语言指令，可影响 DeepL 的翻译方式。来自我们租户的真实例子：

- _"始终使用'Sie'形式，而不是'du'"_一家德国律师事务所
- _"将'deployment'翻译为'Bereitstellung'，而不是'Deployment'"_对于依赖于上下文的术语，这超出了简单的词汇表映射范围
- _"使用英式英语拼写（颜色、组织、许可证）"_适用于在英语变体之间进行翻译的英国公司
- _"在数字金额后加上货币符号"_用于欧洲惯例

每个租户的每种目标语言都可以有完全不同的指令，但都使用相同的 API 密钥。每一次翻译调用都只包含属于请求租户的 `glossary_id` 和 `style_id`_，从而实现了隔离。其他租户的 DeepL 资源永远不会被引用。

## 翻译调用：所有内容组成

当协调器翻译一个程序块时，它会将所有租户的特定设置合并到一个请求中：

代码块_1__

这里的每个参数都针对租户。`glossaryId` 是通过租户过滤查询解决的。`styleId` 也是通过同样的方式解析的。`formality`_来自`TenantLanguageConfig`_，也是租户作用域。甚至___DEBLOCK_23__（为提高翻译质量而发送的周边段落，不计费）也来自租户自己的文档。

我想强调的一点是：当设置 `style_id` 时，DeepL 会自动使用他们的 `quality_optimized` 模型。您不能将样式规则与 `latency_optimized` 结合使用。这是一个 DeepL 限制，但说实话是一个合理的限制。如果您要投资自定义样式规则，您可能希望获得最佳的输出质量。

## 块级缓存：数据库作为翻译记忆库

我们不会调用 DeepL 来处理没有变化的代码块。缓存机制就是 `TranslationBlock` 表本身。

每个 `EntryBlock`源都有一个 `ContentHash`，这是其语义内容的 SHA256 值（去除了 `blockId`和 `deleted`等元数据属性）。每个 `TranslationBlock`_都存储了翻译时的 `SourceContentHash`_。当源代码块发生变化时，其哈希值也会发生变化。协调器会比较哈希值，并只对不匹配的区块进行排队。

每个区块的决策树如下所示：

1.**哈希值匹配、翻译存在** = 跳过（缓存、最新）
2.**哈希值已更改、机器翻译、未锁定** = 自动重新翻译
3.**哈希值已更改，人工编辑或锁定** = 标记为过时，不覆盖

第三种情况至关重要。如果您的德语译员对某个段落进行了人工润色，我们不会因为英文来源发生了变化就将其删除。我们会将其标记为 "过期"，以便他们知道需要对其进行审核，但翻译后的文本保持不变。

实际结果是：在 30 个段落的文档中编辑一个段落，只需触发一个 DeepL API 调用（嗯，一个批次，包括一个区块）。所有语言的其他 29 个段落都已缓存，不会产生任何费用。

## 为什么不对每个租户使用单独的密钥？

我认真考虑过。给每个租户一个自己的 DeepL API 密钥，完全消除隔离问题。

我们没有这么做有三个原因

1.** 计费复杂性。** 每个租户都需要自己的 DeepL 订阅或提供子账户的方法。DeepL 本身不提供多租户密钥管理。
2.**共享基础设施意味着共享费率限制和批量折扣。我们的总使用量可获得更优惠的价格。
3.** 操作简单。**只需轮换一个密钥，监控一个配额，维护一个集成。

代价是我们需要我所描述的隔离层。但是，鉴于我们已经为系统中的其他一切提供了租户范围的 EF Core 查询，因此将其添加到词汇表和样式规则中就变得简单易行了。模式已经存在。

##真正保护您的是什么

总结一下隔离保证：

- 词汇表条目**存储在 `TenantGlossary`（实现 `ITenantScoped`）中，由 EF Core 全局查询过滤器过滤。DeepL 词汇表 ID 是不透明引用，只能在租户上下文中解析。
- 样式规则和自定义说明**通过 `TenantStyleRuleList`_遵循相同的模式。
- 翻译内容**位于 `TranslationBlock`中，通过其父 `Entry` → `Hub`链进行作用域划分，该链也是租户作用域划分。
- `SaveChanges`_防护**会在新实体上自动设置`TenantId`_，并在跨租户写入时抛出。
- 生产代码中没有 ____DEBLOCK_42__**。永远不会

> 设计原则很简单：DeepL 查看资源 ID。Rasepi 看到的是租户范围内的实体。它们之间的映射永远不会跨越租户边界，因为解析映射的查询在物理上无法返回另一个租户的数据。

如果你正在构建一个多租户的 SaaS，并与没有原生租户支持的第三方应用程序接口集成，那么这种模式就能很好地发挥作用。将外部 API 视为无状态执行引擎，将所有配置保存在你自己的租户范围数据库中，懒散地同步，永远不要相信外部资源列表的隔离性。
