让正确的做法成为默认路径:Holonic Asset 工程质量闭环实践

#Holonic Asset
#工程实践
#CI/CD
#团队协作
#AI工程化

在 Holonic Asset 中,我负责项目整体的工程门禁。

做这件事的决心,其实来自团队成员提交第一个 PR 的时候。

当时我看到仓库里冒出来的分支和提交,直接就是名字首字母缩写+_干了什么。代码逻辑还没开始看,分支名和提交记录就已经五花八门。这让我意识到一件事:如果规范只停留在口头约定或群里提醒,一到赶进度的时候,所有人都会凭着各自的习惯野蛮生长。

但只写一份“大家务必遵守”的规范文档毫无用处,发布时再隆重,过几天也会沦为摆设;直接从老项目搬一套严苛复杂的规则,又只会逼得大家到处想办法绕过门禁。真正要想守住质量,唯一的办法是把规范做成日常开发里绕不开的默认路径——需求先定义、变更有边界、提交有验证、合入有审核。只要顺着默认路径走,就自然做对。

1. 我想搭的不是检查清单,而是一条路

在日常迭代中,尤其是在多人协作与快节奏开发下,项目很容易在四个关键环节失控:

失控位置典型表现最终代价
质量失控格式、Lint、测试和构建散落在各人本地问题发现得晚,Review 变成无休止的低级挑错
契约失控前后端靠口头同步接口字段变更字段遗漏、类型错误、联调和部署阻塞
运行失控业务入库与异步消息发布脱节任务丢失、状态悬置、故障难以恢复
协作失控Issue 范围不清,PR 缺乏有效验证记录变更无限膨胀,风险直接溜进主分支

靠负责人人肉 Review 仔细一点固然能挡住一部分问题,但这不可持续。人会累、会漏看,更不应该把精力耗在肉眼查格式和拼字段上。

为了防止这些失控,我想要的不是一份写满规则的检查清单,而是一条把关卡内嵌进去的默认研发路径:

从需求到合入的工程质量闭环

我最后把整个变更过程收成了四个要求:

  • 需求先定义:先把问题背景、范围、排除项和验收标准讲清楚。
  • 变更有边界:模块职责要清楚,PR 和 Commit 也不能无限膨胀。
  • 提交有验证:本地先做快速检查,完整测试和生产构建交给远端环境。
  • 合入有审核:机器负责重复检查,人负责判断设计、兼容性和风险。

单独看,这些要求都不新鲜。真正有用的是把它们连成闭环:Issue 说明白了,后续设计才有依据;变更有边界,Review 才能聚焦;有了自动验证,代码质量才不用靠“我觉得没问题”来凭空担保。

2. 这套规范不是一天搭出来的

工程规范在组内的分阶段落地过程

这套门禁不是一蹴而就的。我们没有一开始就把所有规则压上去,而是分成了四个阶段逐步演进:

先把协作入口统一起来

2026 年 7 月中旬,项目建立了主分支保护规则、结构化 Issue 表单、PR 模板,以及覆盖全流程的仓库专用指引。

这一阶段解决的是“大家在不在说同一件事”。入口统一后,问题背景、实现范围和验证结果才有处可寻,避免代码写到一半还在争论这次到底改什么。

再把重复检查交给工具

2026 年 7 月底到 8 月初,接入统一的提交前检查与前后端自动验证,并补齐排障文档。

这一步直接把 Review 从格式和低级错误中解放出来。原则是本地只做快检,耗时较长的完整测试与构建交给远端 CI,避免门禁过度打断开发节奏。

接着补上契约、测试覆盖率和模块职责

2026 年 8 月起,逐步建立后端驱动的契约生成链路,接入前后端覆盖率统计,并明确模块职责。

到这里,工程治理开始深入架构层面:格式正确不等于接口不漂移,跑通测试也不等于关键路径有覆盖。

最后,根据真实问题持续修补

规范不是定型文档,而是随项目生长的活代码。真实开发中遇到的接口漂移、无效构建、跨域故障或模块逆向依赖,都会被沉淀为一条新规则或自动化检查。

3. 前端 CI 是怎么一步步收紧的

前端 Lint 同样经历了三次清晰的演进:

第一阶段:只保护 types 的导入入口

PR #64 首次用 Oxlint 约束模块边界,禁止直接引用内部 types:

@/model/<module>/types/**
@/features/<feature>/types/**

它强制 Model 和 Feature 的类型必须经由模块 index.ts 导出,初步解决了类型散落和循环依赖。但它的局限在于只拦截路径带 types 的文件,组件、Hook 和内部 API 仍然能被深层穿透导入。

第二阶段:从导入限制扩展到通用代码质量

PR #275 为 Oxlint 引入 React、React Perf、TypeScript、Unicorn、Promise、Import 和 Vitest 插件,全面检查 Hook 依赖、自引用、异步处理与测试断言。

配合这次升级,代码库同步统一了 import type、将 .then()/.catch() 重构成 async/await。最终 388 个前端文件达成 0 error / 0 warning,81 个测试文件共 377 个测试全部通过。

第三阶段:把单向依赖写进 Lint

PR #301 彻底解决了目录划分沦为“君子协定”的问题。此前虽然分了 app、pages、features、model,但其他模块依然可以越过入口写出 @/features/generation/generation-queue 这样的深层依赖。

PR #301 将依赖关系收敛为严格的向下单向依赖:

前端单向依赖分层与保护边界

具体规则包含两方面:

  1. 保护模块入口:跨模块只能导入 @/features/<feature> 或 @/model/<module> 的公共入口,禁止深层穿透(保留 @/model/generated/** 为合理例外),模块内部依然可以使用相对路径。
  2. 禁止下层逆向依赖上层:
    • model 不得依赖 app、pages、features、components、hooks;
    • features 不得依赖 app、pages;
    • components、hooks、lib 等共享层不得依赖上层业务模块;
    • pages 仅供 app 装配,不能反向引用 app。

规则一上线,潜藏的违规立刻暴露:GenerationQueue 和 CreateAssetForm 被收回至统一入口导出,文档 Feature 内部引用被修正为相对路径,登录重定向校验也从 app 移回了所属的 Auth Feature。架构边界不再靠 Review 肉眼盯防,而是由 pnpm lint 和 CI 强制执行。

4. 最难的不是写规则,而是让规则不招人烦

落地中遇到的阻力实际表现针对性调整
配置有了但不会用新成员不知道如何配置提交前检查编写中英文安装与排障文档
本地门禁过重卡顿每次提交跑全量构建耗时过长本地只做快速检查,完整验证放远端 CI
CI 资源浪费文档等无关改动频繁触发全量预览构建增加路径过滤,按需触发
接口变动脱节后端改字段导致前端构建失败建立后端数据契约直接生成前端类型
架构隐式退化模块深层导入与反向依赖将单向依赖关系编码进 Oxlint 规则
Agent 输出随心所欲格式与提交流程不符合仓库规范编写专属 Skills,强制先读模板再输出

每加一条门禁,我都要求回答两个问题:它挡住了什么真实风险?它给日常开发增加了多少摩擦? 如果摩擦过大而收益模糊,这条规则就还不成熟。

相关 Issue 记录:

5. 大家为什么会遵守

从引导到硬约束的四层遵守机制

让人人自觉遵守规范是不切实际的,我们把落地机制拆成了四层递进:

  1. 看得懂:通过结构化 Issue/PR 模板与文档,讲清标准写法。
  2. 容易做:通过自动生成类型与专用 Skills,让正确做法的操作成本最低。
  3. 自动提醒:提交前检查、远端 CI 与预览环境提供即时反馈。
  4. 硬约束:主分支受保护,必须通过 PR、至少 1 位批准、所有 Review 意见解决、且仅允许 Squash 合并。

Review 本身也做了明确分层:

  1. 机器:接管格式、Lint、测试、构建、竞态与预览环境等重复检查。
  2. 我负责工程预审:重点把关职责边界、契约一致性、验证结果和向后兼容。
  3. 导师把关高层设计:负责关键方案设计与高风险改动的终审。

这套分层不是为了把流程搞复杂,而是因为机器和人擅长的事情不同:让人肉眼去抠格式和字段是浪费精力,指望机器判断跨模块架构是否合理也并不现实。

6. 门禁不是越多越好,反馈速度同样重要

门禁设计的关键是分层治理:把轻量快速的检查尽可能左移到本地,把重量完整的验证收拢到远端流水线。

本地 Lefthook 只需数秒即可拦截语法、格式与基础静态错误;远端 CI 再执行竞态检测、类型校验与完整测试套件。这样既保证了低级错误在本地秒级现形,又不会让每次 Commit 沦为漫长等待。

7. Vercel 预览环境到底在检查什么

CI 全绿只能证明“代码可编译、单测能跑通”,但证明不了“换到线上真实网络环境还能工作”。PR 自动生成的 Vercel 预览环境,帮我们在合入前抓到了两类本地完全无法察觉的线上故障:

对象存储桶的 CORS 配置

前端请求对象存储中的图片时,签名地址完全合法,存储桶也能正常返回内容,但浏览器却报 CORS 拦截。根本原因在于本地开发域名(如 localhost:3000)与 Vercel 预览生成的专属域名属于不同 Origin。如果没有真实线上预览,这个问题只有在合入发布后才会被用户发现。

HTTPS 页面连不上 HTTP 后端(Mixed Content)

本地联调时前后端均跑在 HTTP 下,一切正常。但 Vercel 预览环境强制使用 HTTPS,而后端环境最初只有 HTTP 端口。浏览器出于安全机制直接拦截了 HTTPS 页面对 HTTP 接口的调用(混合内容限制)。代码逻辑没有任何错误,但在浏览器安全策略下直接瘫痪。这一问题直接推动我们提早完成了后端的域名证书配置与全链路 HTTPS 化。

线上真实故障为什么本地测试无法发现预览环境暴露的核心问题
存储桶 CORS 报错本地 Origin 已放行或同源预览域名未配置进存储桶白名单
HTTP 后端请求被拒本地前后端同为 HTTP 协议浏览器禁止 HTTPS 页面请求非安全 HTTP 接口

8. 吃过契约漂移的亏以后

从后端数据契约到前端构建的生成链路

前后端契约脱节一度是联调中最消耗精力的痛点:后端改了字段名,前端只有在页面渲染白屏或构建报错时才察觉。

现在我们确立了一条单向事实源链路:

  1. 请求与响应数据结构在后端 Go DTO 中唯一定义;
  2. 工具根据 DTO 自动化生成 OpenAPI 规范与接口文档;
  3. 前端据此自动生成 TypeScript 类型,并直接融入前端编译与单测。

字段改名、枚举调整或可空性变化,在代码编译阶段就会立即报错定位。不再依赖“记得群里同步一声”的口头传话。

9. 工程治理不能只停在 CI

Holonic Asset 是一个 AI 游戏资产生产平台,业务链路长且异构:上游对接不确定的多模态大模型生成,中游做确定性的图像与视频加工,下游处理持久化和异步任务。

如果模块之间可以随意调用,那么 CI 全绿也只能证明“这次还能跑”,不能证明系统出问题时找得到原因。所以除了代码入口的检查,我们也对系统内部划分了清晰的职责边界:

模块核心职责明确不负责
领域模块项目、资产、版本等核心领域模型AI 接口调用与任务调度
生成模块模型调用编排与生成策略底层媒体算法处理
加工模块抠图、缩放、切图等确定性图像处理供应商选择与模型交互
任务模块任务状态机、队列消费与事务消息具体业务资产加工逻辑
存储模块临时上传、对象存储持久化与 URL 转换资产业务生命周期

收益 1:故障归因极快,爆炸半径可控

职责拆清之后,线上出问题时排查路径变得极其直接:

  • 生成效果或 Prompt 异常:直接定位生成模块,不用怀疑媒体处理算法;更换大模型供应商(如 Midjourney / Flux / SD)时,也绝不会波及下游的切图与入库逻辑;
  • 抠图白边、缩放拉伸、格式损坏:问题必定出在加工模块的确定性算法中。这类纯算法单测不依赖任何网络调用与昂贵的大模型 Mock,本地秒级复现与调试;
  • CDN 链接失效、预签名过期:排查范围完全收敛在存储模块,与上层资产业务逻辑解耦。

收益 2:消灭“僵尸任务”,异步链路有迹可循

异步任务最让人头疼的故障,是“业务数据写入了数据库,但往队列投递消息时网络抖动失败”。以前没有机制保护时,前端显示创建成功,任务却永远卡在 PENDING,运维排查时很难确认消息到底是丢了、还是 Worker 没消费,最后只能人肉进数据库改状态。

为了彻底解决这个断点,任务模块引入了 事务消息表(Transactional Outbox) 模式:

异步任务与事务消息表数据流

  • 本地事务强一致:业务资产落库与待发布任务记录在同一个数据库事务中提交,事务失败直接整体回滚,彻底杜绝“入库了却没发出来”的僵尸任务;
  • 排查与重试有据可查:独立 Dispatcher 轮询未发送记录推入队列,投递失败自动递增重试计数并保留现场。线上追查任何卡顿任务,只需查一眼 outbox_events 表中的投递状态与重试次数,无需大海捞针般翻查分布式日志;
  • 业务解耦:业务代码只关注定义任务类型和处理逻辑,完全不依赖具体的底层消息队列实现。

10. AI 在工程质量中扮演什么角色

在 Holonic Asset 中,一个无法忽视的工程现实是:我们组几乎每一行代码都是 AI 写的。

当开发主力从人类完全转向 AI 时,大家最常有的担忧是:AI 会不会以极快的速度制造大量垃圾代码与架构坏味道?但随着整个工程闭环的建立,我们发现 AI 并不是工程质量的破坏者;相反,在很多维度的质量把控上,它甚至比人类工程师更具优势:

  • 比开发者更懂改动细节的“文书与上下文梳理者”:AI 特别适合撰写 Issue 拆解和 PR 描述。因为它直接遍历了完整的 Diff 和调用链路,往往比负责 Prompt 的开发者本人更能精准列出改动的每个边缘细节、波及函数和测试断言,从而产出高度客观且结构清晰的变更报告。
  • 不知疲倦、极致敏锐的“微观代码审查员”:在单点代码审查上,AI 拥有惊人的洞察力。面对复杂的并发竞态、边界空值、内存泄漏或未处理的异步异常,AI 发现问题的概率甚至比一个拥有 20 年经验的顶级架构师更高。更重要的是,AI 永远不会因为一天内反复 Review 几十次而感到疲惫或敷衍,每一次检查都能保持毫无波动的严格标准。

但是,AI 不能做什么?

AI 虽然擅长微观实现与局部检查,但它永远无法替代人完成以下关键职责:

  1. AI 不能凭空拥有架构品味(Taste):AI 会顺应上下文中的坏味道。如果现有系统反向依赖丛生、模块边界不清,AI 也能在混乱的结构上迅速堆砌出“功能看似可用、但系统性退化”的代码。定义架构边界(如单向依赖规范、事务消息表模式)必须由人来主导。
  2. AI 不能替团队做出工程与业务的取舍(Trade-off):何时为了研发效率放宽规则、何时为了系统稳定严格设卡、在什么阶段引入何种技术方案,这些充满权衡的顶层决策必须由人来裁定。
  3. AI 不能承担系统最终的责任(Accountability):AI 没有责任感,也不会对线上故障承担后果。把关系统设计的最终合理性、守住主分支安全底线,始终是人类工程师不可让渡的责任。
环节AI 的极致优势(机器的擅长面)工程师的核心防线(人的不可替代性)
需求与定义快速将粗粒度诉求扩充为结构化 Issue 与完整检查项明确业务真实目标,裁定范围边界与明确不做什么
代码与架构高速生成样板代码、单元测试与局部类型实现设计模块单向依赖,建立架构边界与领域模型
PR 与追溯深度解析 Diff,生成详尽无遗漏的变更说明与影响分析确认业务逻辑符合预期,为人肉审核与发布背书
Review 与合入毫秒级扫描并发竞态与边界异常,不知疲倦反复审查评估方案整体合理性、向后兼容性与故障爆炸半径

因此,我们没有把 AI 视为需要处处提防的“风险源”,也没有盲目放权让其“自我管理”。正确的做法是把规范沉淀为 Lint 规则、数据契约与 Agent 专属 Skills:让 AI 在被边界保护的轨道里发挥极致的生产力与微观审查能力,而人类工程师则专注于顶层架构、规则设计与终审把关。

11. 总结:让正确的做法成为默认路径

工程门禁不是为了把流程搞得繁琐,更不是为了考核谁能背下规范。

一个团队的工程质量,往往不取决于成员最清醒时的上限,而取决于大家赶进度、疲惫时哪怕随手一写也不会跌落的下限。把规范做进工具链、把边界写进编译器、把验证做成自动化,让大家在日常开发中顺着默认路径走,就能自然而然把事情做对。