☰
AGENTS.md、测试和 CI 都写同一条规则,会不会重复?
2026/10/3 2:35:29 网站建设 项目流程

一、一次评审里的三句话

shop订单服务的评审会上,三个人几乎同时说出三句话。

第一个人说:"'取消成功后审计事件恰好一条’这条,我已经写进 AGENTS.md 了,为什么还要再写测试?"第二个人说:"测试里已经有断言了,AGENTS.md 里再抄一遍,改的时候忘一边怎么办?"第三个人说:“CI 每次都跑整仓库测试,本地跑一遍不就行了,何必再配一条流水线?”

三个人说的都是同一种担心:重复。这个担心是合理的——重复的规则确实会带来维护成本,而且三处措辞不一致时,最坏的情况是它们互相矛盾。但把这条规则放到实际发生过的几次问题里看,会发现另一种情况:只写文档,它会被忘;只写测试,别人不知道要做什么;只接 CI,红了以后没人知道是为什么。

所以问题不是"要不要重复",而是"三层各自负责什么、怎么让重复不变成负担"。这一篇用一条具体规则走完三层:文档层、测试层、CI 层,并给出减少重复的三个技巧。

二、先把几个词讲明白

文档层:写在 AGENTS.md 或者任务单里的规则,回答"应该做什么"。它面向执行者的意图,是动作与约束的来源。

测试层:写在代码里的断言,回答"做成了没有"。它面向行为,是证据的来源。

CI 层:在持续集成流水线里运行的检查,回答"允不允许合入"。它面向流程,是门禁;门禁的意思是:不通过就不让合并,不依赖任何人记得。

门禁:一个自动的、强制性的检查点。生活里的类比是地铁闸机:票不对,门不开;它不需要工作人员记住你是谁。

重复与冗余:重复指同样的信息在多处出现;冗余指多余的备份。两者不完全等价——在三层结构里,"同一条规则出现在三层"是分层带来的冗余,只要三层各自表达的信息不同(意图、证据、门禁),它就不是多余的。

单一事实源:同一件事只在一处定义,其他位置引用它。在本篇的场景里,命令和测试名最适合做单一事实源:命令只在脚本里定义一份,文档和 CI 都引用脚本名。

失败可见性:检查失败时,人能不能快速知道"哪条规则被违反了、违反了会怎样"。失败信息越具体,修复越快。

漂移:同一件约束在两处的表述逐渐不同,最终互相矛盾。漂移通常不是一次造成的,而是多次"顺手改一处"累积出来的。

失败信息:检查不通过时输出的内容。好的失败信息包含三样:哪条规则、哪次运行、期望与实际。它是三层之间最后一段黏合剂——因为无论哪一层拦住你,最后被读到的是这几行字。

三、三层为什么都要有

3.1 三层回答三个不同的问题

把它们写成三句话就清楚了:

文档层:应该做什么?(给方向) 测试层:做到了没有?(给证据) CI 层:能不能合入?(给门禁)

三个问题没有互相替代关系。只有方向没有证据,你无法判断做没做到;只有证据没有方向,执行者不知道要做什么,测试也可能压根没覆盖到那条规则;有方向有证据但没有门禁,在忙的时候可以被绕过——而"忙的时候"恰好是最容易出问题的时候。

3.2 少一层的三种典型失败

缺文档层:测试写了,但没人知道背后的约定。新同事看到assert len(events) == 1时能读懂代码,却不知道"为什么必须恰好一条";代理想改动时也容易把它当作"可以放宽的细节"。测试是证据,不是说明书。

缺测试层:文档写了,但没有证据。代理在交付时只能说"按约定做了",你只能选择相信或者逐行读代码。更糟的是,当实现出现偏差时,没有任何东西会变红——规则变成了"希望"。

缺 CI 层:文档和测试都有,但依赖人记得运行。本地跑的时候忙起来跳过、评审时看到"测试通过"的口头说明就放行,几次之后规则的分量就被自然削弱了。CI 的价值不在于"再跑一遍",而在于它不依赖任何人的记性。

3.3 为什么"重复"在这里是好事

回到最开始那个担心:三层都写,是不是浪费?把它和一份只写在一处的规则对比一下就清楚了。

只写在一处的规则有一个致命弱点:它的"可被绕过性"取决于那一处的性质。只写在文档里,可以不被执行;只写在测试里,可以不被理解;只写在 CI 里,可以不被知道。三层各写一份,等于把"绕过成本"提高到了需要同时绕过三个地方——而这三处恰好覆盖了意图、证据和流程。

真正的浪费不是"同一条规则出现在三处",而是"三处说的不是同一件事"。所以这一篇的重点不是讨论要不要写三层,而是怎么让三层保持同一个意思。

3.4 三层的"写入顺序"

三层都写,那么按什么顺序写?实践中最省事的顺序是:先测试,再 CI,最后文档。这个顺序的理由是每一步都能验证前一步。

先写测试的原因在前面提过:写不出断言的动作,说明它还不够具体。测试写完之后,接到 CI 是一次机械动作——如果发现接不上(比如测试依赖本地环境、需要人工准备数据),说明这条规则的验证方式还不稳定,要回去调整。最后写文档时,你已经有了一组可运行的证据和一条确定会执行的流程,文档只需要描述"做什么、去哪里看证据",写起来最快。

反过来(先文档后测试)也可以,但有一个常见的陷阱:文档写完会带来"已经完成"的心理感受,测试和 CI 的部分容易被推后。而测试和 CI 恰好是三层里唯一能自动执行的部分——推迟它们,等于让规则回到只靠人记的状态。

3.5 三层与"三次法则"

还有一个常见疑问:是不是每条规则都要三层齐全?答案是"看它违反的代价"。可以用一个简单法则:第一次靠口头提醒,第二次写成文档,第三次补测试与门禁。

第一次出现问题时,口头说明就够了——此时你甚至不确定它会不会重复发生。第二次出现同类问题时,把约定写进文档,让执行者有据可依。第三次出现时,说明这条约定反复被违反,靠人记不住了,这时候补测试、接 CI 才是划算的。这个法则能避免两个极端:把所有问题都变成流程(成本高、执行不下去),以及所有问题都靠口头(反复踩同一个坑)。

四、完整例子:一条规则的三层落点

4.1 规则本身

规则(示例):取消订单成功时,审计事件恰好一条;重复取消不新增事件。

这句话在两处场景里被违反过:一次是重复请求写了两条审计事件;一次是状态变更成功但审计写入失败,留下了一条"没有对应事件"的取消订单。规则要同时防住这两种情况,三层落点也要覆盖两者。

4.2 文档层怎么写

文档层的目标是让人(和代理)在动手前知道要做什么。写法沿用上一篇的三要素:

### 取消流程的数据要求 - 触发:任何修改订单状态的操作 - 动作: - 状态变更与审计事件在同一事务中提交 - 同一订单重复请求不新增审计事件 - 失败时状态与审计事件都不保留 - 验证:tests/orders/test_cancel.py 中的三条断言 - 检查时机与责任人:CI 运行该测试文件;评审人确认三条断言都在

文档层里最值得注意的是"验证"那一行:它指向具体测试文件,而不是笼统地说"有测试"。指向文件名的好处有两个:一是执行者知道去哪里看证据;二是文档和测试之间形成了可核对的对应关系——改名或者删掉测试时,这句话会立刻显得不对劲。

4.3 测试层怎么写

测试层负责把三条约定翻译成可以运行的断言(示例):

deftest_cancel_writes_exactly_one_event(repo,audit):repo.add(Order(id="o-1",status=OrderStatus.PENDING))cancel_order(repo,audit,"o-1")assertlen(audit.events_of("o-1","order.cancelled"))==1deftest_repeat_cancel_does_not_add_event(repo,audit):repo.add(Order(id="o-2",status=OrderStatus.PENDING))cancel_order(repo,audit,"o-2")cancel_order(repo,audit,"o-2")# 第二次调用assertlen(audit.events_of("o-2","order.cancelled"))==1deftest_cancel_rollback_leaves_no_event(repo,audit,failing_save):repo.add(Order(id="o-3",status=OrderStatus.PENDING))withpytest.raises(Exception):cancel_order(repo,audit,"o-3")assertaudit.events_of("o-3","order.cancelled")==[]

三个断言分别对应三条约定,命名也保持一致(恰好一条、重复不新增、回滚不留痕)。这种一一对应是文档层与测试层之间最重要的关系:文档里的每条动作,在测试里都有一条名字相近的断言;反过来,如果某个断言找不到对应的文档条目,说明文档漏写了,或者测试多测了。

第三个测试依赖failing_save这个夹具:它让保存订单的动作抛异常,用来验证回滚行为。这个夹具的存在也提示了一件事——回滚测试需要主动构造失败,不会自然发生。写这类测试时,夹具体现的正是"我们关心失败路径"这个约定。

4.4 CI 层怎么写

CI 层的目标是让检查不依赖人的记性。最小版本只有一件事:运行验证命令,不通过则阻止合入(示例思路):

触发:提交到任意分支,以及合并请求更新时 步骤: 1. 安装依赖 2. 运行 pytest -q tests/orders 3. 运行 ruff check . 4. 任一步骤退出码非 0,流水线失败,禁止合并

这里有三个设计点值得说明。第一,CI 里跑的命令和文档里写的命令完全一致——不一致会造成"本地过了 CI 不过"的困惑,也会让文档失去权威。第二,CI 触发条件写的是"提交与合并请求更新",而不是"仅合并时"——早失败早修复,成本更低。第三,失败时保留产物(测试输出、失败用例名),让排查不用重跑。

4.5 减少重复的三个技巧

三层都写之后,剩下的问题就是怎么让维护成本可控。三个技巧按投入从低到高:

第一个,用"引用"代替"复制"。文档里不重复写命令细节,而是写"运行 scripts/check.sh"或者指向测试文件名;CI 也调用同一个脚本。这样命令变更时只改一处。文档里保留的是动作与意图,不是命令的完整实现。

第二个,让三层的措辞保持一致。文档里的动作名、测试名、失败信息里的描述尽量用同一批词(恰好一条、重复不新增、回滚不留痕)。措辞一致带来的好处在失败时最明显:CI 报出的失败信息能直接对应到文档条目,不用再翻译一遍。

第三个,定期做一次一致性检查。方法很朴素:把文档里每条规则的"验证"一栏抄出来,去测试文件里找对应的断言,去 CI 配置里确认这些测试真的会被运行。三处都对得上,这条规则就是健康的;有一处对不上,就修那处。

4.6 三层的对照表

层回答什么写什么谁维护失败时表现
文档层应该做什么触发、动作、验证指向规则责任人被忽略或争议(无自动信号)
测试层做到了没有断言与夹具开发者测试变红,给出具体断言
CI 层能不能合入运行哪些命令、何时运行团队/维护者流水线失败,阻止合并

表格可以当检查表用:一条规则上线时,三行是否都落实了;如果某一行为空,就意味着这条规则在对应环节上有缺口。

4.7 只写两层的对照记录

把这条规则在三种"缺一层"的配置下各跑一次(示例记录),看看差别出在哪里:

配置第一个月的结果出问题的时刻
只有文档执行者凭理解做事,多数任务正确一次紧急改动里没人想起这条规则,审计事件写了两条
文档 + 测试本地可以验证,评审有依据有人忘记跑测试,合并后发现断言失败,返工重来
文档 + 测试 + CI三层一致,红灯阻止合并没有出现"越过规则"的情况;出现的是断言本身需要调整

(示例对比。)三行的差别不在"第一个月",而在"出问题的时刻"。前两种配置在顺利的时候看不出毛病,问题只在压力出现时暴露;第三种配置也会出问题,但问题变成了"规则本身该怎么调整",而不是"规则有没有被执行"。这正是三层结构想要达到的状态:把争论从"有没有做"移到"规则是否合适"。

记录这张表还有一个附加用途:当你需要说服别人"为什么还要接 CI"时,直接展示第二行和第三行的差别,比讲道理有效——因为它们描述的是同一条规则、同一批人,唯一变量是那一层自动检查。

4.8 一次演练:故意破坏一层,看会发生什么

前面的对照表是推理,这一节做一次演练。三层都接好之后,故意在代码里制造一次违规,观察每一层的表现。做法是把"重复取消不新增审计事件"这条约定破坏掉——去掉审计写入前的去重判断。

演练步骤与观察(示例记录):

1. 改坏实现:去掉重复取消时的去重判断 2. 本地运行:pytest -q tests/orders 结果:test_repeat_cancel_does_not_add_event 变红 观察:测试层给出具体断言,失败信息里有订单编号和实际条数 3. 推送:CI 运行同一条命令 结果:流水线失败,禁止合并 观察:这一层不依赖任何人记得,红灯自己出现 4. 读文档:AGENTS.md 的"验证指向"仍然指向同一个测试文件 观察:文档层不会变红,它负责的是"去看哪个证据"

第四步是这次演练最值得留意的地方。文档层不会因为实现被改坏而报警,这是分工的正常状态:文档指向证据,测试给出判定,CI 负责阻断。文档层的健康要靠另一种检查维护——定期确认它指向的测试仍然存在,也就是前面第五步讲的做法。

演练还有一个附带收获:它顺手检查了失败信息够不够用。若失败信息只写"数量不符",你还得回去读代码;若写清"订单 o-2 期望 1 条、实际 2 条",修复路径就短得多。演练时把失败信息也当作检查对象,一举两得。

4.9 三层记录放在哪里

三层材料各有归属:文档条目在 AGENTS.md,断言在测试文件,运行方式在 CI 配置。规则条数多起来之后,可以再维护一份索引表,把"哪条规则、落在哪一层、当前状态"压成一页:

编号 一句话规则 文档位置 测试文件 CI 任务 状态 最近核对 R-04 取消成功审计事件恰好一条 AGENTS.md 第4节 tests/orders/test_cancel.py 订单测试 生效 2026-09-29 R-05 领域层不依赖基础设施 AGENTS.md 第5节 tests/architecture/test_domain_isolated.py 架构检查 生效 2026-09-29

这张表的用法是体检时按行推进:一行对应一条规则,核对三处是否都还在、是否都还准确。它的代价是自己也会过期,所以每行都要带"最近核对"日期;日期过久的行,就是下一轮体检的优先项。

规则在十条以内时可以先不建这张表——文档里的"验证指向"已经能充当索引。等条数上去、开始出现"这条规则到底有没有测试"的问题时再补,避免一上来就维护两份材料。

五、反例与代价:五种让三层互相打架的做法

5.1 反例一:三处各写各的,措辞不一致

做法:文档写"审计事件不能重复",测试断言"事件数量小于等于二",CI 只跑一个无关的检查。三处看起来都在讲这件事,实际说的不是同一件事。

它为什么看起来能行:措辞模糊时,三处很难被当场发现矛盾,评审只看其中一处也会觉得没问题。

最后的代价在出问题时集中爆发。有人按文档理解"绝对不能重复",有人按测试理解"可以有一两条",争执时没有裁决依据——因为三处都是"官方文件"。更麻烦的是修复:你不知道该改哪一处才算对。避免方式很简单:动作级别保持一致(恰好一条就是恰好一条),测试断言与文档动作一一对应,CI 跑的命令覆盖这些测试。

5.2 反例二:命令散落在多处

做法:文档里写一遍测试命令,CI 配置里写一遍,部署脚本里再写一遍。

它为什么看起来能行:三处都能单独运行,各自看起来都对。

最后的代价是漂移。测试目录调整后,只改了其中一处,于是出现"本地通过、CI 失败"或者"CI 通过、部署后失败"的分歧,而分歧的排查成本通常比修这条命令高得多。修法是建立单一事实源:命令只在脚本或者项目配置里定义一次,文档与 CI 都引用它。判断标准很直接:全仓库搜索这条命令,如果出现次数大于一处,就存在漂移风险。

5.3 反例三:文档里写死实现细节

做法:文档写"必须使用UPDATE ... WHERE status='PENDING'完成状态变更",测试也按这条 SQL 断言。

它为什么看起来能行:越具体越像"有约束",评审时显得严谨。

最后的代价是规则绑死了实现。合理的技术方案(比如改成带乐观锁的更新)会因为"违反文档"而被拒;测试也因为绑定了具体写法而变得难以维护。改法是把行为作为规则(状态必须从 PENDING 变到 CANCELLED,且并发下只有一次成功),把实现自由度还给开发者。三层结构里,文档层和测试层都应该面向行为,而不是面向写法。

5.4 反例四:测试和 CI 都做了,文档却没写

做法:团队习惯是"改完就补测试、CI 自动跑",从来不写文档,因为"代码就是文档"。

它为什么看起来能行:对熟悉代码的人确实如此,测试名和断言往往能说明大部分意图。

最后的代价集中在两类人身上:新成员和代理。新成员能读懂断言,但读不出"为什么恰好一条",遇到边界情况时不知道是否可以放宽;代理在动手前需要的是动作清单,而测试文件是结果清单——它得先反推意图,再决定怎么写。文档层的成本其实很低(三行),缺失带来的沟通成本却会反复发生。

5.5 反例五:CI 里堆满检查,但没人知道失败原因

做法:CI 把仓库里所有命令都跑一遍,失败时输出一大段日志,不区分是哪条规则没通过。

它为什么看起来能行:覆盖越全越安全,失败日志越长信息越多。

最后的代价是失败不再提供方向。人看到长长一段红色输出,第一反应是重跑一次,而不是去修问题;几次之后,“CI 红了"变成常态,门禁的威慑力随之消失。修法是把检查分组、给每组一个清晰的名称(比如"订单测试”“静态检查”“契约校验”),失败时能直接定位到对应的规则条目。CI 的价值一半来自"阻止合入",另一半来自"明确告诉你是谁被拦下了"。

5.6 五种反例的共同点

五种做法的共同特征是:它们都让三层之间失去了对应关系。措辞不一致、命令散落、写死实现、缺一层、失败不可读,本质上是同一件事的不同表现——某一层的存在没有和另外两层"对上"。反过来说,判断三层是否健康,不需要复杂检查,只要问一句:从文档里的这条动作,能不能一路走到 CI 的一次失败?能走到,三层就是连着的;走不到,中间就有一层在自说自话。

这句话也可以当成设计顺序:先想清楚"失败时会怎样报出来",再决定文档怎么写。以终为始的写法会让三层天然对齐,因为它们共享同一个终点。

5.7 换个词看这件事:不是重复,是分工

如果把"三层都写"重命名为"三层分工",很多争论会自然消失。分工的含义是:同一件事,不同角色承担不同的部分。文档是写作角色,测试是验证角色,CI 是守门角色——三者说的是同一件事的不同侧面,而不是同一句话的三份副本。

用分工视角检查现状,会得到三个具体问题:文档有没有说清触发与动作?测试有没有覆盖这些动作的边界?CI 有没有真的在跑并阻止失败?三个问题都回答"有",分工是完整的;某个问题回答不上来,那一层就是缺失的。相比"我们是不是重复了"这种抽象疑问,这三个问题都指向具体动作,也更容易在评审里被回答。

分工视角还有一个附加收益:它让三层的责任人自然分开。文档由规则责任人维护,测试由开发者维护,CI 由团队或平台维护——不同的人有不同的关注点,改动时也容易判断"这次该改哪一层"。重复的焦虑往往来自"所有内容都堆给一个人",分工之后,焦虑变成清单上的三条待办。

六、落地步骤:给一条规则设计三层落点

第一步,选出这条规则,用一句话写清楚。比如"取消成功时审计事件恰好一条"。为什么强调一句话?因为一句写不出来的规则,通常混合了两三条约束,分层时会互相纠缠。怎么检查:这句话里有没有"和""并且"之类的连接词,有就拆开。

第二步,写出动作与验证的对应关系。动作几条,验证条目就有几条,一一对应。为什么在写文档之前先想验证?因为验证方式会暴露动作里含糊的地方——比如"保证一致性"会因为找不到验证方式而被改写成更具体的动作。怎么检查:每条动作后面能不能跟一个可以执行的检查。

第三步,先写测试。按动作逐条写断言,命名与动作保持一致。为什么先写测试?因为它能立刻验证动作描述得够不够具体;写不出测试的动作,说明它还需要再拆。怎么检查:每条测试都能对应到文档里的某一条动作。

第四步,把测试接入 CI。CI 里运行同一个脚本或者同一条命令,失败即阻止合并。为什么要阻止合并而不是只做提醒?因为提醒在忙的时候一律被跳过。怎么检查:故意提交一次会失败的改动,确认它无法合入。

第五步,回填文档。文档里写清触发、动作、以及"验证指向哪个测试文件",不复制命令细节。为什么用引用而不是复制?因为命令的单一事实源在脚本里,文档复制一份就会产生漂移。怎么检查:文档里的动作名称与测试名称是否一一对应。

第六步,做一次三层一致性检查。从文档抄出验证条目,去测试文件找断言,去 CI 配置确认它会被运行。为什么这三处都必须走一遍?因为"测试存在"和"测试会被跑"是两件事。怎么检查:三处结果放在一张表里,每一行都有结论。

第七步,定期复查。命令、目录、测试名都可能变化,三层里任何一层过期都会造成错觉。为什么定期而不是随时?因为随时检查无法执行,定期才有落点。怎么检查:找一个季度以上的规则,看它的验证指向是否还准确。

三层落点模板:

规则:<一句话> 文档层(AGENTS.md / 任务单): - 触发:<条件> - 动作:<1..n 条> - 验证指向:<测试文件与用例名> 测试层: - <用例 1>:<断言> - <用例 2>:<断言> CI 层: - 触发:<提交 / 合并请求更新> - 命令:<脚本名或命令> - 失败处理:阻止合并 + 保留输出

6.1 三条容易忽略的顺序纪律

在三层落地的过程中,有三条顺序纪律能让整个过程顺畅很多。

第一条:先复现,再上新规则。如果这条规则是为了解决某个已发生的问题,先把那个问题复现成一个失败的测试。这样做的收益是规则从第一天起就有证据——它的测试不是"为了覆盖而写",而是"因为真的出过问题"。另外,复现过程通常会修正你对规则的理解:你以为是并发问题,复现出来发现是事务边界问题。

第二条:先接门禁,再写说明。很多人习惯先把文档写完整,再去接 CI,结果文档和 CI 之间长期没有验证过。换个顺序:先让检查跑起来并且能拦住失败,再写文档描述它——此时你描述的是一件已经成立的事实,而不是一个计划。写出来的文档也因此不会有"描述了一个没落地的流程"这类问题。

第三条:先小范围,再全量。一条新规则先只在新代码上强制,观察一段时间再考虑覆盖历史代码。历史代码里往往存在大量"合理的例外",一次性全量强制会把例外也判成违规,进而逼着人们去关掉检查。小范围试点的另一个好处是给规则留出调整期:前两周发现措辞不合适、断言太严,改起来成本很低。

三条纪律的共同逻辑是:让每一步都有可验证的产物。复现给出失败证据,门禁给出强制点,试点给出适用范围——三者都是具体的东西,而不是"已经安排上了"这种状态描述。

顺带说一个和顺序有关的细节:给规则编号。三层材料里,同一条规则在文档、测试、CI 中出现时,带上同一个编号(比如 R-04)。编号让跨文件的对应关系一眼可见,也让沟通有共同语言——"R-04 现在卡在 CI"比"那条审计事件的规则"精确得多。编号不需要复杂体系,按加入顺序递增即可;退役的编号保留空缺,不重复使用,这样历史记录不会错位。

编号还有一个隐含好处:它让"三层是否齐全"变成一次可以快速完成的盘点——把编号列出来,逐个检查三处是否存在,缺哪层一目了然。规则条目少的项目,十分钟能盘完一遍;盘完之后,你会对"自己的规则体系到底长什么样"有一个具体印象,而不是模糊的"应该都写了吧"。

最后,如果你的项目目前只有一两层,也不要觉得落后。三层是一个目标状态,不是准入门槛;从"文档 + 现有测试"开始,让每条新规则至少落在两层上,等积累了几条之后再考虑接 CI,同样能收到大部分收益。关键是让每一层都真实存在,而不是在文档里描述一个还没有发生的流程。

用一句话收束这一篇:同一条规则出现在三层,是为了让它在三种情况下都不被绕过——想不起它的时候、想偷懒的时候、以及忙不过来的时候。

把这句话贴在文件的末尾,比任何口号都实用:它解释了为什么值得多写两处,也提醒你三层缺一不可。

七、常见问题

问:三层都写,规则改动时要改三处,维护成本是不是很高?

成本确实存在,但和它换来的一致性相比通常值得。降低成本的三个做法:第一,把命令做成单一事实源(一个脚本,文档和 CI 都引用它),这样最常见的变更(命令调整)只需要改一处;第二,文档只写动作和验证指向,不复制断言细节,断言变更时文档通常不用动;第三,把三层写在一份"规则条目"文件里(文档条目 + 测试名 + CI 任务名),改动时按条目走,不容易漏。三层里最常变的是实现细节(不动文档),最不常变的是动作(一旦确定能撑很久),所以实际维护量往往比想象中低。

问:CI 里应该跑整个测试套件,还是只跑相关的部分?

分阶段跑。提交阶段先跑受影响范围的测试(快,反馈及时),合入前再跑完整套件(慢,但覆盖全)。原因是反馈速度和覆盖率很难同时满足:只跑一切会让人等太久,只跑一部分又可能漏掉跨模块影响。具体怎么划范围取决于你的项目,但有一个通用建议:把"必须通过才能合入"的检查写得少而硬(比如完整测试 + 静态检查),把"建议看的情况"做成提示(比如覆盖率变化),避免门禁里混入不确定的检查——一旦门禁出现不稳定判断,它很快就会失去权威。

问:文档里写测试文件名,测试改名之后文档就过期了,怎么办?

这是引用式写法必须接受的代价,但可以做两件事减轻。第一,改名时把文档更新列入同一个改动(测试改名的提交顺带更新引用),并在评审清单里加一条"引用是否仍然有效"。第二,给一致性检查留一个自动化的可能:写一个小脚本扫描文档中的测试文件引用,检查文件是否存在——这类检查成本极低,却能挡住大部分过期引用。如果连这个也不想做,那就退一步:文档里只写"由订单测试覆盖",不写具体文件名;代价是准确度下降,收益是永远不会指错。

问:我们团队很小,只有两三个人,需要三层吗?

需要,但可以简化。小团队的分工通常是"文档 + CI"两层为主,测试层依赖已有的用例(不专门为这条规则补测试)。判断是否需要补齐测试层的标准是:这条规则被违反过吗?如果被违反过哪怕一次,就值得一条专门断言;如果从来没被违反,可以先靠 CI 跑现有测试兜住,等出现问题时再补。CI 在小团队里更重要,因为没有人专门做评审,门禁就是最稳定的"第三个评审人"。

问:能不能只用测试,不写文档?

能,但要接受两个代价。第一,执行者(新人、代理)需要从测试里反推意图,这个过程会出错——测试覆盖不到的边界,反推也反推不出来。第二,测试本身会演化:断言可能因为夹具调整而变化,此时"代码即文档"会把噪声当成约定。实践里的折中是:文档写三行(触发、动作、验证指向),剩下的理解交给测试。三行的成本很低,却能把"为什么"固定下来。当这条规则只对一个人有效时,不写文档也可以;当它需要被第二个人执行时,就该补上。

问:CI 慢,团队总是抱怨,怎么平衡?

把 CI 分成两条路径。快速路径:只跑与改动相关的测试和静态检查,用于每次提交,几分钟内出结果。完整路径:跑全套测试与契约校验,在合并前触发,允许更长的时间。划分的关键是把"反馈速度"当成设计目标之一:如果一次提交要等半小时才有结果,人会开始一次提交多个改动、或者绕过检查,门禁就名存实亡。另外,把所有检查都塞进一条流水线的做法通常会同时牺牲速度和可读性——失败时也看不出是哪一层的问题。

问:三个地方都写了规则,评审时应该看哪个?

按顺序看:先看文档层(这次要做什么、边界在哪),再看测试层(证据是否覆盖这些边界),最后看 CI 层(门禁是否真的会拦住)。这个顺序的原因和三层的关系一致:先对齐意图,再检查证据,最后确认强制。只抓 CI 结果会让评审变成"绿灯放行";只读文档会让评审变成"看描述对不对";先文档后证据的顺序,能让评审在一次对话里完成"意图对齐 + 证据核对"。

问:历史遗留项目里三层都不完整,从哪里开始补?

从最近发生过问题的那条规则开始,而不是从"最重要的那条"开始。理由是:出过问题的规则有现成的场景和证据,改造它时你不需要说服任何人;而"最重要的那条"往往争论最多,容易停在讨论阶段。补齐的顺序建议是先测试(有可运行的断言),再接 CI(让它自动跑),最后回填文档(把触发和动作写清楚)。顺序反过来的风险是:先写文档,写完之后大家以为已经完成了,测试和 CI 反而被拖着不做。

问:文档层和任务单的关系是什么?

两者都是文档层,但作用范围不同。AGENTS.md 是仓库级的长期约定,任何任务都会读到;任务单是这次任务的约定,只在本次任务里有效。分界线的用法和前面几篇一致:有效期超过一个季度的写进 AGENTS.md,只对本次有效的写进任务单。把两者混为一谈会带来两类麻烦:一次性要求长期化(上次为了不改前端写进 AGENTS.md,这次要改前端时被卡住),或者长期约定临时化(每次任务都要重新交代一遍测试命令)。分清楚之后,两处都变短了。

问:CI 失败了但是规则本身有问题,怎么区分是"实现错了"还是"规则错了"?

看失败信息的内容。如果断言失败指向一个明确的、违反约定的行为,多半是实现错了;如果断言失败指向"这条约定本身太严",比如要求恰好一条事件但业务上确实允许补发,那是规则需要调整。区分的动作是先把失败复现出来,然后对着文档问一句:"这条动作在当前业务下还成立吗?"答案是不成立,就改规则(同时改文档、测试、CI 三处的表述);成立,就修实现。最怕的处理方式是直接放宽断言让 CI 变绿——那会让三层同时失真。

问:怎么让新成员理解这三层的关系?

用一条真实规则做例子讲一遍最快。步骤是:带他看文档里的这条规则,指出"动作"和"验证指向";打开对应的测试文件,看断言和命名怎么对应;打开 CI 配置,指出哪一步会运行它;最后故意改坏一处(本地实验),让他看红灯出现的位置和内容。整个过程十分钟,但能把三层关系讲清楚——比抽象地讲"我们有文档、测试和流水线"有效得多。讲完之后给他一个任务:为另一条规则做同样的三层检查。

问:如果 CI 暂时不可用(故障),流程怎么走?

把门禁降级成"人工替代 + 记录"。具体做法:本地运行同一条命令并把输出贴进 PR;评审人核对命令与输出;合并记录里注明"CI 不可用,已完成人工验证"。这个降级方案的关键是"同一条命令"——它保证恢复之后不会有行为差异。同时保留一条纪律:CI 恢复后,把降级期间合并的提交补跑一遍。补跑不是为了追责,而是因为人工验证的覆盖率通常低于自动检查,补跑能发现遗漏。把降级方案写进团队文档,比临时决定要可靠。

问:一条规则只在文档里、还没有测试,怎么标记才不会造成错觉?

给它加一个状态字段,写清它现在落在哪一层。例如"状态:仅文档(待补测试)“。这样读者一眼能看出这条规则的强度,不会把"写下来了"当成"已经被检查”。

状态字段还有一个用途:它让补齐工作变成可见的待办。每季度体检时,把状态仍是"仅文档"的条目列出来,按违反代价排序决定先补哪一条,比凭感觉挑要靠谱。

问:测试层该写单元测试还是集成测试?

看这条规则对应的行为边界。只涉及对象内部判断的(例如"只有 PENDING 可以取消"),单元测试足够,跑得快、失败原因单一;涉及事务、数据库或外部调用的(例如"失败时状态与事件都不留痕"),必须用集成测试,因为这类行为只在真实的事务和存储里才成立。

两者都写不算重复,它们覆盖的是不同的失败模式:单元测试挡住逻辑写错,集成测试挡住提交时机和边界写错。省掉后者的典型后果是逻辑全对、事务拆错,测试却始终是绿的。

问:CI 全绿,评审还是放行了一个有问题的改动,三层结构能挡住吗?

挡不住,也不该指望它。三层防的是"已知规则被违反",防不住"规则没有覆盖到的情况"。三次结构能保证的是:凡是写在规则里的约束,不依赖人的记性;没写在规则里的事情,仍然需要人的判断。

遇到这类问题时,正确的收尾动作是复盘并把结论转成新规则:如果这个错误以后还会出现,就写进文档、补一条断言、接进同一条流水线。这样一次遗漏会变成一条长期有效的检查,而不是只换来一句"下次注意"。

问:三层里哪一层最值得先建?

如果只能先建一层,选测试层。理由是它的产物最硬:一条能跑的断言既是可以复现的证据,也是将来接 CI 的现成材料,还能反过来校正文档里含糊的动作。

如果只能先建两层,加 CI。文档层可以稍后补,因为它的成本最低(三条信息),而缺了 CI 的测试会退回到"靠人记得跑"。最不推荐的顺序是先写文档、把测试和 CI 一起推后——那时的进度看起来最快,风险却最高。

八、动手练习与小结

练习:给一条规则找到三层落点

选一条你项目中"大家都同意、但偶尔会被违反"的规则,按下面四步处理,产出一份三层落点记录。

第一步,把它写成一句话。如果写不出一句话,先拆成两条。写完之后自检:这句话里的每个词,执行者能不能理解成同一个意思——有歧义的词(一致性、合理、适当)换成具体动作。

第二步,写测试。按动作逐条写断言,命名与动作对齐。如果发现某条动作写不出断言,把这条动作再拆细,或者把它降级成建议(不写进规则)。这一步的产出是可运行的测试文件或测试函数名。

第三步,接 CI。把第二步的测试接入流水线,并确认失败会阻止合并。如果还没有 CI,至少把命令写进提交前检查清单和任务交付要求,并把这个"临时方案"记在文档里,等有 CI 时替换。

第四步,回填文档并做一致性检查。文档写触发、动作、验证指向;然后从文档出发,逐一验证"测试里有断言"“CI 会运行它”。三处一一对上,记录完成。

做完的产出是一份包含三层的规则记录,以及一次一致性检查的结果。下次再有类似规则,把这份记录当模板。

小结

同一条规则出现在文档、测试和 CI 三层,不是重复劳动,而是三种不同功能的组合:文档给方向,测试给证据,CI 给门禁。三层回答的问题不同——应该做什么、做到了没有、能不能合入——所以任何一层都不能替代另一层。少一层的典型后果是:缺文档,规则没人知道;缺测试,规则没有证据;缺 CI,规则依赖记性。

让三层不变成负担的关键是减少"同一信息的多处副本":命令只在一处定义(脚本),文档引用测试名而不是复制断言,CI 调用同一个命令。三层真正需要保持一致的是"意思"——动作名称、断言、失败信息用同一批词;不一致的措辞会在出事时变成争论。

三个可以直接带走的判断标准:从文档里的动作能不能一路走到 CI 的一次失败;全仓库搜索验证命令,出现次数是否只有一处;失败时能不能一眼看出是哪条规则被违反。三项都满足,这条规则的三层就是健康的。

和前后篇的关系:上一篇讲怎么把坏规则改成可执行的条目,这一篇讲这些条目应该在哪些层落地、怎么配合。下一篇处理规则的另一半——知识:除了"怎么做",还有"为什么这样做",这部分内容适合跟代码一起评审,而不是留在聊天记录里。

补充:一份可以直接套用的三层检查表

文档层 [ ] 触发明确(什么时候适用) [ ] 动作具体(动词开头,1..n 条) [ ] 验证指向具体(测试文件或用例名) 测试层 [ ] 每条动作都有对应断言 [ ] 断言覆盖失败路径(回滚、重复、并发) [ ] 命名与动作一致,失败信息可读 CI 层 [ ] 运行的是同一条命令(单一事实源) [ ] 失败会阻止合并并保留输出 [ ] 触发时机覆盖提交与合并请求更新 一致性 [ ] 从文档动作可以一路走到 CI 的一次失败 [ ] 全仓库搜索命令,只有一处定义

这张表可以在评审一条新规则时使用,也可以作为季度复查的底稿:把仓库里最重要的三到五条规则拿出来,逐条过一遍,缺口会非常具体地显示出来。复查结束后,把"缺哪一层"记进任务记录——下一次改动时,这些缺口就是优先要补的部分。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询