第3课 Image: AI generated

速成技巧 — 知道这些就能指挥AI

四句话就够了。

对Agent说:“创建Hurl测试” AI编写验证功能是否正常工作的契约。是不懂代码也能读的纯文本。

对Agent说:“添加这个功能。但是现有Hurl测试必须通过” 这一句话就能防止漂移。如果AI在添加新功能时破坏了现有功能,Hurl会用红色文字告诉你。

对Agent说:“提交” 保存当前正常工作的状态。就像游戏的存档点。下一个任务搞砸了就回到这里。

对Agent说:“回退” 回到最后的存档点。撤销AI搞坏的东西。

这四句话的模式

功能完成 → “创建Hurl测试” → 确认通过 → “提交” → 下一个功能 → “现有Hurl必须通过” → 出问题就"回退"

这就是棘轮。只向前转、不会倒退的齿轮。不管有5个还是50个功能,现有的都不会坏。

为什么这样有效?

第2课学过了。给AI意见就谄媚,给事实就修正。Hurl返回的不是意见——是事实。“test failed: status 401, expected 200”——这没有什么可谄媚的。

动手体验

给第2课实验中的待办事项应用做一个Hurl测试。3分钟就够。

对Agent说:“写一个Hurl测试,验证当前的任务添加功能是否正常工作”

AI会创建一个.hurl文件。

对Agent说:“运行Hurl测试”

通过就是绿色。现在故意搞坏它。

对Agent说:“把任务添加API响应中的id改成todo_id”

对Agent说:“运行Hurl测试”

红色文字显示失败。这就是漂移检测。

对Agent说:“回退”

又变绿了。这就是棘轮的核心。


为什么必须这样指挥

第2课看到了问题。逻辑漂移、上下文消失、谄媚偏差。超过5个功能后现有的就坏了,AI还虚假宣告"运行良好"。

这节课学习防止这些问题的三个工具。全是软件工程师用了几十年的东西。不需要会读代码。AI编写,AI运行。你只需确认"通过了吗?"

三个工具的角色:

工具比喻做什么
Hurl契约声明"这个功能必须这样运行"
Git存档点保证"可以回到这个时间点"
CI/CD自动监控摄像头机械化"每次自动确认"

Hurl — 用纯文本声明API契约

Hurl是什么

Hurl是一个描述"这个API应该怎么运行"的文件。

用游戏来比喻:在RPG中从NPC那里买药水时,有个规则"1瓶药水 → 金币-50,生命值+100"。检查这个规则在补丁后是否改变——这就是Hurl做的事。

来看一个实际的Hurl文件:

# 添加任务
POST http://localhost:8080/api/todos
{
  "title": "买牛奶",
  "priority": "high"
}
HTTP 201
[Asserts]
jsonpath "$.id" exists
jsonpath "$.title" == "买牛奶"
jsonpath "$.priority" == "high"
jsonpath "$.completed" == false

不懂代码的人也能读:

  • POST — 向服务器发送"添加这个"的请求
  • http://localhost:8080/api/todos — 待办事项列表的地址
  • { “title”: “买牛奶” } — 发送这样的数据
  • HTTP 201 — 成功的话必须返回201号响应
  • jsonpath “$.title” == “买牛奶” — 返回的数据中必须有"买牛奶"

这就是契约。“添加任务时应该返回201,标题和优先级应该原样返回。“如果这个契约被打破,Hurl会用红色文字告诉你。

再看一个:

# 未认证访问必须被拒绝
GET http://localhost:8080/api/todos
HTTP 401

“不登录就访问待办事项列表应该返回401(需要认证)。“这也是契约。如果AI在"整理"认证代码时打破了这个规则,Hurl会立即捕获。

为什么是Hurl——与单元测试的区别

“测试工具那么多,为什么是Hurl?“对氛围编程者来说有特别的理由。

单元测试检查代码内部的函数。用汽车来比喻,单元测试是拆开发动机零件逐一检查,Hurl是在实际道路上的行驶测试。如果函数名变了,测试也会坏;AI重构时还要一起修改测试。**如果给AI同时修改代码和测试的权限,AI会把测试调整成与代码匹配。**测试通过了,但原来的规则已经消失了。

Hurl不同。它在服务器的入口进行检查。发送请求,检查响应。它不知道代码的内部结构。不管AI怎么改代码,从外部观察到的行为相同就通过,不同就失败。

单元测试Hurl
汽车比喻发动机零件拆解检查道路行驶测试
AI改了代码时测试也可能跟着变测试不变,只判定结果
阅读难度需要懂代码像普通文字一样读
漂移检测AI改了测试就漏掉独立于代码,自然检测到

**Hurl验证的不是代码而是行为。**AI可以自由改代码。行为不能变。这个区分是捕获漂移的关键。

为什么这种方法有效——研究证明

第2课学了谄媚偏差。“写测试"的建议怎么给,结果也完全不同。

TDAD(Test-Driven AI Development)研究(2026)精确测试了这一点。让AI修复bug时改变测试条件:

条件回归率(现有功能损坏比例)
基准(无测试指示)6.08%
“做TDD"过程式指示9.94%(恶化!)
提供受影响的测试文件作为上下文1.82%(减少70%)

惊人的结果。**“做TDD"反而更差。**AI试图遵循过程式指示时反而偏离了实际任务。但提供"这个测试文件必须通过"的具体上下文,回归减少70%。

差异很明确:

  • “边写测试边开发” → 过程式指示 → AI混乱
  • “这个Hurl文件必须通过” → 具体契约 → 回归减少70%

不是指示方法,而是给出什么必须通过的契约。上面的"指令3"正是这个意思。

Git — 可以回退的存档点

Git是什么

玩游戏时会存档。在Boss战前存档,死了就读档。

Git是代码的存档功能。“当前状态正常” → 存档(提交)。下一个任务搞砸了 → 回到之前的存档。

没有Git的氛围编程:

功能1添加 → 正常
功能2添加 → 正常
功能3添加 → 功能1坏了!
→ 想回退...功能2的状态是什么来着?
→ 对AI说"回到之前" → AI不知道"之前"是什么
→ 从头开始

有Git:

功能1添加 → 正常 → 提交(存档1)
功能2添加 → 正常 → 提交(存档2)
功能3添加 → 功能1坏了!
→ "回到存档2" → 恢复到功能1、2正常的状态
→ 用其他方法重新尝试功能3

Git用法:两个词就够

不需要学Git的几十个命令。氛围编程者需要的只有两个。

“提交” — 保存当前状态

"提交当前状态。消息用'任务添加功能完成'"

AI执行:

git add .
git commit -m "任务添加功能完成"

“回退” — 恢复之前的状态

"回退到最后一次提交"

AI执行:

git checkout .

或者回到更早:

"回退到'任务添加功能完成'提交"

什么时候提交

规则很简单:

  1. 功能完成并正常工作时 → 提交
  2. 所有Hurl测试通过时 → 提交
  3. 开始下一个功能之前 → 务必提交

不提交就继续的话,出了问题就没有可以回退的地方。就像玩了3小时游戏不存档一样。

好的模式:
功能完成 → Hurl通过 → 提交 → 下一个功能

坏的模式:
功能1 → 功能2 → 功能3 → ... → 什么东西坏了 → 没地方回退

Git的比喻:珠穆朗玛峰的营地

登珠穆朗玛峰不会一次登顶。大本营 → 营地1 → 营地2 → … → 顶峰。在每个营地搭帐篷、储备物资。天气变坏就退到前一个营地。没有营地的话,暴风雪来了就死了。

Git提交就是营地。每完成一个功能就建一个营地。即使AI在下一个功能中搞坏了,也可以退回到前一个营地。

CI/CD — 机器自动守护

CI/CD是什么

CI(持续集成)是"每次上传代码时自动运行测试”。 CD(持续部署)是"测试通过后自动部署”。

现在只需知道CI。CD以后再说。

没有CI:

你:"添加功能"
AI:"完成了!"
你:(在屏幕上只确认新功能)"看起来不错!"
→ 没发现现有功能坏了就继续了

有CI:

你:"添加功能"
AI:(写代码)
机器:(自动运行所有Hurl测试)
机器:"现有登录测试失败!"
你:"登录坏了。修好它。"
AI:(修复)
机器:"所有测试通过"
你:"提交"

不需要每次手动运行Hurl测试。机器每次自动运行。

用GitHub Actions创建CI

代码上传到GitHub后,GitHub Actions自动运行测试。只需一个配置文件。

让AI来做:

"用GitHub Actions设置CI。
 - 每次push时自动运行Hurl测试
 - 需要先启动服务器再运行Hurl测试
 - 测试失败就阻止合并
   (PR是'这个代码可以合并吗?'的请求,合并是实际的合并操作)"

AI创建.github/workflows/ci.yml文件。不需要精确理解内容。AI来创建,你只需知道关键点:

  • 每次代码push时自动执行
  • 启动服务器并运行Hurl测试
  • 只要有一个失败就亮红灯

大致是这样的内容:

name: CI                          # 这个自动化的名字
on: [push, pull_request]          # 每次上传代码时执行

jobs:
  test:
    runs-on: ubuntu-latest        # 在云服务器上执行
    steps:
      - uses: actions/checkout@v4 # 获取代码

      - name: 启动服务器           # 启动应用服务器
        run: |
          docker compose up -d
          sleep 5

      - name: 运行Hurl测试        # 运行所有测试
        run: |
          hurl --test tests/*.hurl

      - name: 停止服务器           # 停止服务器
        run: docker compose down

CI的比喻:建筑的火灾报警器

建筑有火灾报警器。起火时自动响。不需要保安24小时巡逻。

CI是代码的火灾报警器。Hurl测试坏了自动通知。不需要你每次手动确认。

差异:

手动确认CI
确认时机想起来的时候每次自动
确认范围只有新功能全部
遗漏确认经常没有
成本时间和精力免费(GitHub Actions免费计划)

三个工具合在一起:棘轮锁定

Hurl + Git + CI合在一起就成了棘轮(ratchet)。棘轮是只能向一个方向转动的齿轮。转动就前进,松手就停住但不会倒退。

功能1完成 → 写Hurl测试 → 全部通过 → 提交 → 锁定
功能2完成 → 添加Hurl测试 → 现有+新测试全部通过 → 提交 → 锁定
功能3工作 → 现有Hurl测试失败 → 提交被拒绝 → 修复 → 全部通过 → 提交 → 锁定

规则很简单:

  1. Hurl测试通过就锁定
  2. 被锁定的测试不能删除/修改
  3. 添加新功能时也添加新Hurl测试
  4. 所有现有测试+所有新测试都通过才能提交

当你让AI"重构代码"时,AI自由地修改代码。但如果Hurl测试坏了,提交就被拒绝。AI必须在保持所有现有行为的同时工作。

这与上面的TDAD研究结果完全一致。不是"写测试"的过程式指示,而是"这个Hurl文件必须通过"的具体契约。Agent可以选择方法,但不能违反契约。

第2课的问题如何解决

第2课的问题第3课的解决
逻辑漂移Hurl保护现有行为。AI改了代码,行为变了就失败
上下文消失Hurl文件永久保存决定。会话换了契约还在
谄媚偏差(“都完成了”)CI机械判定。不是AI的自我报告而是pass/fail
决定-实现混杂Hurl在独立于代码的文件中声明决定(行为)
乘法衰减每一步用棘轮锁定,衰减就重置

再看一次第2课的关键实验结果:

自主Agent:  40 / 527  (7.6%)  — Agent宣告"完成"
棘轮CLI:   527 / 527 (100%)  — 机器宣告"还剩487个"

同一个模型。差异在于谁来决定"完成”。AI决定就停在40,机器决定就到527。Hurl + CI正是扮演那个"机器"的角色。

已有应用的事后适用

**如果你还没有做过应用,可以跳过这一节。**需要时再回来。

“我已经用氛围编程做了应用并在运营了,需要从头开始吗?”

不需要。不需要从头开始。**不是打地基而是抗震加固。**不关门营业的店铺,加固建筑。

第1步:用Hurl捕获当前行为

用Hurl记录应用当前如何运行。有API文档就直接转换,没有就让AI做。

提示:如果没有API文档(OpenAPI规范),可以试试codistill。codistill静态分析现有的Go+Gin、NestJS、FastAPI源代码,自动提取OpenAPI规范和DDL。“把代码放进去,规范就蒸馏出来了。“有了提取的OpenAPI,写Hurl测试就容易多了。codistill在第4课会再次讨论。

"分析当前应用的所有API端点,编写Hurl测试。
 必须原样捕获当前运行方式为测试。"

目标:用纯文本声明"这就是当前的运行方式”。

不需要一次全做。从最重要的开始,一个一个来:

  • 登录/注册——这个坏了什么都用不了
  • 支付——涉及钱的最优先
  • 核心业务逻辑——你的应用做的主要事情
优先级:
1. 登录API → 写Hurl测试 → 确认通过
2. 支付API → 写Hurl测试 → 确认通过
3. 核心CRUD → 写Hurl测试 → 确认通过
...(有时间再做剩下的)

第2步:用Git保存当前状态

如果还没用Git:

"把这个项目初始化为Git仓库,提交当前状态。
 消息用'保存现有应用状态'"

如果已经在用Git,在所有Hurl测试通过的状态下提交。

第3步:加上CI

如果代码在GitHub上:

"用GitHub Actions设置CI。每次push时自动运行Hurl测试。"

第4步:从现在开始安全了

从这里开始,不管你让AI做什么,Hurl都保护现有行为:

"添加这个功能。但是所有现有Hurl测试必须通过。"

漂移发生时CI立即捕获。在到达生产环境之前。

反馈的力量:意见 vs 事实

记住第2课的谄媚偏差。给AI意见就谄媚,给事实就修正。

Hurl给AI的不是意见——是事实:

意见:"登录功能好像有点问题"
→ AI:"我确认了,运行正常!"(谄媚)

事实:"test failed: status 401 ≠ expected 200"
→ AI:(在行级精确修复)

问"确定吗?“AI就会推翻正确的答案。但"line 41: expected user_id, got userId"没有什么可谄媚的。数字和位置不是情感。

这就是Hurl、Git、CI这样的确定性工具(对相同输入始终产生相同结果的工具)能工作的根本原因。这些工具不谄媚。pass就是pass,fail就是fail。

常见问题

Q:怎么知道Hurl文件是正确的?AI可能写错了。

初次编写Hurl文件后运行通过,说明那个时间点的行为被捕获了。之后代码变了Hurl失败,那是行为变了的信号。不是Hurl本身错了,而是在检测行为是否变化。

初次编写与预期不符时:运行后用眼睛确认结果。“任务添加应该返回201但返回了200”——这个你自己能判断。

Q:Hurl测试太多了管理不过来怎么办?

从3-5个开始就够了。登录、核心功能、最重要的业务规则。需要时再一个一个添加。不需要一次做到完美。

端点变多时huma来帮忙。huma读取OpenAPI规范,找到没有Hurl测试的端点,用棘轮循环一个一个填满。机械地强制"有42个端点就必须有42个测试”。huma在第6课会再次讨论。

Q:需要背Git命令吗?

不需要。“提交"和"回退"两个词就够了。AI执行适当的Git命令。

Q:GitHub Actions收费吗?

公开仓库免费。私有仓库每月提供2,000分钟免费(Free计划)。对小项目足够了。

Q:已有应用的话,使用Hurl会改变当前代码吗?

不会。Hurl不碰代码。只是在代码旁边添加.hurl文件而已。只捕获当前行为,不修改代码。

实验:构建Hurl + Git + CI管道

使用第2课实验中的待办事项应用。

前提准备:安装Hurl

让Claude Code来做:

"安装Hurl"

或者手动安装:

# Ubuntu/WSL
curl --proto '=https' --tlsv1.2 -sSf https://hurl.dev/install.sh | bash

确认安装:

hurl --version

显示版本号就是成功了。

步骤1:用Hurl捕获当前状态(15分钟)

对AI说:

"为当前待办事项应用的API编写Hurl测试。
 至少包含以下场景:

 1. 添加任务 → 201响应,标题原样返回
 2. 查询任务列表 → 200响应,返回数组
 3. 完成任务 → 200响应,completed变为true
 4. 删除任务 → 200或204响应
 5. 未认证访问 → 401响应(如果有认证的话)"

运行测试:

"运行Hurl测试"

确认全部通过。有失败的就让AI修复。

步骤2:Git提交(5分钟)

"Git提交当前状态。消息用'添加Hurl测试——保护基本CRUD'"

这是第一个存档点。

步骤3:添加功能+棘轮确认(20分钟)

还记得第2课实验中坏掉的功能吗?这次用Hurl保护着添加。

"给任务添加优先级(高/中/低)功能。
 但是现有Hurl测试必须全部通过。
 新功能也要添加Hurl测试。"

确认要点:

  • 现有Hurl测试全部通过了吗?
  • 新Hurl测试也通过了吗?
  • 第2课中坏掉的这次被保护了吗?

通过就提交:

"提交。消息用'添加优先级功能+Hurl测试'"

再添加一个:

"添加截止日期功能。
 现有Hurl测试全部通过+新功能Hurl测试添加。"

通过就提交。这就是棘轮。只前进。不后退。

步骤4:GitHub Actions CI设置(10分钟,选做)

**没有GitHub账号就跳过这一步。**仅步骤1-3就能体验棘轮的核心。GitHub以后再创建也行。

如果有GitHub仓库:

"用GitHub Actions设置CI。
 - 每次push时自动启动服务器并运行Hurl测试
 - 测试失败就阻止代码合并"

Push到GitHub,在Actions标签页确认测试自动运行。

步骤5:故意制造漂移实验(10分钟)

确认CI工作后,故意搞坏:

"修改任务添加API的响应格式。
 把任务编号的名字从id改成todo_id。"

确认Hurl测试失败。确认CI亮红灯。这就是漂移检测。

"回退。恢复原样。"

确认绿灯回来。

需要记录的:

  • 第2课实验 vs 第3课实验:添加同样的功能时,现有功能坏了吗?
  • Hurl捕获了几次漂移?
  • AI的"完成了"和Hurl的判定不同的情况有吗?

总结

这节课学到的:

  1. Hurl — 用纯文本声明"应该这样运行"的契约。验证的是行为不是代码
  2. Git — 创建保证"可以回到这个时间点"的存档点
  3. CI/CD — 安装"每次自动确认"的机械验证
  4. 棘轮 — 三者结合就成了"通过就锁定,不会倒退"的齿轮

核心原理:

不要指示AI方法。给出什么必须通过的契约。

“做TDD” → 回归恶化。“这个Hurl必须通过” → 回归减少70%。差异在于指示和契约。

不要换模型,添加契约。


下一课预告

第3课学了用Hurl逐个保护API。但项目变大后需要保护的不只是API。数据库结构、安全策略、UI组件,这些都必须互相一致。

第4课学习yongol。用一个声明式规范管理API、DB、安全、UI,把AI的工作对象从代码转移到规范。突破氛围编程在200个端点时崩溃的墙的方法。


相关文章

  • Hurl防止氛围编程漂移 — 详细分析使用Hurl进行API契约验证如何防止氛围编程的漂移
  • Ratchet Pattern — AI被要求测试527个函数时为什么停在40个,以及用机械验证器让它跑到底的模式

Reins Engineering 全部课程

标题
第0课安装Claude Code
第1课如何指挥AI
第2课如何不信任AI
第3课不会崩坏的应用
第4课把决定移出代码
第5课有缰绳的AI
第6课通过就锁定
第7课翻转谄媚的方法
第8课Agent的工厂
第9课代码之外的自动化
第10课数据的法
第11课拯救失败的氛围编程

依据资料

  • TDAD (Test-Driven AI Development) 2026 — “做TDD"的过程式指示使回归率恶化至9.94%,提供测试文件作为上下文使回归率降至1.82%(减少70%)
  • Ratchet Pattern实验 — 自主Agent 40/527 (7.6%) vs 棘轮CLI 527/527 (100%),同一模型上完成判定主体的差异

参考文献

  • Meyer, B. (1992). “Applying ‘Design by Contract’.” Computer 25(10), 40-51. link
  • Golmohammadi, A., Zhang, M. & Arcuri, A. (2024). “Testing RESTful APIs: A Survey.” ACM TOSEM 33(1), 27:1-27:41. link
  • Lightman, H., Kosaraju, V., Burda, Y. et al. (2024). “Let’s Verify Step by Step.” ICLR 2024. link
  • Tyen, G., Mansoor, H., Carbune, V. et al. (2024). “LLMs cannot find reasoning errors, but can correct them given the error location.” Findings of ACL 2024. link

变更历史

  • 2026-05-24: 初版