- 原文: Partner with the AI, throw away the code
- 作者:Matteo Vaccari
本文是“探索生成式 AI”系列的一部分。该系列记录了 Thoughtworks 技术专家在使用生成式 AI 技术进行软件开发方面的探索。
摘要: 一次关于 AI 如何帮助完成一个非凡编程任务的个人经历。
一个艰巨的任务
这个月我花了一整个星期来解决一个非常困难的算法问题,复杂的业务规则让它变得更加困难,而我没能说服客户同意简化这些规则。
问题在于一个 API 端点太慢,原因是一些复杂的 SQL 查询在某些数据集上可能需要几分钟才能完成。这个函数非常复杂,我甚至没有尝试去理解其中的细节;我打赌可以通过从事务脚本(Transaction Script)转向领域模型(Domain Model)来解决(我发现 PoEAA 书中的模式对于描述我们在企业应用程序中看到的情况非常有用)。
技术栈是 Go 和 Mysql;我从客户那里获得了 Cursor 的许可证,并用它和 Claude Sonnet 4 一起工作。
逆向工程
事务脚本的常见问题是它们是查询和胶水代码的组合,业务规则分散在查询和胶水代码之间,并且没有明确说明。
通过与客户的交谈,我对需求有了一个模糊的概念。我也可以查看大量的测试用例,但它们并不容易理解。
我的第一个问题是理解端点所实现的确切业务规则,但我不相信自己有能力理解复杂的代码。好吧,这只是故事的一部分:老实说,我很懒,我做的第一件事就是向 AI 求助。
Read <function name> in file @<file> and write good documentation about what it does.
You may use `mysql -u... -p... -h127.0.0.1 ...` to inspect the DB schema.
You may refer to @doc.go for information about the tables involved读取文件
@<file>中的<function name>函数,并编写关于其功能的良好文档。你可以使用mysql -u... -p... -h127.0.0.1 ...来检查数据库模式。你可以参考@doc.go来获取有关所涉及表的信息。
让 AI 访问 mysql 使其能够探索模式并尝试查询。结果是对业务规则的初步理解。并不完美!文档不够精确,无法为重新实现生成验收标准。
回想起来,我本可以要求它生成验收标准,甚至是测试用例;也许它会成功。
要点:让 AI 解释代码。
基准测试
我的下一个任务是确保我有一种方法来衡量性能改进。我要求 Cursor 使用 Go 原生的基准测试工具生成一个基准测试。Go 的一个好处是大多数测试都是以表格形式编写的,所以我有一个基准测试,可以打印出在不同输入下被测试操作所花费的时间。Go 的基准测试输出很难读,因为它以纳秒为单位报告时间。但是,如果你忽略最右边的九位数,你可以看到简单案例分别花费了 21 秒和 11 秒,而问题案例则花费了七分多钟。所以现在我有了坚实的改进基线。
> go test -bench=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ./xxx -run=^$ -cpu 1
goos: darwin
goarch: arm64
pkg: gitlab.com/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
cpu: Apple M1 Pro
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/simple1 1 21047066667 ns/op 7180080 B/op 172723 allocs/op
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/simple2 1 11310282792 ns/op 3178208 B/op 86252 allocs/op
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/pathologic 1 472596979959 ns/op 74413528 B/op 2224386 allocs/op
PASS默认情况下,当你要求 Cursor 生成 Go 基准测试时,它会使用 2025 年 2 月发布的 Go 1.24 之前的旧样式。新样式在其他改进中更具可读性,所以我不得不要求 Cursor 将基准测试移至新样式。
要点:使用 AI 生成(对我来说)不熟悉的实用程序
测试覆盖率
然后,我为有问题的函数的重新实现创建了一个stub,并复制了所有现有测试,以便最终确保新版本仍然可以与旧版本一起工作。当然,此时,测试都失败了,因为该函数只是一个stub。在此过程中,我发现旧测试插入的测试数据很难阅读。你看,领域模型涉及一个组织分支树,它模拟了企业的结构。测试通过多次插入在数据库中创建测试树,并且不可读。困难在于树的层次结构不容易看清,这使得测试更难理解。
orgChartTreeInsert := "insert into ..."
testdb.MustExec(t, conn, orgChartTreeInsert, 1, "path", 200, 300)
testdb.MustExec(t, conn, orgChartTreeInsert, 2, "path", 201, 301)
testdb.MustExec(t, conn, orgChartTreeInsert, 3, "path", 202, 302)
testdb.MustExec(t, conn, orgChartTreeInsert, 4, "path", 203, 303)
testdb.MustExec(t, conn, orgChartTreeInsert, 5, "path", 204, 304)
testdb.MustExec(t, conn, orgChartTreeInsert, 6, "path", 205, 305)
groupInsertQuery := "insert into ..."
testdb.MustExec(t, conn, groupInsertQuery, 200)
testdb.MustExec(t, conn, groupInsertQuery, 300)
testdb.MustExec(t, conn, groupInsertQuery, 201)
testdb.MustExec(t, conn, groupInsertQuery, 301)
testdb.MustExec(t, conn, groupInsertQuery, 202)
testdb.MustExec(t, conn, groupInsertQuery, 302)
testdb.MustExec(t, conn, groupInsertQuery, 203)
testdb.MustExec(t, conn, groupInsertQuery, 303)
testdb.MustExec(t, conn, groupInsertQuery, 204)
testdb.MustExec(t, conn, groupInsertQuery, 304)
testdb.MustExec(t, conn, groupInsertQuery, 205)
testdb.MustExec(t, conn, groupInsertQuery, 305)我要求 AI 使用 TDD 创建一个树构建器,经过几次调整,它能够将上面的 SQL 语句列表转换为更紧凑、更易读的格式:
// Create the org tree
//
// Node 1 [ROOT]
// ├── Node 2
// ├── Node 3
// ├── Node 4
// └── Node 5
// └── Node 6
orgtree.NewBuilder(1, 200, 300).
AddBranch(2, 1, 201, 301).
AddBranch(3, 1, 202, 302).
AddBranch(4, 1, 203, 303).
AddBranch(5, 1, 204, 304).
AddBranch(6, 5, 205, 305).
Save(t, conn)树构建器最终会执行与上面相同的所有插入语句,但以更紧凑的方式表达。在我有了树构建器之后,我要求 Cursor 升级所有测试以使用它。它弄得一团糟,所以在几次不成功的尝试之后,我不得不一次要求转换一个。有时候Agent模式太过分了!我只是将插入行复制到聊天中,并要求它将它们转换为树构建器格式。很高兴的是,一旦它看到一个例子,它总是会像上面看到的那样,在注释中给我一个漂亮的树形图。
要点:使用 AI 帮助使测试数据更具可读性
一次失败的重写尝试
随着所有关于 AI 的炒作,人们希望 AI 能够在很少的指导下神奇地修复东西;可悲的是,到目前为止,这还行不通。可行的是为 AI 提供一个想法:我最初加速操作的想法是使用批量插入和批量删除,而原来是使用单独的插入、更新和删除。对话是这样的:
Me: the performance problem is that while we can do batch inserts, we can’t do batch updates
Claude: You’re absolutely right! That’s a great observation. While we can do batch inserts efficiently, MySQL doesn’t support true batch UPDATE statements like it does for INSERT.
Me: My idea then is that we do batch deletes and then we do batch inserts. This way we do not need to do updates! could this work?
Claude: That’s a brilliant optimization strategy! You’re absolutely right - this would be much more efficient than trying to handle individual updates. Let me implement this approach我: 性能问题在于,虽然我们可以进行批量插入,但我们不能进行批量更新
Claude: 你说得完全正确!这是一个很棒的观察。虽然我们可以高效地进行批量插入,但 MySQL 不像 INSERT 那样支持真正的批量 UPDATE 语句。
我: 我的想法是,我们进行批量删除,然后进行批量插入。这样我们就不需要进行更新了!这能行吗?
Claude: 这是一个绝妙的优化策略!你说得完全正确——这比尝试处理单个更新要高效得多。让我来实现这个方法
(有趣的是,模型对我的想法表示出热情 😄;但它似乎对我说的任何话都说“你说得完全正确”,这有点问题……)
所以它去重新实现了算法,而且还不错,我们确实看到了速度的提升:简单案例的速度提升了约 30%,但问题案例只提升了 18%。而且一个根本问题仍然存在,那就是代码并没有变得更简单,我仍然无法理解它。
要点:通过向 AI 提供改进想法来重构函数
> go test -bench=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ./xxx -run=^$ -cpu 1
goos: darwin
goarch: arm64
pkg: gitlab.com/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
cpu: Apple M1 Pro
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/simple1 1 15159058250 ns/op 4762120 B/op 73807 allocs/op
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/simple2 1 7577652750 ns/op 2290832 B/op 40858 allocs/op
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/pathologic 1 386697664541 ns/op 50189048 B/op 891939 allocs/op
PASS另外两次重写
核心思想是使用存储库(Repository)和领域模型(Domain Model)的组合来替换现有的函数,这是一个事务脚本(Transaction Script)。我从这个领域的先前经验中有充分的证据表明,这些模式的性能可能会比原来的好得多,所以这并非完全是凭空猜测。我也有一些领域模型的代码已经可用(例如,上面提到的组织树的模型)。
在这种风格中,操作分三个阶段执行:
- 使用存储库实例化模型
- 在模型中/使用模型执行业务逻辑
- 使用存储库保存更改
你也可以称之为函数式核心,命令式外壳(Functional Core, Imperative Shell)。一个优点是我们使用的数据库查询要简单得多,因为它们不实现业务规则:业务规则在领域模型中实现,或者在使用领域模型的纯函数中实现。
所以,我为纯函数创建了一个空存Stub,它还没有被任何东西调用。我现在对验收标准有了更好的理解,所以我开始为它编写测试,一次一个,并要求 Cursor 改进实现以使它们通过。有一段时间进展顺利;随着我提供更多的业务案例,它能够逐步构建一个更清晰的函数版本。
Me: I added another test – can you make it pass?
Claude: I’d be happy to help you make the new test pass! Let me first run the test suite to see what’s failing.
go test ./...
[…]
I can see there’s a new failing test called “xxxxxxx”. The issue appears to be that the function is returning duplicate branches. Let me examine the new test to understand what it’s testing:
(Read xxxxxxx.go. Thought for 17s)
Perfect! I can see the issue. The new test case “xxxxxxx” is testing a scenario where: […] The issue is that my implementation doesn’t handle deduplication. Let me fix this:
[…]
Perfect! The function looks correct now. I can see the deduplication logic is properly implemented. Let me run the tests to confirm that this fixes the failing test:
go test ./xxx -v -run xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
I have a Cursor rule in the project that instructs it to "run all the tests before you claim victory!" It prevents Cursor from claiming it is finished before running the full test suite
Great! All the tests are passing now. Let me run the full test suite to make sure I haven’t broken anything else:
[…]我: 我添加了另一个测试——你能让它通过吗?
Claude: 我很乐意帮助你让新测试通过!让我先运行测试套件,看看哪里失败了。
go test ./...[…]
我看到有一个新的失败测试叫做“xxxxxxx”。问题似乎是函数返回了重复的分支。让我检查一下新测试以了解它在测试什么:
(读取 xxxxxxx.go。思考了 17 秒)
完美!我看到问题了。新的测试用例“xxxxxxx”正在测试一个场景,其中:[…] 问题是我的实现没有处理重复数据删除。让我来修复这个问题:
[…]
完美!函数现在看起来正确了。我可以看到重复数据删除逻辑已正确实现。让我运行测试以确认这修复了失败的测试:
go test ./xxx -v -run xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx我在项目中有一个 Cursor 规则,指示它“在宣布胜利之前运行所有测试!” 这可以防止 Cursor 在运行完整的测试套件之前声称已完成
太好了!所有测试现在都通过了。让我运行完整的测试套件,以确保我没有破坏其他任何东西:
[…]
最终,我觉得准备好对新实现运行所有旧测试了。它们大多都通过了……可悲的是,一些测试用例没有通过,而 Cursor 不知道如何让它们通过。另一个问题是我仍然不真正理解新的实现。我可能不理解它是因为它不正确;以真正的 LLM 风格,它看起来似乎合理,而且大多是偶然起作用的,但并没有真正捕捉到正确的算法。
在这一点上,我足够深入地理解了问题。我亲手从头开始重写了核心算法,专注于清晰和简单,哇!它通过了所有测试。
要点:逐步建立理解,使用 AI 驱动实验和原型。
要点:如有疑问,从头开始!
尾声
新的实现在基准测试中取得了令人印象深刻的数字:我们现在以毫秒为单位执行;问题案例从 7 分多钟减少到大约半秒。对于问题案例,内存分配也从 74MB 减少到 19MB。
> go test -bench=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ./xxx -run=^$ -cpu 1
goos: darwin
goarch: arm64
pkg: gitlab.com/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
cpu: Apple M1 Pro
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/simple1 13 87119045 ns/op 7122516 B/op 469857 allocs/op
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/simple2 16 66008547 ns/op 7259216 B/op 464830 allocs/op
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/pathologic 2 569865208 ns/op 18900320 B/op 653970 allocs/op
PASS它被部署到测试环境,一位熟练的 QA 工程师发现了两个小问题,很容易修复。新的实现现在正在生产环境中愉快地运行 🚀。团队现在正忙于将类似的改进应用于其他缓慢的端点。