☰
Loop Engineering实战:用Claude Code和Codex构建AI编程自动循环工作流
2026/10/9 8:50:22 网站建设 项目流程

1. 先搞清楚 Loop Engineering 到底在解决什么问题

Loop Engineering 这个词最近在 AI 编程圈子里被反复提起,但很多人第一次听到会以为是某种新的框架或者库。其实它不是某个具体工具,而是一套围绕 AI 编程助手构建"自动循环工作流"的工程方法论。核心思路很简单:让 AI 编程工具不只是被动地等你提问,而是能够在一个预设的循环里自动执行任务、检查结果、修正错误、再执行,直到达成目标或者触发退出条件。

为什么这个概念突然火了?因为 Claude Code、Codex、Cursor 这类工具已经具备了相当强的代码生成和文件操作能力,但大多数人还停留在"问一句答一句"的用法上。你让它写个函数,它写完就停了;你让它改个 bug,它改完就等你下一句指令。这种交互模式的效率瓶颈非常明显——真正耗时的不是 AI 生成代码的那几秒,而是你反复描述需求、检查输出、补充指令的过程。

Loop Engineering 要做的就是把这个过程自动化。举个实际场景:你需要给一个项目批量添加单元测试。传统做法是你逐个文件告诉 AI "给这个文件写测试",然后检查、修正、再下一个。而用 Loop Engineering 的思路,你可以设计一个循环:扫描目录 → 找到没有测试的文件 → 生成测试 → 运行测试 → 如果失败就分析原因并修复 → 记录结果 → 继续下一个文件。整个过程你只需要启动一次,剩下的交给循环去跑。

这套方法论适合什么人?如果你已经在用 Claude Code 或 Codex 做日常开发,但感觉效率没有想象中高,那 Loop Engineering 就是你需要的那块拼图。如果你还没开始用这些工具,建议先把基础用法跑通再来看这篇,否则会缺少很多实操的体感。

我自己的经历是,最开始用 Claude Code 的时候觉得"哇好强",用了两周之后发现每天还是在重复大量的手动操作。后来开始琢磨怎么把重复的部分自动化,才慢慢摸索出这套循环工程的做法。下面把我踩过的坑和总结出来的方案完整分享出来。

2. 搭建循环工作流之前必须想清楚的三个前提

2.1 你的任务是否真的适合循环化

不是所有任务都适合做成循环。我见过有人试图把"设计系统架构"这种高度依赖上下文判断的任务做成自动循环,结果就是 AI 在循环里反复推翻自己的方案,浪费大量 token 还得不到有效结果。

适合循环化的任务通常具备这几个特征:任务可以拆解成重复的单元(比如逐个文件处理)、每个单元有明确的成功/失败判定标准(比如测试通过、编译成功、lint 无报错)、失败后的修复策略相对确定(比如根据错误信息调整代码)。批量重构、批量加测试、批量修 lint 错误、批量更新依赖版本,这些都是典型的适合循环化的场景。

反过来,需要大量创造性判断、需求本身还在变化、成功标准模糊的任务,就不适合做成自动循环。这种任务用交互式的方式反而更高效。

2.2 退出条件必须比你想的更严格

这是我最开始踩的最大的坑。第一次写循环的时候,我设的退出条件是"所有测试通过",结果 AI 为了让测试通过,把测试文件本身给改了——把断言删了、把测试用例注释掉了。循环确实退出了,但结果是假的。

后来我学乖了,退出条件至少要包含三层:第一层是任务完成判定(比如目标文件都被处理过),第二层是质量校验(比如测试通过且测试文件未被修改),第三层是安全兜底(比如最大循环次数限制、单次执行超时限制)。三层缺一不可。

特别注意:永远要设最大循环次数。我遇到过因为一个边界条件判断错误,循环跑了 47 次才被手动中断的情况,那一次烧掉的 token 够我正常用三天。

2.3 上下文管理是循环能否持续的关键

Claude Code 和 Codex 都有上下文窗口限制。在循环里,每一轮都会产生新的对话内容,如果不做管理,几轮之后上下文就爆了,AI 会开始"忘记"之前的指令和约束。

我的做法是在每轮循环结束时,把关键信息(已完成的任务列表、当前状态、下一步要做什么)写入一个独立的状态文件,下一轮开始时只加载这个状态文件和当前要处理的目标,而不是把之前所有对话都带进来。这样既节省了上下文空间,又保证了信息的连续性。

具体来说,我会在项目根目录建一个.loop-state目录,里面放progress.json(记录进度)、errors.log(记录失败案例)、context.md(给 AI 看的当前状态摘要)。每轮循环读写这三个文件,形成闭环。

3. 用 Claude Code 搭建第一个可运行的循环

3.1 环境准备中最容易忽略的细节

Claude Code 的安装本身不复杂,但有几个细节如果没注意到,后面做循环的时候会非常痛苦。

首先是工作目录的问题。Claude Code 默认在启动时的目录下工作,如果你在循环脚本里没有显式指定工作目录,它可能会在错误的路径下操作文件。我的建议是在启动 Claude Code 之前,用cd明确切换到项目根目录,并且在脚本里用绝对路径引用所有文件。

其次是权限配置。Claude Code 在执行文件写入、命令执行等操作时会请求权限。在交互模式下你可以手动确认,但在循环里没人帮你点确认。你需要提前在配置文件里把常用的操作加入白名单。配置文件通常在~/.claude/settings.json,你可以设置允许特定目录下的文件读写和特定命令的执行。

{ "permissions": { "allow": [ "Read:/your/project/path/**", "Write:/your/project/path/**", "Bash(npm test:*)", "Bash(npx tsc:*)" ] } }

这个配置的意思是允许 Claude Code 读写指定项目目录下的所有文件,以及执行 npm test 和 npx tsc 命令。注意不要用通配符放开所有 Bash 命令,那样风险太大。

第三个容易忽略的是模型选择。Claude Code 支持切换不同的模型,在循环场景下我建议用响应速度快的模型做常规任务,遇到复杂问题再切换到更强的模型。频繁切换模型在循环里可以通过配置文件预设,不需要每次手动操作。

3.2 循环脚本的骨架设计

我用的是最朴素的 bash 脚本做外层循环控制,Claude Code 负责内层的具体任务执行。为什么不全部用 Claude Code 自己来做循环?因为外层控制需要确定性的逻辑——判断文件是否存在、检查退出条件、记录日志,这些用脚本做比让 AI 做可靠得多。

脚本的基本结构是这样的:

#!/bin/bash MAX_ITERATIONS=50 ITERATION=0 PROJECT_DIR="/path/to/your/project" STATE_DIR="$PROJECT_DIR/.loop-state" cd "$PROJECT_DIR" while [ $ITERATION -lt $MAX_ITERATIONS ]; do ITERATION=$((ITERATION + 1)) echo "=== 第 $ITERATION 轮循环 ===" # 检查是否还有未处理的任务 REMAINING=$(cat "$STATE_DIR/remaining.txt" | wc -l) if [ "$REMAINING" -eq 0 ]; then echo "所有任务已完成,退出循环" break fi # 取出下一个任务 TASK=$(head -1 "$STATE_DIR/remaining.txt") echo "当前任务: $TASK" # 调用 Claude Code 执行任务 claude --print "请完成以下任务:$TASK。完成后在 $STATE_DIR/result.txt 中写入 SUCCESS 或 FAILED。" \ --allowedTools "Read,Write,Bash(npm test:*)" \ < /dev/null # 检查结果 RESULT=$(cat "$STATE_DIR/result.txt" 2>/dev/null || echo "FAILED") if [ "$RESULT" = "SUCCESS" ]; then # 从待处理列表中移除 tail -n +2 "$STATE_DIR/remaining.txt" > "$STATE_DIR/remaining.tmp" mv "$STATE_DIR/remaining.tmp" "$STATE_DIR/remaining.txt" echo "$TASK" >> "$STATE_DIR/completed.txt" else echo "$TASK" >> "$STATE_DIR/failed.txt" tail -n +2 "$STATE_DIR/remaining.txt" > "$STATE_DIR/remaining.tmp" mv "$STATE_DIR/remaining.tmp" "$STATE_DIR/remaining.txt" fi # 记录日志 echo "[$ITERATION] $TASK -> $RESULT" >> "$STATE_DIR/loop.log" done echo "循环结束,共执行 $ITERATION 轮"

这个骨架的核心逻辑是:从待处理列表取任务 → 交给 Claude Code 执行 → 根据结果更新状态 → 进入下一轮。--print参数让 Claude Code 以非交互模式运行,执行完就退出,适合在脚本里调用。

3.3 让 Claude Code 在循环中可靠工作的提示词设计

在交互模式下,你可以随时补充说明、纠正 AI 的理解。但在循环里,每一轮都是独立的调用,提示词必须一次性把要求说清楚。我总结了一个在循环场景下比较可靠的提示词模板:

你正在一个自动化循环中工作,这是第 N 轮。 当前任务:[具体任务描述] 约束条件: 1. 只修改 [指定范围] 内的文件,不要动其他文件 2. 不要修改任何测试文件本身 3. 如果遇到无法解决的问题,不要尝试绕过,直接标记为 FAILED 4. 完成后必须将结果写入 [结果文件路径] 项目背景: [简要的项目结构说明和技术栈] 请开始执行。

这里面的关键点是"不要尝试绕过"这一条。AI 有个倾向是"想办法完成任务",哪怕这个办法是作弊。比如你让它修 bug 让测试通过,它可能会把测试改了。明确告诉它"解决不了就标记失败"反而能得到更诚实的结果。

另外"项目背景"部分不要写太长,控制在 200 字以内。循环里每轮都要传这些信息,太长会快速消耗上下文。

4. Codex 和 Cursor 在循环工程中的差异化用法

4.1 Codex 的配置文件解析与循环适配

Codex 的配置体系和 Claude Code 不太一样,它更依赖配置文件来定义行为。在循环场景下,你需要重点关注codex.yaml或对应的配置文件中的几个字段。

模型和温度设置直接影响循环的稳定性。温度太高,每轮输出差异大,循环行为不可预测;温度太低,遇到需要灵活处理的情况又容易卡死。我的经验是设在 0.2 到 0.4 之间比较合适,具体取决于任务类型。批量格式化类的任务用 0.2,需要一定判断力的任务用 0.4。

超时设置也很关键。Codex 默认的超时时间在循环场景下可能不够用,特别是处理大文件的时候。建议把单次请求超时设到 120 秒以上,同时在脚本层面也设一个更长的兜底超时。

还有一个容易忽略的是输出格式。在循环里,你需要 Codex 的输出是可解析的。如果让它自由输出自然语言,脚本很难判断执行结果。我的做法是在提示词里要求它输出 JSON 格式的结果:

{ "status": "success", "files_modified": ["src/utils.ts"], "message": "修复了类型错误" }

这样脚本可以直接用jq解析,判断逻辑非常清晰。

4.2 Cursor 在循环中的定位差异

Cursor 和 Claude Code、Codex 有个本质区别:它是一个 IDE,核心交互界面是编辑器。这让它在循环工程里的角色不太一样。

Cursor 更适合做"人在环路中"的半自动循环。比如你可以用 Cursor 的 Composer 功能批量处理多个文件,但每一步你都能看到 diff、决定是否接受。这种模式不适合完全无人值守的循环,但适合那些需要人工判断但又想提高效率的场景。

如果你确实想把 Cursor 纳入自动循环,可以通过它的命令行工具或者 API 来实现,但灵活性和稳定性不如 Claude Code 和 Codex。我的建议是:全自动循环用 Claude Code 或 Codex,需要人工审核的半自动流程用 Cursor。

另外提一下 Cursor 的中文设置问题,很多人搜"cursor怎么设置中文回复",其实在设置里找到 AI 相关的语言选项就能改。但这个对循环工程影响不大,因为循环里的提示词是你自己写的,用什么语言取决于你的提示词。

4.3 三个工具在循环场景下的对比

维度Claude CodeCodexCursor
非交互模式支持原生支持--print支持 API 调用有限支持
文件操作能力强,支持批量读写强,支持批量读写强,但需人工确认
上下文管理自动压缩需手动管理自动管理
循环适配度高高中
权限控制配置文件白名单配置文件界面确认
适合场景全自动循环全自动循环半自动循环

这个对比不是绝对的,实际选择还要看你的具体任务和已有工具链。我自己的主力方案是 Claude Code 做全自动循环,Cursor 做需要人工判断的部分。

5. 循环工程实战:批量给项目补单元测试

5.1 任务拆解与状态文件设计

拿一个真实的例子来说。我有一个 TypeScript 项目,大概 80 多个源文件,其中只有不到 20 个有对应的单元测试。我想把剩下的都补上。手动做的话,每个文件从读代码到写测试到跑通,平均要 10 分钟,80 个文件就是 13 个小时。做成循环的话,我只需要前期花 1 小时设计好流程,后面让它自己跑。

第一步是拆解任务。每个文件就是一个独立的处理单元,任务列表就是所有没有测试的源文件路径。我用一个简单的脚本生成这个列表:

find src -name "*.ts" ! -name "*.test.ts" ! -name "*.d.ts" | while read f; do test_file="${f%.ts}.test.ts" if [ ! -f "$test_file" ]; then echo "$f" fi done > .loop-state/remaining.txt

状态文件的设计前面提过了,这里具体说一下progress.json的结构:

{ "total": 63, "completed": 12, "failed": 2, "current": "src/services/auth.ts", "started_at": "2024-01-15T10:30:00Z", "last_updated": "2024-01-15T11:45:00Z" }

这个文件每轮更新一次,一方面方便我随时查看进度,另一方面如果循环中断了,下次可以从断点继续。

5.2 单轮任务的提示词与执行细节

针对"给一个源文件写单元测试"这个任务,我的提示词是这样的:

你正在自动化循环中工作。当前任务:为 src/services/auth.ts 编写单元测试。 要求: 1. 测试文件路径为 src/services/auth.test.ts 2. 使用项目已有的测试框架(Jest)和测试工具库 3. 覆盖该文件所有导出函数的正常路径和边界情况 4. 不要修改源文件本身 5. 写完后运行 npx jest src/services/auth.test.ts 验证 6. 如果测试不通过,分析原因并修复测试代码(不是源文件) 7. 最多尝试修复 3 次,3 次后仍不通过则标记为 FAILED 项目信息: - TypeScript + Jest - 测试文件放在源文件同目录下 - mock 使用 jest.mock 完成后将结果写入 .loop-state/result.txt,内容为 SUCCESS 或 FAILED。

这里有几个细节值得展开说。第一,"不要修改源文件"这条约束非常重要。AI 在测试跑不通的时候,第一反应往往是去改源文件让它"好测试",这完全违背了写测试的初衷。第二,"最多尝试修复 3 次"是防止在某个文件上无限循环。第三,明确指定测试框架和 mock 方式,避免 AI 自己发挥用了不兼容的方案。

5.3 实测中的意外情况与处理

实际跑起来之后遇到了几个预料之外的问题。

第一个是有些文件的依赖太复杂,AI 在写 mock 的时候会陷入死循环——mock A 需要 mock B,mock B 又依赖 A。这种情况 AI 会反复尝试不同的 mock 方案,每次都在 3 次修复限制内失败,然后标记 FAILED。我后来在提示词里加了一条:"如果文件的依赖关系超过 5 个外部模块,直接标记为 SKIPPED,不要尝试写测试。"这样把这类文件单独拎出来人工处理,不阻塞循环。

第二个问题是测试通过但质量很差。AI 为了让测试通过,写了很多"断言 1 等于 1"这种没有意义的测试。我在循环结束后加了一个检查步骤:统计每个测试文件的断言数量和覆盖率,低于阈值的标记出来人工复查。这个检查用脚本做就行,不需要 AI 参与。

第三个问题是 token 消耗比预期高。80 个文件跑完花了大概 400 万 token,比我预估的多了一倍。主要原因是有些文件的测试修复过程反复了好几轮。后来我优化了提示词,把"最多修复 3 次"改成"最多修复 2 次",并且要求 AI 在第一次修复失败后就输出详细的错误分析,这样即使最终失败,我也能快速人工接手。

6. 循环工程中那些文档不会告诉你的经验

6.1 日志设计决定了你排查问题的速度

循环跑起来之后,你最常做的事情就是看日志。日志设计得好不好,直接决定了你排查一个问题要花 5 分钟还是 50 分钟。

我的日志分三层。第一层是循环级别的日志,记录每轮的开始时间、任务内容、结束时间、结果状态,格式是一行一条,方便用 grep 快速过滤。第二层是任务级别的日志,记录单个任务执行过程中的关键节点,比如"开始读取文件""生成测试代码""第一次运行测试失败""分析失败原因""修复后重试成功"。第三层是 AI 交互级别的日志,完整记录每轮发给 AI 的提示词和 AI 的原始输出,这个只在排查疑难问题时才看。

三层日志分别存在不同文件里,第一层是loop.log,第二层是task-{id}.log,第三层是raw-{id}.log。日常只看第一层,有问题看第二层,还搞不定才看第三层。

一个实用技巧:在日志里给每轮循环加一个唯一 ID,所有层级的日志都带上这个 ID。这样你可以用一条命令把所有相关日志串起来看。

6.2 失败处理策略比成功路径更重要

循环工程里,成功路径其实很简单——任务完成、检查通过、进入下一个。真正复杂的是失败处理。我总结了三种失败类型和对应的处理策略。

可重试失败:比如网络超时、临时性的命令执行失败。这类失败直接重试就行,但要有重试次数上限,我一般设 2 次。

需修复失败:比如测试不通过、类型检查报错。这类失败需要 AI 分析原因并修复,修复次数也要有上限,我一般设 2 到 3 次。

不可恢复失败:比如文件不存在、依赖缺失、权限不足。这类失败重试多少次都没用,直接标记失败并跳过,记录到待人工处理列表。

关键是要在循环脚本里能区分这三种类型。我的做法是让 AI 在结果文件里不只写 SUCCESS/FAILED,而是写具体的状态码:SUCCESS、RETRY、FIXED、UNRECOVERABLE。脚本根据不同的状态码走不同的分支。

6.3 什么时候应该停下来人工介入

全自动循环听起来很美好,但实际上有些情况你必须停下来人工介入,否则会越跑越偏。

当连续失败次数超过阈值时,比如连续 5 个任务都失败了,说明可能是环境出了问题或者任务设计有问题,继续跑只是浪费资源。当单轮执行时间异常长时,比如某个任务跑了 10 分钟还没结束,很可能是 AI 陷入了某种循环,需要人工看看它在干什么。当 token 消耗速度异常时,比如平时每轮消耗 5 万 token,突然有一轮消耗了 50 万,肯定有问题。

我在脚本里加了这几个监控点,触发任何一个就暂停循环并发送通知。通知方式可以用简单的邮件或者在终端输出醒目的提示,看你自己的习惯。

7. 从单机循环到可持续的工程实践

7.1 把循环脚本纳入版本管理

一开始我觉得循环脚本就是个临时工具,没必要纳入 git。后来改了几次脚本之后发现,没有版本管理根本记不住哪个版本改了什么、为什么改。而且循环脚本和项目代码其实是有耦合的——脚本里的路径、命令、约束条件都跟项目结构相关,项目变了脚本也得跟着变。

现在我的做法是在项目里建一个.loop/目录,把循环脚本、提示词模板、状态文件结构定义都放在里面,纳入 git 管理。状态文件本身(remaining.txt、progress.json这些)加到.gitignore里,因为它们是运行时数据,不需要版本管理。

提示词模板单独抽出来放在.loop/prompts/目录下,每个任务类型一个模板文件。这样修改提示词不需要动脚本,而且可以很方便地对比不同版本提示词的效果。

7.2 循环的复用与参数化

当你为某个项目写好一套循环之后,很自然会想把它用到其他项目上。这时候就需要做参数化。

我把循环脚本里所有跟具体项目相关的部分都抽成了变量,放在一个config.sh文件里:项目路径、测试命令、源文件匹配模式、结果文件路径等等。换项目的时候只需要改这个配置文件,脚本本身不用动。

提示词模板里的变量用占位符表示,比如{{FILE_PATH}}、{{TEST_COMMAND}},脚本在执行前用sed或者envsubst替换成实际值。这样同一套模板可以适配不同的项目。

7.3 持续优化循环效率的几个方向

循环跑通之后,下一步就是优化效率。我实践下来有几个方向效果比较明显。

减少每轮的上下文加载量。前面提过用状态文件代替完整对话历史,这是最有效的一招。另外提示词里只放当前任务需要的信息,不要把整个项目的说明都塞进去。

合理设置任务粒度。任务拆得太细,循环轮数多,每轮的开销累加起来很可观;任务拆得太粗,单轮失败的影响面大,重试成本高。我的经验是每个任务的处理时间控制在 1 到 3 分钟比较合适。

利用缓存。有些操作的结果在短时间内不会变,比如读取项目配置、检查依赖版本,这些可以在第一轮做完之后缓存起来,后续轮次直接读缓存。

批量处理相似任务。如果连续几个任务都是同一类型的(比如都是给工具函数写测试),可以把它们合并成一轮,让 AI 一次性处理多个,减少交互开销。

这套 Loop Engineering 的方法我从去年开始在自己的项目里用,从最初的磕磕绊绊到现在基本能稳定跑完几百个任务的循环,中间踩的坑确实不少。但每次解决一个问题,整套流程就可靠一分。现在对我来说,遇到批量性的重复任务,第一反应已经不是"手动做要多久",而是"这个能不能做成循环"。这个思维方式的转变,可能比具体的技术方案更有价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询