1. 循环工程到底是什么:从“一次性对话”到“可迭代系统”的认知转变
很多人第一次听到“Loop Engineering”这个词,会下意识觉得它又是一个被包装出来的新概念。我一开始也这么想,直到我在一个真实项目里被反复折磨了整整两周,才真正理解它要解决的问题有多具体。
先说结论:循环工程的核心,是把“向模型提问”这件事,从一次性的、靠运气的对话,变成一套可设计、可观测、可收敛的迭代系统。它关注的不是某一次回答好不好,而是整个“提问—执行—观察—修正”的闭环能不能稳定地把你带到目标。
为什么这个概念现在才火起来?因为早期的模型能力有限,你问一句它答一句,能答对就不错了,根本没有“循环”的空间。但当下的编码类工具已经能读写文件、执行命令、跑测试、看报错、再改代码,这就意味着一次任务里模型可能要自主决策几十甚至上百步。步数一多,问题就暴露了:它会在某个地方卡死、会反复改同一个文件、会忘记最初的目标、会在错误的假设上越走越远。
我踩过最典型的一个坑:让工具帮我重构一个模块的日志输出,结果它改着改着开始“顺手”优化旁边的工具函数,最后提交的 diff 里混进了七八个我没要求的改动,其中一个还引入了空指针。那次之后我才明白,没有循环约束的自动化,本质上是把失控的风险也一起自动化了。
所以循环工程要回答的是三个问题:循环的目标怎么定义得足够清晰,循环的每一步怎么被观测和验证,循环的终止条件怎么设定才不会过早停止或无限打转。这三个问题听起来朴素,但真正落地时,每一个都能让新手栽跟头。
这篇文章适合两类人:一类是刚开始用编码工具、还在“它能帮我写代码”这个阶段的朋友;另一类是用了一段时间、发现结果时好时坏、想搞清楚怎么把它用稳的从业者。我会把原理、实操、参数、避坑经验都摊开讲,尽量让你看完就能在自己的项目里复现。
2. 循环工程的整体设计思路:为什么是“闭环”而不是“长提示词”
2.1 长提示词的幻觉与循环的真实价值
新手最容易走的一条路,是把所有要求塞进一个超长的提示词里,指望模型一次性理解全部意图。我早期也这么干过,写过一个将近两千字的提示词,把代码规范、目录结构、命名习惯、测试要求全列进去。结果呢?模型确实“看”了,但执行到第三步就开始偏离,因为它没有机制去检查自己有没有偏离。
长提示词的问题在于它是静态的。它假设任务环境不变、假设模型一次就能规划正确、假设执行过程中不会出现意外。但真实的编码任务里,意外才是常态:依赖装不上、测试挂了、文件被别的进程占用、接口返回格式和文档不一致。
循环工程的思路完全不同。它承认“第一次大概率不对”,然后把精力放在如何快速发现不对、如何低成本修正上。这就像调试代码:你不会指望一次写完就没 bug,你会加日志、加断点、跑测试,让问题自己暴露出来。
提示:判断一个工作流是不是“循环工程”,最简单的标准是——它有没有一个独立的、由外部工具(而不是模型自己)执行的验证步骤。如果验证也是模型“自己觉得对”,那它仍然是长提示词,不是循环。
2.2 三类循环模式与适用场景
在实际项目里,我总结出三种常见的循环模式,它们的成本和适用场景差别很大。
| 循环模式 | 验证方式 | 适用场景 | 单次成本 | 收敛速度 |
|---|---|---|---|---|
| 人工确认循环 | 人看结果后决定下一步 | 探索性任务、架构设计 | 高(占用人力) | 慢但可控 |
| 测试驱动循环 | 跑测试/类型检查/构建 | 有明确验收标准的编码任务 | 中 | 快 |
| 自评循环 | 模型自己检查输出 | 文案、注释、简单重构 | 低 | 快但不可靠 |
我的经验是:能用测试驱动循环的,绝不用自评循环。自评循环最大的问题是模型对自己的错误有“盲区”,它倾向于认为自己的输出是对的,你让它检查,它经常回一句“看起来没问题”。而测试是客观的,红就是红,绿就是绿,没有商量余地。
人工确认循环虽然慢,但在任务目标本身还不清晰的时候是必须的。比如你要重构一个模块,但你自己都还没想好新架构长什么样,这时候让模型自主循环就是灾难,它会基于错误的假设狂奔。正确做法是先人工确认几轮,把目标收敛清楚,再切换到测试驱动循环。
2.3 循环的“燃料”与“刹车”:上下文管理
循环能跑起来靠的是上下文,但上下文也是循环最大的敌人。每跑一轮,对话历史就长一截,模型要处理的信息越来越多,注意力被稀释,早期的重要约束容易被淹没。
我做过一个粗略的观察:在一个中等复杂度的重构任务里,当对话轮次超过十五轮之后,模型开始明显“健忘”,会重复问已经确认过的问题,或者忘记某个已经改过的文件。这不是模型变笨了,是上下文里噪音太多了。
所以循环工程里必须有一套上下文管理策略。常见的做法有三种:一是定期摘要,把前面的关键决策压缩成一段简短的状态描述;二是外部记忆,把任务状态、已改文件、待办事项写到一个单独的文件里,每轮开始时读进来;三是分阶段重置,把大任务拆成几个小任务,每个小任务用新的对话窗口。
我个人最推荐第二种,因为它最稳定。摘要依赖模型总结能力,可能丢信息;重置会丢失跨阶段的上下文。而外部记忆文件是你自己控制的,写什么读什么完全透明,出问题也好排查。
3. 核心细节解析:循环工程里那些决定成败的关键参数
3.1 目标定义:把“帮我优化一下”翻译成可验证的断言
循环工程失败的第一大原因,是目标定义得太模糊。“帮我优化一下这个函数”这种指令,模型没法验证自己有没有完成,因为它不知道“优化”的标准是什么——是更快?更短?更可读?还是修掉了某个隐藏 bug?
我的做法是把目标翻译成可验证的断言。比如“优化这个函数”可以翻译成:
- 函数执行时间从 120ms 降到 50ms 以内
- 单元测试全部通过
- 圈复杂度从 12 降到 8 以下
- 不改变对外接口签名
这四条里,前三条都能被工具自动验证,第四条可以靠类型检查或接口对比来验证。一旦目标变成断言,循环就有了明确的“完成信号”,模型也知道该往哪个方向使劲。
注意:断言不要一次给太多。我试过给一个任务列了十几条验收标准,结果模型顾此失彼,改完这条违反那条。后来我改成“核心断言不超过三条,次要断言放到后续循环里”,成功率明显提升。
3.2 步长控制:为什么“一次只做一件事”反而更快
新手容易犯的另一个错误,是让模型在一个循环里做太多事。比如“重构这个模块,顺便把测试补上,再把文档更新了”。听起来很高效,实际上每一件事都会引入新的变量,一旦出错你根本不知道是哪一步导致的。
循环工程里有个反直觉的原则:步长越小,整体收敛越快。因为每一步的验证成本低、失败定位快、回滚容易。我现在的习惯是把任务拆到“一次改动只影响一个关注点”的粒度,比如这一轮只改数据层,下一轮只改调用方,再下一轮补测试。
这样做看起来轮次变多了,但每轮的成功率高,返工少。我统计过自己最近十个任务,拆细之后平均总轮次反而比“大步走”少了三成左右,因为大步走经常走到一半发现方向错了,前面全白做。
3.3 终止条件:三种必须设置的“刹车”
循环没有终止条件,就是无限烧钱。我见过有人让工具跑了一晚上,第二天发现它在两个文件之间反复横跳,改了又改回去,token 消耗惊人,代码一行没净增。
必须设置的终止条件有三类:
- 成功终止:所有核心断言通过,循环正常结束
- 失败终止:连续 N 轮(我一般设 3 轮)验证都不通过,且没有明显进展,强制停止并报告
- 预算终止:设定最大轮次或最大 token 消耗,到点就停
第三类最容易被忽略,但最救命。我现在的默认配置是最大轮次 20、连续失败 3 轮即停。这两个数字不是拍脑袋来的:20 轮足够覆盖大多数中等任务,3 轮连续失败基本可以判定是方向性错误而不是偶发问题。
3.4 验证工具的选择:测试、类型检查、构建的优先级
验证工具的选择直接决定循环的质量。我的优先级排序是:类型检查 > 单元测试 > 构建 > 静态分析 > 模型自评。
类型检查排第一,因为它最快、最确定、覆盖最广。很多低级错误(拼写、参数类型、空值)在类型检查阶段就能拦住,根本不用等到跑测试。单元测试排第二,因为它验证的是行为正确性,比类型检查更接近业务。构建排第三,主要验证依赖和配置问题。
静态分析(lint)我放在后面,因为它的规则经常和实际需求冲突,容易产生噪音,让循环在无关紧要的格式问题上打转。模型自评放最后,只在前面都没有的时候用。
4. 实操过程:从零搭一个能跑通的循环工作流
4.1 环境准备与工具链搭建
先说环境。我用的是一台普通的开发机,系统是 Ubuntu 22.04,内存 16G。工具链方面,核心是编码助手加一个能跑命令的终端环境。安装过程这里不展开,重点说配置。
配置文件我建议单独放一个目录,不要和项目代码混在一起。我的习惯是在用户目录下建一个~/.loop-config/,里面放三样东西:主配置文件、外部记忆文件模板、验证脚本。
主配置文件里最关键的是验证命令和终止条件。验证命令我一般写成一行 shell,把类型检查、测试、构建串起来,任何一步失败就返回非零。这样循环只需要看退出码,逻辑简单可靠。
# 验证脚本示例 verify.sh set -e npm run typecheck npm run test -- --runInBand npm run build echo "ALL CHECKS PASSED"外部记忆文件我命名为STATE.md,里面固定几个区块:当前目标、已完成项、待办项、已知问题、关键决策。每轮循环开始时读进来,结束时更新。这个文件是循环的“短期记忆”,比对话历史可靠得多。
4.2 第一个循环:从失败中学习的最小案例
我拿一个真实的小任务来演示:给一个工具函数补上边界处理。原始函数处理数组时没考虑空数组和 null,会抛异常。
第一轮,我把目标写成断言:空数组返回空数组、null 返回空数组、正常数组行为不变、测试通过。然后让工具执行。它改了函数,加了两个判断,跑测试,通过了。看起来一轮就完成了。
但我在 review 的时候发现,它把 null 和 undefined 都处理了,但没处理非数组的输入(比如传了个字符串进来)。这不是我断言里写的,但属于同类问题。于是我加了第四条断言:非数组输入抛出明确的类型错误。第二轮,它补上了这个判断,测试通过。
这个案例说明一个点:第一轮的断言往往不完整,循环的价值就在于让你在过程中发现遗漏并补上。如果我只跑一轮就结束,这个边界问题就会留到生产环境。
4.3 参数计算:轮次、超时、重试怎么定
参数不是拍脑袋定的,我一般按任务复杂度估算。一个粗略的公式是:预估轮次 = 涉及文件数 × 1.5 + 验证步骤数 × 2。
比如一个任务涉及 3 个文件、有 2 个验证步骤,预估轮次就是 3×1.5 + 2×2 = 8.5,取整 9 轮。实际配置时我会把这个数字乘以 2 作为上限,也就是 18 轮,留出返工空间。
超时方面,单轮超时我设 5 分钟。超过 5 分钟还没结束,大概率是卡在某个死循环或者网络请求上了,强制中断比干等划算。重试我一般不设自动重试,因为失败往往意味着需要人工介入调整目标或断言,盲目重试只是浪费资源。
| 参数 | 推荐值 | 调整依据 |
|---|---|---|
| 最大轮次 | 预估轮次 × 2 | 任务复杂度 |
| 单轮超时 | 5 分钟 | 任务类型,网络类可放宽 |
| 连续失败阈值 | 3 轮 | 低于 3 容易误停,高于 3 浪费 |
| 上下文摘要间隔 | 每 5 轮 | 对话长度增长速率 |
4.4 实操现场:一次完整循环的记录
我记录了一次真实的重构任务,涉及 4 个文件,目标是抽出一个重复的校验逻辑。整个过程跑了 11 轮,其中 3 轮失败。
第 1 到 3 轮:模型识别出重复逻辑,抽成公共函数,但调用方替换时漏了一处,类型检查报错。第 4 轮:修复漏掉的调用方,类型检查通过,但测试挂了一个,因为公共函数的错误信息格式和原来不一致。第 5 到 6 轮:调整错误信息格式,测试通过。第 7 轮:我 review 时发现公共函数的参数命名不够清晰,加了一条断言要求重命名。第 8 到 9 轮:重命名并更新所有调用方。第 10 轮:跑全量验证,通过。第 11 轮:更新 STATE.md,记录决策。
这 11 轮里,真正“写代码”的轮次只有 5 轮左右,其余都是验证、修正、记录。这个比例很正常,循环工程里大部分时间花在验证和收敛上,而不是生成上。如果你发现自己的循环大部分轮次都在生成新代码,那说明断言定得太松,或者步长太大了。
5. 常见问题与排查技巧实录
5.1 循环卡死:模型反复改同一个地方
这是最常见的问题。表现是模型连续几轮都在改同一个文件、同一个函数,改来改去回到原点。原因通常是断言之间有冲突,或者验证工具给出了矛盾的信号。
排查方法:先看 STATE.md 里记录的最近几轮改动,找出重复的模式。然后检查断言,看是不是有两条断言互相矛盾。我遇到过一次,一条断言要求“保持函数签名不变”,另一条要求“把参数从位置参数改成关键字参数”,这两条直接冲突,模型就在中间反复横跳。
解决办法是把冲突的断言拆到不同阶段,先做签名变更,再做参数风格调整,分两轮完成。
5.2 验证通过但结果不对:断言的盲区
有时候所有断言都通过了,但结果就是不对。这通常是因为断言覆盖不全,漏掉了某个关键维度。比如测试只覆盖了正常路径,没覆盖异常路径;或者类型检查通过了,但逻辑是错的。
我的应对方法是定期做“反向验证”:故意构造几个断言没覆盖的边界输入,看结果是否符合预期。如果不符合,就把这个输入补成新的断言。这个过程重复几次,断言的覆盖度就上来了。
提示:反向验证不需要每轮都做,我一般每 5 轮做一次,或者在一个阶段完成、准备进入下一阶段时做。
5.3 上下文丢失:模型忘记了早期约束
跑得轮次多了,模型会忘记早期的约束。表现是它开始违反之前确认过的规则,比如命名规范、目录结构、错误处理方式。
解决办法前面提过,用外部记忆文件。但光有文件不够,还要确保每轮开始时文件被正确读入。我遇到过配置文件路径写错,导致 STATE.md 根本没被读进去,模型自然就“失忆”了。所以配置好之后,一定要验证一次:故意在 STATE.md 里写一条奇怪的约束,看模型下一轮会不会遵守。如果遵守了,说明读取正常。
5.4 成本失控:token 消耗远超预期
成本失控通常有三个原因:轮次太多、上下文太长、验证太频繁。轮次太多往往是断言太松或步长太小;上下文太长是摘要策略没做好;验证太频繁是把重验证(比如全量测试)放到了每一轮。
我的优化顺序是:先检查断言,能收紧就收紧;再检查摘要间隔,从每 5 轮改成每 3 轮;最后把重验证改成阶段性执行,比如每 3 轮跑一次全量测试,其余轮次只跑类型检查。
| 问题现象 | 可能原因 | 排查动作 | 解决方向 |
|---|---|---|---|
| 反复改同一处 | 断言冲突 | 检查断言列表 | 拆分阶段 |
| 验证过结果错 | 断言盲区 | 反向验证 | 补充断言 |
| 忘记早期约束 | 上下文丢失 | 检查记忆文件读取 | 修复配置 |
| 成本失控 | 轮次/上下文/验证 | 逐项排查 | 收紧断言、调整间隔 |
5.5 独家避坑:三个我踩过的坑
第一个坑是在循环里做格式化。我一开始让循环顺便跑格式化工具,结果格式化改动和逻辑改动混在一起,diff 巨大,review 时根本看不出逻辑变化。后来我把格式化单独拎出来,在循环开始前和结束后各跑一次,循环内部不碰格式。
第二个坑是让模型自己决定何时停止。我试过在提示词里写“你觉得完成了就停止”,结果它要么过早停止,要么永远不停止。终止条件必须由外部工具判断,不能交给模型。
第三个坑是忽略失败轮次的价值。失败轮次不是浪费,它暴露了断言的盲区或目标的模糊之处。我现在会专门记录失败轮次的原因,这些记录后来成了我优化断言库的重要素材。
6. 循环工程的扩展玩法与个人体会
6.1 把循环工程用在非编码场景
循环工程的思路不限于编码。我后来把它用在了文档写作上:目标是“写一篇技术说明”,断言是“覆盖三个核心概念、每个概念有示例、总字数在范围内、术语一致”。验证工具换成了字数统计脚本加术语检查脚本。效果同样不错,尤其是术语一致性,靠人眼检查很容易漏,脚本一跑就出来了。
数据清洗场景也适用。断言可以是“空值率低于 1%、字段类型符合预期、行数在合理范围”。每轮清洗后跑验证,不通过就调整清洗规则。这个思路比一次性写个大脚本然后祈祷它跑对要稳得多。
6.2 循环工程与团队协作的结合
一个人用循环工程是一回事,团队用是另一回事。团队场景下,STATE.md 可以变成共享的任务看板,断言可以变成代码评审的检查项,验证脚本可以进 CI。这样循环工程就从个人技巧变成了团队规范。
我参与过的一个小团队就是这么做的:他们把验证脚本接进了 CI,每次提交自动跑,断言写在 PR 模板里,review 时逐条对照。结果是返工率明显下降,因为问题在提交阶段就被拦住了,而不是等到测试环境才暴露。
6.3 我个人的几条经验
用到现在,我最大的体会是:循环工程的门槛不在工具,而在目标定义。工具再强,目标模糊它也帮不了你。我见过太多人把精力花在折腾工具配置上,却不肯花十分钟把目标写清楚,结果循环跑得越多,偏得越远。
第二条体会是:验证要趁早,断言要从严。早期松一点看起来省事,但问题会累积到后面,修复成本指数上升。我现在的习惯是宁可第一轮多花时间把断言写严,也不愿意后面反复返工。
第三条是:失败轮次要复盘。每次循环失败,我都会花两分钟想想为什么失败,是断言问题、目标问题还是工具问题。这个习惯让我慢慢积累了一套自己的断言模板,现在开新任务时直接套用,起步快很多。
最后分享一个小技巧:如果你不确定断言该怎么写,可以先手动做一遍任务,把你实际检查的每一步记下来,这些步骤就是天然的断言。手动做一遍花不了多少时间,但能让你的循环少走很多弯路。这个内容后续还可以这样扩展:把常见任务的断言模板整理成一个库,新任务直接引用,进一步降低启动成本。