
1. 先搞清楚没有Loop的Agent只是一次性的演员1.1 单轮调用为什么撑不住真实任务我先问一个很基础的问题你拿大模型API输入帮我分析这份财报并生成一份报告模型是不是真的会去查财报不会。它只是根据训练记忆里见过的财报长什么样给你编一份出来。这就是一次性的演员——拿到Prompt演一段谢幕。真实任务不一样。真实任务包含状态变化你要先读文档、再查资料、中间发现某个数据缺失、需要换个来源补充、最后还要检查报告有没有算错。这串动作不是一条直线而是一个循环干一步、看结果、根据结果决定下一步。那个根据结果决定下一步的动作一旦反复出现你的代码就从调用一次模型变成了跑一个循环。而Loop Engineering要研究的就是怎么把这个循环设计得稳定、可控、省钱、还能收敛。很多同学照着教程搭LangGraph节点画得漂漂亮亮一跑起来就失控本质上是没把循环当成工程对象来对待而是当成一个多调几次接口的拼凑。1.2 Loop的四要素感知、决策、执行、观测任何Agent循环抽象到最后就是四个环节感知把当前状态输入给模型。可以是用户任务、上次工具返回的结果、环境报错等。决策模型根据当前状态选择下一步动作。可能是调用工具也可能是直接给出最终答案。执行如果选择了工具调用就真正执行那个工具搜网页、跑代码、查数据库。观测把工具的执行结果作为新的状态塞回上下文进入下一个循环。这是标准的感知-决策-执行-观测闭环。你去看LangGraph、CrewAI、AutoGen这些框架不管套了多少层壳核心都是这个闭环。学习的时候不要被框架API迷惑先在自己的代码里徒手实现一遍这个循环你对循环工程的体感会完全不一样。框架解决的是怎么画图、怎么管理状态但循环的质量——终止条件、上下文预算、错误恢复——这些真正决定项目生死的东西框架帮不了你得靠你自己在工程层面设计。1.3 开环和闭环的直观对比为了说明闭环的价值我做个最简单的对比实验同样一个任务根据订单表写一份周度销售分析开环让模型直接写闭环让模型先查表结构、再查询数据、最后根据真实数据写报告。维度开环单次调用闭环循环调用任务直接写分析先查表结构再生成并自测幻觉风险高会编字段名和数据低基于真实数据生成可恢复性出错就结束可重试、可换策略成本低高每次循环都计费适用场景摘要、翻译、分类数据问答、爬虫整理、代码生成、多步推理一句话总结开环适合模型一次性就能完成的任务闭环适合需要多次访问外部信息的任务。大部分Agent项目里让人头疼的问题比如模型自己编数据结果不稳定根源都是该闭环的时候用了开环。反过来说也有该开环的时候上了闭环的本来一个分类任务几秒钟出结果非要套个五轮循环纯属给自己添堵。循环工程的第一步不是选框架是判断你的任务到底需不需要循环。2. 四种常用循环模式的拆解与选型2.1 基础循环while 工具调用的最小实现先徒手实现最朴素的循环不依赖任何框架。模型返回一段原始文本你检查里面有没有ACTION:这样的约定标记有就执行对应工具没有就当成最终答案结束。这个模式我私下管它叫裸循环存在的意义不是拿来生产而是让你理解循环的最小骨架——一旦你手写过这个再看任何Agent框架的节点-边模型都能一眼看穿它底层在干什么。import json from openai import OpenAI client OpenAI() SYSTEM_PROMPT 你是一个数据助手。如果需要查询数据请输出: ACTION: 函数名 参数JSON 如果已经拿到答案请直接输出最终答案。 def run(task: str, tools: dict, max_steps: int 8) - str: messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: task} ] for step in range(max_steps): resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages, temperature0 ) text resp.choices[0].message.content if text.startswith(ACTION:): _, func_name, args_json text.split(\n, 2) args json.loads(args_json) result tools[func_name](**args) # 真正执行工具 messages.append({role: user, content: f工具返回: {result}}) else: return text return 达到最大步数强制结束这个例子刻意没用任何框架也刻意没用Function Calling。你会发现一个关键事实循环能不能走通主要取决于模型能不能稳定输出ACTION:这种约定格式。如果模型哪天抽风不按格式输出你的循环要么打转要么误判结束。这就是为什么后续出现了Function Calling、Tool Calling——把动作结构化从模型的自由文本里抽出来变成API层面的强约束。但即便有了强约束循环设计和终止控制依然是独立的一层问题它不是API帮你解决的。2.2 ReAct循环思考一步、行动一步的经典范式裸循环最大的问题是模型只会无脑执行工具缺少推理过程。ReActReasonAct说白了就是在每一步循环里让模型同时输出思考和行动把我为什么要做这个动作放进上下文后续步骤再决策时模型就能参考之前的推理链条。这个模式是2022年底到2023年那一批Agent框架的标配现在虽然被Function Calling盖过了风头但理解它对理解循环工程至关重要。ReAct的经典输出结构长这样Thought: 用户想知道上周的销售额这需要先查订单表。 Action: query_database {sql: SELECT SUM(amount) FROM orders WHERE created_at now() - interval 7 days} Observation: 34567.8 Thought: 拿到了销售额还需要对比上上周的数据。 Action: query_database {sql: SELECT SUM(amount) FROM orders WHERE created_at now() - interval 14 days AND created_at now() - interval 7 days} Observation: 31233.2 Thought: 两个数据都拿到了可以写结论了。 Final Answer: 上周销售额34567.8环比前一周增长10.7%。代码层面ReAct和裸循环几乎一样区别只是Prompt要求模型输出Thought/Action/Observation三段。我建议每个做Agent的人都手写一次ReAct原因很简单它让你直观看到模型在循环里怎么想和怎么动调试时可观测性最好。等你有了这个体感再上LangGraph这类把它封装成AgentNode的框架出了问题你也能猜到内部发生了什么。2.3 Reflection循环生成后自我批判再迭代一版第三种循环不访问外部工具而是让模型反复检查自己的输出。核心思路是生成一版答案 → 让模型以批评者身份审阅 → 根据意见重写 → 直到审阅通过或达到轮次上限。这个模式在写作润色、代码审查、逻辑推理类任务上极其有用。我做过一个需求文档Agent第一版直接生成的效果只能打6分加了Reflection循环之后模型自己找出了需求表述不明确缺少验收标准两类问题第二轮就把分数提到8分以上。原理上这是让模型用第二视角重新审视自己弥补单次生成时上下文理解不深、注意力分布不均的缺陷。同一个模型换一个批判者的身份设定输出质量差异可以非常大。Reflection循环在工程上要注意一点批判者和生成者要做角色分离。让同一个模型又当选手又当裁判往往会被自己的惯性带偏。实际实现时哪怕底层调的是同一个模型也要用两套完全不同的System Prompt一套告诉它你是一个严谨的写作者另一套告诉它你是一个挑剔的编辑只找问题不写正文。很多LangGraph里画的反思节点本质就是做了这件事。2.4 Plan-Execute循环先画地图再一步步走Plan-Execute循环和ReAct最大的区别是它先把整个任务的子步骤规划好形成一个计划列表然后逐个执行。每个子步骤执行完模型可以选择更新剩余计划或者标记完成。这种模式的优点是上下文压力小。因为每次只需要携带当前子步骤 对应上下文不需要把全部历史工具结果塞进窗口。缺点是计划可能出错如果初始规划不准确后面每一步都在执行错误的路线。实务上通常会让模型每执行2-3步就重新审视一遍剩余计划发现偏了马上调整。我一般在处理环节多、每一步相对独立的任务时比如从多个网页抓数据后做汇总报告优先选Plan-Execute实测Token消耗比ReAct低大概30%中途也不容易跑歪。2.5 四种模式对比与选型建议循环模式核心原理适合任务主要风险裸循环模型直接输出动作标记教学、最小验证格式不稳定ReAct想一步做一步推理与行动交替工具调用密集、需要推理轨迹的任务上下文增长快、成本高Reflection生成-批判-重写写作、代码审查、逻辑推理可能反复横跳不收敛Plan-Execute先规划再分步执行多步骤流水线、信息收集类初始计划错误导致跑偏选型不是越高级越好。我见过一上来就上多智能体循环的项目几个Agent互相扔消息Debug到怀疑人生最后发现单Agent的ReAct加一个Reflection就完全够用。循环工程的第一原则是够用即可先把复杂度控制在你能观测的范围再根据瓶颈逐步叠加。另外提醒一句模式之间不互斥Plan-Execute执行完叠加一轮Reflection是常见组合ReAct循环里也可以嵌入反思节点关键是你得清楚每一层循环在解决什么问题。3. 决定循环能不能收敛的三个关键控制点3.1 终止条件为什么你的Agent会无限转圈我踩过最深的一个坑是循环条件只写了模型没输出Final Answer就继续。结果某个任务里模型开始重复调用同一个搜索工具上下文越滚越大几轮之后连自己之前的输出都看不清了然后又继续调用……最后烧掉上百次API调用返回一句我还在努力找答案。这是循环工程里最经典的问题循环发散。解决办法不是靠模型自觉而是靠工程上强制加终止闸门最大迭代次数必加一般15-20轮就该怀疑任务设计有问题。如果超过这个数大概率是Prompt没说明白或者工具返回信息有问题。动作重复检测记录最近N个动作序列如果连续3轮调用同一个工具且参数几乎相同直接打断说明模型在空转。进度评估闸门每隔几轮让模型单独评估距离完成还有多远如果连续两轮评分没有提升强制结束。人类确认闸门成本较高或风险较大的操作前要求人工点击确认。哪怕只是是否继续执行下一步的一个按钮都能避免大量无效烧钱。这四个闸门不是选一个而是叠加使用。最大迭代和动作重复检测必加进度闸门在生成类循环里必加人类确认闸门在高风险操作里必加。别把终止条件的希望寄托在模型自己想停下来上模型没有这个自觉它只会顺着概率往下续。3.2 上下文预算Token爆炸和记忆压缩循环最耗钱的地方不是每次调用的输入而是历史越滚越长导致的输入Token持续膨胀。举例每次工具返回2千Token循环15轮光历史就有3万Token还不算系统提示词和推理内容。按市场价估算跑一次任务光输入成本可能就是几毛钱再乘以并发和迭代费用很快就失控。更麻烦的是窗口塞满之后模型会忽略早期内容循环质量反而下降——你花了更多钱得到更差的结果。控制上下文有三个层次的手段第一层限制单次工具返回长度。比如搜索API默认返回10条你截断成3条数据库查询结果只保留前20行网页抓取只截取正文前2000字。这是成本最低、见效最快的措施。第二层历史裁剪。固定只保留最近N轮对话更早的内容用摘要代替。实现方式也简单当消息列表长度超过阈值时调用一次模型请把以上历史压缩成200字摘要然后把旧消息删掉只保留摘要。注意摘要本身也要占用Token所以摘要长度要控制并且只在真正需要时才触发压缩。第三层分阶段任务。这是Plan-Execute模式的底层优势——不需要一个循环处理完整个大任务而是每个子任务单独建一个循环循环之间只传递结构化结果。这样每个循环的上下文都是独立的不会无限叠加。我在生产项目里比较常用的组合是限制工具返回长度 摘要压缩 保留最近3轮实测能把Token消耗压掉60%以上任务质量基本不掉档。这个数字不是理论值是跑了几个真实任务对比出来的你现在就可以拿一个自己的任务去试。3.3 错误恢复与人工介入闸门循环跑起来的另一个现实问题是工具会报错。数据库连不上、网页404、代码执行超时。最差的做法是模型报错后整个任务退出稍好一点的做法是把错误信息当成一次正常的观测结果塞回循环里让模型自己决定下一步。这也是我为什么反复说观测这个环节重要——错误也是一种状态它应该被模型看到而不是被代码吞掉。我在系统提示词里会写这样一段当工具返回错误时不要立即放弃。请分析错误原因尝试以下策略 1. 如果是参数错误修正参数后重试一次 2. 如果是数据不存在尝试其他数据源或换一种查询方式 3. 如果尝试两次仍失败在最终回答中如实说明。这是循环工程的韧性设计让错误成为循环里的正常状态而不是异常状态。同时对高成本或高风险动作比如调用外部付费API、执行写操作一定要在代码层面加人工确认接口而不是把最终决定权全交给模型。这不是技术洁癖是责任边界——模型判断错了背锅的是你的系统不是模型。4. 项目实战从零搭一个资料整理Loop Agent4.1 需求拆解与方案选型我们做一个能落地的例子输入一堆零散的会议纪要加数据片段Agent需要整理成一份带结论的结构化周报。这个任务看起来简单但如果你用开环单次调用模型大概率会把缺失数据自己编出来。我们要用闭环保证每个数字都来自输入且推理链条可追溯。需求拆解成三步读取所有原始资料识别关键主题按主题归类对缺失信息发起追问无法追问就明确标注数据缺失生成周报初稿再用Reflection做一轮质量检查。方案选型单Agent就够了不需要多智能体。循环模式用Plan-Execute 最后叠加一轮Reflection。工具只有一个本地检索函数search_snippets。终止条件计划全部执行完且Reflection通过或者达到最大15轮。这个选型思路你可以记一下先判断任务复杂度再选最小够用的循环组合最后才谈框架。4.2 循环的代码骨架我不打算直接上LangGraph画图而是用普通Python把闭环写出来这样你看到的每一步都是循环工程的真实逻辑而不是框架的魔法。import json from dataclasses import dataclass, field from typing import List, Dict, Any from openai import OpenAI client OpenAI() dataclass class AgentState: task: str raw_materials: List[Dict[str, Any]] field(default_factorylist) plan: List[Dict[str, Any]] field(default_factorylist) summaries: List[str] field(default_factorylist) attempts: int 0 done: bool False PLAN_PROMPT 你是周报整理助手。请把下面的原始资料整理为3-5个子步骤计划。 要求每个计划项必须包含{step_name, target, needed_data}。 只允许使用提供的资料不得编造数据。 资料{materials} def extract_plan(state: AgentState) - List[Dict[str, Any]]: resp client.chat.completions.create( modelgpt-4o, messages[ {role: user, content: PLAN_PROMPT.format(materialsstate.raw_materials)} ], response_format{type: json_object} ) return json.loads(resp.choices[0].message.content)[steps] def execute_step(state: AgentState, step: Dict[str, Any]) - str: 执行一个计划步骤返回该步骤的产出文本 query step[needed_data] snippets search_snippets(state.raw_materials, query) # 本地资料检索函数 resp client.chat.completions.create( modelgpt-4o, messages[ {role: system, content: 根据资料片段回答指定问题必须引用资料原文不要编造。}, {role: user, content: f主题{step[target]}\n资料片段{snippets}} ] ) return resp.choices[0].message.content这里search_snippets你可以实现成简单的关键词匹配也可以接向量检索不影响循环逻辑。注意我用了response_format{type: json_object}强制规划阶段输出JSON这是循环稳定性的关键——规划输出格式不确定后面所有步骤都会跟着乱。4.3 主循环编排从执行到反思接下来是主循环逻辑。先按计划顺序执行把每个子步骤的产出积累起来合并成初稿再做最多三轮反思迭代。def reflect(state: AgentState, draft: str) - str: 质量检查返回问题清单不重写 resp client.chat.completions.create( modelgpt-4o, messages[ {role: system, content: 你是严格的编辑。检查周报初稿找出所有缺少依据的结论和缺失的关键数据。只反馈问题清单不要重写。}, {role: user, content: draft} ] ) return resp.choices[0].message.content def rewrite(state: AgentState, draft: str, feedback: str) - str: resp client.chat.completions.create( modelgpt-4o, messages[ {role: system, content: 根据编辑意见重写周报修正被指出的问题。}, {role: user, content: f初稿{draft}\n\n编辑意见{feedback}} ] ) return resp.choices[0].message.content def run_weekly_report(task: str, materials: List[Dict]) - str: state AgentState(tasktask, raw_materialsmaterials) # 阶段1规划 state.plan extract_plan(state) # 阶段2按计划执行累计子步骤产出 for step in state.plan: result execute_step(state, step) state.summaries.append(f[{step[target]}]\n{result}) state.attempts 1 if state.attempts 15: break # 阶段3合并成初稿 draft_resp client.chat.completions.create( modelgpt-4o, messages[{role: user, content: f合并以下子主题产出生成周报初稿{.join(state.summaries)}}] ) draft draft_resp.choices[0].message.content # 阶段4反思循环最多3轮 for _ in range(3): feedback reflect(state, draft) if 无问题 in feedback or len(feedback) 30: return draft draft rewrite(state, draft, feedback) return draft这套代码的关键在阶段4reflect只返回问题清单不直接重写rewrite拿问题清单去改。把批判和修改拆成两次独立调用而不是让模型一句请检查并改进一步到位质量显著更稳。这也是Reflection循环里最容易忽略的工程细节——你越是把任务拆小拆单模型做得越好。4.4 运行效果与成本估算我拿一份真实会议纪要约10段文字含3个数字口径不一致的片段跑了一遍。开环单次生成的结果结论基本对但其中一个销售额数据直接编出个环比下降12%完全没依据。闭环版本Plan-Execute阶段把两个口径的数据分别标注Reflection阶段揪出了销售额结论缺少数据支撑的问题最终输出里所有结论都带来源标记缺失数据也如实写了未提供。成本方面一次完整运行大约是35次模型调用其中规划2次、执行12次、合并1次、反思最多3轮约9次总输入输出Token约4.8万。按gpt-4o当时的价格估算单次不到0.3美元。如果换成gpt-4o-mini这类小模型成本能压到1/20以下。对生产环境我建议前期用大模型跑通逻辑确认循环收敛后再降级到小模型省钱还够用。反过来做很容易把时间浪费在跟小模型的格式斗争上。5. 踩坑复盘循环工程里最容易被低估的细节5.1 模型返回格式不稳定导致的死循环用了Function Calling之后很多人觉得格式问题消失了其实没有。我遇到的最典型场景某个工具返回的JSON因为包含换行符被截断了解析失败触发解析异常→重试→还是异常的循环连续重试5次才被最大迭代闸门拦住。我的解决办法是加一个专门的parse_and_validate函数工具结果返回后先做JSON校验失败就进行暴力修复——提取最外层花括号、修正多余逗号、补全缺失引号再不行把原始文本塞给模型让它重新格式化一遍之后才允许进入下一轮循环。这个步骤看着不起眼但能让循环稳定性从看运气变成可预期。情绪上你会觉得这不就是解析JSON吗有什么好讲的但等你线上跑一周发现一半的循环中断都是这种问题就会回来感谢这个函数。5.2 评分型循环的内卷陷阱有段时间我做一个文案优化Agent用模型自评分数作为循环终止条件。结果发现它陷入一种诡异的稳态第二轮改完自评9分第三轮开始为了刷分而刷分把文案改得越来越浮夸但模型自评分反而上升。这就是模型的自我评估与任务真实质量脱钩。这件事给我的教训是不要用模型自评作为唯一的终止条件。要么引入固定规则比如字数范围、关键词覆盖率、必须包含某个结构要么引入外部客观度量比如代码任务的单测通过率、数据任务的字段完整性校验模型自评只能作为辅助信号。如果任务实在没有客观度量那就加一个自评分连续两轮不提升就终止的闸门至少能避免无限内卷。5.3 循环的可观测性让每一次循环都有迹可循循环工程Debug的难度远大于单次调用的Debug原因在于状态每一轮都在变化。我见过很多同学循环跑挂了第一反应是改Prompt但根本不知道是哪一轮、哪一步出了问题。更麻烦的是循环有随机性同一段代码跑两次可能坏在不同的地方没有日志就只能靠猜。我现在的习惯是每个循环都维护一份结构化的运行日志{ step: 7, mode: execute_step, step_name: 核对销售额口径, model_output: 已标注两个口径差异, tool_result: ok, 1760 tokens, token_cost: 3420, next_action: mark_complete }把这套日志接到控制台或者LangSmith这些平台上每次循环出问题都能快速定位到具体环节。我还会在循环入口打印一行当前迭代次数/总预算跑完再输出一份汇总。别觉得这些都是无用的仪式感循环工程到了后期调试能力就是工程能力日志是这一切的地基。最后再分享一个小习惯我会把每次调完Prompt之后的关键循环日志沉淀成测试用例下次改任何Prompt就自动跑一遍回归防止改好一个任务改坏十个任务。这个习惯救过我太多次尤其是项目里循环一多一条回归用例能顶你熬三个通宵。