第 29 章 · 多租户设计
Harness 锚点|判别清单第 3 项「状态持久化」——多用户的会话隔离。
一套系统,同时服务几十家卖家
做外贸 SaaS 的老周,把前面几章的 Agent 拼成了一个“选品助手”:接入卖家的后台数据,帮他们看竞品、算利润、写 listing。第一个月只有三家试用客户,跑得很顺。到了第三个月,客户涨到四十多家,问题一下子全冒出来了。
先是一家做家居的卖家在群里炸锅:他在助手里问“我上周的爆款是哪个”,助手回了一段销量分析——数字对,但 SKU 全是别人家的。老周一身冷汗:两家客户的会话,串了。紧接着是账单,一个重度使用的大客户一天跑了两千多次分析,把当天的模型额度吃掉大半,其他客户的助手集体变慢。月底对账时又发现,四十家客户到底各花了多少钱、该收谁多少,他一笔都算不清。
老周踩的这三个坑——数据串了、资源被抢了、账算不清——本质是同一件事:他把一套本来服务“一个人”的系统,直接拿去服务“很多个组织”,却没有在中间划出边界。多个组织共用一套系统、彼此互不可见、各自计费,这套设计范式有个正式名字:多租户(multi-tenancy)。这里的“租户(tenant)”就是一个独立的组织或客户——老周的每一家卖家,就是一个租户。
多租户到底要隔离什么:四重隔离
多租户的核心,可以浓缩成一个词:隔离(isolation)。但“隔离”不是一道墙,而是要贯穿系统的每一层。老周踩的三个坑,恰好对应其中三层。完整地说,一个 Agent 系统的多租户隔离有四重:
- 数据隔离(data isolation):A 租户的会话、记忆、知识库、文件,绝不能被 B 租户看到。老周第一个坑就出在这里。
- 执行隔离(execution isolation):每个租户的 Agent 执行环境彼此隔开——一个租户让 Agent 跑了段炸掉进程的代码,不能连累其他租户。这直接接上第 28 章的沙箱。
- 资源隔离(resource isolation):给每个租户设配额(quota)——token 用量、并发数、请求速率——防止一个大客户挤占所有人。老周第二个坑,就是没有配额。这接上第 26 章的预算分层。
- 配置隔离(configuration isolation):不同租户可以用不同的模型、不同的工具集、不同的策略规则。付费高的客户用更强的模型,某个行业客户屏蔽掉某些工具,都在这一层实现。
四重隔离各管一摊,缺一层就漏一处。数据隔离防的是“看见不该看的”,执行隔离防的是“互相拖垮进程”,资源隔离防的是“互相抢额度”,配置隔离保证的是“各按各的规矩跑”。把这四个词记牢,后面所有的落地讨论都围着它们转。
数据隔离:物理分库还是逻辑分区
四重里最要命的是数据隔离——串数据是安全事故,会直接赔掉客户信任。实现上有两条主流路线,值得讲透:
物理分库(database-per-tenant):每个租户一套独立的数据库(或独立 schema)。A 租户的数据和 B 租户的数据在物理上就不在一起,天然不可能串。好处是隔离最彻底、合规最省心、单租户出问题不影响别人;代价是租户一多,运维成本陡增——几百个库的迁移、备份、监控,是一笔真实的负担。
逻辑分区(shared database with tenant column):所有租户共用一套库,每一条数据都带一个 tenantId 字段,靠查询时强制过滤来隔离。好处是省资源、易扩容;代价是隔离全靠代码纪律——只要有一条查询忘了带 WHERE tenant_id = ?,就会串数据。
// 概念示意:逻辑分区下,所有查询强制注入 tenantId
function scopedQuery(tenantId: string, sql: string, params: unknown[]) {
// 绝不允许裸查询绕过租户过滤——用一层封装把 tenantId 强制注入
return db.query(`${sql} AND tenant_id = $tenant`, {
...params,
tenant: tenantId,
});
}
工程上的通行做法是:不允许业务代码直接写裸 SQL,一律走这层带 tenantId 的封装,从机制上杜绝“忘了过滤”。这也引出了整章最重要的一条原则。
核心原则:隔离默认开启,显式打破
多租户设计里,只有一条原则值得反复强调:隔离要“默认开启、显式打破”。
意思是,系统的默认假设永远是“这条数据属于某个租户、不可跨租户访问”,只有在明确授权、明确写出来的少数场景(比如平台管理员的跨租户报表)才例外。这叫默认隔离(isolation by default)。
反过来的做法——默认共享、需要时再加隔离——极其危险。因为“忘了加隔离”是沉默的:代码照跑、测试照过,直到某天一个租户看到了另一个租户的数据,你才发现漏了一处。而“默认隔离、显式打破”是响亮的:想跨租户访问,你必须显式地写代码绕过防线,这个动作本身就会在 code review 里被看见、被质疑。
这条原则不只管数据。执行隔离默认“每租户独立沙箱”,配置隔离默认“取当前租户的配置、取不到就报错而不是取全局默认”。把安全的那条路设成默认路,把危险的那条路设成需要显式声明的岔路——这是多租户设计最省心也最救命的一招。
用 pi 实现:会话隔离与诚实的边界
讲完概念,落到 pi 上。这里必须先把定位说清楚,否则会给人错误预期。
pi 能支撑的部分——会话隔离。 回顾第 12 章:pi 的会话是持久化、可寻址的,会话目录按来源路径组织在 ~/.pi/agent/sessions/ 下。这为“按租户/用户隔离会话”提供了天然基础——你可以为每个租户维护独立的会话空间:
// 概念示意:按租户隔离会话空间
function sessionScopeFor(tenantId: string, userId: string) {
return `~/.pi/agent/sessions/tenant-${tenantId}/user-${userId}/`;
}
// 每个租户的会话在文件系统上物理隔离,一个租户的会话查询
// 天然不会串到另一个租户的目录里去
再叠加第 28 章的容器化(Gondolin 微 VM / Plain Docker / OpenShell),可以做到“每租户一个独立沙箱执行”——这就同时覆盖了会话层面的数据隔离和执行隔离。
但必须诚实的部分——pi 不是多租户平台。 这里要坦白:pi 是一个单进程的 agent harness(Agent 执行单元),它不是为“云端多租户 SaaS 平台”设计的。真正的企业级多租户,还需要一大堆 pi 本身不提供的基础设施:
- 租户身份与认证:谁是谁,某个请求属于哪个租户——这套身份体系 pi 没有。
- 集中式数据分区与访问控制:跨所有租户统一的数据边界与权限校验,pi 不管。
- 跨节点的资源调度与配额执行:在多台机器上给每个租户分配、限制资源,pi 只是被调度的那个执行单元。
- 租户级计费与账单:算清每个租户花了多少、该收多少(老周的第三个坑),pi 不做。
pi 本身也不含权限系统——默认以启动它的用户权限运行。所以实事求是地说:用 pi 可以做到“按租户/用户隔离会话和执行”,适合中小规模,或者作为大平台里的执行单元。但要做面向公网、大规模、强合规的多租户 SaaS,身份、调度、计费这些必须在 pi 之外自建。作为对照,原书里的 Shannon 是为多租户企业场景而生的重型平台——Go 编排层管调度和配额、Rust 层管执行隔离、独立策略层管访问控制,这是 pi 这类轻量 harness 达不到、也不打算达到的量级。pi 是多租户架构里的一块砖,不是整座房子。
一个务实的架构:pi 当执行单元,外包一层控制平面
既然定位清楚了,落地方式也就顺理成章。一种被反复验证的做法是:把 pi 当作“每租户/每会话的执行单元”,外面包一层你自己的多租户控制平面(control plane)。
你的多租户控制平面(自建)
├── 身份认证 / 租户管理 ← 谁是谁、属于哪个租户
├── 资源配额 / 计费 ← 限额度、算账单(第 26 章)
└── 调度器
└── 为每个租户/会话拉起隔离的 pi 实例(容器 / 微 VM)
└── pi 负责单个会话的 agent 能力(ReAct 循环、工具、记忆)
这里有个术语值得点明:控制平面(control plane)与数据平面(data plane) 的分离,是分布式系统里的经典设计。控制平面管“决策”——谁能进、给多少资源、怎么调度、怎么算钱;数据平面管“干活”——真正跑 Agent、调模型、执行工具。在这套架构里,你自建的那层是控制平面,每个 pi 实例是数据平面。
这样分工的好处是各司其职:控制平面用你最顺手的技术栈写(一套认证网关 + 配额中间件 + 调度器 + 计费服务),它不需要懂 Agent 的内部;pi 只管把单个会话的 Agent 跑好,它不需要懂租户、配额、账单。两边通过“拉起一个隔离实例”这个清晰的接口对接。用对每个工具的定位——重活由控制平面扛,Agent 能力交给 pi。
动手看看
打开你机器上的 ~/.pi/agent/sessions/ 目录看一眼——这就是 pi 会话持久化的落脚点。留意它按来源路径组织会话的方式:不同来源的会话落在不同的子结构里。想象一下,如果把这个“来源路径”换成 tenant-<id>/user-<id>/,会话的物理隔离是不是就自然成立了?
再做个小实验:在两个不同的目录里各起一个 pi 会话,跑几轮对话,然后回到 ~/.pi/agent/sessions/ 看它们是不是落在了各自独立的位置、互不干扰。这就是“逻辑上的会话隔离”最朴素的形态。想清楚这一层能做什么、不能做什么,你就能准确判断:哪些隔离 pi 帮你兜住了,哪些还得靠外面那层控制平面。
实战中的几个坑
坑一:逻辑分区漏了一处 tenantId 过滤。
- 现象:某个查询偶尔返回别的租户的数据,且难以复现。
- 原因:共用库、靠代码纪律过滤,某条查询忘了带租户条件。
- 对策:禁止裸查询,一律走强制注入
tenantId的封装层;关键路径加自动化测试专门查越权。
坑二:没有配额,一个大客户拖垮所有人。
- 现象:某租户高频调用,其他租户集体变慢甚至排队超时。
- 原因:资源隔离缺位,没给每租户设 token/并发/速率上限。
- 对策:在控制平面按租户设配额(第 26 章),超限限流或降级,而不是让它无限占用。
坑三:配置隔离取到了错误的默认值。
- 现象:某租户本该用高级模型,却悄悄跑在了便宜模型上。
- 原因:租户配置没取到时回退到了“全局默认”,出错却不报错。
- 对策:配置遵循“默认隔离”——取不到当前租户配置就明确报错,而不是静默回退。
坑四:把 pi 当成了多租户平台本身。
- 现象:直接拿单进程 pi 对公网多租户开放,身份、配额、计费全没有。
- 原因:混淆了执行单元和平台的定位。
- 对策:pi 只当被调度的执行单元,身份/调度/计费在 pi 之外自建控制平面。
对比:其他框架
多租户是平台/架构层面的问题,几乎没有哪家开源 Agent 框架把它当一等特性——四重隔离通常要靠外层的控制平面自建:
| 框架 | 多租户怎么做 | 特点 |
|---|---|---|
| LangGraph | 框架本身不管租户隔离,但 LangGraph Platform(托管产品)把多租户、持久化、部署作为平台能力提供 | 自托管时租户边界要自己划,托管产品才有一等支持 |
| CrewAI | 聚焦团队协作编排,不内建多租户 | 身份、数据隔离、配额都需框架之外自建 |
| PydanticAI | 定位“像写普通 Python 一样写 Agent”,不涉及多租户 | 隔离与调度完全交给你的应用层 |
| Agno | 提供 AgentOS 运行时与监控面板,让单应用更完整 | 不等于多租户平台,跨租户隔离与计费仍需外层承担 |
| pi | 提供会话隔离 + 容器化执行的基础,适合作执行单元 | 身份、调度、计费需在 pi 之外自建控制平面 |
差异的本质:多租户是控制平面的职责。只有托管平台(如 LangGraph Platform)替你把这层扛下来;开源框架大多只当“被隔离的执行单元”,pi 也是如此——它把执行单元这块做扎实,其余交给你的架构。
小结
- 多租户(multi-tenancy) 要解决“一套系统安全、公平地服务多个租户”,靠的是数据、执行、资源、配置四重隔离,缺一层漏一处。
- 数据隔离有物理分库和逻辑分区(带 tenantId 强制过滤) 两条路;核心原则是隔离默认开启、显式打破——把安全的路设成默认路。
- pi 提供会话隔离 + 容器化执行的基础,适合作执行单元,但不是多租户平台——租户身份、集中访问控制、跨节点调度配额、租户级计费都需 pi 之外自建。
- 务实架构是控制平面 + 数据平面分离:你自建的控制平面管身份/配额/计费/调度,每个 pi 实例当数据平面跑单个会话的 Agent。
到这里,Part 8 讲完了——预算、治理、安全、多租户,这是企业级 Agent 绕不开的四道关。接下来的 Part 9 是前沿实践,我们把全书所学落到真实、复杂的 Agent 形态上,第一个就是最近格外热门的 Deep Research(深度研究)。