一百个人同时要 AI 搭子,谁说了算?——我写了个调度大脑,但真正的设计是三条死规矩 前一篇我们把 AI 搭子封成了一个标准化的罐头——开盖即用谁打开都有。可你很快会遇到一个新问题一百个人同时打开罐头呢不是技术上的并发处理不过来——是更本质的问题谁来管谁什么时候能有搭子、用完了谁收、一个用户能在多少台机器上同时开着、怎么知道他还用不用、用完了资源给谁腾出来。这不是你写几行调度代码能解决的事。这是一个治理问题——你怎么给一群人要同时用搭子这件事立一套秩序。所以我写了一个大脑一个叫agent-master的控制面服务。但这一篇我不想跟你聊这个服务本身——我想跟你聊的是它背后的三条设计规矩。因为这三条规矩才是真正的设计。没有它们调度代码不值钱。一、调度这件事本质是管三件事在动任何代码之前我先问了自己三个问题一个搭子归谁——两个人同时请求是开两个实例还是复用同一个我怎么认出是谁在要什么时候能碰文件什么时候绝对不能碰——第一次给一个用户开搭子和它掉线了重新拉起来操作能一样吗用完了谁管收——用户关掉页面直接走了那个运行着的搭子谁来关、什么时候关你仔细看这三个问题没有一个是写调度代码能回答的。它们问的是边界、状态和责任——这是设计不是实现。想清楚了这三条我才开始落在 Kubernetes 和 Redis 上。你让我把这三条掰碎了讲给你听。二、第一条规矩一人一个实例归属焊死第一条以一个用户为最基本的调度单位。一个用户始终只有一个活跃的 AI 搭子实例。这意味着什么如果你在 A 电脑和 B 电脑上同时登录后者会把前者的实例顶掉——而不是给你开两个。你的搭子始终跟着你的身份走不跟着你的设备走。这个选择否决了别的选择比如一个会话开一个实例那一个人开三个标签页就有了三个搭子各聊各的自己跟自己打架、“一个任务开一个实例”频繁创建销毁成本极高。怎么实现的服务端不信任客户端自己报的身份。上游网关做完鉴权后在请求 Header 里写死x-user-id服务端以此为唯一索引去 Redis 里查这个用户有没有正在运行的实例。x-user-id缺失或空直接拒绝。WebSocket 连接也要通过 query 或子协议传同样的 id 才能连上。谁叫你谁就是你的——不认客户端自己说的身份。这一条秩序把归属这个最基本的问题焊死了。调度调度的从来不是机器是人。想清楚调度单位是什么比想清楚怎么调度重要一万倍。三、第二条规矩第一次创建才写文件重启绝不碰第二条更微妙。它管的是状态什么时候能被改变。一个用户第一次请求搭子系统要干的事情很多创建目录、挂载 NAS 持久化空间、从模板拷贝 AGENTS.md 和 opencode.json、设权限——然后才发起 Kubernetes Deployment。但如果这个用户之前已经有过搭子只是 Pod 挂了需要重启——流程完全不一样。重启时只做一件事确认目录存在。绝不写任何文件。AGENTS.md 即使丢了也不重新创建对不创建。opencode.json 即使没了也不重新写入对不写入。重启就是重启不是初始化。为什么要这么严格因为一旦重启流程也可以写文件就会引入一种极其隐蔽的 bug用户的配置更新了比如升级了 AGENTS.md重启又被覆盖成旧的了。或者反过来你写在重启流程里的逻辑和首次创建的逻辑冲突了——同一个用户的第一次和第二次启动行为不一致。这条规矩落到代码里写着就一句话首次创建用O_EXCL模式写文件不存在才创建重启只mkdir -p。但这一条路是设计里最难想明白的那一层——你想清楚了什么状态下能做什么操作你的系统才是可预测的。我把这条规矩也掰碎了写在服务端的文档里AGENTS.md里专门有一整节讲这个因为这是新来的人最容易弄混的地方——他以为重启时顺手补一个文件没什么大不了其实这一下就把状态确定性砸了。四、第三条规矩租约当警察不信任任何常驻第三条管的是用完了谁收。用户打开搭子界面、用着、然后用完直接关了浏览器——或者网络断了或者关机了。那个在 Kubernetes 上跑着的 AI 搭子 Pod 不会知道你走了。它还在那跑着、占着资源、等着你回来。传统思路是等用户发一个关机的信号再回收。但这个方案有根本缺陷你信不过那个信号。用户网络断了发不过来、浏览器崩溃了没发出去、恶意客户端假装发了但你收不到——你这个大脑就被动地等着一个可能永远不会来的信号。所以我们换了一个思路所有搭子都是有期的。每个实例创建时配一个 TTL生存时间比如 3600 秒。实例必须在 TTL 内持续续约告诉大脑我还在用如果超过 TTL 没续约——大脑自动回收这个实例删 Deployment、清 Redis 映射一个不留。用户真正在用的场景下续约是 5 分钟一次的固定心跳不会断。但用户走了、网络断了心跳断了——TTL 一到期立刻回收。大脑不等人它只认心跳。这就像图书馆的座位管理系统你扫了码坐下系统给你 2 小时。你每 30 分钟刷一次码续时间座位一直是你的。但你走了没续——时间一到座位自动释放给下一个人。不是等人走了再收是到期了收、你还在用就续。这个区别就是产品化调度和随缘调度的分水岭。五、三个巨人打底我只写编排和规矩这三条规矩立住了我只用站在三个开源巨人肩膀上写一层很薄的调度胶水Kubernetes它负责把 Deployment 开出来、重启、健康检查、Service 接入。我只是告诉它这个用户要一个搭子剩下的 Pod 怎么跑、怎么活、怎么重启——我不管交给 K8s 这个巨人。Redis它负责存实例的状态映射、租约、TTL。我只是读写agent-runtime:user:{userId}这个 Key过期自动删除靠 Redis 的 TTL 机制。我不写状态管理引擎Redis 就是状态管理引擎。FastifyNode.js web 框架它负责接收请求、路由、插件化——大部分是胶水工作真正的后台逻辑是 port/adapter 模式隔离开的可以随时换巨人。你看又是同一个模式不自己造底座爬到巨人肩膀上再划界。上一篇封罐头是这样这一篇装大脑也是。六、透明的边界/agent/* 透传不篡改用户调过来了找到了属于他的搭子实例——接下来大脑怎么把请求交给具体那个搭子我的设计原则就一句话透明代理不篡改。具体到代码里就是把请求路径的/agent前缀去掉把剩下的路径、查询参数、HTTP 方法和请求体一次全部转发给 Target Service。不做任何拦截、修改、注入或缓存。这跟划地盘的思路是一脉相承的——我把你送到搭子门口你自己推门进去。我不在门口翻你的包。但有一个例外我清掉了一个 Header——Authorization。因为用户的鉴权在上游网关已经做完了我把这个 Header 清掉不让它被透传到 Agent 容器内部避免 Agent 拿到一个不该它用的凭证去做越权的事。这不是篡改用户请求是替用户守一道安全门。同样的逻辑也用在 WebSocket 代理上字节级透明转发不发源于大脑内的任何帧。不解析、不缓冲、不改内容。只是把用户的字节流一分为二——它来的方向发到搭子方向搭子方向发回用户方向。这背后的设计思路跟上面三条规矩是一样的明确边界。大脑只负责把人送到不负责人在里面做什么。七、写在最后你仔细想想这一篇跟上一篇封罐头的思路是一模一样的上一篇我们问的是哪些归平台、哪些归用户划了三条地盘的线把它做成镜像。这一篇我们问的是归属怎么定、状态谁能改、资源怎么收定下三条死规矩把它做成调度。技术只是执行设计才是决定系统好坏的那一刀。这一刀下去不是切代码是切责任边界——谁管什么、什么情况下能做什么、什么情况下绝不能做。这几件事想清楚了用什么技术去落地只是选工具的事。站在开源巨人肩膀上不是为了偷懒是为了把精力省出来——不用从零造底座可以从容想清楚那几道关键的线怎么划。关于 ArchAIHarness这篇文章是「看懂 AI 与智能体」专栏的一部分由ArchAIHarness持续输出。ArchAIHarness 是一套面向 AI 时代软件工程的人机协同架构哲学与公开工程资产主张架构师定义秩序AI 在秩序中生长。人立法AI 执行体系审计。如果你也希望 AI 在明确的架构边界内协作而不是在混沌中碰运气欢迎到 GitHub 上看看我们在做什么组织主页github.com/ArchAIHarness — 了解完整理念与资产全景本专栏zhuanlan-ai-and-agents— 所有文章的源码与发布记录实践指南docs— 架构哲学、工程方法和落地指南开源工具agent-workflows— 可复用的 AI 协作 Agents、Skills 与 Tools调度引擎agent-master— 本文讲的调度大脑把单机 AI 搭子变成云上可服务化的控制面标准镜像build-on-vscode-opencode-image— 被调度的标准身体VS Code 网页版 OpenCode 的开箱即用容器镜像Engineered by Architects · Empowered by AI · Audited by Discipline