为了构建 Omnilude,我正在搭建这样的后端
这篇文章并不是为了介绍某一个功能。它更像是在整理 Omnilude 这个项目究竟建立在什么样的结构之上,以及我为什么要用这样的方式来组织后端。
我现在正在做的事情,其实有两条线同时重叠在一起。第一条线,是测试编码代理的边界。我一直在验证,只靠 vibe coding 和接近意识流的指令,究竟能把实现推进到什么程度,以及这种方式能不能真正延伸到生产级开发。第二条线,是不让这场实验只停留在实验本身,而是最终落到一个真实可运行的服务上。Omnilude 就处在这个服务的中心,而我希望先在这套结构之上,通过我过去一直想做的游戏开发,把这场实验真正往前推下去。
所以这篇文章更接近一次结构介绍,而不是实现成果展示。但我也不想只是罗列一串服务名来说明它。这个后端虽然分成了多个模块,但我真正看待它的方式稍微有些不同。我把它整体看成一个产品后端。认证、AI 执行、内容管理、实时连接、文件存储和事件流,都是围绕同一个目标被绑在一起的。
这可能会是一篇有些长、也有些难读的文章,不过我还是会尽量具体地把它说明白。
Omnilude 想成为什么样的项目
对我来说,Omnilude 同时有两层意义。
第一层意义是实验。我认为,在现在这样的阶段,开发者只是去试一试新工具已经不够了。会用 AI 的人和不会用 AI 的人之间,差距大概率还会继续拉大,而且这道差距最终拉开的地方,不会只是实现速度,更会体现在问题定义、结构设计和验证方式上。我不想只是站在旁边看着这种变化发生,而是想通过真正做一个产品,亲自穿过这场变化。
第二层意义是一个非常现实的产品目标。我想做游戏已经很久了。但现在我已经不想只用过去那套方式去接近这个目标。我想把 AI 驱动的创作工具、内容生成流水线、可以真正运营的后端,以及实时体验串成一个完整流程来推进。Omnilude 就是放在这条方向上的项目。
换句话说,这个项目一方面是一个尽可能把 AI 用到极致的 AI 代理实验,另一方面,它也必须最终收敛成一个可以真正上线的产品开发项目。
虽然有多个服务,但我把它看成一个产品后端
以我写下这篇文章的当下为准,Omnilude 的后端仍然还是一个单体服务。不过,我并不把这个单体仅仅看成一台很大的服务器。我更把它看成一种内部已经并存着多条责任边界的结构。本文中出现的 auth-service、ai-service、blog-service、game-service、storage-service、backbone-service、mmorpg-service,以及共享基础层 common(jspring),与其说是当前的部署单元,不如说更接近我理解和扩展这套系统时所使用的视角。本文里,我会把这个内部共享层简化称作 common。
因此,本文里所说的「虽然有多个服务,但我把它看成一个产品后端」,并不是在夸大当前的结构。更准确地说,这是在解释一个仍然粘连在一起的系统内部,哪些责任已经分开了,以及这些边界将来还可能朝着什么方向继续变得更清晰。重要的从来不是服务数量本身,而是每一项责任是否在共享规则之上,朝着同一个产品目标被组织在一起。
与其把今天的 Omnilude 只看成一台巨大的服务器,我更愿意把它看成一个在同一套系统内部并存着多条边界的产品后端。至于这些边界将来如何真正演变成仓库层面和部署层面的拆分,我准备留到另一篇文章里再写。在这篇文章里,我想先把当前单体结构里已经存在的责任分离,用今天的术语重新展开说明。
下面这张图,是把当前的非遗留后端重新按照一个产品后端的视角重新归拢之后的总览。
在这张图里,我最看重的是三件事。
第一,common 是整个系统的底座。每个服务虽然独立存在,但认证规则、内部 HTTP 调用方式、事件发布、存储处理以及分布式任务处理,都在共享基础之上反复出现。正因为如此,这套结构不会散掉。
第二,产品领域和运营型基础设施是分开的。auth、ai、game、mmorpg、blog 代表的是产品能力,而 storage 和 backbone 则承担这些产品能力真正跑起来所需要的公共运营基础。
第三,这种拆分本身也让结构更适合 AI。编码代理并不擅长处理一个没有清晰上下文的巨大代码库。相反,当角色边界清楚、规则明确时,它们会稳定得多。我设计这套结构,不只是为了让人更容易维护,也是为了给 AI 提供一个更好的工作环境。
这套结构的中心是 common
理解这个项目时,最先需要说明的就是 common。因为归根结底,整套后端之所以能像一个产品那样运转,原因就在这里。
common 并不是一个简单的工具集合。我更愿意把它看成一套内部平台 SDK。只要观察服务之间的连接方式,就会发现很多东西其实已经在 common 里被标准化了。
common-core提供最底层的基础能力,比如 JWT、公共异常和基础工具。common-web提供内部 HTTP client 和共享的 Web 层规范。common-data把 JPA、QueryDSL 以及公共数据访问基础整合在一起。common-messaging统一事件发布路径。common-cloud负责把存储 commit、访问 URL 等文件处理相关能力标准化。common-dte提供分布式任务、workflow 以及 Job Event 流程。
我认为这一层之所以重要,原因很直接。在 AI-First 开发里,真正重要的并不只是代码能写多快,而是系统是否拥有可重复的模式。如果每加一个新功能,不管是人还是代理,都要重新理解一套完全不同的结构,那么生产力很快就会掉下来。反过来,共享基础越强,责任就越容易拆成更小的单元,审查点也会更清楚。
例如,game-service 在调用 AI 能力时,并不会直接去对接外部模型,而是内部调用 ai-service。文件存储也是一样,它不是让每个服务各自去碰 S3,而是尽量统一走 storage-service。事件在需要的时候,也会围绕 backbone-service 来流动。正是因为有了这种一致性,我才能给 AI 更精确的指令,也能更快地验证结果。
这对开发者来说同样重要。虽然看上去服务很多,但它们实际上共享同一种语言、同一个框架、同一套规则和同一个内部平台,所以思考的单位不会被彻底切碎。我认为这正是 Omnilude 结构里最重要的优势之一。
产品领域为什么要这样拆分
接下来,我想更具体一些,说明每个服务分别承担什么职责。与其单纯罗列名字,不如解释清楚为什么要这样分。
auth-service:信任的起点
auth-service 负责登录、账号、设备、权限、额度以及活动日志。从表面上看,它也许像一个很常见的认证服务,但我并不把它看成一个单纯的登录 API。这个服务负责的是整个产品的信任边界。
用户是谁,持有什么 session,拥有哪些权限,具备怎样的额度和使用记录,几乎都从这里开始。如果其他服务想要更快地试验功能、不断挂接新能力,那么这层基础的信任能力反而必须更加保守、更加明确。
有意思的是,这个服务并不是完全孤立的。它在启动时会与 backbone-service 以及平台配置同步,并共同形成整套后端共享的 JWT 规则。也就是说,认证既是一个独立功能,也是整个系统的连接语言。
ai-service:AI 执行的中枢
ai-service 是这个项目里最具象征性的服务。LLM 调用、多代理 workflow、媒体生成、商品代理、角色扮演流程,以及各类系统级 AI API,都集中在这里。
重要的是,这个服务并不只是对外部模型 API 做一层代理。我更把它看成 AI 执行的中枢。它是决定调用哪个模型、走哪条 workflow、发出哪些事件、把结果保存到哪里、以及把进度如何向外流出的中心层。
我认为,这套结构今后只会变得更重要。对于那些积极使用 AI 的项目来说,一旦外部模型调用散落到各处,控制就会很快变得困难。把成本、失败处理、prompt 规则、执行记录、结果存储和提供商切换策略都考虑进去之后,AI 反而更适合在一个地方被统一编排。ai-service 承担的正是这个角色。
game-service:我最想优先落地的领域
game-service 是这个项目的核心产品领域。Scenario、story quiz、game session、共享的游戏元数据,以及 asset studio 相关能力都汇集在这里。
这个服务之所以重要,原因很简单。我最想从 Omnilude 里率先推出的东西,最终都会落到这个领域里。所以它不是一个被动的数据仓库,而是一个与 AI 发生最多交互的产品领域。无论是生成 scenario、生产资源、创建 session,还是组织实际的玩法流程,都会集中到这里。
尤其重要的是,game-service 与 ai-service 是直接相连的。内容生成、审核、辅助任务、资源制作以及一部分自动化流程,本质上都不可避免地会被 AI 加速。我认为,与其把这层连接刻意藏起来,不如一开始就在结构上把它明确暴露出来。
mmorpg-service:我把实时运行时看成另一条轴线
mmorpg-service 虽然也属于游戏领域,但它的性质很不一样。这个服务不只是提供基于 HTTP 的管理 API,它还会在一个独立的 gateway port 上运行实时 WebSocket runtime。
如果有人问我,为什么不把它和其他内容服务混在一起,答案其实很明确。以 CRUD 为中心的内容 API 和实时游戏服务器,在失败模式、性能要求以及状态管理方式上都完全不同。我认为,不把它们当成同一个思考单元会更合理。
它的认证方式也很有意思。在实时连接建立时,mmorpg-service 内部的 gateway 会本地校验 auth-service 签发的 JWT 规则。也就是说,它不会在每次运行时交互时都重新通过 HTTP 去请求认证服务。这种结构很好地说明了,实时系统应该在哪些地方与普通业务 API 分开。
blog-service:项目的外部记忆装置
blog-service 看起来可能没有其他服务那么复杂,但对我来说它的位置很重要。这个服务并不只是一个用来运行博客的 CMS。它更像是一个对外积累记录的通道,用来保存我正在做什么、做了哪些判断、又进行了哪些实验。
如果 Omnilude 不只是一个在内部自转的项目,那么它的结构、实验和试错过程就必须能够被外部理解和解释。blog-service 承担的就是这份记录。文章、评论、翻译、SEO、静态资源链接这些功能集中在这里,本质上也是因为产品需要一层面向外部的沟通能力。
为什么把运营型基础设施单独拆出来
一个服务并不能只靠产品功能本身完成运营。还必须有能保存结果、管理文件生命周期、流式传递进度、输送事件、跟踪长时间运行任务,以及维持实时连接的基础层。
在 Omnilude 里,这部分职责主要由 storage-service 和 backbone-service 承担。
storage-service:文件真正的生命周期在这里被管理
在很多项目里,文件存储常常被当成一个后来补上的功能。但在真实的产品运营中,文件上传、临时状态、commit / cancel、访问 URL、缩略图 URL,以及静态对象元数据都很重要。文件并不是上传完就结束了,它有自己的生命周期。
所以我把 storage-service 独立出来。无论是博客图片、AI 生成结果,还是游戏资产,我都尽可能把它们统一到这条路径里。这样一来,不管哪个服务在处理文件,都能运行在相同的规则之上。以后如果访问策略或存储策略发生变化,影响范围也更容易被控制住。
backbone-service:运营与异步的中心
backbone-service 就像它的名字一样,非常接近系统的骨架。事件中心、SSE、WebSocket、平台配置,以及 DTE Job 的事件流都集中在这里。
这个服务尤其重要,是因为它和 AI 执行天然契合。我现在在做的很多能力,并不是一个简单的同步 API 调用就能结束。用户发起一个任务之后,需要有中间进度,需要有完成事件,有时还需要实时连接或通知。如果这些问题都让各个领域服务自己去分别解决,结构很快就会变脏。
所以我把异步传递和运营型实时层都收拢到了 backbone 里。我认为,把默认事件路径整理成 HTTP -> backbone -> Kafka/RabbitMQ,并让 DTE 相关进度通过 SSE 对外流出,是更容易维护的做法。
下面这个流程,简化展示了一个 AI 生成任务实际是如何推进的。
我喜欢这种结构的原因很明确。生成由 ai-service 负责,结果存储由 storage-service 负责,进度的对外传递由 backbone-service 负责。正因为责任分开了,每一块都可以被更清楚地验证。
数据存储与消息层是如何使用的
如果想把这套结构看得更立体,就还要一起看数据存储层和消息层。Omnilude 当前的非遗留后端并不是把基础设施胡乱混在一起用的。每一层承担什么角色,其实相当明确。
- PostgreSQL 是几乎所有服务的默认持久化存储。
auth、ai、blog、game、storage、backbone和mmorpg都共享这条主轴。 - Redis 与其说是简单缓存,不如说更多承担了 Pub/Sub 和带有 session 属性的事件传递。它分布在多个服务中,但用途是很有策略性的。
- Cassandra 目前更接近
auth-service和backbone-service周围的活动日志与事件写入层。这里是有边界控制的,不会让所有服务都无差别接入。 - Kafka 和 RabbitMQ 在实际使用中几乎都以
backbone-service为中心。不是每个服务都直接挂到消息代理上,而是先把事件送到 backbone,再由 backbone 向外层消息系统传播。 - S3 由
storage-service直接处理,其他服务尽量通过它来访问。这是为了把文件处理规则收束在一个地方的有意选择。
刚看起来,这种方式可能会显得有点绕。但我反而认为,这样的拆分在长期里更重要。一旦功能服务开始直接承担存储和消息系统的细节,责任边界就会随着时间快速变得模糊。相反,像现在这样,把基础设施访问先整理到几条网关型路径上,后面无论是策略变化,还是缩小故障范围,都会更有优势。
从 AI 协作的角度看,这种结构也有明显好处。当我把某个功能交给代理时,只要能够清楚说明这个功能应该直接处理到哪里、又应该从哪里开始走共享路径,结果就会稳定得多。
为什么认证请求与实时连接要走不同的路径
如果想更具体地理解 Omnilude,最好把同步请求和实时连接分开来看。
大多数普通的内容查询和管理操作,都是先通过 auth-service 登录,再去调用领域 API。在这个流程里,最重要的是共享的 JWT 规则和权限体系是否保持一致。由于 blog-service、game-service 和 ai-service 都共享这套共同约定,所以即便服务已经拆开,用户获得的体验仍然相对统一。
但在实时连接里,要求就不一样了。尤其是 mmorpg-service 使用了独立的 gateway port,也就是 :9088/ws。用户手里已经持有 auth-service 签发的 token,而 gateway 会在本地完成校验。这样做的目的,是为了在不反复往返认证服务的情况下,更快地建立会话。
下面这个流程展示了这种差异。
这种差异的重要性比看上去更大。通过这种结构,我想明确表达的是,普通业务 API、长时间运行的 AI 任务,以及实时游戏 session,天然就应该拥有不同的重心。把一切都硬塞进同一种模式,看上去也许更简单,但在真实运营里往往会把问题放大。
这套结构如何与 AI-First 开发相连接
读到这里,可能会自然出现一个问题。为什么这样的结构,会和是否能更好地使用 AI 有关。
我认为这个问题非常关键。因为直到现在,很多开发者仍然只是把 AI 看成一个会代写代码的工具。但在我看来,生产力真正出现大幅提升的时刻,并不是单纯生成更多代码的时候,而是系统本身开始拥有一种更容易被 AI 理解的结构的时候。
比如说。
- 比起一个角色模糊的巨大服务,一组责任清晰的服务更容易被拆进不同的 prompt 里。
- 像
common这样的内部平台,会给代理提供比没有共享约定的代码库稳定得多的上下文。 - 如果文件处理、事件处理、AI 执行和实时连接彼此分开,就更容易追踪问题到底发生在哪条边界上。
- 最后需要由人类来审查的点,也会清楚得多。
归根结底,AI-First 开发并不是把 AI 接到更多地方上去。它更接近于把系统整理到一种让 AI 可以更稳定工作的位置上。Omnilude 对我来说,正是在验证这一点的项目。
从这个角度看,我认为现在开发者真正需要的,不只是最新模型的新闻,而是训练。不是只训练怎么写更好的 prompt,而是训练如何做出更好的结构、如何用更好的标准去 review,以及如何设计出真正让 AI 可以工作的上下文。我认为,这个项目最终也应该成为这种训练的结果。
最终目的地仍然是 Omnilude 的上线
我不希望这篇文章只停留在结构介绍上,还有一个原因。这套结构并不是为了说明而存在的结构,它是为了最终做出某个真正会被上线的东西。
我现在做的事情,是先做 AI 驱动的创作工具,再在它们之上实现一个游戏平台。这套后端正是为了让这两条路径汇合而设计的。AI 推动内容生产,游戏领域把它转化成产品体验,而运营型基础设施则把整件事抬升到真实服务的水平。
我依然想上线一款真正做得好的游戏。而且我也想亲自确认,这件事是不是不只依赖直觉和热情,也可以依赖更好的结构、更快的实验和更精确的验证来实现。对我来说,Omnilude 就是承载这份期待的项目。
所以接下来,这个博客不会只谈架构。我想持续记录,实际有哪些功能正在被接上去,AI 到底把事情推进到了什么程度,人类还必须在哪些地方做判断,以及这套结构究竟能不能真的走到一个可上线的产品上。
现在的我在介绍后端结构,但在我心里更重要的问题其实只有一个。用这种方式,真的可以走到最后吗。
我不想只用语言来回答这个问题。我想通过真正把 Omnilude 做出来,亲自验证它。