跳至正文
Ende Sun
返回博客

人工智能与自动化

用规格驱动开发并行调度 AI 智能体:哪些奏效,哪些出了问题

阅读需 6 分钟

用规格驱动开发并行调度 AI 智能体:哪些奏效,哪些出了问题

这个网站是在一次大规模改动中重建的:重新定位为工程师个人主页,让所有内容都能在管理后台翻译成西班牙语、英语和中文,增加带筛选和搜索的项目分类、技术博客、教育经历板块以及全新的视觉层。内容多到无法全部记在脑子里,也多到无法用一个笼统的提示词交给 AI 助手。真正让它变得可控的,是先写规格说明,并把它当作一份契约。

先访谈,每次只问一个问题

在写下任何一条需求之前,我先回答了一系列会改变实现内容的问题,一次一个,从不列成清单。客户名称能否公开展示?只有一种语言版本的博客文章,是隐藏还是加提示后展示?两份简历日期不一致时以哪份为准?本科毕业设计的真实素材能否公开?每个回答要么砍掉一条工作分支,要么增加一条约束。这次访谈产生了九项决定,其中好几项如果不问清楚,就会变成悄无声息的假设。例如:喉部分割项目的临床影像不能公开,因此该项目只能用原创插画来呈现。

EARS:一句话只描述一种行为

最终规格包含 78 条功能需求,编号并按模块分组(定位、可翻译内容、经历、教育、项目、图片、技能、联系方式、博客、视觉呈现、SEO),另有两条是在访谈中补充的。每条需求都遵循一种 EARS 句式:

  • 通用型:系统应……
  • 事件驱动型:当访客选择某个分类时,系统应只显示该分类下的项目。
  • 状态驱动型:在启用"减少动态效果"期间,系统应……
  • 可选型:在存在某项功能的情况下……
  • 异常型:如果没有项目符合筛选条件,则系统应显示"无结果"提示,并提供清除筛选的操作。

有两条规则起了决定性作用。第一,一句话只描述一种行为:如果一条需求要用"并且"连接两种行为,那它就是两条需求。第二,不用无法度量的形容词。"快速""直观"都不是需求,阈值才是。每条需求都必须可验证;如果我想不出怎么验证,说明它写得不对。

我最看重的是不在范围内这一节。评论、邮件订阅、多作者、可下载的简历:都明确写下来,防止功能自己悄悄膨胀。

从规格到 TDD 任务计划

规格说明只写"做什么"和"为什么",刻意不包含技术栈、文件名或数据结构。这些内容放进了单独的计划:13 个任务,每个任务列出要修改的文件、依赖前序任务的接口、为后续任务提供的接口,以及按测试先行排列的步骤:先写失败的测试,运行,写最少的实现,再运行,提交。接口部分比我预想的更重要。实现某个任务的智能体只能看到这一个任务,所以相邻任务依赖的方法名和类型必须写清楚,否则两个智能体会给同一个东西起两个不同的名字。

并行执行相互独立的任务

基础部分(翻译机制和数据迁移)完成后,很多任务彼此独立:管理后台、项目插画、博客、内容同步命令。我按批次推进,每个任务交给一个独立的 AI 智能体,在隔离的 git worktree 和各自的分支中工作。每个智能体运行自己的测试,最后产出一个提交,再由我用 cherry-pick 合入功能分支。

从产出速度看,这很有效。但它也带来了一个具体的盲区:每个智能体的测试单独看都是绿的,却说明不了这些部分合在一起能否正常工作。

合并之后出了什么问题

指向已删除 worktree 的过期自动加载器。 某一批合并之后,主工作区里对控制器的修改似乎完全不生效,某个视图报"变量未定义",可源码明明是对的,也没有任何错误能解释。原因是某个智能体安装依赖时,用指向它自己 worktree 的路径重新生成了 Composer 的类映射,PHP 就照常从那里加载旧文件。(new ReflectionClass(Foo::class))->getFileName() 暴露了错误的路径,在主工作区执行 composer dump-autoload 后问题解决。此后,每个智能体的指令里都加上了提交前做这项检查。

绕过模型的视图。 模型里有一个正确的 displayImage() 方法:项目处于插画模式时返回插画,这正是喉部项目应有的展示方式。但项目卡片和详情页把原始的 image 字段作为主图片地址,只在图片加载失败时才回退到插画。旧的临床照片能正常加载,回退从未触发,于是一张不应公开的照片被展示了出来。模型测试全部通过,因为模型本身没错。我是在查看渲染后的页面时才发现的。修复只是每个视图改一行,再加一个测试:渲染这两个页面,断言其中不包含该照片的地址。

全新数据库上的"抢行"问题。 内容同步命令按稳定的 key 匹配已有记录;第一次运行时,则按记录在生产环境中已有的 id 匹配。在没有这些记录的数据库上,自增 id 会在同一次运行中重新分配,于是后面某个条目预期的 id 恰好等于刚为另一个条目创建的记录时,就会把那条记录据为己有并覆盖掉。在生产环境中这些 id 早已存在,所以从不会发生,也正因如此才一直没被发现。修复办法是:只有当记录还没有 key 时才按 id 认领;测试数据也改为按真实 id 预置历史记录,而不是在不真实的空数据库上测试。

我的收获

  • 规格说明是最有价值的产物,比任何生成的代码都重要。有了它才能拆分工作、评审工作,并判断何时算"完成"。
  • 并行智能体能成倍提高产出,但集成是另一项独立的工作。每个任务的测试通过是必要条件,而不是充分条件。
  • 合并后的验证必须包括运行真实应用并亲眼查看,而不只是跑测试。上面三个 bug 中有两个,在我看到问题并补写测试之前,测试根本发现不了。
  • 集成阶段发现的每个 bug 都变成了回归测试;适用时,也变成下一批智能体指令中的一条。

这种工作方式并没有消除工程工作,只是把它转移了:写代码的时间少了,写规格、评审和验证的时间多了。