如果你用过几个 AI Agent,很容易产生一种错觉:Agent 的关键就是让模型多执行几轮。
第一次生成错了,就把错误信息喂回去;第二次还不对,就再试一次;如果实在不行,就把最大循环次数调大一点。于是,Agent 的工程优化最后变成了一个参数:max_iterations。
但循环次数增加,并不等于 Agent 的能力增加。
如果每一轮都没有得到新的证据,没有改变解决问题的假设,也没有缩小目标与当前状态之间的差距,那么多跑十轮只是把失败重复十遍。更糟的是,Agent 可能在一个已经错误的方向上越走越远,最后留下一个“做了很多事情但没有人知道哪里出了问题”的工作区。
所以我现在理解的 Loop Engineering,不是简单地给 Agent 加一个 while 循环,而是研究一件更具体的事情:
如何让一个由模型驱动、受环境反馈约束、带有随机性的执行过程,持续产生有效证据,并最终收敛到目标状态。
这里的 Loop Engineering 不是一个已经有严格统一定义的学术术语,而是我用来描述一类工程问题的名字。它关心的不是模型这一轮说得像不像,而是整个任务在十轮、二十轮之后,是否仍然可解释、可恢复、可验证。
一、Agent 不是在“回答”,而是在控制一个外部状态
聊天模型的基本形式是:
输入文本 -> 输出文本
这种形式下,输出本身就是结果。你可以评价答案是否准确、是否有帮助,但模型并没有改变一个需要持续维护的外部世界。
Agent 不一样。它的输出通常是动作:调用工具、修改文件、执行 SQL、发送请求、更新数据库、点击页面。动作一旦执行,外部世界就发生了变化;下一轮模型面对的已经不是原来的任务,而是一个被前几轮动作改变过的任务环境。
可以用一个简化的状态转移来描述它:
世界状态 s_t
|
| 模型根据上下文选择动作 a_t
v
环境执行 a_t,产生新的状态 s_(t+1)
|
| 观测器返回结果 o_(t+1)
v
评估器根据目标 g 判断进展 e_(t+1)
|
v
形成下一轮上下文 c_(t+1),继续选择动作
更形式化一点:
a_t ~ πθ(a | c_t)
s_(t+1) ~ P(s' | s_t, a_t)
o_(t+1) = O(s_(t+1), a_t)
e_(t+1) = E(o_(t+1), s_(t+1), g)
其中:
s_t是真实的环境状态,比如代码仓库、数据库或浏览器页面;c_t是传给模型的上下文,它只是对真实状态的一个投影;a_t是模型选择的动作;o_t是工具和环境返回的观测;g是任务目标;E是评估器,不是模型自己说“完成了”就算完成。
这个视角会带来一个很重要的结论:Agent 的失败不一定发生在模型生成错误答案的那一刻,也可能发生在状态没有被正确观察、反馈没有被正确解释、进展无法被衡量的地方。
二、真正需要维护的是四种状态
很多 Agent 只有一份消息历史。模型每次拿到“之前的对话 + 上一次工具结果”,然后继续生成。这种方式在短任务里足够,但在长任务里会把几种本质不同的状态混在一起。
1. 世界状态:外部系统实际上是什么样
这是最硬的一层:文件是否修改、测试是否通过、表中是否写入数据、页面当前停在哪一步。
世界状态不应该由模型的陈述代替。模型说“文件已经保存”,不代表文件真的存在;模型说“数据库中没有重复数据”,也不代表查询已经验证过。
2. 信念状态:Agent 认为世界是什么样
Agent 不可能每一轮都重新读取整个世界,它需要根据观测形成一个内部判断:哪些文件相关、哪个字段对应用户意图、上一次错误的原因是什么。
这是一种信念状态,而不是真实状态。它可能是错的。
如果 Agent 认为“错误来自类型转换”,于是连续三轮都修改类型转换,那么问题可能并不是它不会修,而是它从一开始就把错误归因错了。
3. 工作流状态:任务已经完成了哪些阶段
“已经查过 Schema”“已经生成候选 SQL”“已经完成权限检查”“已经验证结果”这些不是普通聊天记录,而是工作流状态。
它们需要有明确的状态值,比如:
pending -> running -> passed
\-> failed
\-> blocked
否则,模型会从文本中猜测自己做过什么。长上下文里同一件事可能出现多个版本,模型很容易把“曾经尝试过”误认为“已经完成”。
4. 证据状态:哪些结论已经被什么东西证明
这是最容易被忽略、但最有价值的一层。
“SQL 可以执行”是一条证据;“SQL 使用了正确的时间字段”是另一条证据;“结果中的金额单位符合用户预期”又是另一条证据。它们不能被一个笼统的 success = true 替代。
一个可靠的 Agent 不应该只有任务状态,还应该维护类似这样的证据账本:
{
"claims": [
{
"claim": "SQL 语法正确",
"status": "proven",
"evidence": "sqlglot.parse succeeded"
},
{
"claim": "查询使用了自然月时间范围",
"status": "unknown",
"evidence": null
},
{
"claim": "结果包含 5 个渠道",
"status": "proven",
"evidence": "result row count = 5"
}
]
}
很多“Agent 明明测试通过了但结果还是错”的问题,本质上不是缺少重试,而是证据粒度太粗。
三、Loop Engineering 和 Prompt、Context、Harness 的关系
上一篇文章里我把 Agent 简化为:
Agent = Model + Harness
这个公式仍然成立,但它没有描述 Agent 如何在一个任务中前进。Loop Engineering 关注的是 Harness 的动态部分:如何把工具、上下文、评估和权限组织成一个可控制的状态转移过程。
| 概念 | 主要问题 | 典型产物 |
|---|---|---|
| Prompt Engineering | 如何表达要求 | 指令、示例、输出格式 |
| Context Engineering | 当前这一轮应该让模型看到什么 | 状态摘要、相关文件、工具结果 |
| Harness Engineering | Agent 运行所需要的基础设施 | 工具、权限、记忆、沙箱、调度 |
| Loop Engineering | 下一步行动如何由证据驱动 | 状态机、评估器、重规划、退出策略 |
Prompt 决定模型“应该做什么”;Context 决定模型“知道什么”;Harness 决定模型“能做什么”;Loop 决定模型“做完一件事之后如何继续”。
因此,Loop Engineering 并不是在 Harness 上面再堆一层 Prompt。它更接近一个控制器:模型负责提出动作,控制器负责检查动作是否在允许范围内、是否产生了有效反馈、是否应该继续或切换策略。
四、循环的关键不是重复,而是产生信息增益
一个动作是否值得执行,不应该只看它“可能修好什么”,还应该看它能不能区分不同的假设。
假设当前接口返回 500,有三种可能:
h1:数据库连接失败
h2:字段类型转换失败
h3:异常被错误地映射成 500
如果下一步只是再次运行完全相同的请求,那么它对这三个假设没有区分能力。无论结果是什么,Agent 都很难知道应该相信哪一个。
但如果下一步是检查数据库连接、打印结构化异常类型,或者用固定输入绕过数据库,那么这个动作就具有诊断价值。它可能不直接修复问题,却能减少不确定性。
这说明 Agent 的动作至少有两种:
- 利用(exploitation):沿着当前最有希望的方案继续实现;
- 探索(exploration):执行诊断动作,获取能够区分假设的新信息。
只会利用的 Agent 会在错误方向上打补丁;只会探索的 Agent 会不断调查却不完成任务。Loop Engineering 要做的是在两者之间进行预算分配。
可以把一轮动作的价值粗略拆成:
动作价值 = 预期目标收益
+ 预期信息增益
- 执行成本
- 风险成本
这不是一定要在线计算的数学公式,但它提供了一个比“让模型自行决定下一步”更有用的思考框架。
当一个动作既不能提高目标指标,也不能验证或排除任何假设时,它通常就是低价值动作。连续出现这种动作,就是循环开始空转的信号。
五、进展不能只用一个布尔值表示
很多 Agent 的评估接口是:
{"success": true}
这个字段过于粗糙。真实任务通常不是“完成/未完成”两种状态,而是多个约束同时成立:功能正确、没有回归、符合权限、成本可接受、结果可解释。
更实用的评估结果应该拆开:
{
"status": "failed",
"completed_claims": ["SQL 语法正确", "只读权限通过"],
"failed_claims": ["时间范围语义不明确"],
"unknown_claims": ["结果是否符合业务口径"],
"new_evidence": ["query uses event_time, not report_date"],
"recommended_transition": "ask_user",
"confidence": 0.91
}
这里最重要的不是 confidence,而是三种区分:
- 已经证明了什么;
- 明确不满足什么;
- 目前仍然不知道什么。
进展的三个层次
可以把进展拆成三个层次:
目标进展
任务离最终验收还有多远。例如失败测试从 8 个减少到 3 个,或者查询结果从空集变成了符合预期的 5 行。
证据进展
我们对系统的认识是否更准确。例如已经确认不是数据库连接问题,而是日期字段映射问题。
状态进展
环境是否更接近目标。例如新增了正确的索引、修复了一个字段映射、完成了权限校验。
一次动作可能没有带来目标进展,却带来了证据进展。诊断动作就是这种情况。但如果三种进展都没有产生,继续重复同类动作就没有理由了。
进展的单调性
不是所有状态变化都是进展。代码行数增加、工具调用次数增加、对话变长,都不能证明任务在接近完成。
更有意义的约束是:在不违反安全不变量的前提下,每一轮至少满足下面一项:
- 完成一个新的可验证子目标;
- 排除一个重要的错误假设;
- 让失败更具体、更接近根因;
- 发现当前方案不可行,并切换到新的方案。
如果一轮动作没有满足任何一项,就应该被记录为 no_progress,并触发重新规划、回滚或停止,而不是无条件重试。
六、评估器不是“最后跑一下测试”
评估器决定了 Loop 看到的现实是什么。一个糟糕的评估器会让模型非常努力地优化错误目标,这比没有评估器更危险。
1. 评估器要和任务目标对齐
“代码可以编译”只能证明语法和类型层面没有问题,不能证明业务行为正确。
“SQL 可以执行”只能证明数据库接受了这条语句,不能证明它选对了表、字段、时间范围和聚合口径。
评估器应该针对目标中的每个重要约束提供检查,而不是用一个便宜指标替代全部目标。
2. 评估器要尽量独立于生成器
如果模型生成了代码,又由模型自己阅读代码并宣布“符合需求”,这是一种自我确认。生成器和评估器拥有相同的盲点,错误很容易被两者共同忽略。
更可靠的评估信号来自不同机制:
- 解析器和类型系统;
- 固定测试与随机测试;
- 沙箱中的真实执行;
- 独立的 Schema 或权限规则;
- 人工确认不可逆操作;
- 与历史基线的差异比较。
LLM-as-a-judge 不是不能用,但它更适合判断开放式质量,或者决定“是否需要人工复核”;对于权限、金额、数据删除和 SQL 语义这类高风险约束,不能让它成为唯一裁判。
3. 评估器要区分“失败”和“不确定”
没有拿到结果,不等于结果错误。
网络超时、资源不足、权限不足、数据库不可用时,评估器应该返回 blocked 或 uncertain,而不是 failed。否则模型会把环境故障误认为实现故障,开始修改完全无关的代码。
一个实际可用的结果枚举至少应该包括:
passed 目标约束已被证据支持
failed 有明确证据表明约束不满足
uncertain 证据不足,不能下结论
blocked 由于环境或权限无法继续
unsafe 动作可能带来不可接受的风险
这些状态会导向完全不同的下一步。把它们压缩成一个 false,等于主动丢弃了 Loop 最重要的决策信息。
七、一个完整例子:Text2SQL 为什么必须是多层 Loop
以一个看起来很简单的问题为例:
查询上个月各投放渠道的消耗,返回消耗最高的 5 个渠道。
很多 Text2SQL Demo 的循环是:
问题 -> 生成 SQL -> 执行 SQL -> 返回结果
这个循环只能解决“SQL 是否能执行”,不能解决“SQL 是否理解了问题”。
第一步:把自然语言目标变成约束
这个问题至少包含这些隐含约束:
时间:上个月,且需要确定时区和自然月边界
指标:消耗,可能对应 cost、spend 或 total_cost
维度:投放渠道,可能是 channel_name 或 channel_id
排序:按消耗降序
数量:只返回前 5 个
权限:只能读取当前用户有权限访问的数据
如果这些约束没有被显式记录,后续即使 SQL 执行成功,也无法判断它是否完成了任务。
第二步:Schema Loop
模型不应该一开始就看到整个数据库 Schema。系统可以先检索候选表和字段,然后让一个独立检查器验证字段之间的关系:
问题解析 -> 检索候选表 -> 检查字段描述、粒度、关联关系
-> 候选 Schema 不足?继续检索或请求澄清
这里的反馈不是数据库报错,而是 Schema 证据:
spend是日粒度还是订单粒度?channel是否需要通过维表转换?report_date和event_time哪一个才是业务口径?- “上个月”是否有统一的时区定义?
如果 Schema 证据不足,直接生成 SQL 是提前进入下一阶段,而不是高效。
第三步:SQL 生成与静态检查 Loop
生成 SQL 后,先不要直接打生产库。可以按下面顺序检查:
SQL 解析
-> 表和字段是否存在
-> 是否包含越权表
-> 时间条件是否存在
-> 聚合粒度是否匹配
-> 是否包含 LIMIT 5 和正确排序
-> 是否只读
这一步能够捕获大量错误,而且成本比真正执行查询低。静态检查失败时,反馈应尽量指出违反了哪条约束,而不是只返回“SQL 不合法”。
第四步:沙箱执行 Loop
静态检查通过后,在受限环境中执行查询。执行结果需要结构化:
{
"status": "passed",
"columns": ["channel", "spend"],
"row_count": 5,
"duration_ms": 842,
"sample": [
{"channel": "A", "spend": 120000.0}
]
}
如果返回的是数据库错误,可能是实现错误;如果查询超时,可能是索引或扫描范围问题;如果结果为空,则不能马上让模型改 SQL,还需要判断“确实没有数据”还是时间条件选错了。
第五步:结果语义 Loop
SQL 能执行、结果有 5 行,仍然不代表完成。还要检查:
- 是否真的按消耗降序;
- 是否出现重复渠道;
- 消耗单位是否是元、分或其他单位;
- 时间范围是否覆盖完整的上个月;
- NULL 是否被错误地当成 0;
- 结果是否明显超出历史范围。
如果这一步发现“使用了事件发生时间,但用户问的是报表日期”,下一步不是继续修 WHERE 条件,而是回到 Schema Loop,重新确认字段语义。
这就是一个很典型的回退边:循环不是只向前走,也可能从结果验证回到 Schema 理解阶段。
第六步:什么时候应该问用户
如果数据库里同时存在多个“消耗”字段,或者“上个月”在业务上可能指自然月和最近 30 天,那么 Agent 不应该擅自选择一个看起来合理的答案。
在这种情况下,最好的 Loop 动作不是生成更多 SQL,而是提出一个低成本澄清问题:
这里的“上个月”按自然月统计,还是按最近 30 天统计?消耗口径使用广告报表中的
spend字段可以吗?
能够识别不可消除的不确定性,并在正确的地方停下来,也是 Loop Engineering 的能力。
八、失败归因:为什么“修复错误”经常修错地方
一个工具输出的错误,通常不是根因本身。
例如:
数据库报错:column xxx does not exist
它可能意味着:
- 模型确实选错了字段;
- Schema 检索漏掉了正确字段;
- 字段属于另一张表,需要先 JOIN;
- 用户使用了业务别名,但系统没有完成术语映射;
- 当前连接的数据库版本与 Schema 缓存不一致。
如果 Agent 只把错误文本交给模型,模型很可能选择第 1 种解释,然后在 SQL 上反复打补丁。
因此,Loop 中需要把“错误观察”与“失败归因”分开:
observation:column xxx does not exist
diagnosis:字段检索或映射阶段可能出错
next_action:重新检索相关 Schema,并检查缓存版本
诊断不一定由另一个大模型完成。很多领域可以用规则、日志、调用链和历史数据先完成粗分类,再把有限的候选原因交给模型选择。
用动作边界改善归因
如果一轮里同时修改了 Schema 检索、Prompt、SQL 生成和执行配置,最后查询失败时,几乎无法知道是哪项改动造成的。
所以,一个有利于归因的循环应该尽量满足:
- 一轮动作的目标单一;
- 变更范围可记录;
- 动作前后都有可比较的状态;
- 失败时可以回到最近的稳定点。
这不是要求 Agent 每次只改一行代码,而是要求每次实验都尽量能够回答:我改变了什么,结果因此发生了什么变化?
九、循环什么时候在收敛,什么时候在发散
循环的“运行中”状态不代表它是健康的。可以从几个信号判断它正在收敛还是发散。
收敛的信号
- 未满足的约束数量减少;
- 失败信息越来越具体;
- 变更范围逐渐收敛;
- 重复动作减少;
- 评估器获得了更多独立证据;
- 从失败到下一次动作之间的诊断链条清晰。
发散的信号
- 每轮都修改不同的文件,但没有稳定的目标;
- 测试数量增加,却没有解释为什么;
- 相同错误或相同动作反复出现;
- 为了通过一个检查破坏了另一个不变量;
- 上下文越来越长,但有效信息比例越来越低;
- 模型频繁宣布“应该已经解决”,评估器却没有新增证据。
可以记录一个简单的循环指标:
有效进展率 = 产生新证据或完成子目标的轮数 / 总轮数
还可以记录:
首次有效进展所需轮数
每个子目标的平均修复轮数
重复动作比例
回滚比例
误报完成比例
无进展连续轮数
单位成本的任务完成率
这些指标比“平均跑了多少轮”更能反映 Loop 的质量。一个只跑 3 轮但经常误报完成的 Agent,可能比一个跑 8 轮、但每轮都有明确证据的 Agent 更差。
十、Loop 的退出策略:停止是一个主动动作
很多系统把停止看成异常情况:只要没有成功,就继续循环,直到达到最大次数。
但在真实系统中,停止本身也是策略选择。至少有以下几种停止原因:
completed 所有必要约束都有证据支持
blocked 外部依赖、权限或环境阻塞
ambiguous 缺少用户决策,继续执行会替用户猜测
unsafe 下一步动作风险超过允许范围
budget_exhausted 已达到时间、Token 或调用预算
no_progress 连续多轮没有产生有效进展
unrecoverable 当前方案已被证据证明不可行
不同的停止原因应该产生不同的交付结果。
completed 可以给出最终结果和证据;ambiguous 应该给出待确认的问题;blocked 应该说明依赖哪一个外部条件;no_progress 则应该总结已经尝试过的路径,避免下一次从同一个错误起点重新开始。
这也是为什么“最大循环次数”只能是最后一道保险,而不能是主要的退出策略。
十一、如何分配探索、修复和验证预算
复杂任务的每轮成本并不相同。读取文件可能只需要几秒,跑一次完整集成测试可能需要几分钟,访问生产数据则可能带来真实风险。
可以把循环预算拆成三类:
诊断预算
用于理解环境和排除假设。任务刚开始或连续失败时,诊断预算更重要。
实现预算
用于修改代码、生成 SQL、更新配置。只有当当前假设有足够证据支持时,才应该增加实现投入。
验证预算
用于运行测试、执行沙箱查询、做回归和人工复核。验证不是实现完成后的附属步骤,而是循环本身的一部分。
一个常见错误是把几乎所有预算都花在生成和修改上,最后只用一个很便宜的检查宣布完成。更合理的做法是根据风险动态分配:
低风险、强反馈任务:快速行动,频繁验证
高风险、弱反馈任务:先增加诊断和人工确认
连续失败任务:减少修复,增加探索或重新规划
接近完成的任务:减少探索,增加回归验证
十二、把 Loop 实现成有限状态机,而不是一段 Prompt
如果所有循环逻辑都写在一个大 Prompt 里,系统很难测试,也很难知道模型为什么做出了某个决定。
更稳妥的设计是由代码维护高层状态,由模型负责局部决策:
class Phase(Enum):
UNDERSTAND = "understand"
PLAN = "plan"
ACT = "act"
VERIFY = "verify"
REPLAN = "replan"
ASK = "ask"
DONE = "done"
STOP = "stop"
def transition(state, result):
if result.unsafe:
return Phase.STOP
if result.completed:
return Phase.DONE
if result.blocked or result.ambiguous:
return Phase.ASK
if result.no_progress or result.hypothesis_rejected:
return Phase.REPLAN
return Phase.ACT
模型可以在 ACT 阶段选择查文件、修改代码、执行测试还是调用工具,但它不能自行绕过 VERIFY,也不能在没有证据的情况下把状态改成 DONE。
状态机的价值不是把 Agent 变成僵硬的工作流,而是为模型的自由度划边界:
- 哪些状态可以执行什么动作;
- 哪些动作需要确认;
- 哪些结果必须回退;
- 哪些状态不能自动跳过。
这是一种很重要的分工:让模型处理开放式决策,让系统代码处理不可违反的约束。
十三、记忆不是保存聊天记录,而是保存可复用的实验结果
如果一个 Agent 每次失败后只保存“模型说了什么”,下一次任务很难真正受益。
对 Loop 更有价值的记忆应该是:
假设:report_date 是报表口径字段
动作:使用 report_date 生成查询
结果:与基准样例一致
证据:3 个固定样例 + 1 次边界测试
适用范围:广告报表查询
失效条件:跨时区实时事件查询
这样的记忆包含假设、动作、结果、证据和适用范围。它可以帮助下一次循环减少探索,但不会把一次偶然成功误用到所有场景。
从这个角度看,记忆系统应该保存“哪些策略在什么条件下有效”,而不是无限追加对话摘要。记忆质量直接影响 Loop 的初始信念状态,错误记忆则会让 Agent 从错误假设出发,并且越来越难以纠正。
十四、安全不变量必须高于局部进展
Loop 的目标不是不惜一切代价完成任务。
在数据库 Agent 中,不能因为查询一直失败,就放开只读限制;在代码 Agent 中,不能因为测试不通过,就删除测试;在自动化 Agent 中,不能因为页面操作卡住,就绕过二次确认。
这些规则属于安全不变量,它们的优先级高于任何局部进展:
先检查动作是否合法
再执行动作
再判断动作是否产生进展
如果动作违反不变量,循环应该进入 unsafe 或 stop,而不是让模型继续寻找绕过规则的方法。
这也解释了为什么权限、沙箱、回滚和人工确认是 Loop 的组成部分。它们不是 Harness 中与循环无关的基础设施,而是控制状态转移边界的机制。
十五、一个实用的设计顺序
如果要为一个新 Agent 设计 Loop,我会按下面的顺序推进,而不是先写一个大 Prompt:
先定义完成,而不是先定义动作
把“用户满意”拆成可验证的约束,并明确哪些约束必须有客观证据。
再定义观测,而不是先定义重试
列出每种动作可能返回的结果,以及结果如何区分实现错误、环境错误、方案错误和不确定性。
再定义状态和回退边
决定哪些阶段可以循环,哪些失败会触发重新规划,哪些情况必须回到上一个检查点。
最后才让模型选择动作
给模型一个受约束的动作空间,并把当前状态、未满足约束、已有证据和预算放进上下文。
可以用这份问题清单检查设计是否完整:
- 最终目标能否拆成明确的验收约束?
- 每个约束对应什么证据?
- 当前观测是世界状态,还是模型的猜测?
- 下一步动作会带来目标收益、信息增益,还是两者都没有?
- 失败后是修复、探索、回滚、重规划,还是询问用户?
- 连续几轮没有进展时,系统会发生什么?
- 哪些动作有不可逆风险?
- 如果模型误报完成,独立评估器能否拦住它?
- 任务失败后,下一次循环能否复用本次得到的有效证据?
结语:好的 Loop 不是跑得更久,而是越来越确定
Agent 的能力上限可能由模型决定,但 Agent 能不能稳定完成任务,取决于它和现实之间有没有一个设计良好的反馈回路。
一个好的 Loop 不会把所有失败都交给模型重新生成,也不会把“命令执行成功”当成“任务已经完成”。它会区分世界状态和信念状态,记录每一条重要证据,判断动作是否带来信息增益,在错误假设被证伪时及时重规划,在风险和不确定性不可接受时主动停止。
从这个角度看,Loop Engineering 更像是在设计一个小型的实验系统:
提出假设 -> 执行动作 -> 获取证据 -> 更新信念
^ |
| v
+---------- 继续、回退或停止 <------+
模型负责提出下一步猜想,环境负责提供现实反馈,评估器负责判断证据,状态机负责控制边界。只有这几部分配合起来,循环才不是“多试几次”,而是一个会因为失败而变得更确定、因为证据而逐渐收敛的工程系统。
最终我们想要的,不是一个永远愿意继续尝试的 Agent,而是一个知道下一步为什么值得尝试、知道什么证据足以完成、也知道什么时候继续行动只是在浪费成本的 Agent。
