我为什么做 engineering-workflow
我开始认真用 AI agent 写代码之后,很快意识到一个问题:AI 的能力已经不只是"补全一段函数"了。它可以读仓库、改多处文件、写测试、跑命令、解释错误,甚至能在一个长任务里连续推进好几步。
这当然很有用。但越是把 AI agent 放进真实工程里,我越发现:真正危险的不是它不会写代码,而是它太容易跳过工程过程。
一、我的痛点:AI 写得快,但工程过程很容易被跳过
在 demo 或一次性脚本里,AI 写得快通常就是优点。但在真实项目里,快会把很多问题藏起来。
它能快速找到文件,快速补逻辑,快速写测试,也能用很确定的语气告诉你"已修复"。但真正把代码合进去、跑起来、过几天再改同一块逻辑时,问题会浮出来:
- 需求里明明提到了边界条件,代码只做了 happy path。
- 方案写得挺完整,最后实现漏了其中几条。
- 测试看起来不少,但测的是内部 helper,不是用户真正触发的入口。
- bug 没有稳定复现,agent 先猜了一个修复。
- 线上问题只测了更低层函数,没有走真实 API / job / CLI 入口。
- agent 说"完成了",但没有刚刚跑过 lint、test 或同路径验证。
- 下一次会话重新开始,又要解释一遍项目术语、测试命令、哪些目录不能碰。
这些问题单独看都不新。人写代码也会犯。但 AI agent 的速度会放大它们:它可以在你还没来得及确认需求边界时就把实现写完,也可以在没有复现 bug 的情况下给出一个看起来合理的修复。
所以我真正想解决的不是"让 AI 更会写代码",而是让 AI 少猜、少漏、少自作主张,并且把每一步的工程证据留下来。
二、于是我做了 engineering-workflow
我把这套实践整理成了一个仓库:
https://github.com/Mickls/engineering-workflow
它是一个面向 Codex / AI agent 协作的工程工作流配置,主要包含两部分:
- 一个全局
AGENTS.md模板,用来定义常驻原则、指令优先级、工作产物边界、skill 路由和硬门禁。 - 一个
engineering-workflowCodex plugin,里面按场景拆了多个 skills,比如项目初始化、需求分析、普通诊断、线上问题排查、原型、需求拆分、架构审查、编码规范、测试策略、工程交接和最终验证交付。
它不是一个框架,也不是一个代码生成器,更不是"神奇 prompt"。它做的事情更朴素:把我希望 AI agent 遵守的工程过程拆成可触发、可复用、可检查的流程。
这套工作流有几个基本判断:
- 不是所有任务都需要重流程,但高风险任务必须自动变严。
- 需求不能只停留在自然语言里,要被拆成可追踪的约束。
- 实现不能只靠 agent 自己觉得合理,要对齐已确认的 design/plan。
- 测试不能只测内部函数,要证明用户关心的行为。
- bug 不能靠猜,先建立反馈循环和复现证据。
- "完成"不能靠语气,要靠刚刚运行过的验证命令和覆盖核销。
下面具体说这套东西怎么组织,以及每个部分在防什么问题。
三、我不想让 AGENTS.md 变成一本巨大的工程圣经
这个项目分成两层:
global/AGENTS.md:全局常驻规则,只放原则、指令优先级、工作产物边界、skill 路由和硬门禁。plugins/engineering-workflow/skills/*:具体场景的工作流程,比如需求、诊断、编码、测试、交付。
这样拆是有原因的。AGENTS.md 如果写得太厚,会变成所有任务都要背的一本大书;真正执行时,agent 反而抓不到重点。所以我的做法是:全局文件只负责"什么时候该进入哪条流程",具体细节交给 skill。
比如:
- 新项目第一次协作,走
project-setup。 - 非轻量需求,编码前先走
requirements-workflow。 - 普通 bug 或失败测试,先走
diagnosis-workflow。 - 线上问题、数据误删、重复调用、并发、幂等,走
incident-debugging。 - 进入代码实现,走
coding-standards。 - 涉及测试,走
testing-policy。 - 最终交付,走
verification-delivery。
这套路由的意义是:不要让 agent 每次凭感觉决定工作方式。任务类型不同,风险不同,应该触发的门禁也不同。
四、.codex/engineering-workflow/ 是 agent 的工程记忆
我不希望每个会话都从零开始解释项目。因此这套工作流默认把中间产物放在目标仓库的:
text
.codex/engineering-workflow/
project/
issues/
adr/
prototypes/
debug/
reports/
handoffs/
这里面放的不是生产代码,而是 agent 协作需要的工程记忆。
project-setup 会沉淀几类东西:
project-profile.md:项目类型、主要语言、关键入口、高风险任务定义。commands.md:lint、test、build、format 命令,以及哪些测试不稳定、哪些依赖外部服务。domain.md/context.md:项目自己的领域术语,避免用户说 A、代码里叫 B、agent 又发明 C。issue-workflow.md:这个项目怎么维护需求、状态和确认门禁。out-of-scope.md:已经明确不要做、或者 agent 很容易反复误提的方案。adr/:只记录难逆转、反直觉、有真实取舍的决策。
这一步很朴素,但很有用。AI agent 的上下文窗口再大,也不是项目记忆。把项目协作信息写下来,后续需求、测试、诊断、交付都能复用同一套事实。
五、我把"需求"拆成可核销的约束
我之前最常遇到的问题是:design 写得挺像样,最后实现还是漏。
原因是自然语言太容易让关键边界藏在段落里。比如一句话里可能同时包含:
- 必须支持已有配置。
- 不能影响旧数据。
- 手动触发和自动触发语义不同。
- root 目录外的文件必须被拒绝。
- 第二次调用不能重复删除。
- 没有权限时不能返回部分数据。
如果这些只停留在段落里,agent 实现时很容易只抓主线。
所以 requirements-workflow 要求非轻量需求在 design.md 里写"关键约束覆盖表":
| ID | 类型 | 关键约束 | 影响入口/调用链 | 实现要求 | 验证意图 |
|---|---|---|---|---|---|
| C-001 | behavior | 用户提交后应创建任务 | HTTP handler -> service -> repository | 复用现有创建流程 | 断言返回值和 DB 状态 |
| C-002 | safety | 重复提交不能重复创建 | 同上 | 使用已有幂等键 | 连续两次调用同一入口 |
| C-003 | compatibility | 旧配置不需要迁移即可读取 | config loader | 保持兼容读取 | 用旧 fixture 验证 |
然后 plan.md 必须把每个约束映射到实施步骤:
| 约束 ID | 实施步骤 | 预计代码落点 | 测试/验证入口 | 状态 |
|---|---|---|---|---|
| C-001 | Step 2 | handler/service/repository | integration test | planned |
| C-002 | Step 3 | service idempotency check | 同一 API 连续调用两次 | planned |
| C-003 | Step 4 | config loader | old config fixture | planned |
这个表不是为了好看,而是为了防漏。后面编码、测试、交付都要回头核销它。没有实现落点、没有验证证据、没有解释为什么不适用,就不能说完成。
这也是我现在理解需求的方式:需求不是一段描述,而是一组必须被实现和证明的约束。
六、为什么非轻量需求写完 design/plan 后必须停
这套工作流里有一个很硬的门禁:非轻量需求创建或更新 design.md / plan.md 后,当前回合必须停下来,让用户 review。确认之前不能编码、不能写测试、不能生成 migration/proto,也不能安排实现类工作。
这个规则看起来麻烦,但它是从 AI 协作里的一个真实风险来的:agent 很容易把"自己刚刚写出来的方案"当成已经被确认的事实,然后立刻继续实现。
对简单文案、注释、格式化、小范围无行为改动,当然不需要这么重。但只要涉及下面这些内容,就不能把用户确认省掉:
- 行为变化。
- 跨模块调用链。
- API、proto、schema 或数据库结构。
- 权限、租户、幂等、并发。
- 数据删除、状态迁移、异步任务。
- UI 独立流程。
- 线上问题修复。
我希望 agent 在这里承担的是"调查和整理方案"的角色,而不是替用户做产品和工程决策。真正进入实现前,用户必须有机会看见:本次到底做什么、不做什么、哪些边界被覆盖、哪些风险需要接受。
七、UI 原型不是为了炫,是为了提前暴露理解偏差
如果是独立 UI 需求,工作流要求在确认 design/plan 前生成静态 HTML 原型。
这个要求也不是为了把流程搞复杂,而是因为 UI 需求特别容易"文字上理解一致,画出来完全不是一回事"。
原型默认放在:
text
.codex/engineering-workflow/issues/<REQ-or-EPIC>/prototype.html
它只需要回答需求确认问题,不进入生产目录,不接真实接口,不跑生产级 lint/test/build,也不反复打磨。它至少要表达:
- 默认态。
- 关键操作态。
- 空态、错误态、成功态里和需求确认相关的状态。
- 如果会在窄屏使用,要覆盖移动端风险。
我对原型的定位很明确:它不是生产实现,是用最低成本把理解偏差提前暴露出来。
八、编码时我最在意三件事:复用、边界、少造东西
coding-standards 不是某种语言规范,它主要约束 agent 的编码习惯。
第一,先复用,再实现。顺序是:
- 项目已有 helper、service、repository、client、组件。
- 标准库、运行时、框架原生能力。
- 项目已经引入的第三方库。
- 自己实现。
新增依赖不是不能做,但要解释为什么已有能力不够,是否影响 lockfile、license、体积、兼容性和维护成本。
第二,校验放在真实边界。比如:
- HTTP/RPC handler。
- CLI 命令入口。
- event/job 接收处。
- webhook。
- 前端表单提交。
- public/exported API。
- 文件、网络、数据库、序列化边界。
内部函数不要为了"保险"到处重复判空、判枚举、判权限。这样写出来的代码看似稳,实际上会让调用契约变模糊,也会制造一堆永远到不了的分支。正确做法是追到数据来源,看上游是否已经校验和标准化。如果安全性依赖上游,就在 plan、注释或交付说明里写清楚校验来源。
第三,不为了局部传值造中间类型。AI 很喜欢新建 DTO、interface、struct、class,好像这样更"工程化"。但如果只是在一条局部链路上传几个值,局部变量和直接参数往往更清楚。只有跨模块、异步、序列化、稳定 API 或复杂状态真的需要命名结构时,再定义新类型。
这几条背后的原则是:代码应该尽量贴近当前项目,而不是展示 agent 的抽象能力。
九、bug 不能靠猜,先建立反馈循环
diagnosis-workflow 处理普通 bug、失败测试、性能回退和本地可排查异常。它第一步不是改代码,而是建立反馈循环。
反馈循环可以是:
- 从真实入口出发的 failing test。
curl/ HTTP 脚本。- CLI 命令加 fixture。
- 浏览器自动化脚本。
- 事件或日志 replay。
- 最小 harness。
- property / fuzz / stress loop。
- differential loop。
git bisect runharness。- 人机协作脚本。
关键不在形式,而在质量:
- 是否能观察用户描述的问题,而不是附近另一个失败?
- 是否足够快,可以支持多轮验证?
- 是否稳定?如果不稳定,是否提高了复现率?
- 断言是否具体到用户可见结果、数据状态或副作用?
- 是否从真实入口或等价顶层入口触发?
有了反馈循环之后,才进入假设验证。工作流要求修改生产逻辑前列出 3-5 个可证伪假设:
text
如果 X 是根因,那么观察或改变 Y 会让 Z 发生。
每次只验证一个假设。可以加临时 instrumentation,但要带唯一前缀,比如 [DEBUG-a4f2],交付前 grep 确认清理掉。
这个流程能防止 agent 根据报错位置直接猜修复。很多 bug 的根因不在最显眼的地方,尤其是调用链长、状态复杂、异步或缓存参与时。
十、线上问题比普通 bug 更严格
incident-debugging 专门处理线上问题、生产 bug、数据误删、重复调用、幂等、并发、状态错乱和事故排查。
它有四个硬门禁:
- 复现门禁:已经从真实入口或等价顶层入口复现。
- 根因门禁:已经用调用链和状态变化证明具体根因。
- 修复门禁:只改已证明根因,不做无关重构。
- 回归门禁:修复后重新运行同一条复现流程。
比如问题是"两次调用同一个接口导致误删除",那复现必须包含:
- 构造能触发问题的数据。
- 连续两次调用同一个 API/RPC/CLI/job 入口。
- 比较第一次和第二次之后的真实持久化状态。
- 断言误删、重复删除或状态错乱确实发生。
不能直接调用内部删除 helper,不能 mock 掉 repository 到看不见数据库状态,也不能只断言某个 service 返回值。线上问题的证明对象不是"内部函数表现",而是"真实入口触发后的真实副作用"。
这条很重,但值得。因为线上问题最怕"修了一个看起来相关的地方",过几天同类事故又出现。
十一、测试不是越多越好,而是证明力越强越好
testing-policy 里我最看重的一句话是:一个有证明力的链路测试,优先于多个低价值内部测试。
测试应该优先从这些入口进入:
- HTTP/RPC handler、route、controller、resolver。
- event/job 接收处。
- CLI 命令。
- 页面或用户操作入口。
- public package/module API。
低层 service 或 helper 不是不能测,但要满足一个条件:它本身就是稳定 contract,或者外层没有相关参数绑定、权限、租户、事务、副作用和错误映射,或者已有测试已经覆盖外层。
我不想要这种测试:
- mock 掉核心逻辑,然后断言 mock 被调用。
- 断言内部函数被调用次数。
- 断言偶然排序、局部变量形状、日志文本。
- 为了覆盖率把同一条路径拆成一堆没有独立风险的测试。
- 在测试期望里复制生产逻辑,导致生产和测试一起错。
我想要的是这种测试:
- 从真实入口或稳定 public contract 进入。
- 能观察用户关心的返回值、错误、数据库状态、消息、副作用。
- 能覆盖关键约束 ID。
- 失败时能说明真实行为错了,而不是实现细节变了。
如果是 bugfix,回归测试还要先证明旧逻辑会失败。没有复现旧失败,就不要直接写一个"修复后应该通过"的测试来自我安慰。
十二、大需求要按用户行为切,不要只按技术层切
很多计划会自然拆成:
- 改数据库。
- 写 DAO。
- 写 service。
- 写 API。
- 写前端。
- 写测试。
这种拆法在执行上有用,但如果只有这种拆法,就会出现一个问题:每一步单独完成后都不能被用户验证,也不能说明某条需求已经闭环。
所以 issue-slicing 要求优先拆垂直切片。每个切片尽量经过完整调用链,完成后有可观察结果,并关联约束 ID。
例如不要只写:
text
Slice 1: database
Slice 2: backend
Slice 3: frontend
而应该尽量写成:
text
Slice 1: 用户可以创建草稿,覆盖 C-001/C-002,可通过 API 和 DB 状态验证
Slice 2: 用户可以提交草稿进入审核,覆盖 C-003/C-004,可通过状态机和消息副作用验证
Slice 3: 审核失败后用户可修改重提,覆盖 C-005/C-006,可通过页面流程和 API 验证
技术层任务可以作为支撑任务存在,但最终要服务于某个可验收的用户行为。
十三、架构审查不等于顺手重构
architecture-review 只在明确要求架构审查、模块边界分析、测试 seam 分析,或者某片代码反复难改时使用。它默认不修改生产代码。
它关注的问题很具体:
- 模块 interface 是否清晰,还是只是浅层转发?
- 复杂度是否集中在可测试 seam 后面?
- 调用方是否需要知道太多实现细节?
- 错误、事务、权限、状态机、副作用是否散落?
- 改一个行为是否要同时动很多无关位置?
- 测试是否只能测内部函数,无法从稳定入口证明行为?
输出也不是"大重构建议",而是候选项:
- 涉及文件。
- 当前摩擦。
- 建议方向。
- 预期收益:locality、leverage、testability。
- 风险。
- 推荐强度。
如果用户选择其中一个候选项,再进入需求流程或切片流程。这样可以避免 agent 在"审查"时顺手改一堆架构代码。
十四、交付时必须把证据说清楚
verification-delivery 解决的是最后一步:什么时候可以说完成。
我的规则是:没有刚刚运行过相关验证,就不要说"已完成""已修复""通过了"。
编码后至少要考虑:
- lint:全项目太贵时,可以只跑改动文件或受影响 package,但要说明范围。
- test:运行与本次行为变化相关的测试,优先覆盖调用链。
- build/typecheck/compile:接口、依赖、生成代码、配置变化时必须考虑。
- bugfix:需要修复前复现证据、根因证据、修复后同路径验证。
- 非轻量需求:需要核销关键约束覆盖矩阵。
最终回复也不能只写"完成了"。应该包含:
- 改了什么。
- 关键文件路径。
- 跑了哪些命令,结果是什么。
- 哪些没验证,为什么。
- 剩余风险。
- 是否需要用户继续确认。
这不是官样文章。它是在保护协作信任。AI agent 真正可靠,不是因为它语气自信,而是因为它知道自己凭什么下结论。
十五、这套工作流实际改变了什么
如果只看表面,这套项目里有很多规则:skill 路由、design/plan、覆盖矩阵、原型、反馈循环、测试策略、交付门禁。
但它真正改变的是 AI 协作的默认姿势。
以前默认是:
text
用户提需求 -> agent 找文件 -> agent 改代码 -> agent 写测试 -> agent 说完成
现在我希望默认变成:
text
用户提需求
-> agent 判断任务类型和风险
-> 读取项目上下文和相似实现
-> 追踪入口、调用链、数据来源和副作用
-> 提炼关键约束
-> 写 design/plan 或 no-doc 最小约束清单
-> 必要时做原型或切片
-> 用户确认
-> 实现时逐条对齐约束
-> 从真实入口或稳定 contract 测试
-> 回填实现落点和验证证据
-> 交付时说明验证、未验证项和风险
这条链路看起来长,但不是每个任务都要完整跑一遍。轻量任务可以轻量处理。真正重要的是:一旦任务风险上来,流程要能自动变严。
十六、我对 AI 编码的理解
AI agent 最大的价值不是替我按键盘,而是把工程执行力放大。它可以快速阅读代码、整理调用链、生成候选方案、补测试、跑命令、做重复验证。但前提是,它不能在关键问题上悄悄替我猜。
所以我更关心的不是"怎么让 AI 一次写对",而是"怎么让 AI 写错时更早暴露,怎么让遗漏更难发生,怎么让完成声明必须有证据"。
这套 engineering-workflow 本质上是在给 AI 协作加几个工程护栏:
- 用项目上下文减少重复解释和术语漂移。
- 用需求约束表防止自然语言遗漏。
- 用 plan 覆盖矩阵连接需求、实现和验证。
- 用用户确认门禁阻止 agent 自我推进。
- 用反馈循环和可证伪假设处理 bug。
- 用真实入口测试证明用户行为。
- 用交付验证约束"完成"这个词。
它不是为了让开发显得正式,也不是为了把所有任务都流程化。它只是承认一件事:AI 越快,越需要清晰的边界、证据和停顿点。
我现在越来越觉得,AI 时代写代码的能力,会从"我会不会实现这个函数",扩展成"我能不能设计一套让人和 agent 一起可靠工作的工程系统"。
代码最后当然还是要落到仓库里。但在代码落下之前,需要有人把需求变成约束;在代码落下之后,需要有人证明这些约束确实被满足。我的这套工作流,就是围绕这件事展开的。