ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Loop Engineering实战:用Claude Code和Codex构建AI编程自动循环工作流

Loop Engineering实战:用Claude Code和Codex构建AI编程自动循环工作流 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_ITERATIONS50 ITERATION0 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 Bmock 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 的方法我从去年开始在自己的项目里用从最初的磕磕绊绊到现在基本能稳定跑完几百个任务的循环中间踩的坑确实不少。但每次解决一个问题整套流程就可靠一分。现在对我来说遇到批量性的重复任务第一反应已经不是手动做要多久而是这个能不能做成循环。这个思维方式的转变可能比具体的技术方案更有价值。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进