一般

实用循环工程

7
分类技术博客
作者Addy Osmani
来源跳转
发表时间

内容

我通常的工作方式是同时并行使用五到十个代理。对于一些任务,只要我对停止条件和相关约束有非常清晰的认识,我会非常乐意把它们完全委派给代理;而对于另一些任务,我则会更密切地盯着,并对代理正在做的事情进行代码审查。

在此基础上,你可能听说过 循环工程(loop engineering)。几个月前,我写了一篇很长的博客文章专门谈过这个话题。

循环是一种自主、可自我纠正的反馈回路:AI 代理会反复执行、测试结果并调整方法,直到达成某个特定目标

现在,基本上可以把两种核心原语(primitive)纳入考虑。在 Claude Code 中,你有一个 目标 原语,它可以推动一个有边界的单一任务不断向前,直到达成某个具体目标,比如达到一个可衡量的完成线;而 循环 会按计时器或固定间隔重复运行,因此可以用来安排某种变更的执行。

在这些原语还是“原语”之前

我记得在 Claude Code 和 Codex 还没有内置这些原语的时候,循环工程在很大程度上依赖于自己搭建 bash 循环,也就是手工拼出来的那种方式。我最初也是这么做的。你可能还记得,今年早些时候,我们中有不少人都在尝试 Geoff Huntley 提出的 Ralph 循环。我们做实验、分享工作流、分享哪些有效、哪些无效;但这些尝试大多都发生在个人项目上,因此如果卡住了,代价也不算大,毕竟只是个人项目。

随着我们试图弄清楚,哪些循环工程的模式、哪些方面如今已经更“内建”一些,我觉得我们对如何照看它有了更清晰的认识。现在我在很大程度上可以依赖 Claude Code 和 Codex 中这些原语的输出。我们已经向前走了一小段路。但与此同时,你仍然需要非常谨慎,因为如果做循环工程时把它丢在一边,完全没有认真思考终极目标或约束是否定义清楚,就可能让自己陷入麻烦。也正因如此,当你要把它用于一个没有用户、也没有太多历史复杂性的常青代码库,和把它用于一个陈旧复杂、像银行系统那样的遗留代码库时,选择就很有讲究。

Claude Code 团队如何定义循环

Claude Code 团队发布了他们对四类循环的看法,这和我使用这些原语的方式非常一致(他们的说明)。在进入细节之前,先给出我的简要总结:

在 Claude Code 团队,我们把循环定义为:代理反复执行工作周期,直到满足停止条件。我们会根据几个维度来区分不同类型的循环:它们如何被触发、如何停止、使用了 Claude Code 的哪种原语,以及哪类任务最适合它。并不是所有任务都需要复杂循环;请从最简单的方案开始,并有选择地使用这些模式。

你发送的每一个提示都会启动一个手动循环,由你来指挥每一轮。Claude 会收集上下文、采取行动、检查自己的工作,如有需要就重复,然后给出响应。我们把这个过程称为代理式循环(agentic loop)。例如,让 Claude 创建一个点赞按钮。它会读取你的代码、进行修改、运行测试,然后交还一个它认为可行的结果。接着你再手动检查,并写下下一条提示。

他们的文章逐层展开说明。

关于基于目标的循环:

有时,单轮并不够,尤其是对于更复杂的任务。代理在能够迭代时表现会更好。你可以通过用 /goal 定义“完成”是什么样子,来延长 Claude 持续迭代的时间。当你定义了成功标准后,Claude 就不必自己判断什么算“足够好”并过早结束循环。每当 Claude 试图停止时,评估模型会检查你的条件,并把它送回工作流程,直到目标达成或达到你定义的轮次数上限为止。这就是为什么像“通过的测试数量”或“达到某个分数阈值”这类确定性标准如此有效。例如:/goal 将首页 Lighthouse 分数提升到 90 以上,最多尝试 5 次后停止。

关于基于时间的循环:

有些代理式工作是周期性的:任务本身不变,变化的只有输入。例如每天早上总结 Slack 消息。另一些工作依赖外部系统,一个简单的交互方式就是按固定间隔去检查它,并对变化做出反应。例如可能收到代码审查意见或 CI 失败的 PR。对于这些场景,你可以通过 /loop 来触发,它会按间隔重新运行某个提示。例如:/loop 5m 检查我的 PR,处理审查意见,并修复失败的 CI。/loop 在你的电脑上运行,所以如果你把它关掉,它就会停止。你也可以通过创建一个 /schedule 例程,把循环迁移到云端。

而关于主动式循环,也就是最高一层:

触发方式:由事件或计划触发,实时没有人类参与。停止标准:每个任务在目标达成时退出。例程本身会一直运行,直到你将其关闭。最佳用途:有明确规范的重复性工作流,例如 bug 报告、问题分诊、迁移、依赖项升级。使用方式:将例程路由到更小、更快的模型,并在需要做判断时使用最强大的模型。

他们对验证的建议非常值得完整摘录,因为它把人工检查变成了 Claude 可以自行执行的动作:

---
name: verify-frontend-change
description: 在宣称完成之前,端到端验证任何 UI 变更。
---
# 验证前端更改
不要仅仅因为修改成功就报告 UI 变更已经完成。
要像人类审查者那样进行验证:
1. 启动开发服务器,并在浏览器中打开已编辑的页面。
2. 直接与变更交互。对于新的控件(按钮、输入框、
   开关):点击它,确认预期的状态变化,并在变更前/后截图。
3. 检查浏览器控制台:确保没有新的错误或警告。
4. 使用 Chrome DevTools MCP,运行性能追踪并审计
   Core Web Vitals。
如果任何一步失败,修复问题并从第 1 步重新开始——不要交回只完成了部分验证的工作。

目标

我使用目标(goal)的方式,是把它用于构建任何特定工作项,直到它能够被证明已经完成。比如说,你可以用一个目标来要求:确保这个体验在五秒内加载完成,持续推进,直到它真的完成。它会继续借助独立的评估检查,持续确认完成标准是否已经满足。更好的做法是把所使用的工具也定义得更具体一些。

至于我实际跑过的一些目标,也有不少例子。我曾用目标来处理 GitHub issues:审查并关闭最近 10 个问题,或者审查并推动最近 10 个问题继续向前,类似这样。这其实是半开放式的,对吧?或者:让这个页面快 50%。有时效果很好,有时不行,但本质上还是在于实验。

循环

循环更像是一种调度器,所以它会关注某件事,或者按某个节奏反复执行某种模式。你可以把它稍微理解成 cron。它最适合做日志轮询或监控外部状态之类的事情。你也可以把它用于检查进度。如果你发现自己有一些按固定周期反复做的任务,循环就很适合。

我会委派什么,以及我会盯住什么

对我来说,我每天大概会使用五到十个代理。通常情况下,我同时并行使用的上限大约是五个。其中一些任务可能会更安全一些。比如:“嘿,我已经实现了这个功能,去帮我写文档吧。”或者“去再核对一下,我们的测试覆盖是否足够。”又或者类似的事情。如果我正在处理一个更复杂的问题,或者那种即使我给出了一个不错的规格说明——或者我自认为不错的规格说明——并且也尽量给了停止条件,但仍然很有可能不会把所有事情都做对的任务,我就会尝试更密切地盯着它。如果任务涉及任何稍微敏感一点的内容,不管是我已经把某个系统的访问权限交给了它,还是这个功能恰好碰到了身份验证,或者和安全、金融有关的东西,我都会非常仔细地看着。

一般来说,我确实认为我们会逐步走到这样一个阶段:人们会越来越愿意去委派,只要他们能够拥有这些清晰的方法来验证目标或停止条件是否已经满足。但你仍然需要看一看代码,看一看生成出来的东西,确保它达到了你的标准。

这里还有一个同样重要的习惯:不要让完成工作的那个代理自己决定工作是否合格。一个子代理负责起草变更,另一个独立的子代理负责验证。

有时候,代理会对某件事非常自信,但验证代理能捕捉到一些它并未预料到的问题。比如,它可能认为自己生成的某个体验的基线性能已经足够好,但它只是在桌面端的基础上评估性能,而你实际上在意的是移动端体验。这就意味着,代理在问题的某一个维度上非常自信,但在另一个维度上却不一定。

而这一点我是吃过亏的。我当时很好奇:我们是不是遗漏了什么东西,而用户并没有在 issue 跟踪器里或者某个评论里直接反馈给我们?于是我让它去看看我们的一些竞争对手,整理一份清单,并且起草一些 PR——不是推送出去的,只是本地的一些 PR——来展示解决这些差距可能是什么样子。我差点就把其中一些改动推上去了。但我并没有把它们看得足够仔细。我读了它的研究内容,但没有足够仔细地查看实现。所以我委派了任务,但也几乎把判断一起委派出去了。后来当我真正仔细看这些改动时,我意识到:它会给用户引入很多额外复杂性,而我个人认为收益其实并没有那么大。所以我觉得你有时需要提醒自己,不要把品味和判断也一并委派给代理。你委派的是任务,然后你还要认真回头检查它是否达到了你的标准。

顺便说一下,位于 goal 后面的评估器并不是那个检查者。它不会从内容层面判断好坏,完全不是那回事。它所做的只是检查对话转录,看你指定的硬性规则是否已经满足。

/goal 重构 Dashboard.tsx 中的数据获取层,直到 Lighthouse 性能分数 >= 92 且 LCP 低于 1.8 秒,并且这些结果体现在 Lighthouse CLI 输出中。不要更改任何 hooks 的公开 API。每一轮都必须至少改善一项已报告的指标;如果连续两轮都没有改进,就中止。最多停止在 10 轮后。

我每天都会运行的工作流

我每天都会做的一项工作流是:我维护着一个很受欢迎的开源仓库,叫做 Agent Skills。我们已经有超过 8 万颗星,而且直到最近,我们每天要审阅的拉取请求数量甚至高达 80 到 90 个。所以每天我都会花些时间去检查这些内容。现在有了循环,你可以这样做:每隔 24 小时或 12 小时检查一次 GitHub 仓库里是否有新的 open issue,并给出一份按紧急程度分类的摘要,或者做一轮初步审查,等等。

/loop every 1h “检查 GitHub 仓库里是否有新的 open issue。请用项目符号给出按紧急程度分类的摘要。”

结合循环和目标

现在你也可以把循环和目标结合起来使用。也就是说,用循环来安排检查,再用目标来解决问题。比如你可以这样说:每 24 小时循环一次,检查 GitHub 上标记为 bug 的问题。如果有,就用目标把修复实现出来,直到所有本地测试都通过,然后推送分支。

/loop every 24h "检查 GitHub 上标记为 'bug' 的问题。如果存在,就使用 /goal 实现修复,直到所有本地测试通过并推送分支。"

但也要记住,goal 在能往里塞多少内容这件事上也有一些限制。

他们的文章里还给出了一个组合示例,展示了这一切未来的走向:

上述原语,加上 Claude Code 的其他功能,例如 自动模式 和动态工作流(研究预览),可以组合成一个用于长时间运行工作的循环。例如,为了处理传入反馈,你可以使用:/schedule(研究预览)运行一个例程来检查新报告,用 /goal 定义完成的标准,再用 skills 记录如何验证它。动态工作流 用来编排代理去分流每一条报告、修复它并审查修复结果。再加上自动模式,这样例程就能无需停下来请求许可而持续运行。把这些组合起来,一个提示可能会像这样:/schedule every hour: 检查 project-feedback 频道中的 bug 报告。/goal: 不要停止,直到本次运行中发现的每一条报告都已完成分流、处理并回复。在修复 bug 时,使用一个工作流在多个并行工作树中探索三种解决方案,并让一个裁判进行对抗式审查。

分诊系统实际上在做什么

在 PR 分诊中,当我把循环和目标一起用起来时,我最终得到的是一个系统,它让我能够持续接收 PR 和 issue,随时掌握每天涌入我面前的内容,尤其是能够进行交叉引用。这对我来说帮助非常大。如果我想推进某个具体目标,比如:我们要重做系统的某一部分,我需要确保任何碰到这一部分的 issue 都会因为这次重构而被关闭,或者不会踩到别人的脚,那么我就可以把这些要求定义得很清楚。

定时任务对于定期审查新 PR 并关闭那些明显不合适的内容非常有用。一个不错的停止条件,比如:我们有一套贡献指南,而贡献指南里包括这样一条:比如我们目前不接受翻译。

这并不是因为我们不在乎,而是因为维护起来比较困难,因为我们并不总是懂所有新提交内容所使用的语言。所以如果我们告诉它:关闭任何触及我们贡献指南中这方面内容的 issue 或 PR,这在按计划运行时就能做得很好。这样一来,我们需要审查的内容总量就会减少。

循环不能带来什么

别人经常问我,循环工程在哪些场景下并不适合。一般来说,如果你对完成的终态/结束/“好”究竟意味着什么没有清晰认识,那么它可能就不是适合你工作的模式。比如,一个模糊的目标可能是“继续做,直到这个 UI 设计变好”。这到底是什么意思?对谁来说算好?是怎么评估的?那些需要人类品味、主观设计判断,或者开放式创意探索的任务,并不适合这种方式。当你对目标有相当清晰的认识时,我觉得循环是一个值得考虑的好选择。

细则

循环一直原地打转的一个经典信号,就是同一条命令反复尝试,却没有任何结果变化。如果同一条命令在第二次之后又原样试了第三次,通常就该停下来了。

还有一条值得知道的细则:重复循环在创建七天后会过期。我之前一直跟别人说是三天,实际上是七天。而且循环是会话范围内的,所以当你开启新的对话时它就会停止——不过如果你用 --resume 或 --continue 恢复那个会话,任何仍处于七天窗口内的重复任务都会回来。如果你需要能超越当前会话存在的东西,就用 /schedule 把它放到云端运行。

如果有一项检查你每天早上都手动做一遍,那就是你的第一个循环。我的第一个循环就是那堆拉取请求。

评论

(0)
未配置登录方式
暂无评论