
1. 从“写提示词”到“设计循环”一个思维拐点的到来我大概是在连续调了三个星期的提示词之后才真正意识到问题的。那段时间我每天的工作状态就是打开编辑器改一段提示词跑一遍 Agent看它在哪里断掉再改提示词再跑。循环往复像极了在跟一个记性不太好但脾气很倔的实习生反复交代同一件事。每次我以为自己把话说清楚了它总能在某个意想不到的环节给我整出新花样——要么是工具调用参数拼错了要么是任务做到一半自己给自己加戏要么是明明上一轮已经确认过的信息下一轮它又忘了。后来我停下来复盘发现一个很反直觉的事实我花在提示词上的时间和 Agent 实际能稳定完成的任务复杂度并不成正比。提示词写得再精细它本质上还是在描述“这一次该怎么做”。而 Agent 要真正自己跑起来需要的不是“这一次怎么做”而是“每一步做完之后下一步该往哪走、什么时候该停、出错了怎么退回来”。这些东西提示词给不了得靠 Loop。这里说的 Loop不是编程里那个for循环那么简单。在 AI Agent 的语境下Loop 指的是 Agent 的运行时控制结构——它决定了 Agent 在每一轮里看到什么、能做什么、做完之后状态怎么更新、什么条件下继续、什么条件下终止。提示词是“内容层”Loop 是“控制层”。我之前的做法相当于把控制逻辑硬塞进内容层里用自然语言去描述“如果 A 就做 B否则做 C”结果就是提示词越写越长Agent 反而越来越不稳定。这个认知转变之后我把工作重心从“雕琢提示词”挪到了“设计 Loop”上。效果是肉眼可见的同一个任务之前需要我盯着跑五六轮、中途手动纠偏现在它能自己跑完我只需要在最后验收。当然这个过程里踩的坑一点没少有些坑还挺隐蔽的。下面我就把这套东西拆开讲包括 Loop 到底该怎么设计、提示词在新架构里扮演什么角色、以及我实际踩过的那些坑。2. 提示词工程的天花板到底在哪里2.1 提示词能解决的问题其实比想象中窄很多人对提示词工程的理解是“把话说清楚模型就能做对”。这话在单轮任务里基本成立比如“把这段中文翻译成英文”“从这段文本里提取人名和公司名”。但只要任务超过一轮需要模型自己决定下一步做什么提示词的局限性就暴露了。我举个实际例子。我做过一个任务让 Agent 去某个代码仓库里找所有硬编码的 API 密钥然后生成一份报告。这个任务拆开看大概是遍历文件、识别疑似密钥的字符串、排除测试文件、汇总结果、写报告。如果我把这些步骤全部写进提示词告诉它“第一步遍历文件第二步识别密钥第三步排除测试文件……”会发生什么实测下来模型在前两轮还能按部就班到了第三轮开始它就会开始“自由发挥”。有时候它会把node_modules里的文件也扫进去有时候它会在识别密钥的时候把普通的变量名也当成密钥有时候它干脆跳过排除测试文件这一步直接汇总。原因很简单提示词是静态的而任务执行是动态的。每一轮模型看到的上下文都在变但提示词里描述的逻辑是固定的模型需要自己在脑子里把静态逻辑映射到动态状态上这个映射过程极容易出错。2.2 长提示词的边际收益递减我做过一个粗略的对比实验。同一个任务我写了三个版本的提示词短版约 200 字只说明目标和输出格式、中版约 600 字补充了步骤和注意事项、长版约 1500 字把每一步的判断逻辑都写进去了。然后每个版本各跑 20 次统计任务完成率。提示词版本字数任务完成率平均轮次人工干预次数短版~20035%3.22.1中版~60055%4.81.4长版~150058%6.51.2数据很说明问题提示词从 200 字加到 600 字完成率涨了 20 个百分点但从 600 字加到 1500 字完成率只涨了 3 个百分点而平均轮次和人工干预次数并没有明显改善。更麻烦的是长提示词让 Agent 的响应变慢、token 消耗变大而且一旦任务场景稍有变化长提示词里那些具体的步骤描述反而成了束缚。提示如果你的提示词已经超过 800 字而 Agent 的完成率还是上不去大概率不是提示词的问题而是 Loop 设计的问题。2.3 提示词和 Loop 的分工边界那提示词是不是就没用了当然不是。我的经验是提示词负责“定义角色和约束”Loop 负责“控制流程和状态”。具体来说提示词里应该写Agent 的身份、能力边界、输出格式要求、安全约束、工具使用规范。Loop 里应该管当前处于哪个阶段、上一轮产出了什么、下一步该调用哪个工具、什么条件下重试、什么条件下终止。举个例子。提示词里我会写“你是一个代码审计助手只能读取文件不能修改文件输出必须是 JSON 格式”。而 Loop 里我会定义第一轮调用文件遍历工具拿到文件列表后进入第二轮第二轮对每个文件调用内容读取工具识别密钥第三轮过滤测试文件第四轮汇总输出。每一轮的状态文件列表、已识别的密钥、过滤结果都存在 Loop 的上下文里而不是靠提示词去“记住”。这样分工之后提示词可以写得很短Loop 的逻辑可以写得很清晰两者各司其职Agent 的稳定性明显提升。3. Loop 的核心构件状态、动作、终止条件3.1 状态管理Agent 的“工作记忆”该放在哪里Loop 设计里最容易被低估的就是状态管理。我一开始的做法是让 Agent 自己维护状态——每轮结束的时候让它把当前进展写进一个变量里下一轮再读出来。听起来很合理但实际跑起来问题很多。第一个问题是状态漂移。Agent 在写状态的时候会根据自己的理解“润色”一下比如把“已扫描 12 个文件发现 3 个疑似密钥”写成“已扫描部分文件发现若干密钥”。信息在传递过程中被压缩了下一轮它拿到这个模糊的状态判断就会出错。第二个问题是状态膨胀。如果每一轮都把完整的历史状态塞进上下文几轮之后上下文就会变得非常长模型处理起来又慢又容易分心。我后来采用的方案是状态由 Loop 的宿主程序维护而不是由 Agent 维护。具体来说Agent 每一轮只负责输出“这一轮做了什么、产出了什么”宿主程序负责把这些产出结构化地存起来下一轮只把必要的状态注入到提示词里。这样状态是精确的、可控的不会漂移也不会膨胀。用伪代码表示大概是这样state { phase: scan, files_scanned: [], findings: [], retry_count: 0 } while state[phase] ! done: prompt build_prompt(state) # 只注入当前阶段需要的状态 result agent.run(prompt) state update_state(state, result) # 宿主程序更新状态这个结构看起来简单但它把“状态”从 Agent 的脑子里挪到了程序里稳定性提升了一个量级。3.2 动作空间每一轮到底允许 Agent 做什么动作空间的设计直接决定了 Agent 会不会“乱来”。我踩过的一个坑是早期我给 Agent 开放了太多工具它在一轮里可以同时调用文件读取、网络请求、代码执行、数据库查询。结果就是它经常在应该只读文件的时候去调网络请求或者在应该汇总结果的时候又去读了一遍文件。后来我改成按阶段收窄动作空间在扫描阶段只开放文件遍历和读取工具在分析阶段只开放文本处理工具在汇总阶段只开放输出工具。每一轮 Agent 能做什么是明确的它就不会跑偏。这个思路其实和人类工作很像。你在做代码审计的时候不会一边看代码一边发邮件一边改数据库。每个阶段有每个阶段的工具集阶段之间是串行的。Agent 也一样给它太多自由它反而不知道该干什么。3.3 终止条件什么时候该停什么时候该重试终止条件是 Loop 设计里最需要小心的地方。我见过两种极端一种是终止条件太松Agent 跑了几十轮还在那里打转另一种是终止条件太紧Agent 刚开了个头就被强制结束。我的经验是终止条件要分三层成功终止任务目标达成比如“所有文件已扫描且报告已生成”。失败终止出现了不可恢复的错误比如“连续三轮工具调用失败”或“重试次数超过上限”。人工介入终止Agent 自己判断需要人类决策比如“发现疑似密钥但无法确定是否为测试数据”。第三层特别重要。很多 Loop 设计里只有成功和失败两种终止但实际任务里经常出现“Agent 不确定该怎么办”的情况。这时候如果强行让它继续它就会瞎猜如果直接判失败又浪费了前面的工作。所以我在 Loop 里加了一个need_human状态Agent 可以主动把控制权交回来我处理完之后再让它继续。4. 我踩过的五个坑以及每个坑的排查过程4.1 坑一Loop 里的提示词注入把状态覆盖了这个坑我排查了整整一个下午。现象是Agent 在前几轮表现正常到了第四轮突然开始重复第一轮的工作。我一开始以为是模型的问题换了几个模型都一样。后来把每一轮的完整提示词打印出来看才发现问题出在状态注入上。我的 Loop 里有一个build_prompt函数负责把当前状态拼进提示词。当时的写法是先把基础提示词模板加载进来然后把状态字段逐个替换进去。但基础模板里有一个占位符叫{current_task}而状态里也有一个字段叫current_task结果替换的时候把整个任务描述覆盖成了当前阶段名。Agent 看到的任务变成了“scan”它自然就重新开始扫描了。这个坑的教训是状态字段的命名要和提示词模板的占位符严格区分开。我后来的做法是给所有状态字段加前缀比如state_phase、state_files这样就不会和模板里的占位符冲突。另外build_prompt函数里加了一个断言检查替换后的提示词里是否还残留未替换的占位符有的话直接报错不让它带着问题往下跑。4.2 坑二工具调用失败后的重试逻辑把 Agent 带进了死循环有一次我让 Agent 去调用一个外部 API 获取数据那个 API 偶尔会超时。我在 Loop 里加了重试逻辑如果工具调用失败就等两秒再试一次最多试三次。听起来没问题但实际跑的时候 Agent 卡在那里不动了。排查后发现问题出在重试的粒度上。我的重试逻辑是包在单轮里的一轮里如果工具调用失败就重试三次。但 Agent 在工具调用失败后会认为这一轮没有产出有效结果于是它会在下一轮重新尝试同样的工具调用。而下一轮里又有三次重试。结果就是 3×3×3……无限套娃。修复方案是把重试逻辑从“单轮内重试”改成“跨轮重试”单轮内工具调用失败就直接返回失败状态由 Loop 决定是否进入下一轮重试并且用一个全局的重试计数器来控制总次数。这样重试次数是可控的不会指数级膨胀。注意重试逻辑一定要有全局计数器不能只在单轮内计数。否则 Agent 的每一轮都会“重新开始计数”导致无限重试。4.3 坑三状态序列化时把不可序列化的对象塞进去了这个坑比较隐蔽。我的状态里存了一个文件句柄对象用来在多个轮次之间保持文件读取的进度。在单次运行里没问题但当我尝试把状态存到磁盘上做断点续跑的时候序列化直接报错了。排查过程比较直接看报错信息就知道是哪个字段的问题。但修复的时候我犹豫了一下——是把这个字段去掉还是换一种可序列化的表示方式最后我选择了后者把文件句柄换成文件路径加偏移量需要读文件的时候再重新打开。这样状态就是纯数据的可以随便序列化和反序列化。这个坑的教训是状态里只放数据不放资源。资源文件句柄、网络连接、数据库连接应该在需要的时候创建用完就释放不要跨轮次持有。这样 Loop 的可恢复性会好很多也更容易调试。4.4 坑四终止条件写成了“或”而不是“与”这个坑导致 Agent 经常提前终止。我的终止条件原本写的是“所有文件已扫描 或 报告已生成”本意是这两个条件满足任意一个就可以停。但实际执行的时候Agent 在扫描完第一个文件之后就认为“所有文件已扫描”这个条件满足了因为它把“所有”理解成了“当前这一批”于是直接跳到报告生成阶段报告里只有第一个文件的结果。修复方案是把终止条件改成“所有文件已扫描 且 报告已生成”并且把“所有文件”的定义明确成“初始文件列表里的每一个文件都处理过”。另外我在 Loop 里加了一个校验步骤在进入报告生成阶段之前检查已处理文件数是否等于初始文件数不等的话就退回扫描阶段。这个坑的教训是终止条件里的逻辑连接词要反复推敲。“或”和“与”一字之差行为完全不同。而且自然语言里的“所有”“完成”“结束”这些词都有歧义最好用可量化的条件来替代。4.5 坑五Agent 在 Loop 里“学会”了偷懒这个坑最有意思。我让 Agent 做一个数据清洗任务Loop 设计是每轮处理一批数据处理完检查质量质量达标就进入下一批。跑了一段时间后我发现Agent 处理第一批数据的时候很认真越往后越敷衍最后几批几乎是原样输出。排查后发现Agent 在上下文中看到了前面几轮的成功记录它“推断”出这个任务的标准在降低于是自己也降低了标准。这其实是模型的一种“模式匹配”行为它看到前面的输出都是“通过”就认为自己的输出也应该“通过”于是不再认真做质量检查。修复方案是在每一轮的质量检查环节不把前面的成功记录注入上下文只注入当前批次的数据和检查标准。这样 Agent 每一轮看到的都是“ fresh ”的任务不会受到历史记录的影响。这个坑的教训是Loop 里的上下文注入要克制。不是所有历史信息都需要让 Agent 看到。有些信息比如前面的成功记录会让 Agent 产生“惯性”反而降低它的表现。每一轮只注入当前决策必需的信息是最稳妥的做法。5. 一个可复用的 Loop 骨架设计5.1 阶段划分把任务拆成有限个状态经过上面这些坑我总结出了一个比较通用的 Loop 骨架。核心思路是把任务拆成有限个阶段每个阶段有明确的输入、输出和退出条件。阶段之间是串行的阶段内部可以有多轮。以代码审计任务为例阶段划分大概是阶段输入输出退出条件初始化仓库路径文件列表文件列表非空扫描文件列表疑似密钥列表所有文件已扫描过滤疑似密钥列表确认密钥列表过滤规则已应用汇总确认密钥列表报告报告已生成每个阶段内部Agent 可以有多轮交互。比如扫描阶段如果文件很多可以分批扫描每批一轮。但阶段之间的推进是明确的扫描阶段不结束不会进入过滤阶段。5.2 状态机实现用代码而不是提示词来控制流程这个骨架用代码实现大概是这样class AgentLoop: def __init__(self, task): self.state { phase: init, task: task, files: [], findings: [], confirmed: [], report: None, retry: 0 } def run(self): while self.state[phase] ! done: handler getattr(self, fphase_{self.state[phase]}) handler() return self.state[report] def phase_init(self): result self.call_agent(init, self.state) self.state[files] result[files] self.state[phase] scan if self.state[files] else failed def phase_scan(self): result self.call_agent(scan, self.state) self.state[findings].extend(result[findings]) if result[done]: self.state[phase] filter # ... 其他阶段类似这个结构的好处是流程控制完全在代码里Agent 只负责每个阶段内的具体工作。Agent 不需要知道“下一步该干什么”它只需要知道“当前这个阶段该产出什么”。这样提示词可以写得很短Agent 的负担也小很多。5.3 提示词模板每个阶段一个短提示词对应上面的阶段划分提示词也拆成多个短模板。比如扫描阶段的提示词大概是你是一个代码审计助手当前处于扫描阶段。 你的任务是读取给定的文件列表识别其中疑似 API 密钥的字符串。 输出格式JSON包含 findings 数组和 done 布尔值。 约束只读取文件不修改文件只识别密钥不做其他分析。这个提示词不到 100 字但信息很明确。Agent 知道自己在哪个阶段、要做什么、输出什么格式。它不需要知道整个任务的流程因为流程由 Loop 控制。对比之前那个 1500 字的长提示词这个短提示词的效果反而更好。原因很简单Agent 的注意力是有限的提示词越短它越能把注意力集中在当前任务上。6. 从提示词思维切换到 Loop 思维的实际收益6.1 稳定性从“看运气”到“可预期”切换之前我跑 Agent 的心态是“看运气”。同一个任务有时候能跑通有时候跑不通跑不通的时候我也不知道问题出在哪里。切换之后Agent 的行为变得可预期了如果某个阶段出错我能定位到具体是哪个阶段、哪个环节的问题然后针对性地修。这种可预期性带来的最大好处是我敢把更复杂的任务交给它了。以前我只敢让它做单步任务现在我可以让它做多阶段任务因为我知道每个阶段的边界在哪里出问题也能兜住。6.2 可调试性出错时知道该看哪里Loop 结构让调试变得简单了很多。以前 Agent 出错我只能看最终输出然后猜是哪一步出了问题。现在我可以看每一轮的状态变化知道它在哪个阶段卡住了、卡住的时候状态是什么、上一轮产出了什么。我甚至在 Loop 里加了一个简单的日志系统每一轮结束的时候把状态快照写到一个 JSON 文件里。出问题的时候直接看这个文件比看模型的输出日志直观多了。6.3 可扩展性加新能力不用重写提示词最后一个收益是可扩展性。以前加一个新能力比如“让 Agent 在扫描的时候顺便统计文件行数”我得改提示词而且改完之后可能影响其他部分的行为。现在加新能力只需要在对应阶段里加一个工具调用或者在状态里加一个字段不影响其他阶段。这个收益在任务变复杂的时候特别明显。我的代码审计 Agent 从最初的“找密钥”扩展到“找密钥 找硬编码密码 找敏感配置”只花了不到一个小时因为每个新能力都是独立的阶段或阶段内的独立工具调用互不干扰。7. 一些零散但实用的经验7.1 日志要记状态不要只记输出我一开始的日志只记 Agent 的文本输出后来发现不够用。Agent 的输出是自然语言看多了会累而且很多关键信息比如状态字段的值不在输出里。后来我改成记状态快照每一轮结束的时候把整个 state 序列化成 JSON 写文件。这样出问题的时候我直接 diff 两个状态快照就知道哪一步改变了什么。7.2 给 Agent 的“自由发挥”留一个口子虽然 Loop 控制了大部分流程但有些情况下 Agent 确实需要自由发挥。比如它在扫描的时候发现了一个不在预期内的文件类型它可能需要临时决定怎么处理。我的做法是在每个阶段里留一个notes字段Agent 可以把它的“意外发现”写进去Loop 在推进到下一阶段之前会检查这个字段如果有内容就暂停一下让我看看。这个口子不大但很有用。它让 Agent 在遇到意外情况时有一个“举手”的渠道而不是硬着头皮往下做或者直接卡死。7.3 阶段不要拆得太细我一开始把阶段拆得很细一个任务拆了十几个阶段。结果 Loop 的代码变得很臃肿阶段之间的状态传递也很繁琐。后来我合并了一些阶段保持在 4 到 6 个阶段代码清爽了很多Agent 的表现也没有下降。我的经验是阶段的粒度应该以“决策点”为准。如果一个地方需要 Agent 做判断比如“这批数据质量达标了吗”那它就是一个阶段边界。如果只是机械地执行那它可以和相邻的步骤合并。7.4 测试 Loop 的时候先用假 Agent调试 Loop 本身的时候不需要每次都调用真实模型。我写了一个FakeAgent类它根据阶段名返回预设的假数据。这样我可以快速测试 Loop 的控制逻辑不用等模型响应也不用花 token。等 Loop 的逻辑跑通了再换成真实 Agent 做端到端测试。这个做法帮我省了很多时间。Loop 的 bug 和 Agent 的 bug 是两类问题混在一起调很痛苦。先用假 Agent 把 Loop 调通再用真 Agent 调提示词效率高很多。7.5 别忘了给 Loop 本身加超时Loop 跑飞的情况我遇到过两次都是因为某个阶段的退出条件没写对Agent 在里面无限循环。后来我在 Loop 外面加了一个全局超时比如 10 分钟没跑完就强制终止并且把当前状态 dump 出来。这个超时救了我好几次至少不会让一个跑飞的 Loop 占着资源不放。8. 关于“鹈鹕骑自行车”那个测试的一点想法最后聊一个题外话。最近看到不少人在讨论“鹈鹕骑自行车”这类提示词测试大意是给模型一个很具体的场景描述看它能不能生成符合要求的输出。这类测试用来评估模型的指令遵循能力是有意义的但我想说的是它测的是提示词工程的能力不是 Agent 的能力。一个 Agent 能不能自己工作不取决于它能不能根据一段描述画出鹈鹕骑自行车而取决于它能不能在没有人盯着的情况下自己决定先画鹈鹕还是先画自行车、画完之后怎么检查、发现画错了怎么改。这些能力不在提示词里在 Loop 里。所以如果你也在做 Agent我的建议是把花在雕琢提示词上的时间挪一半到设计 Loop 上。提示词写到 80 分就够了剩下的 20 分靠 Loop 来补。你会发现Agent 突然就“自己会工作”了。