在 Holonic Asset 中,我负责项目整体的工程门禁。
做这件事的决心,其实来自团队成员提交第一个 PR 的时候。
当时我看到仓库里冒出来的分支和提交,直接就是名字首字母缩写+_干了什么。代码逻辑还没开始看,分支名和提交记录就已经五花八门。这让我意识到一件事:如果规范只停留在口头约定或群里提醒,一到赶进度的时候,所有人都会凭着各自的习惯野蛮生长。
但只写一份“大家务必遵守”的规范文档毫无用处,发布时再隆重,过几天也会沦为摆设;直接从老项目搬一套严苛复杂的规则,又只会逼得大家到处想办法绕过门禁。真正要想守住质量,唯一的办法是把规范做成日常开发里绕不开的默认路径——需求先定义、变更有边界、提交有验证、合入有审核。只要顺着默认路径走,就自然做对。
1. 我想搭的不是检查清单,而是一条路
在日常迭代中,尤其是在多人协作与快节奏开发下,项目很容易在四个关键环节失控:
| 失控位置 | 典型表现 | 最终代价 |
|---|---|---|
| 质量失控 | 格式、Lint、测试和构建散落在各人本地 | 问题发现得晚,Review 变成无休止的低级挑错 |
| 契约失控 | 前后端靠口头同步接口字段变更 | 字段遗漏、类型错误、联调和部署阻塞 |
| 运行失控 | 业务入库与异步消息发布脱节 | 任务丢失、状态悬置、故障难以恢复 |
| 协作失控 | Issue 范围不清,PR 缺乏有效验证记录 | 变更无限膨胀,风险直接溜进主分支 |
靠负责人人肉 Review 仔细一点固然能挡住一部分问题,但这不可持续。人会累、会漏看,更不应该把精力耗在肉眼查格式和拼字段上。
为了防止这些失控,我想要的不是一份写满规则的检查清单,而是一条把关卡内嵌进去的默认研发路径:
我最后把整个变更过程收成了四个要求:
- 需求先定义:先把问题背景、范围、排除项和验收标准讲清楚。
- 变更有边界:模块职责要清楚,PR 和 Commit 也不能无限膨胀。
- 提交有验证:本地先做快速检查,完整测试和生产构建交给远端环境。
- 合入有审核:机器负责重复检查,人负责判断设计、兼容性和风险。
单独看,这些要求都不新鲜。真正有用的是把它们连成闭环:Issue 说明白了,后续设计才有依据;变更有边界,Review 才能聚焦;有了自动验证,代码质量才不用靠“我觉得没问题”来凭空担保。
2. 这套规范不是一天搭出来的
这套门禁不是一蹴而就的。我们没有一开始就把所有规则压上去,而是分成了四个阶段逐步演进:
先把协作入口统一起来
2026 年 7 月中旬,项目建立了主分支保护规则、结构化 Issue 表单、PR 模板,以及覆盖全流程的仓库专用指引。
- PR #29:建立 Bug、Feature、Document 和 Experiment 四类 Issue 模板
- PR #37:加入 Issue、分支、Commit、PR 和 Issue 拆分 Skills
- PR #42:加入统一的 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 将依赖关系收敛为严格的向下单向依赖:
具体规则包含两方面:
- 保护模块入口:跨模块只能导入
@/features/<feature>或@/model/<module>的公共入口,禁止深层穿透(保留@/model/generated/**为合理例外),模块内部依然可以使用相对路径。 - 禁止下层逆向依赖上层:
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. 大家为什么会遵守
让人人自觉遵守规范是不切实际的,我们把落地机制拆成了四层递进:
- 看得懂:通过结构化 Issue/PR 模板与文档,讲清标准写法。
- 容易做:通过自动生成类型与专用 Skills,让正确做法的操作成本最低。
- 自动提醒:提交前检查、远端 CI 与预览环境提供即时反馈。
- 硬约束:主分支受保护,必须通过 PR、至少 1 位批准、所有 Review 意见解决、且仅允许 Squash 合并。
Review 本身也做了明确分层:
- 机器:接管格式、Lint、测试、构建、竞态与预览环境等重复检查。
- 我负责工程预审:重点把关职责边界、契约一致性、验证结果和向后兼容。
- 导师把关高层设计:负责关键方案设计与高风险改动的终审。
这套分层不是为了把流程搞复杂,而是因为机器和人擅长的事情不同:让人肉眼去抠格式和字段是浪费精力,指望机器判断跨模块架构是否合理也并不现实。
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. 吃过契约漂移的亏以后
前后端契约脱节一度是联调中最消耗精力的痛点:后端改了字段名,前端只有在页面渲染白屏或构建报错时才察觉。
现在我们确立了一条单向事实源链路:
- 请求与响应数据结构在后端 Go DTO 中唯一定义;
- 工具根据 DTO 自动化生成 OpenAPI 规范与接口文档;
- 前端据此自动生成 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 虽然擅长微观实现与局部检查,但它永远无法替代人完成以下关键职责:
- AI 不能凭空拥有架构品味(Taste):AI 会顺应上下文中的坏味道。如果现有系统反向依赖丛生、模块边界不清,AI 也能在混乱的结构上迅速堆砌出“功能看似可用、但系统性退化”的代码。定义架构边界(如单向依赖规范、事务消息表模式)必须由人来主导。
- AI 不能替团队做出工程与业务的取舍(Trade-off):何时为了研发效率放宽规则、何时为了系统稳定严格设卡、在什么阶段引入何种技术方案,这些充满权衡的顶层决策必须由人来裁定。
- AI 不能承担系统最终的责任(Accountability):AI 没有责任感,也不会对线上故障承担后果。把关系统设计的最终合理性、守住主分支安全底线,始终是人类工程师不可让渡的责任。
| 环节 | AI 的极致优势(机器的擅长面) | 工程师的核心防线(人的不可替代性) |
|---|---|---|
| 需求与定义 | 快速将粗粒度诉求扩充为结构化 Issue 与完整检查项 | 明确业务真实目标,裁定范围边界与明确不做什么 |
| 代码与架构 | 高速生成样板代码、单元测试与局部类型实现 | 设计模块单向依赖,建立架构边界与领域模型 |
| PR 与追溯 | 深度解析 Diff,生成详尽无遗漏的变更说明与影响分析 | 确认业务逻辑符合预期,为人肉审核与发布背书 |
| Review 与合入 | 毫秒级扫描并发竞态与边界异常,不知疲倦反复审查 | 评估方案整体合理性、向后兼容性与故障爆炸半径 |
因此,我们没有把 AI 视为需要处处提防的“风险源”,也没有盲目放权让其“自我管理”。正确的做法是把规范沉淀为 Lint 规则、数据契约与 Agent 专属 Skills:让 AI 在被边界保护的轨道里发挥极致的生产力与微观审查能力,而人类工程师则专注于顶层架构、规则设计与终审把关。
11. 总结:让正确的做法成为默认路径
工程门禁不是为了把流程搞得繁琐,更不是为了考核谁能背下规范。
一个团队的工程质量,往往不取决于成员最清醒时的上限,而取决于大家赶进度、疲惫时哪怕随手一写也不会跌落的下限。把规范做进工具链、把边界写进编译器、把验证做成自动化,让大家在日常开发中顺着默认路径走,就能自然而然把事情做对。