企业AI的上下文工程,第5部分:不泄露、不饥饿、不超支的多租户模式

Dev.to ML 2026-06-25T15:10:54.599196

企业AI的上下文工程,第5部分:不泄露、不饥饿、不超支的多租户模式

原文发布于 PrepStack。这是企业AI上下文工程的第5部分

第1-4部分为 Mattrx 构建了上下文层,这是一个多租户营销分析 SaaS(110k MAU,约 9,000 个租户,峰值约 3,200 req/sec,基于 ASP.NET Core / .NET 9 加上一个独立的 Python FastAPI AI 服务)。每一部分都静默地依赖一个原语——租户边界。本部分将其作为设计面,而非事后才考虑的因素。

TL;DR

多租户并非在查询中加一个 WHERE tenant_id = ? 就能解决。它是一个必须贯穿上下文管道所有表面的单一边界——身份、检索、记忆、提示词、缓存、速率限制、模型路由、成本和驻留——每个表面都有其自身的故障模式。

在将租户范围作为第一类上下文原语后,Mattirx 的结果(2周构建):

一个思维转变

不要将租户视为一个你需要记得添加的过滤器。将租户范围视为上下文本身的一部分——在边缘解析一次,作为不可伪造的令牌通过检索、提示词、缓存、模型和分类账传递。如果隔离依赖于任何人记得添加过滤器,那么当有人忘记的那一天,它就会泄露。

1. 租户身份:解析一次,永不信任请求体

第一个多租户版本从方便的地方读取 tenant_id——查询字符串、JSON 请求体——并将其传递给 AI 服务。任何经过身份验证的用户只需更改一个字段即可读取另一个租户。

修复方法:在边缘从 JWT 中已签名的 workspace_id 声明中一次解析租户。所有下游组件收到一个它们无法伪造或拓宽的 TenantScope,并且每个方法签名都获得一个必需的 TenantScope——因此类型系统拒绝编译忘记租户的查询。将身份从请求体移到令牌中,关闭了整个“更改一个字段,读取另一个租户”类别:红队测试中0次成功的跨租户读取。

2. 知识隔离:默认池化,达到阈值则隔离

三种模型,以及为什么混合模型胜出:

模型 是什么 隔离级别 最适合
池(Pool) 一个索引,每个文档标记 tenant_id,读取时硬过滤 逻辑(缺少过滤器 = 泄露) 大量小租户
隔离仓(Silo) 每个租户一个索引 物理(无可忘记) 巨鲸、企业、欧盟驻留
桥接(Bridge) 默认池化;达到阈值时提升为隔离仓 大多数为逻辑,少数为物理 Mattrx 的选择

纯隔离仓在 9,000 个租户下不可能(Azure AI Search 限制每个服务的索引数量)。纯池无法提供驻留,并让一个租户的语料库降低所有人的召回率。桥接模式为 99% 的租户保留廉价、即时的池,并且只在规模、SLA 或法规要求时才投入物理隔离。关键是,tenant_id 过滤器由路由器应用,从不由调用点应用——因此没有调用者可以忘记它。将巨鲸提升到其自己的索引将其 p95 降到 22 ms,并将长尾租户的 recall@5 从 0.88 恢复至 0.94

3. 提示词与策略变体:行为即数据,而非分支

每个租户的行为过去以 if (tenantId == ...) 分支的形式累积,大型租户每次调用都要重建自定义提示词(仅在系统块中的 DateTime.UtcNow 就导致了 0% 的缓存命中率)。修复方法:租户行为是数据(一个 TenantConfig 行),提示词首先组装成一个字节稳定的共享前缀(每个租户的字节相同,因此提示词缓存可以重用),然后跟着一个小的租户差异片段。结果:系统块缓存命中率 0% -> 71%,并且每个租户都继承相同的注入/编辑规则——没有每个租户的安全漂移。

4. 缓存、配额与路由:阻止一个租户饿死(或冒充)另一个

两个共享资源中没有租户信息。答案缓存仅由问题哈希键控——所以 A 的缓存答案被返回给 B。而一个全局速率限制器让巨鲸的夜间批处理消耗整个预算。

修复措施:缓存键携带租户区域和租户标识(residency:tenant:hash),因此答案无法跨越边界;速率限制按租户分区,每个计划都有令牌桶(QueueLimit = 0 → 返回干净的429状态码并附带Retry-After头部,而非无限队列);模型路由尊重计划剩余预算(超预算则降级模型,绝不返回500错误)。分区公平性使得小租户的p95延迟在“鲸鱼租户”批量任务期间从6.4秒降至1.9秒

5. 成本归属、数据驻留与RLS兜底策略

每次模型调用都会将成本记入每个租户的分类账,该分类账为仪表盘、第4节的预算上限以及基于使用量的计费导出提供数据。数据驻留通过区域固定的索引Azure SQL的行级安全性(Row-Level Security, RLS)来强制实施,因此即使出现忘记租户过滤器的错误查询,也会返回空结果而非数据泄露。该分类账将归属率从0%提升至100%,并将失控租户的小时成本限制在5美元(原本约为140美元);RLS + 区域固定确保欧盟数据留在区域内,审计中0次跨区域读取。

一个微妙之处:RLS可能通过静默返回空行来掩盖缺失的应用层过滤。因此,Mattrx会在RLS过滤掉应用本应限制的行时发出告警——兜底策略捕捉到异常是一个信号,而非成功。

总结思维模型

多租户是一条边界,在多个位置强制执行,一次性解析后永不重新推导。三个习惯:

  1. 在边缘解析,作为令牌携带。 一次性从认证中推导出TenantScope;让每一层都将其作为必需输入;让类型系统拒绝任何缺少租户的调用。
  2. 默认池化,超过阈值再升级。 为99%的情况保留廉价的共享路径;仅在规模、SLA或监管要求下才投入物理隔离——并自动化升级流程。
  3. 双重强制执行隔离。 应用层过滤保证速度,RLS在过滤缺失时兜底。如果单个遗漏的WHERE子句就能导致数据泄露,那么你并未真正隔离任何东西——你只是记录了一个意图。

查看原文