写出一个完美的Prompt,只能换来一次漂亮的回答;要让Agent稳定地干完一件多步骤的活,你真正需要设计的,是一个能持续执行、检查结果、处理错误、并且知道何时停止的Loop。这篇文章从一个常见的翻车场景讲起,拆解Agent Loop的核心组件和停止条件,最后给出一个测试通过、可以直接跑起来的最小Python工程。01 | 先看一个熟悉的翻车现场不少人第一次搭Agent,走的都是同一条路:花一个下午打磨出一段几百字的 Prompt,角色、目标、约束、输出格式一应俱全,然后把它丢给模型,期待它自己分析 issue、改代码、跑测试、提交结果。第一轮输出往往确实惊艳。问题出在第三轮、第五轮之后。我自己搭执行框架(harness)时反复见过这些场景:跑着跑着,模型开始改一个和 bug 毫无关系的文件,原始目标早被冲出了上下文;工具报错了,它不看错误信息,若无其事地编一个执行成功继续往下走;同一个文件连读四遍;测试还红着,它已宣布任务圆满完成。而最要命的是——整个系统里没有任何一处代码回答过一个问题:这件事什么时候算结束?每次翻车,第一反应都是回去改Prompt:加一句不要重复操作,加一句必须确认测试通过。改完好两天,然后在另一个地方以新的姿势翻车。折腾几轮你会意识到,这不是Prompt的问题。Agent的可靠性,不取决于Prompt写得多精致,而取决于执行它的Loop 怎么设计。Prompt决定模型懂不懂这个任务;Loop决定这个任务最后成不成。02 | Prompt 解决什么,Loop 解决什么先把概念摆清楚。一次模型调用,本质上是个无状态的纯函数:文本进,文本出,调用结束,什么都不记得。而一个真正的任务是有状态的过程:做一步,世界变了,要看一眼结果,再决定下一步。这两者之间的鸿沟,就是Loop要填的。把一个Agent系统拆开,大致是这几层东西:Prompt:定义角色、任务、约束和输出要求,是模型理解任务的入口;Context(上下文):为当前这一步提供必要信息——文件内容、上一轮的报错、已经试过什么;Tools(工具):让模型的输出能真正作用于外部世界,而不只是说说;State(状态):记录任务从开始到现在的完整轨迹;Loop(循环):决定模型什么时候思考、什么时候行动、什么时候验证、什么时候重试、什么时候停;Guardrails(护栏):轮数上限、预算上限、权限边界这些保命机制。可以粗略写成一个公式:Agent Model Prompt Context Tools State Loop Guardrails。需要说明,这是我做工程时用的抽象,不是学术定义——业界至今对什么是 Agent没有统一说法,Anthropic 在《Building Effective Agents》里就专门区分过预编排的workflow和模型自主决定流程的agent。但不管哪条路线,有一点是共通的:模型只负责提议下一步,真正把一个个提议串成持续执行的,是Loop。换句话说,新的核心技能不再是写出一条完美的 Prompt,而是搭出那个替你写下一条Prompt的系统。03 | 解剖一个完整的 Agent Loop一个能用的Loop,每一圈大致要走完这几个阶段:读取目标和当前状态;观察环境(目录里有什么、上一步发生了什么);规划下一步;选择并调用工具;把工具结果原原本本记下来;验证结果是否符合预期;然后做决策——继续、重试、换路、求助,还是停止;最后输出结论并保存整条执行轨迹。听起来像是把while True套在模型调用外面就完事了,但魔鬼全在两个细节里。第一个细节是上下文由状态生成,而不是由人手打。很多人所谓的Agent,其实是自己坐在对话框前,一轮一轮把报错复制粘贴给模型——那不是 Loop,那是你在当Loop。正确的做法是维护一个显式状态对象,每轮的 Prompt都从状态里渲染出来:目标、已执行的动作、每次的观察结果。状态变了,Prompt自然跟着变,人不需要参与。第二个细节是失败信息是资产,不是垃圾。工具执行的结果——diff、标准输出、报错栈——必须全部捕获并回写进状态,因为这一轮的失败,恰恰是下一轮最有价值的上下文。模型看到密码校验对空字符串返回了true这条具体报错,远比看到一句干巴巴的上次失败了,请重试有用得多。闭环就是在结果回写状态、状态生成下一条Prompt这一步形成的。所以一个好的Loop不是裸奔的死循环,而是一个带状态转换、资源预算和完成条件的受控执行系统。04 | 最难设计的不是继续,而是何时停止让循环转起来很容易,让它在正确的时机停下来才是真功夫。一个没有出口的Loop不是系统,是一台持续烧钱的机器——尤其是挂在夜里跑的那种,你睡一觉,它跑了几百轮,账单还在涨,活没干完。按我的经验,停止条件至少要覆盖这几类:目标已通过验证(正常出口);达到最大迭代轮数;达到最大工具调用次数;超出时间或 Token 预算;连续多轮没有有效进展;同类错误反复出现;即将执行高风险操作;以及信息不足、必须人工介入。前面是刹车,后面是方向盘,缺哪个都不行。这里面有一条值得单独拎出来:模型自称完成和系统证明完成是两回事。模型非常乐意宣布胜利——它说测试已全部通过的时候,测试可能压根没跑过。所以完成的判定权必须交给模型之外的东西:跑一遍真实的测试、校验输出是否符合schema、检查目标文件是否真的存在且非空。模型的我做完了只是一个申请,验证器点头才算数。另一条容易被忽略的是重复动作检测。模型卡住时的典型表现,就是用一模一样的参数反复调用同一个工具。一个简单有效的做法:对最近几轮的(工具名,参数)做指纹比对,连续重复先注入一条警告提醒它换思路,再重复就直接熔断、升级人工。成本很低,但能拦住大部分原地打转烧Token的事故。05 | 从 Demo Loop 到生产 Loop一个演示级的Loop三件套就能跑:模型调用、工具调用、最大轮数。发朋友圈够用了。但要放进真实业务,清单会长很多:显式状态对象、结构化动作输出、工具权限控制、超时、重试与退避、幂等设计、日志与可观测性、checkpoint、错误分类、成本预算、人工审批、最终验证器,以及可恢复的失败状态——挂掉之后能从断点继续,而不是从头再来。看到这串清单先别慌,没有人第一天就把它们全堆上去。更现实的路径是:从最小闭环开始,根据真实出现的失败模式逐步加复杂度。跑出重复动作,就加指纹检测;账单超预期,就加成本预算;出现误删文件,就收紧权限、加审批。每个机制都应该对应一次真实的疼,而不是凭想象预防。人工审批的边界倒值得一开始就划清楚:不可逆的操作必须停下来问人。删除文件、转账、推生产环境,这类动作错一次就没有撤销键。我的做法很朴素——要么不给Agent这个工具,要么在执行前插一道确认。自动化每提高一档,对验证、权限和可观测性的要求就跟着提高一档,这笔账躲不掉。06 | The Full Loop in Code:一个最小可运行的 Agent Loop道理讲完,上代码。我写了一个最小但完整的工程,文末附录给出全部文件,可以直接拼起来运行:minimal-agent-loop/ ├── main.py # 入口:接收任务、注册验证器、打印执行摘要 ├── agent.py # Loop 核心:上下文构建 / 决策 / 执行 / 验证 / 熔断 ├── tools.py # 沙箱工具集:目录、读写文件、存在性检查、受限计算 ├── models.py # Pydantic 数据模型:Action / StepRecord / AgentState ├── test_loop_offline.py # 离线自检脚本,无需 API Key ├── requirements.txt ├── .env.example └── README.md几个设计决策说明一下。模型每轮只允许返回一个JSON动作{thought: ..., tool: ..., args: ...},用Pydantic校验,解析失败会带着错误信息重试,而不是让脏输出流进系统。工具全部是沙箱内的安全本地工具——读写指定目录的文本文件、列目录、检查文件、做四则运算——刻意没有提供任意 Shell 执行,路径也做了越界检查。模型调用走OpenAI兼容接口,API Key、Base URL、模型名全部通过环境变量配置,接任何兼容服务都行。Loop主干的核心逻辑长这样(完整版见附录的 agent.py):def run(self) - AgentState: hint for turn in range(1, self.max_turns 1): # 护栏:最大轮数 action self._next_action(hint) # 模型基于状态提出下一步 hint if action.tool ask_human: # 出口一:升级人工 self.state.status RunStatus.HUMAN_REQUIRED break if action.tool finish: # 出口二:自称完成 ok, msg self.validator(self.state) # 但要系统验证说了算 if ok: self.state.status RunStatus.SUCCESS break continue # 验证不过?证据缺口喂回去,接着干 repeats self._repeat_count(action) # 重复动作检测 if repeats self.max_repeats: self.state.status RunStatus.HUMAN_REQUIRED # 熔断 break if repeats 1: hint 你在重复同一个动作,换个思路或求助。 ok, observation run_tool(action.tool, action.args) # 执行并捕获一切 self.state.steps.append( # 结果回写状态 StepRecord(turnturn, actionaction, # → 闭环在这里形成 observationobservation, okok)) else: self.state.status RunStatus.FAILED # 出口三:轮数耗尽 return self.state短短几十行,但前面讲的东西都在:三种终态各有出口;完成必须过验证器;重复动作先警告再熔断;每一轮的观察结果都回写状态,成为下一轮 Prompt 的一部分。07 | 跑一遍:从 notes.txt 到 summary.md拿一个具体任务过一遍:读取工作目录中的notes.txt,提炼三条核心结论写入summary.md,并确认文件已成功生成,同时用--expect-file summary.md注册完成验证器。第1轮,状态里只有目标,模型选择list_dir观察目录,看到notes.txt 存在。第2轮,它调用read_file拿到笔记原文,内容进入状态。第3轮,它生成摘要并write_file写入summary.md。第4轮,它主动调用file_exists确认产出——因为系统提示里写明了宣布完成前先验证产出。第 5 轮,它发起finish,系统验证器检查summary.md 确实存在且非空,状态置为SUCCESS,循环结束。全程没有人打过一个字,每一轮的Prompt都是状态自动生成的。这条五轮的路径,在工程自带的离线自检脚本里是可以复现验证的。更有意思的是异常分支。notes.txt不存在?read_file返回文件不存在这条观察,模型下一轮可以重新找相近文件,或调用ask_human问人,而不是硬编一份摘要。写入失败?错误信息回写状态,带着具体原因重试。反复读同一个文件?指纹检测先警告,再犯就熔断成HUMAN_REQUIRED。轮数耗尽?FAILED收场,但完整轨迹都留着,排查不抓瞎。至于删除、覆盖重要文件这类动作——这个最小工程的答案是干脆不提供对应工具;生产环境里,则应该换成审批流。08 | 几条可以带走的工程原则最后不复述细节了,留几条我自己反复验证过的判断:Prompt决定Agent如何理解任务,Loop决定Agent如何完成任务。模型负责提出下一步,系统负责限制、记录和验证这一步。每一次动作都应该产生可观察的状态变化——观察不到,就等于没发生。每一个完成都应该对应可以检查的证据,模型的自我评价不算证据。每一种失败都应该有明确的出口:重试、回退、熔断或升级人工,唯独不能是悬在那里。自动化程度越高,验证、权限和可观测性就必须越强,这三样是自动化的地基而不是装修。真正可靠的Agent,从来不是那个每一步都判断正确的模型,而是那个即使模型判断错了,也能被及时发现、纠正、并安全停下来的闭环。