user-isolation-demo

Python FastAPI React 多租户 Demo

这是什么#

一个完整的多用户 AI 聊天应用,写它是为了验证一个具体问题:多租户隔离应该做在哪一层。

单用户的 AI 应用很容易做。一旦变成多用户,就必须回答”用户 A 绝不能看到用户 B 的会话”——而这句话可以在四个不同层次上实现,代价和可靠性差很远:

在哪一层隔离做法问题
前端界面上只显示自己的会话形同虚设,改个请求就穿透
应用查询每次查询带上 user_id 过滤靠每一处都不写漏,漏一处就泄露
网关/引擎层每个用户一个专属 agent,上下文物理隔离本项目采用
基础设施每个用户一个容器或 VM最强,但成本高、启动慢

这个 demo 选的是第三层:注册时给每个用户分配一个专属 agent,此后他的全部对话都在那个 agent 的独立上下文里跑——隔离不依赖”每次查询都记得加过滤条件”,而是在分配那一刻就定死了。

架构#

user-isolation-demo 架构图

三个设计决策#

① 中间层负责路由,前端从不直连 AI 引擎。

FastAPI 这一层做三件事:认证、把用户路由到自己的 agent、双向代理流式响应。

关键在于前端拿不到 AI 引擎的地址与凭据。如果让浏览器直连引擎,“用户只能访问自己的 agent”这个约束就落到了前端手里——而前端的任何约束都不是约束。隔离必须发生在用户改不到的地方,这是整个 demo 的核心。

② 对话正文不进业务数据库。

PostgreSQL 只存元数据:账号、会话列表、会话标识。真正的消息历史由 AI 引擎的会话存储持有。

这个决定当时是为了避免状态双写——同一份对话存两处,迟早不一致。后来我在 Agent 工程系列写状态所有权时,把这件事想得更清楚了:那份只增不减的消息历史必须有唯一的权威持有者,谁持有它谁就同时获得了 checkpoint、审计与信任边界。分散存两份,等于两边都不权威。

③ 全双工 WebSocket 而不是请求-响应。

一条持久连接同时承载三件事:流式 token、工具调用过程的可视化、会话管理。用轮询或者 SSE 加上另一条上行通道也能做,但连接状态会分散在多处。

顺带一个小优化:WebSocket 连上时,会话列表加载与 AI 引擎握手是并发做的asyncio.gather),而不是串行等待——首屏延迟直接少一个来回。

其它#

支持多会话切换、首条消息自动命名会话、登录后历史恢复、前后端两侧的指数退避自动重连。认证用 JWT 加 bcrypt。

技术栈:Python 3.12 / FastAPI / SQLAlchemy 2.0 async / PostgreSQL;前端 React 18 / TypeScript / Vite / Zustand / Tailwind。


顺带说,“隔离该做在哪一层”这个问题在别处也反复出现——沙箱系列整个系列讨论的其实是同一件事的另一个粒度:那里隔离的是不可信代码,靠的是内核与硬件边界;这里隔离的是租户数据,靠的是网关层的 agent 分配。共同点是:隔离越靠近底层越可靠,代价也越大。