AI编程 2026-08-24 14 次浏览

同一个AI Agent服务多个客户,数据为什么会串到别人那里?

多租户Agent的数据泄漏通常发生在模型调用之前。本文从身份、记忆、RAG、缓存、工具、异步任务和日志出发,建立可验证的租户隔离链路。

多租户 Agent 的数据泄漏,通常不是模型“记住了另一个客户”,而是系统在模型调用之前就把错误的数据放进了上下文。可靠的隔离必须贯穿身份、会话、记忆、检索、缓存、工具、异步任务和日志;任何一层丢失租户边界,都可能让模型把越权数据组织成一段看似正常的答案。

假设一家公司为多个企业客户提供同一个知识助手。

客户 A 问:“总结我们上季度的续约风险。”Agent 从向量库检索资料,读取 CRM,再结合历史对话生成答案。回答逻辑完全正常,引用也像模像样,但其中出现了一家陌生客户的合同金额。

这类问题最容易被解释为“大模型产生了幻觉”。可如果金额、合同名称和联系人确实属于客户 B,它更可能是一次跨租户数据泄漏:检索层、缓存层、记忆层或工具层先拿错了数据,模型只是把已经进入上下文的内容说了出来。

所以,修复方案不能停留在系统提示词里增加一句“不要泄露其他客户数据”。模型不应该承担最终授权职责。租户隔离必须由模型之外的确定性系统强制执行,并且默认拒绝缺少租户身份的任何访问。

本文面向正在建设企业 Agent、RAG 或多客户 AI SaaS 的研发团队,拆解数据串租户的主要路径,并给出一套可以验证的隔离设计。

一、先分清三个概念:用户、会话和租户不是一回事

多租户系统中最常见的设计错误,是拿一个容易获取的 ID 代替真正的隔离边界。

  • 租户 tenant_id:拥有数据、配置和资源的客户组织;
  • 用户 user_id:租户中的具体操作者;
  • 会话 thread_id:一次或一组连续对话的状态容器。

一个用户可以创建多个会话,一个租户可以包含多个用户。同一个用户甚至可能以不同身份加入多个组织。因此,thread_id 不能替代 tenant_iduser_id 也不能单独代表租户。

更完整的资源作用域通常还要包含 Agent 或应用身份:

tenant_id
  └─ user_id
      └─ agent_id
          └─ thread_id

具体层级取决于业务,但有一条原则不变:任何持久状态都要能回答“它属于哪个租户”,任何读取都要同时证明“当前身份有权访问这个租户中的哪些资源”。

AWS SaaS Lens 将租户上下文视为身份体系中的一等属性,并强调认证、授权并不自动等于租户隔离。用户能够登录,只能证明他是谁;系统仍要在所有资源层阻止他跨越租户边界。

二、租户身份必须从可信入口产生,而不是让模型或用户填写

一个危险的接口可能长这样:

{
  "tenant_id": "customer-b",
  "question": "列出全部合同"
}

如果后端直接相信请求体中的 tenant_id,攻击者只要修改一个字段,就可能查询别的客户。把租户 ID 写进提示词,再要求模型“只能访问这个租户”,也没有改变授权仍由不可信输入控制的事实。

可信的租户上下文应该来自已经验证的身份,例如服务端校验过的会话、JWT 声明或工作负载身份,并由授权层绑定用户、租户、角色和权限版本。业务请求可以引用资源,但不能自行声明自己属于哪个租户。

一个最小的上下文可以包含:

{
  "tenant_id": "t_0187",
  "subject_id": "u_0421",
  "roles": ["analyst"],
  "permission_ver": "17",
  "request_id": "r_8102"
}

它应在入口处生成,随后以不可被业务参数覆盖的形式传入 Agent 编排器、检索服务、工具网关、任务队列和审计系统。

租户身份要从可信入口贯穿完整Agent链路

这里有两个经常被忽略的边界。

第一,不能使用进程级全局变量或可被并发请求覆盖的“当前租户”。同一个 Worker 同时处理客户 A 和客户 B 时,共享变量可能在异步切换后读到另一个请求的值。

第二,缺少租户上下文必须失败关闭。不能为了“兼容旧调用”在 tenant_id 为空时取消过滤,也不能默认进入某个公共租户。生产系统里,空上下文往往不是匿名访问,而是隔离链路已经断裂。

三、会话隔离了,不代表长期记忆也隔离了

很多 Agent 框架用 thread_id 保存短期对话状态。这可以防止两个会话直接共享消息,但多租户产品通常还有跨会话的长期记忆:用户偏好、客户术语、业务规则、历史决策和已确认事实。

如果长期记忆只按 assistant_id 或 Agent 名称存储,那么所有使用同一个 Agent 的客户都可能进入同一个命名空间。客户 A 写入的“本公司重点行业是医疗”,可能在客户 B 的新会话中被召回。

LangGraph 官方持久化文档区分了线程状态和跨线程 Store,并用自定义 namespace 组织长期记忆。其多用户后端文档进一步提醒:生产环境应显式提供命名空间工厂;若使用共享 Assistant 作为默认作用域,多个用户可能共享同一存储空间。

多租户记忆键至少要覆盖:

(tenant_id, user_id, memory_type)

如果记忆需要在租户内共享,可以使用:

(tenant_id, team_id, memory_type)

但共享范围必须来自权限策略,而不是让模型决定“这条记忆看起来适合全公司”。模型可以提出记忆候选,确定性的策略层负责选择作用域、保留周期和可见人群。

Agent长期记忆必须按租户和使用范围隔离

还要特别检查以下路径:

  • 会话复制、分享和客服代查是否重新做授权;
  • 摘要和压缩后的历史是否保留租户归属;
  • 删除用户后,长期记忆和向量索引是否同步删除;
  • 测试环境是否导入了未经脱敏的生产记忆;
  • 管理员修复某个会话时,操作结果是否误写入普通用户空间。

四、RAG 最危险的错误,是检索后才过滤

在检索增强生成(RAG)中,租户字段常被作为向量文档的一项 metadata:

tenant_id = t_0187
document_id = contract_302
allowed_groups = legal, sales

但“索引里存了租户字段”不等于查询时一定使用了它。常见缺陷包括:

  • 某条备用检索路径忘记拼接租户过滤条件;
  • 向量检索按租户过滤,关键词检索却查询全局索引;
  • 初次召回正确,重排序或查询扩展阶段重新访问了全量语料;
  • 文档权限已经撤销,索引中的权限 metadata 尚未更新;
  • Top-K 先从全库取回,再由应用删除越权结果;
  • 调试或降级模式在过滤器异常时自动取消过滤。

最后一种尤其危险。权限过滤失败时,正确行为应该是返回无结果或明确报错,而不是为了提高召回率改用全局查询。

Azure AI Search 的安全最佳实践将文档级权限控制列为 Agent、RAG 和企业搜索的重要能力:权限信息在索引阶段保存,并在查询时根据调用者身份过滤。官方文档也提醒,源系统权限变化要经过同步或重新索引后才能反映在搜索结果中,这意味着“权限陈旧窗口”必须被纳入风险设计。

检索授权应该发生在文档内容进入 Agent 上下文之前:

def retrieve(query, ctx):
    scope = {
        "tenant": ctx.tenant,
        "groups": ctx.groups,
    }
    return search(
        query,
        filter=scope,
    )

这段伪代码没有覆盖具体搜索引擎的语法,但表达了关键边界:过滤条件来自可信上下文,检索服务负责强制执行;模型既不能删除过滤条件,也不能通过改写查询扩大权限。

共享索引、独立命名空间还是独立索引

三种模式没有脱离约束的唯一答案:

  • 共享索引 + metadata 过滤:成本和运维效率高,但任何漏过滤路径都会扩大影响面;
  • 共享服务 + 租户命名空间:边界更清晰,但仍要检查跨命名空间管理接口和备份导出;
  • 租户独立索引或独立服务:隔离更直观,适合高合规客户,但成本、配额和运维复杂度更高。

实际系统可以采用 Pool、Bridge 和 Silo 混合策略:普通客户共享受控资源,高风险数据或高级客户使用更强隔离。关键不是选择一个听起来最安全的名词,而是把每条查询路径的隔离机制写清楚并自动验证。

五、缓存键漏一个维度,就可能把正确答案发给错误的人

为了降低模型和检索成本,Agent 系统经常缓存:

  • 问题对应的检索结果;
  • 工具调用结果;
  • 模型最终回答;
  • Prompt 拼装后的中间状态;
  • 语义相似问题的答案。

如果缓存键只有 query_hash,客户 A 问过“本季度合同金额”后,客户 B 提出相同问题,就可能命中 A 的结果。

缓存键至少要考虑:

tenant + subject + permission_ver
+ model + prompt_ver + data_ver
+ query_hash

是否需要 subject,取决于缓存结果是在租户内共享,还是受用户和角色权限限制。这里宁可降低命中率,也不能用错误的共享范围换性能。

语义缓存风险更高,因为它不是按完全相同的问题命中,而是按向量相似度复用答案。缓存写入和读取都必须处于同一租户与权限作用域,缓存向量库本身也要隔离。权限变化后,应通过 permission_ver 或主动失效让旧答案不能继续命中。

RAG检索和语义缓存都要在返回前执行租户隔离

不要把输出脱敏当成缓存隔离的替代方案。脱敏可以减少身份证号、手机号等敏感字段暴露,却无法判断“这份经过脱敏的商业策略是否属于当前客户”。归属与权限问题必须由授权系统解决。

六、工具共用管理员凭证,租户过滤就只剩下一个字符串

很多 Agent 工具被设计成:使用一个平台管理员密钥连接 CRM、数据库或对象存储,再把 tenant_id 作为普通参数传进去。

这意味着工具在技术上拥有读取所有客户数据的能力,隔离完全依赖每个调用者都正确填写参数。只要模型生成错参数、调用链漏字段或代码存在注入,权限边界就会消失。

更稳妥的顺序是:

  1. 工具网关接收可信的租户上下文;
  2. 根据租户和当前主体换取短期、受限的访问凭证;
  3. 工具只能访问该凭证允许的资源;
  4. 数据层再次执行租户或行级策略;
  5. 审计记录保存租户、主体、工具、资源和结果摘要。

AWS SaaS Lens 给出的运行时隔离模式也是让共享函数根据当前租户上下文获取租户作用域凭证,而不是长期持有可访问所有租户的身份。

数据库还可以使用行级安全(Row-Level Security,RLS)作为纵深防御。PostgreSQL 官方文档说明,启用 RLS 后,普通查询和写入需要通过策略;如果没有适用策略,则默认拒绝。但表所有者通常绕过 RLS,超级用户和拥有 BYPASSRLS 属性的角色也不会受普通策略限制。

所以不能让 Agent 使用表所有者或超级用户连接数据库,再宣称“已经启用 RLS”。连接池还要在每个事务设置并清除租户上下文,避免把上一个请求的会话变量带给下一个租户。

Agent调用工具时权限必须缩小到当前租户

对于修改类工具,还需要同时约束:

  • 资源 ID 必须在当前租户内重新解析,不能只相信模型给出的 ID;
  • 高风险操作在批准前展示租户、资源和影响范围;
  • 幂等键中包含租户作用域,避免不同租户互相去重;
  • 补偿、重试和恢复流程继续使用原租户上下文;
  • 工具错误信息不能返回其他租户资源是否存在。

七、异步任务最容易在“离开请求线程”后丢失身份

HTTP 请求阶段做对隔离,并不代表后台任务仍然安全。

Agent 可能把长任务放进队列,等待几分钟后由另一个 Worker 执行;也可能创建定时器、子 Agent、并行任务或人工审批节点。如果队列消息只保存 job_id,Worker 再根据一张全局任务表查询数据,租户上下文就可能在中途丢失。

异步消息至少要携带不可被普通业务字段覆盖的租户作用域,并在消费端重新授权。它不是简单复制入口 JWT:长任务可能跨越令牌有效期,也可能遇到用户被禁用或权限变更。更合理的做法是传递稳定主体与租户声明,在执行时换取新的短期权限,并校验权限版本。

特别需要测试:

  • 任务重试是否仍属于原租户;
  • 子 Agent 是否继承了更大的父级权限;
  • 人工审批链接能否被另一个租户消费;
  • 定时任务是否使用了创建者权限还是系统权限;
  • 死信队列、补偿任务和人工重放是否保留隔离信息。

八、日志和评测系统也可能成为新的跨租户数据仓库

为了排查 Agent,团队往往记录完整 Prompt、工具参数、检索文档、模型回答和状态快照。这样做提高了可观测性,也把原本分散的数据集中到日志与追踪系统中。

如果所有租户轨迹进入同一个可搜索项目,而客服、研发或自动评测服务拥有全局读取权限,业务数据库的隔离可能被旁路。

日志设计应该同时满足可定位和最小披露:

  • 用租户标签做访问控制和审计,而不是只用于搜索;
  • 不记录访问令牌、数据库凭证和完整密钥;
  • 对工具结果按字段分级,默认不保存完整正文;
  • 为会话、检查点、长期记忆和追踪设置保留周期;
  • 导出、训练、评测和故障样本复用前重新确认授权;
  • 运营后台的“跨租户搜索”必须是受控的特权操作。

OWASP 将敏感信息泄露列为大模型应用的重要风险。对多租户 Agent 来说,防止模型输出敏感字段只是最后一道防线;更重要的是减少越权数据进入 Prompt、日志、评测集和长期记忆的机会。

九、如何证明客户 A 永远看不到客户 B

隔离不能只靠代码评审和正常路径测试。最有效的测试不是“客户 A 能否查到自己的合同”,而是主动构造跨租户攻击。

建议建立一套带金丝雀标识的数据集:每个租户放入唯一、可检测但不含真实隐私的标记,然后覆盖以下失败路径。

身份与接口

  • 修改请求体、Header、资源 ID 和会话 ID 指向其他租户;
  • 使用合法用户访问不属于自己的租户;
  • 删除租户上下文,验证系统是否拒绝而非放宽;
  • 使用已禁用用户、过期角色和旧权限版本。

Memory、RAG 与缓存

  • 两个租户使用相同问题、相同文档标题和相似语义;
  • 让客户 A 写入长期记忆,再由客户 B 新建会话召回;
  • 分别测试向量检索、关键词检索、混合检索、重排序和降级路径;
  • 修改源文档 ACL,观察索引同步前后的行为;
  • 验证语义缓存、工具缓存和最终回答缓存不会跨租户命中。

工具与异步任务

  • 伪造属于其他租户的资源 ID;
  • 让任务在队列中重试、超时、人工重放和进入死信;
  • 并发执行两个租户任务,检测上下文是否串线;
  • 让审批人打开其他租户的操作链接;
  • 用受限工具凭证验证数据层仍会拒绝越权。

测试通过条件不能只是“最终回答没有出现标记”。还要检查检索结果、模型输入、工具参数、缓存命中和日志轨迹,证明越权数据从未进入不该进入的层。

十、上线后应该监控什么

跨租户泄漏事件越早在模型调用前被拦截,影响越小。可以监控:

  • 缺少或无法验证租户上下文的请求数;
  • 请求租户与资源租户不一致的拒绝次数;
  • 检索结果中出现多个租户标识的次数;
  • 缓存写入与读取作用域不一致的次数;
  • 使用全局管理员凭证的工具调用;
  • 后台任务权限版本过期或主体已失效;
  • 跨租户管理查询、导出和人工重放行为;
  • 金丝雀标识进入错误 Prompt、回答或日志的告警。

这些指标需要防止高基数失控,也不能把真实客户 ID 直接暴露给所有监控用户。可以使用受控映射、哈希标识和分级访问,在可追踪与隐私之间取舍。

如果发现疑似串租户,首先应冻结相关缓存、任务和工具凭证,保留审计证据,确认越权数据进入了哪一层,再决定是否清理记忆、重建索引、失效缓存或轮换凭证。不要只删除最终对话,因为泄漏源可能仍在继续服务其他请求。

十一、怎样选择隔离强度

不是每个客户都必须拥有独立的一整套 Agent 基础设施。隔离策略通常在成本、规模、合规与运维复杂度之间权衡。

适合共享资源并由策略隔离的场景:

  • 数据敏感度和合规要求可控;
  • 数据层、检索层和工具层都有强制租户策略;
  • 团队具备完整的越权测试和审计能力;
  • 共享组件能够按租户限流、计费和清理。

更适合独立索引、独立密钥甚至独立环境的场景:

  • 医疗、金融、法律或其他高敏感数据;
  • 客户要求独立加密密钥、区域或网络边界;
  • 第三方工具无法提供可靠的细粒度授权;
  • 共享资源的单点配置错误会造成不可接受的影响。

隔离强度可以分层,但不能出现“低价租户不做隔离”。共享和独立的区别是隔离机制与成本模型不同,不是安全要求是否存在。

结语:不要让模型替系统守住租户边界

同一个 AI Agent 服务多个客户,本身并不会必然导致数据串租户。真正的风险来自一条链路中出现了不一致的作用域:会话按用户隔离,Memory 却按 Agent 共享;向量检索加了过滤,缓存却按问题共享;接口完成了认证,工具却使用全局管理员密钥;前台请求带着租户身份,后台任务却只剩一个任务编号。

要避免这种问题,需要坚持五条原则:

租户身份从可信入口产生;
租户上下文贯穿所有执行层;
越权数据在进入模型前被拦截;
缓存、日志和异步任务同样隔离;
缺少上下文时默认拒绝。

提示词可以提醒模型谨慎处理数据,但它不是访问控制系统。真正可靠的多租户 Agent,应该让模型即使“想”访问另一个客户,也拿不到那部分数据。

参考资料

  • AWS Well-Architected SaaS Lens:Identity and access management
    https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/identity-and-access-management.html
  • AWS Well-Architected SaaS Lens:The isolation mindset
    https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/isolation-mindset.html
  • AWS Well-Architected SaaS Lens:Preventing cross-tenant access
    https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/preventing-cross-tenant-access.html
  • PostgreSQL 官方文档:Row Security Policies
    https://www.postgresql.org/docs/current/ddl-rowsecurity.html
  • Microsoft Learn:Azure AI Search Security Best Practices
    https://learn.microsoft.com/en-us/azure/search/search-security-best-practices
  • Microsoft Learn:Document-Level Access Control in Azure AI Search
    https://learn.microsoft.com/en-us/azure/search/search-document-level-access-overview
  • LangGraph 官方文档:Persistence
    https://docs.langchain.com/oss/python/langgraph/persistence
  • LangGraph 官方文档:Backends and Namespace Factories
    https://docs.langchain.com/oss/python/deepagents/backends
  • OWASP Gen AI Security Project:Sensitive Information Disclosure
    https://genai.owasp.org/llmrisk/llm022025-sensitive-information-disclosure/