分享构建多租户 AI Agent 平台(Aulinq)的完整经验,深度讨论模型选择、动态计费、租户隔离、幂等性等硬件基础设施难点,指出基础设施比 AI 能力本身更难复制。
在 2026 年开始做另一个 SaaS 是件很奇怪的事。舆论说这个赛道已经饱和,AI 摧毁了所有的护城河,剩下的唯一值得构建的东西就是围绕前沿模型打造一个漂亮的着陆页。我读过这些言论,大体同意诊断,但完全不同意结论。包装应用正是第一批死掉的东西。剩下的是底层那些枯燥却难以复制的基础设施——多租户、计费、成本控制、密钥管理、重复交付的幂等性——而这些工作并不会因为模型变聪明就变便宜。
这就是构建这样一个平台的故事。它叫 Aulinq。这是一个多租户、多语言的 AI 智能体平台:可以在 Telegram、WhatsApp 或网页聊天上回答客户,将对话路由到能够处理的最便宜模型,按令牌向租户收费(几分之一分钱),并保持每个租户的数据和密钥与其他租户完全隔离。在这些基础设施之上有三条产品路线——一条白标路线供机构将智能体转售给他们的客户,一条 API 供初创公司将智能体嵌入到自己的产品中,以及一条单租户路线供只想要一个可用的机器人而不想获得基础设施博士学位的独立创始人。生产系统包括多个 Go 服务、一个 Postgres、一个 Redis 和一个 NATS JetStream 总线。我是唯一的工程师,而且我还有一份全职工作。
最后这一点是人们真正想了解的。你如何在还有全职工作的情况下单独交付一个分布式平台?诚实的答案是两件事,都不是"我编码很快"。首先,我不再单独编码,而是开始协调一个 AI 智能体团队——一个规划者、一个实现者、一个审查者、一个验证者,每个都有自己的上下文和自己的工具。其次,我不再试图在每天一小时的增量中做这件事。一个工作日晚上的一小时足以审查差异、批准计划或观看验证程序运行。这不足以研究堆栈决策、起草架构或将失败的服务从漂移状态中拉出来。这些需要一整块不间断的注意力,对我来说这个块是周六,以及家人出门的偶尔周日。
所以方法和运营模式相互依赖。周末批次是思考工作发生的地方——研究循环、架构迭代、库间隙填充、智能体规则。工作日晚上是小的验证和合并任务发生的地方。混淆它们会导致你在周二晚上 11 点做出半研究的堆栈选择,这正是这个方法设计要避免的确切失败模式。
本文就是那个方法,写下来的。不是抛光版本——是保留死胡同和回滚的版本,因为这些是真正教会它的部分。它假设你已经是一个有能力的工程师。AI 智能体不会取代这一点。它们放大你带来的任何判断,也同样快速地放大坏判断。
当你问"我应该为分布式 SaaS 使用什么堆栈"时,AI 编码助手会做的第一件事是给你最流行的答案。Python。FastAPI。Postgres。Redis。如果你说"事件驱动",也许还有 Kafka。这对于教程来说是正确的答案,但对于其他 AI 智能体将在未来两年扩展的系统来说,通常是错误的答案。
流行堆栈之所以流行,是因为它对最多的人来说是可理解的。对最多的人来说可理解与对最多的智能体来说可理解是不同的。AI 智能体不从庞大的教程生态系统中受益——它们已经读过了。它们受益于拒绝其错误的类型系统、大声失败的运行时,以及出错的活动部分最少的部署工件。
我通过深度研究(而不是聊天)来处理堆栈问题。四或五次迭代:首先进行广泛的调查,然后缩小到存活的两三个选项,然后对每一个进行深入阅读,然后根据第二个模型进行交叉检查以捕捉第一个模型的偏见。这是我为这个项目中的每一个关键决定遵循的模式,不仅仅是堆栈。它比问一次要慢。它比问一次然后在第四个月重建要快。
Go 赢了。不是因为它在 AI 智能体工具的意义上很流行——它不是,Python 主导那个——而是因为 Go 编译器将一类智能体错误转变为构建错误,而不是凌晨 3 点的堆栈跟踪。当智能体向事件结构体添加字段并忘记更新消费者时,构建会失败。在 Python 的等价物中,消费者无声地反序列化为 None,你在生产中才发现。
权衡是真实的,我想指出它:我放弃了对 Python ML 生态系统的直接访问。平台中的每个模型调用都通过 HTTP API 而不是进程内库进行。这对于 LLM 路由平台来说很好——模型无论如何都在提供商后面——对于做本地推理或繁重预处理的团队来说,这将是一个真正的成本。我将单独写完整的 Go vs Python 论证,包括 Python 本来是正确选择的情况。
在堆栈之后,对于每一层都进行相同的研究循环。事件总线:选择 NATS JetStream 而不是 Kafka,因为基于主题的路由是智能体可以推理的字符串,而基于主题的路由带有模式注册表是智能体会出错的构建步骤。持久化:一个带有每个域模式的 Postgres,因为"智能体可以完整读取的一个数据库"胜过"每个都有自己的迁移工具的十个数据库"在智能体可理解性方面。
然后是我后来描述时让人们感到惊讶的部分。我需要的几个库不存在我想使用的形式。一个类型化的 JetStream 发布者,用信封包装每个事件并公开 PublishInbound 而不是原始的 Publish(subject, []byte)。一个带有滑动失败窗口的断路器,而不是幼稚的连续失败计数器。一个多租户密钥保险库,具有信封加密,不向工具层泄露明文。
来自其他工程师的反应通常是"只需使用库 X"。有时这是对的。有时现有的库是一个薄包装,这会迫使智能体在每个服务中围绕它编写相同的样板代码。当缺口是真实的时,编写库不是最大的问题——这是与智能体一起的几个周末,你最终得到的界面正是系统的其余部分期望的。该平台现在有一套内部 Go 库(go-events、go-ai-providers、go-resilience、go-secrets 以及更多),每个服务都导入。每个都存在,因为公开的替代品会让智能体编写更多的样板代码,而不是更少。
断路器是形状的一个很好的例子。我发现的公开 Go 断路器库计算连续失败并在计时器上重置。我想要的是一个滑动窗口——计算最后六十秒内的失败,如果超过阈值则打开,半开以探测,一次成功就关闭。这是 Go 的六十行:
func (cb *SlidingWindowCircuitBreaker) Call(fn func() error) error {
cb.mu.Lock()
switch cb.state {
case cbOpen:
if time.Since(cb.openedAt) >= cb.cooldown {
cb.state = cbHalfOpen
} else {
cb.mu.Unlock()
return ErrCircuitOpen
}
}
cb.mu.Unlock()
err := fn()
cb.mu.Lock()
defer cb.mu.Unlock()
if err != nil {
cb.failures = append(cb.failures, time.Now())
// prune failures older than the window...
if cb.state == cbHalfOpen || len(cb.failures) >= cb.maxFailures {
cb.state = cbOpen
cb.openedAt = time.Now()
}
return err
}
if cb.state == cbHalfOpen {
cb.state = cbClosed
}
return nil
}
一个智能体一次性写出了那个的第一个版本。我审查了它,发现修剪循环差一个,并让智能体修复它。要点是这个库现在有了系统其余部分调用的确切界面,没有适配器层,导入它的每个服务都得到相同的失败语义。如果我包装了一个流行的库,每个服务都会有不同的临时包装,扩展这些服务的智能体必须学习每一个。关于库本身有一整个系列即将推出——事件发布者、AI 提供商界面、密钥保险库、RAG 选择器——每一个都是公开选项会意味着更多智能体样板代码而不是更少的情况。
在开发之前,我与智能体一起规划了整个架构。这是大多数人跳过的步骤,也是后来节省最多时间的步骤——尽管它感起来像是最慢的部分。
我学到的关键一点是:你必须尽可能把自己掌握的初始上下文都提供给 AI 智能体,因为 AI 智能体默认倾向于尽可能少做事。让 AI 智能体“为一个多租户 AI 平台设计架构”,你得到的会是一张三层架构图,里面有一个标着“AI Service”的方框。如果你在提问时附上约束条件——这些是需要支持的消息平台,这是按成本层级路由的要求,这些是租户隔离规则,这是我预期的事件流,这是我想避免的设计——那么你得到的才会是真正可以落地构建的方案。
我反复迭代了四轮、五轮、六轮。我会拿到一份方案,读完后针对某个具体决策提出质疑,并要求修改。AI 智能体会为某个选择辩护,我则会给出一个能够让这个选择失效的具体场景,随后 AI 智能体再修改方案。这个过程既无聊又烦人。有时我宁愿直接去写代码。但一份经受住五轮“那这种情况怎么办”拷问的方案,才是一份 AI 智能体能够直接实现,而不必边做边凭空发明架构的方案。
在这些迭代中,我逐渐最看重的一件事,是思考那些我尚未选择的发展方向。当前方案只需要覆盖我现在正在构建的内容,但架构必须能够向我接下来可能构建的东西延展。举个例子:我把 AI 智能体配置设计成“不可变模板 + 每租户覆盖配置”的形式,尽管当时我只有一个租户,严格来说并不需要做这种拆分。六个月后,当第二个租户到来,并出现白标需求时,这套覆盖模型已经能够处理。假如第一版采用的是更简单的“每个 AI 智能体一份配置”,那么后来负责扩展系统的 AI 智能体就不得不在系统承载负载的同时迁移数据模型,而这种任务恰恰是 AI 智能体不擅长的。至于具体的架构决策——为什么选 Go 而不是 Python,为什么选择十个微服务而不是单体架构,为什么选择一个 Postgres 而不是每个服务一个数据库——之后都会分别撰文说明。这篇文章讨论的是过程,而不是为这些选择辩护。
方案稳定之后,开发工作就变成了一份大型顶层计划,并被拆解为具体的小任务。不是“构建消息服务”,而是“给事件库添加 PublishInbound 方法,这是结构体,这是 subject 模式,编写对应的单元测试”。然后是下一个小任务,再下一个。
顺序很重要。先开发库,通过小型示例证明每个库都能正常工作,并使用单元测试锁定契约。然后开发核心服务;一旦有两个服务可用,就让它们互相进行交叉测试。之后才是初版 UI,而这又意味着新一轮调研——这次研究的是前端框架,因为 AI 智能体给出的第一个答案是 React 加上一大堆样板代码,而我想要的是最精简、能够编译为静态文件,并且无需在构建步骤上反复折腾就能处理国际化的方案。
角色分工就是在这里发挥作用的。一个 AI 智能体阅读代码库并为任务编写计划。第二个 AI 智能体按照计划实现。第三个 AI 智能体阅读差异并寻找缺陷。第四个 AI 智能体负责构建、重启和运行测试,而且在实际观察到系统处于健康状态之前,不允许宣布工作完成。任何 AI 智能体都不能自行批准自己的工作。实现者不负责评审,评审者不负责验证,验证者不负责批准。
我不会在这里重新推导这套方法——下一篇文章会详细介绍 AI 智能体团队,包括每个角色发现的具体缺陷,以及那一次整个流水线显得过度复杂的情况。对本文而言,重点是:只有当每项任务足够小,能够让单个 AI 智能体在不丢失思路的情况下完成时,这个循环才能运转。“重构计费服务”不是一个任务。“将信用额度冻结 RPC 提取为独立函数,并添加一个测试,验证被冻结后又释放的额度会让余额恢复到原始值”才是一个任务。AI 智能体完成它,验证者确认测试通过且服务日志干净,然后循环进入下一项任务。
构建进行到一半时,我注意到 AI 智能体开始偏离方向。某个本应按租户 ID 路由的服务却按账户 ID 路由。某次迁移添加了一个列,却没有添加相应的权限授予,导致新角色无法读取它。都是些小问题,每一个都能修复,但相同类型的错误在不同会话中不断重现,因为开启新会话的 AI 智能体并不知道这些规则。
于是我把它们写了下来。在代码库根目录放置一个 CLAUDE.md,并刻意保持简短。八条不可妥协的规则,每条只有一句话。多语言意味着后端不能包含任何针对特定语言的代码——LLM 是语言判断的唯一权威,因此后端只使用英文键。修复由你触碰或破坏的测试。代码必须达到生产可用标准,不能使用桩代码。Schema 发生变化后,重置本地技术栈。代码修改后,执行构建、重启并检查日志。使用子 AI 智能体并行工作,并通过循环进行验证。
简短是有意为之。我最初尝试过更长的版本——详细说明每条规则,并配上示例和边界情况——结果 AI 智能体开始忽略列表底部的规则。一条 AI 智能体不会遵守的规则,比没有规则更糟,因为它会给你一种虚假的信心。短版本能够容纳在 AI 智能体的活跃注意力范围内,因此会被遵守。长版本则放在链接的子文档中,只有当任务涉及相应领域时,AI 智能体才会读取。
回报超过十倍的那条规则是:“LLM 是语言判断的唯一权威。”在制定这条规则之前,AI 智能体总是在后端添加西里尔字母计数启发式规则和基于关键词匹配的语言检测,因为每篇聊天机器人教程都采用这种模式。但遇到第一个在对话中途从俄语切换为英语的俄语租户时,这些启发式规则全都失效了。制定规则之后,后端完全不再尝试检测语言,只把文本传给模型,再由模型使用用户输入时所用的语言作答。规则文件里的一句话,就让一整类缺陷不再出现。
如果能重来,我会克制住过早开发 UI 的冲动。在核心服务完成交叉测试之前,我就构建了第一版仪表盘,因为看到屏幕上出现实际内容让人感觉很好。但这并不值得。服务契约稳定后,UI 不得不重写,而负责重写的 AI 智能体总想保留旧的 UI 决策,而不是适配新的契约。如果先完成枯燥的中间部分,把看得见的 UI 留到最后构建,本可以节省一周时间。
我还会在开发第一个服务之前就编写 AI 智能体规则文件,而不是等到项目进行到一半才写。早期会话中的偏离幅度虽然不大,但这些早期决策如今都已经固化在代码里,后续 AI 智能体会在此基础上继续扩展,而它们并不总能分辨哪些决定是有意为之,哪些只是偏离造成的。如果从第一天起就有规则文件,虽然不能捕获所有问题,但至少能给早期的 AI 智能体一个更明确的目标。
这篇文章介绍的是方法。下一篇会详细介绍 AI 智能体团队——包括各个角色发现的具体缺陷,以及那一次整个流水线显得过度复杂的情况。再往后会讨论架构决策(Go 与 Python、微服务与单体架构、NATS 与 Kafka、单一 Postgres 与每个服务独立数据库),然后是我们不得不编写的库,再之后是模型优化和成本层级,最终还会涉及市场进入策略。把这一切写下来的原因在于:对于这样的技术产品,唯一真正有效的传播渠道,就是工程师把真实的构建过程转发给其他工程师。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。