AI Agent 研发工作流的设计与实现
最近用 Agent 处理需求时,我慢慢养成了一套习惯。
实际使用时,我不会一上来就让 Agent 改代码,而是先让它读原始需求、翻相关代码,再整理一份实现方案。方案出来后,我会开一个新会话,交给另一个 Agent 做 Review。确认没什么明显遗漏,才进入开发。
这样做确实比“把需求扔给一个 Agent,然后等它交作业”稳一些。可重复几次以后,我发现 Agent 在干活,我也没闲着。
我得选择下一个 Agent,把前面的结果复制过去,补上缺少的上下文,再告诉它现在要做什么。Review 提出问题后,我还要把意见送回原来的会话。一项任务来回几轮,剪贴板里全是 Prompt 和 Markdown。
有一次我在几个窗口之间切来切去,突然觉得有点好笑:Agent 没有取代我的工作,我只是成了它们之间的消息队列。
为什么我会用两个 Agent
最开始拆成两个 Agent,不是为了追求所谓的“多 Agent 架构”,只是因为我发现,让同一个 Agent 检查自己的方案,效果经常不好。
它已经在第一轮里接受了一些假设。即使再提醒它“仔细检查”,它也很容易顺着原来的思路继续走。换一个没有参与前面讨论的 Agent,反而更容易问出一些直接的问题:这个改动会不会破坏兼容性?失败时怎么处理?测试覆盖了什么?
于是,我把两件事分开了。
第一个 Agent 负责理解需求,找代码,列出影响范围和验收条件。第二个 Agent 只拿原始需求、项目约束和最终方案,从头检查。如果发现问题,方案就退回去修改。
flowchart TD
I["收到需求"] --> A["需求分析"]
A --> R["独立 Review"]
R -->|需要修改| A
R -->|通过| H["我来确认"]
H -->|调整方案| A
H -->|开始开发| D["开发"]
D --> C["代码 Review"]
C -->|存在问题| D
C -->|通过| V["验证与交付"]
这里真正起作用的不是 Agent 数量,而是它们看到的信息不同,承担的责任也不同。一个提出方案,另一个专门挑问题。
有些决定我还是希望自己来做。比如要不要改变现有交互,值不值得承担兼容成本,或者要不要顺手扩大需求范围。这些问题没有唯一正确答案,知道更多代码也不一定能替我决定。
所以图里的“我来确认”并不是自动化没做完,而是我特意留下来的停顿。Agent 先把事实和风险摊开,我做选择,然后再让流程继续。
问题其实不在 Prompt
我最早想做的,只是把常用 Prompt 保存下来:需求分析一份,方案 Review 一份,开发实现一份。需要时选一个模板,总比每次重新写方便。
但实际操作中,最烦的并不是那几段文字,而是很多藏在文字外面的判断:
- 现在应该调用哪个 Agent;
- 它需要看到哪些内容;
- 上一步要交付什么结果;
- Review 没通过时应该回到哪里;
- 哪一步必须停下来等我确认;
- 执行中断以后是继续,还是从头再来。
Prompt 只能告诉 Agent 当前要做什么,回答不了任务下一步往哪里走。真正重复的是这套流转过程。
想通这一点后,我开始把它看成一条很普通的工作流:每一步有输入、有产物,也有进入下一步的条件。Agent 只负责眼前这一步,不需要知道后面还会调用谁。
流程有了以后,还需要一个对象把这些步骤串起来。我把一项需要从需求理解走到开发交付的任务抽象成 Issue。它不一定对应 GitHub Issue,也不是 Agent 自带的概念,只是工作流里的一次任务实例。
原始需求、当前阶段、每轮产物和执行状态都记录在 Issue 下面。Agent 不需要理解 Issue;工作流只从中取出当前阶段需要的信息,再把阶段产物收回来。
有了这个载体,流程配置大概可以写成这样:
1 | |
这份配置并不复杂,但它把一件重要的事写明白了:决定流程怎么走的,不再是某段藏在 Prompt 末尾的自然语言,而是外部的状态和规则。
这样一来,我可以换掉某个 Agent,也可以调整 Review 的标准,而不用重新编排整段对话。Prompt 变回了一个阶段内部的工具,不再承担整个流程。
Agent 之间应该传什么
手工操作时,最顺手的办法是复制上一段聊天记录。看起来信息最完整,也不需要额外整理。
但聊天记录里有太多过程:试过又放弃的方案、还没证实的猜测、工具输出,以及中途被推翻的结论。下一个 Agent 虽然看到了全部内容,却不一定知道最后该信哪一段。
我后来更倾向于让每个阶段交付一份“成品”。
需求分析的结果应该写清楚要解决什么、不做什么、会影响哪里、有什么风险、最后怎么验收。Review 的结果则要明确给出通过或退回;如果退回,还要指出问题和下一轮必须补齐的内容。
这样做以后,前后两个 Agent 不需要共享完整对话。后一个 Agent 只读取原始问题和上一阶段确认过的结果,工作流也能根据 Review 的结论决定继续还是返回修改。
同一个方案改了几轮,也不必互相覆盖。每轮产物都可以单独保存。回头看时,我能知道某个决定是什么时候加进去的,而不是在一条很长的聊天记录里猜。
我现在会用一个很简单的问题判断产物是否合格:如果关掉当前会话,只看它提交的结果,我还能不能接着往下做?如果不能,那它交付的多半还只是聊天过程。
让 Agent 真正进入仓库
前面的流程只处理文档还比较简单,等开发 Agent 开始修改代码,目录和进程也得一起管起来。
从技术上说,只读的分析任务可以共享代码目录。但我最后还是为每次 Agent 执行准备了独立的 Git worktree。这样分析和 Review 面对的是确定的代码状态,开发任务也不会碰到我当前目录里的未提交修改。
worktree 共享原仓库的 Git 对象,不需要重新 clone 一份代码,但每次执行都有自己的工作目录、索引和分支状态。
1 | |
目录隔离以后,还有进程的问题。我用的 Agent 是长时间运行的交互式命令行程序,不能让它的生命周期跟着浏览器连接一起结束。
现在的实现用 tmux 托管 Agent 进程。浏览器断开后,Agent 仍然可以继续运行;我回来时可以重新接上终端查看输出,必要时也能自己接管。
真正把这几部分接起来后,我发现一次阶段任务至少包含四类状态:
flowchart TB
J["阶段任务"] --> S["任务状态
现在进行到哪一步"]
J --> G["Git Worktree
代码改成了什么样"]
J --> T["tmux Session
进程是否还在运行"]
J --> E["Agent Session
它记得哪些上下文"]
这几类状态不能混成一个笼统的“运行中”。代码还在,tmux 进程可能已经退出;Agent 上下文还能恢复,原来的 worktree 却未必还能继续使用;进程正常结束,也不等于阶段已经完成。
所以实现里没有只靠进程退出码判断结果。开发 Agent 结束前要提交结构化产物,写明修改了哪些文件、做过哪些验证、测试是否通过,以及还有什么问题没有解决。Shell 退出码只表示进程状态,不能代替阶段结果。
目前先做到这里
现在,需求分析、独立 Review、worktree 隔离、tmux 托管和结构化阶段结果都已经接进了流程。它不再只是几个窗口之间的操作习惯,确实可以自己推动一项需求往下走。
做出来以后,我也更确定了一点:worktree 和 tmux 解决的是执行问题,最初让我觉得麻烦的却是编排问题。如果阶段之间仍然靠人复制内容,后台进程运行得再稳,我还是那个负责转发消息的人。
还有一些问题没有做完,比如中断以后怎样恢复,任务结束后什么时候清理环境。它们已经出现在设计里,但我不想在这篇文章里把尚未完成的部分写成结论。
这篇文章想记录的,还是我怎么从“少复制几次 Prompt”一路走到工作流。Prompt 可以换,Agent 也可以换,我真正想固定下来的是几个步骤之间的关系:先分析,再独立 Review,遇到取舍时停下来,确认以后再继续。
只要下一次处理需求时,我不用再充当 Agent 之间的消息队列,它就已经解决了一个很具体的问题。