
在LLM应用开发里“Loop Engineering”这个词是越来越常听到了。我第一次接触到这个概念时还以为是某种写while循环的工程化技巧后来真正上手做了一个项目才发现它说的是另一件事让大模型在一个闭环反馈里不断自我修正、自我迭代把“一次性生成”变成“生成→评估→反思→再生成”的持续逼近过程。这篇就用一个完整的实战项目来拆透Loop Engineering把原理、参数设计、完整代码、以及我自己踩过的那些坑一次讲清楚。不管你是刚接触LLM开发的新手还是已经在做Agent落地的工程师这篇都可以直接用。1. Loop Engineering 到底在解决什么问题1.1 从“一次生成”到“迭代生成”先想一个最简单的场景你让LLM写一段代码它一次写对了皆大欢喜但它一旦写错你怎么办普通用户的做法是重新生成一次或者把错误信息贴进去让它改。这其实已经是“循环”的雏形了只是没有系统化。而Loop Engineering做的事情就是把这件事从“出了问题再补救”变成“从一开始就设计一个必然包含纠错环节的流程”。我自己的感受是它本质上是把“结果的不确定性”转换成“过程的可控性”。单次生成是一个高方差事件LLM这次可能写出90分的结果下次可能只有60分。但如果你让它写一轮、评一轮、改一轮五轮之后结果通常会稳定在一个相对高的分数区间。这个现象在写代码、写文案、做数据清洗、跑结构化抽取时都成立。Loop Engineering就是把这种“多轮逼近”的做法固化成工程范式。为什么不直接拿高分的那次呢因为你在生成之前并不知道哪次会得高分。循环的意义在于你可以用“评估”来主动筛选和修正而不是靠运气。打个比方单次生成像闭着眼睛投篮Loop Engineering则是每投一次都看一眼偏了多少然后调整姿势再投——哪怕初始水平一般最后也能越投越准。1.2 循环的四步闭环Plan → Act → Observe → RefineLoop Engineering有一个非常标准的四步闭环几乎所有应用都是这四个环节的组合变体Plan规划设定任务目标、定义成功标准、拆解执行策略。在代码生成场景里Plan可能是“理解需求→列出要实现的函数→确定边界条件”。Act执行让LLM产出一次结果也就是实际动手做。Observe观察对结果进行评估收集反馈。这里可以靠代码运行、单元测试、规则校验、另一个LLM打分或者人工检查。Refine反思与修正把Observe得到的反馈喂给模型让它指出问题、改进方案生成下一个版本。这个闭环跟传统的while循环最大的不同点是循环体里的“状态”是语义化的不是数字化的。普通循环靠i n这种条件来控制Loop Engineering靠的是“当前版本有多好”“还差在哪里”这类语义判断。所以它不能简单地翻译成一段Python代码而是需要设计一套“让模型能自我纠错”的上下文结构。一个很容易犯的错误是把Loop Engineering当成简单的“把错误信息拼接进Prompt再生成一次”。实际上如果观察环节没有给出结构化的、定位准确的反馈模型大概率会在同一个错误上反复打转甚至“越改越错”。所以Observe和Refine这两个环节的设计质量决定了整个循环的上限。2. 搭循环之前先搞清楚这五个关键设计点2.1 终止条件循环什么时候停循环最怕的就是停不下来。Loop Engineering里终止条件一般有三种我建议按不同场景组合使用终止策略适用场景优点风险固定最大轮数成本敏感、任务简单成本可预测可能没改到位就停了评分阈值有明确评估指标质量有保障阈值定高了永远达不到收敛判断长期迭代任务不会浪费预算容易把“震荡”误判成收敛我自己最常用的是**“评分阈值 最大轮数 连续两轮无提升就提前停”**的三重保险。比如设最大五轮如果某轮得分超过8.5分就立刻结束如果连续两轮的得分差小于0.2就认为收敛了提前退出。这样既保证质量又防止模型在一个问题上钻牛角尖烧掉几十美元的token。2.2 状态管理每轮之间传什么不传什么状态管理是Loop Engineering里最容易翻车的地方。很多人第一反应是把所有历史消息全都塞给模型觉得“上下文越多越聪明”但实际效果恰恰相反。上下文一旦超过模型的有效注意力窗口模型反而会忽略关键信息尤其是在长对话中更容易出现“遗忘早期反馈”的问题。我用的做法是每轮只维护四个东西当前最优版本、最近一轮的评估报告、上一轮的反思结论、以及原始任务描述。不需要把每一轮的完整代码都传回去——那是LLM App的日志系统该干的事不是Prompt该干的事。另外还需要一个显式的best_version状态。因为常有这种情况第三轮改出了85分的版本第四轮又改成82分了。循环不能“越改越回去”所以每一轮开始前都要先对比一下如果当前轮评分低于历史最高分就把最高分版本作为下一轮的基础而不是拿着最新但更差的版本继续改。2.3 评估器循环的地基如果让我排Loop Engineering里各环节的重要性评估器排第一反思器排第二生成器反而是最不重要的。原因是评估器决定了模型能看到什么样的反馈而反馈的质量决定了修正的质量。评估器给的是模糊信息循环就只能在模糊里面绕圈圈。评估器分三个层次规则层最便宜、最稳定。代码场景下就是跑测试用例、查编译错误、做lint检查文本场景下就是字数校验、格式校验、关键词出现率检查。模型层用另一个LLM当裁判按你给定的评分卡打分。一个小技巧是不要让它只打一个总分要让它分段评分比如代码的“正确性”“健壮性”“可读性”分开给分因为总分对Refine几乎没有指导作用分项分数才能告诉模型“你到底差在哪里”。人工层最贵但最准。适合放在最终出口作为上线前的最后一道把关而不是每一轮都人工参与。2.4 反思指令怎么让模型“真的改对”Refine的质量很大程度取决于你怎么写反思指令。我发现一个有效的三段式结构指出问题要求模型列出当前版本的所有缺陷按严重程度排序并且必须引用评估报告里的具体信息作为佐证。给出理由要求模型解释为什么会出现这些问题——是为了修复一个测试而引入了另一个bug还是漏掉了边界条件提出修改方案要求模型列出“具体改哪一行”“新增什么逻辑”“删除哪段代码”先列计划再动手改。这个三段式的关键作用是强迫模型在动手之前先想清楚。如果不加这个约束很多模型一拿到反馈就直接重写整个答案不仅改掉了问题还把原本对的实现也改坏了。有了“先分析、再动手”的约束修改的针对性和稳定性都会高很多。2.5 成本与延迟预算Loop Engineering最大的代价不是技术复杂度而是成本和延迟。一轮完整循环至少需要2次LLM调用生成1次评估1次加上反思往往还要再调1次一轮就是3次调用。五轮就是15次。按GPT-4o级别模型的价格一个完整循环的token消耗大概是单次生成的5~8倍——这个账必须提前算清楚。我会在项目初期就把预算写进代码里比如设置max_rounds、每轮token数上限以及一个全局的total_budget参数。如果总消耗超过预算循环就直接终止并返回当前最优结果。你可能会觉得这会让质量打折但项目实战里“在预算内给出最好的结果”比“追求完美但超支”要务实得多。3. 保姆级项目实战做一个自我改进的代码生成器3.1 项目结构与依赖下面做一个具体的项目让LLM写一个Python函数并通过循环反馈自动修复代码里的bug。目标函数是一个带边界处理的列表去重函数要求保持元素相对顺序且能处理非可哈希类型——这个需求有足够多的边角条件单次生成很容易写漏。我会把LLM调用抽象成一个接口你在本地可以替换成任何厂商的SDK。项目结构非常简单loop_engineering_demo/ ├── llm_client.py # LLM调用封装按需替换 ├── evaluator.py # 评估器编译检查 单元测试 ├── reflector.py # 反思器把评估结果转成修改建议 ├── loop_engine.py # 主循环逻辑 └── main.py # 入口依赖只有一个你自己常用的LLM SDK。我用一个基类来做抽象这样后面换模型或者换厂商都只改一个文件。3.2 核心数据结构LoopState循环里的状态用一个dataclass来管理这是整个项目里最重要的数据结构。它承载了前面2.2里说的所有状态信息from dataclasses import dataclass, field dataclass class LoopState: task: str # 原始任务描述 best_code: str # 历史最高分代码 best_score: float 0.0 # 历史最高分 current_code: str # 当前轮代码 current_report: str # 当前轮评估报告 last_reflection: str # 上一轮反思结论 history: list field(default_factorylist) # 每轮得分记录 rounds: int 0 # 当前轮数 def update_best(self) - bool: 如果当前版本得分更高更新历史最优。返回是否有提升。 if self.rounds 1: return True return False # 实际实现里用评估分数来比较注意history只用来记录和输出日志不参与下一轮的Prompt拼接。这个设计是我被坑过之后才改过来的——之前把整个history都塞给模型结果上下文一长反馈质量就明显下降。3.3 生成器写初稿生成器本身很简单就是让LLM针对任务生成代码。但在保姆级教程里Prompt的结构我会拆开讲。第一轮生成时模型没有任何反馈信息所以Prompt里只需要任务描述和输出格式约束。到第二轮以后就不走生成器了而是走“反思修正器”——让它拿到上一轮的代码和评估报告产出修改后的代码。实际上第一轮也可以用反思修正器来处理只是把评估报告设为空。def generate_initial_code(llm, task: str) - str: system_prompt ( You are a senior Python engineer. Write correct, robust, and readable code. Mind edge cases. ) user_prompt fTask: {task}\n user_prompt Output ONLY the function code, no explanations. return llm.chat(system_prompt, user_prompt)这里一个实用的细节是要求模型只输出函数代码不要任何解释、不要markdown代码块包裹。这样解析起来不会有额外的干扰也减少了模型在“解释代码”上浪费token。实测下来这个约束能让后续的编译检查更干净避免把markdown围栏也当作代码内容。3.4 评估器用测试用例和静态检查打分这个项目的评估器我设计了三个维度总共10分编译检查2分能用ast模块解析且没有语法错误直接给2分否则0分。功能测试7分跑一组单元测试用例每个测试通过得1/7分。风格检查1分用几个简单的正则规则检查是否包含类型注解、是否有清晰的函数名。为什么功能测试占7分因为在代码生成场景里“能跑对”是最核心的诉求。但只测功能也不行否则模型可能生成一段能过测试但极其丑陋的代码所以我留了1分给风格用来自动化地“劝它写好一点”。import ast from typing import Callable def evaluate_code(code: str, tests: list[dict]) - dict: report {compile: 0.0, tests: 0.0, style: 0.0, total: 0.0, details: []} # 1. 编译检查 try: ast.parse(code) report[compile] 2.0 except SyntaxError as e: report[details].append(fSyntaxError: {e.msg} at line {e.lineno}) report[total] report[compile] report[tests] report[style] return report # 2. 功能测试 # 把模型生成的代码注入命名空间动态执行测试 namespace {} exec(code, namespace) func namespace.get(dedupe) passed 0 for case in tests: try: result func(case[input]) if result case[expected]: passed 1 else: report[details].append( fFailed test: input{case[input]}, fexpected{case[expected]}, got{result} ) except Exception as e: report[details].append( fRuntime error on input {case[input]}: {e} ) report[tests] 7.0 * passed / len(tests) # 3. 风格检查 if def dedupe in code and : in code: report[style] 1.0 report[total] report[compile] report[tests] report[style] return report这里有一个关键处理测试用例在循环中要保证确定性。不要在测试里用随机数据或者至少固定随机种子否则评估结果会抖动模型会收到前后矛盾的反馈循环就很难收敛了。3.5 反思器把评估结果变成修改建议反思器是整个循环的灵魂。我前面讲到三段式结构这里落到Prompt里就是这样def reflect(llm, task: str, code: str, report: dict) - str: system_prompt ( You are a code reviewer. Analyze the code and the evaluation report. Produce a concise reflection in THREE sections:\n 1. BUGS: list every concrete defect with evidence from the report.\n 2. CAUSES: explain the root cause of each defect.\n 3. FIX PLAN: list step-by-step changes you will make.\n Be specific. Do NOT rewrite the code yet. ) user_prompt fTask:\n{task}\n\nCurrent code:\n{code}\n\nEvaluation report:\n{report} return llm.chat(system_prompt, user_prompt)注意反思器只负责输出反思结论不负责直接生成代码。这一步很多人觉得多余实际上它是防“瞎改”的关键。实测下来让模型先反思再修改比直接让它“根据错误信息修改”的修正准确率高出不少。因为“反思”强制它进入了分析模式而不是复制粘贴模式。得到反思结论之后再把它交给修正器去改代码def revise(llm, task: str, code: str, reflection: str) - str: system_prompt ( You are a senior Python engineer. Rewrite the code according to the reflection. Keep everything that already works. Output ONLY the complete function code. ) user_prompt ( fTask: {task}\n\nCurrent code:\n{code}\n\n fReviewers reflection:\n{reflection}\n\n Output ONLY the complete revised function code. ) return llm.chat(system_prompt, user_prompt)两个小细节第一Prompt里强调“Keep everything that already works”这是为了减少模型把本来写对的部分也推翻重写的概率第二每次都要求输出完整函数而不是补丁不然后续解析和编译检查会非常麻烦。3.6 主循环与停止策略最终的主循环把上面几个组件串起来加上终止条件控制def run_loop(llm, task: str, tests: list[dict], max_rounds5, threshold9.0, min_improve0.2) - LoopState: state LoopState(tasktask) # 第 1 轮初稿生成 state.current_code generate_initial_code(llm, task) report evaluate_code(state.current_code, tests) state.rounds 1 state.best_code, state.best_score state.current_code, report[total] state.history.append(report[total]) print(fRound 1: score{report[total]:.2f}) # 第 2..N 轮反思 - 修正 - 评估 while state.rounds max_rounds: prev_score state.history[-1] state.current_report report reflection reflect(llm, task, state.current_code, report) state.last_reflection reflection new_code revise(llm, task, state.current_code, reflection) state.current_code new_code report evaluate_code(new_code, tests) state.rounds 1 state.history.append(report[total]) print(fRound {state.rounds}: score{report[total]:.2f}) # 更新历史最优 if report[total] state.best_score: state.best_code new_code state.best_score report[total] # 终止条件组 if report[total] threshold: print(Reached threshold.) break if state.rounds max_rounds: print(Reached max rounds.) break if abs(report[total] - prev_score) min_improve: print(Converged (no significant improvement).) break return state这个循环跑起来最基本的预期是第一轮可能有40%~70%的分数然后每轮上升在3~5轮内收敛到80%~95%。我自己实测时常见轨迹是Round 1: 5.14→Round 2: 7.86→Round 3: 8.14→Round 4: 9.00达到阈值停。但有时候也会出现7.86 → 7.86 → 7.86的原地踏步这种时候基本就是评估器反馈信息不够明确或者模型在逃避问题——第四部分我会讲怎么解决。4. 跑完这个 Loop 之后我踩过的五个坑4.1 最大坑反思式幻觉我遇到的最诡异的现象是模型在反思环节写了一大堆“我发现了三个问题”但代码改完之后测试结果还是跟原来一模一样。为什么因为模型在反思时“脑补”了问题但真正改代码的时候并没有落实修改。这种“反思式幻觉”在弱模型上尤其明显。对策分两条第一反思结论里要求引用评估报告中的原始信息作为证据禁止凭空创造问题第二修正之后如果发现代码根本没变化可以加一个“变更检查”——对比新旧代码的字符级diff如果差异太小比如少于10个字符就强制再跑一轮反思并且把“代码没有实际变化”这个事实作为反馈喂给模型。4.2 分数震荡不等于没效果有时候历史记录会是6.0 → 8.5 → 7.2 → 9.0。这是正常的因为LLM的修正过程本来就不是单调递增的。模型可能在第三轮为了修一个边角条件把一个原本正确的核心逻辑改坏了导致分数下降。我在2.2里提到的best_version机制就是专门为这个情况设计的。每一轮结束都更新历史最优最终返回的永远是最优版本而不是最后一轮版本。循环可以震荡但产出结果只看best_code——这让整个系统有了“容错”能力。另外如果你发现震荡幅度太大往往说明评估器的测试用例覆盖不够全模型在“按A修则B坏按B修则A坏”的怪圈里打转这时候应该去补评估器的用例而不是增加轮数。4.3 上下文塞满导致注意力稀释我最早跑这个项目时为了保留完整“决策轨迹”把每一轮的代码、评估报告、反思结论全都拼进下一轮Prompt。结果到了第四轮模型开始忽略最新的评估报告反而去纠结第二轮的旧问题——因为旧内容占了上下文的一半。后来我改成严格只传“原始任务 当前代码 最近一轮的评估报告 最近一轮反思”效果立竿见影。你可以把Loop Engineering里的对话想成一个“工作台”而不是一个“档案库”工作台上只需要放当前正在处理的材料和工具归档文件应该放到数据库/日志里。这个原则也适用于多数Agent类应用。4.4 评估器设计太严或太松评估器太松模型很快达标但结果一上生产就出问题评估器太严模型永远达不到终止阈值白白烧掉大量token。最经典的例子是测试用例里要求输出JSON格式但模型返回的是一个Markdown代码块包裹的JSON于是所有用例都失败——这种情况属于评估器过于“死板”不是模型能力问题。我的建议是分阶段评估第一轮只跑“烟雾测试”函数是否能调用、基本输入是否能跑通第二轮以后跑完整测试集。这样既不会第一轮就因为格式问题直接判死也能在后续迭代里逐步收紧要求。另外一个原则是评估器的标准必须跟最终使用场景对齐如果实际使用中能容忍某些非关键问题就不要把它放进测试用例。4.5 成本失控是必然的预算要提前算说句实在话Loop Engineering跑起来之后你才会真正理解什么叫“token燃烧”。我用一个不算复杂的任务实测过单轮生成评估反思大约消耗6千~1.2万token四轮下来就是3万~5万token。如果用的是中等价位模型一个函数生成任务的实际成本可能是单次生成的6~10倍。所以我现在做Loop Engineering项目一律在代码里加三个成本闸门单轮token上限比如max_tokens_per_round3000全局轮数上限max_rounds通常设3~5总预算开关budget_usd用token数乘单价实时估算超了就强制终止这三个闸门写起来很简单但能防止“某次跑批跑了一夜”的惨案。成本控制不应该靠感觉应该在代码里变成硬约束。5. 从玩具到生产Loop Engineering 的工程化清单5.1 状态落盘与断点恢复Loop跑一次少则几十秒多则几分钟如果一个进程崩了就要全部重来项目小的时候能忍生产环境不能忍。我的做法是在每一轮结束后把LoopState序列化成JSON落盘文件名带上任务ID和轮数。def save_checkpoint(state: LoopState, path: str) - None: with open(path, w, encodingutf-8) as f: json.dump({ task: state.task, best_code: state.best_code, best_score: state.best_score, current_code: state.current_code, current_report: state.current_report, history: state.history, rounds: state.rounds, }, f, ensure_asciiFalse, indent2)为什么只序列化这几个字段因为前面2.2已经讲过了last_reflection虽然有价值但在下一轮Prompt里它会以新内容重新生成不需要完整保存。断点恢复时只要读回JSON构造一个新的LoopState从state.rounds 1继续跑就行。这在批量任务里能省下大量重跑成本。5.2 全过程可观测性Loop Engineering是黑盒迭代过程如果只看最终结果出了问题根本无从排查。我在生产环境里会为每一轮记录这么几条信息round第几轮score_before/score_after评估分数变化prompt_tokens/completion_tokens消耗量reflection_excerpt反思的文本摘要diff_stat新旧代码的差异行数这些数据可以落到日志文件也可以写入数据库。它们的价值在于你能一眼看出“这个任务为什么收敛不了”——是模型一直在改同一个地方diff_stat很小还是评估器本身就矛盾分数反复横跳还是某个Prompt环节产生了幻觉。没有日志Loop就是一团迷雾有日志Loop就是一个可以被调优的普通软件。5.3 缓存、降级与人工护栏最后讲三条生产级经验。第一缓存评估结果。同一份代码只有在对应版本的测试用例下评估一次就够了不要每轮都重复跑同样的测试。可以用代码的哈希值做缓存键存储评估报告省下的不是token而是CPU/API调用次数。第二降级策略。如果LLM调用连续失败比如限流或者超时不要死等默认返回best_code并标记为“未完成”。在Agent系统里卡在某个循环里空转比给出一个不完美结果更危险。超时时间、重试次数都要设置成显式参数。第三人工护栏。Loop产出的最终结果在进入生产环境之前最好有一道人工或半自动审核。代码场景里我习惯在循环跑完后再让一个独立模型从“反向”角度审查一遍代码有没有隐藏问题——类似于让一个“杠精”挑毛病——然后再决定是否放行。这东西不能完全替代人但在大多数低风险场景里已经能拦住绝大多数低级错误。做一个Loop Engineering项目的完整流程现在回想起来最核心的收获其实不是“模型写代码变得多准”而是我意识到“让模型自我纠错”这件事本身是可以被工程化的。它不是靠运气而是靠一套清晰的反馈闭环评估器给方向反思器给计划修正器给执行终止条件给边界。最后再分享一个小技巧如果你刚开始接触Loop Engineering建议先用一个你完全清楚“正确答案”的小任务来练手比如生成一个排序函数、格式化一段JSON。因为只有你自己知道正确答案你才能快速判断循环里的哪一环出了问题——是模型菜还是评估器反馈写得烂还是终止条件设得太苛刻。先在小任务上把循环跑通、把日志看明白再上真实业务场景你会少掉无数头发。