Loop Engineering:让 Agent 自主迭代直到正确的工程循环

本文是「Agent 基础与工程」系列专栏的第 10 篇。专栏总览参见:《Agent 基础认知与工程架构全景》。

在单次调用场景中,开发者往往将精力放在 Prompt 的遣词造句与工具参数的 Schema 定义上。但在长程多步 Agent 系统中,绝大多数严重的线上故障并不源于单次模型调用的语法错误,而是出在循环控制流层面:

  • 模型自称“任务已圆满完成”,但实际交付物错误百出(提前退出的虚假收敛);
  • 模型陷入死循环,使用完全相同的参数连续几十次调用同一个只读接口,直到耗尽全部 Token 预算;
  • 修复 A 问题时引入了 B 错误,修复 B 时又改回了 A,陷入无限震荡;
  • 某一轮工具返回了异常数据,后续所有规划均建立在此错误假设上,导致错误像滚雪球般持续放大。

这些问题的根源在于缺少对循环本身的工程约束。Loop Engineering(循环工程)是对 Agent 执行循环的收敛条件、退出判据、纠错机制与资源熔断进行系统化设计的工程实践。


循环失控的四种典型病灶

  1. 状态乒乓震荡(Ping-Pong Oscillation):模型在两个相互冲突的局部最优解之间来回跳跃。例如在代码修复中,修改某个类型注解破坏了另一个函数签名,反复撤销和重写。
  2. 假性收敛(False Convergence / Premature Exit):大语言模型具备极强的“谄媚性”(Sycophancy)与自我合理化倾向,极易在尚未达成事实目标时主观宣称完成。
  3. 参数重复空转(Repetitive Stagnation):由于缺少历史反思机制,模型在工具报错后缺乏变通,以完全一致的入参反复尝试。
  4. 级联误差放大(Compounding Hallucination):单步感知偏差被写入长期上下文,模型在后续步骤中基于错误前提不断推导,最终导致执行目标彻底漂移。

核心法则:外部验证器驱动退出(Verifier-driven Exit)

解决循环失控的第一铁律是:严禁将“模型的自主回答”作为任务终止的最终信任源。退出判定必须移交至外部确定性验证器(Deterministic Verifier)。

在真实的生产闭环中:

  • 当模型在文本中输出“我已经修好了这个 Bug”时,Harness 并不将其视作任务结束,而是触发外部构建工具运行 pytest -q;
  • 若测试退出码为 0,才正式标记任务成功(SUCCESS);
  • 若退出码非 0,测试运行器的完整标准错误输出(stderr)被直接组装为一条带有明确纠偏指令的新消息注入上下文,强制模型进入下一轮自省与修复。

循环防护机制:熔断器与死循环检测

为了防范无限空转与成本失控,Harness 必须内置硬性防护守卫:

1. 动作指纹检测(Action Fingerprinting)

为每一轮工具调用计算结构化哈希:

Harness 维护一个环形滑动队列。若检测到相同的 Fingerprint 在最近 3 轮内连续出现,判定为局部死循环,直接阻断该次物理执行,并向模型返回警告消息:
"系统告警:你已经使用完全相同的参数重复调用该工具多次且均未解决问题,严禁再次重复,请分析错误日志并更换排查策略。"

2. 步长与资源衰减模型

为任务设置严格的多维资源上限:

  • 最大步长限制(Max Loop Steps):如限制单个任务最多运行 10 轮交互;
  • 累积 Token 预算(Token Budget):单任务累计消耗达 Tokens 时触发强警告,逼近 时触发强制收敛;
  • 回退与降级策略:当步长用尽时,系统不是默默退出,而是触发降级分支,保存当前现场上下文,生成结构化诊断报告并挂起等待人工干预(Escalate to Human)。

生产级 Loop 控制器实现

以下使用原生 Python 实现一个包含外部验证驱动、死循环指纹熔断与自动纠偏的 Agent Loop 控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import hashlib
import json
from typing import List, Dict, Any, Tuple

class LoopBreakerException(Exception):
pass

class AgentLoopEngine:
def __init__(self, max_steps: int = 8, max_repeated_actions: int = 2):
self.max_steps = max_steps
self.max_repeated_actions = max_repeated_actions
self.action_history: List[str] = []

def _compute_fingerprint(self, tool_name: str, args: Dict[str, Any]) -> str:
serialized = json.dumps({"tool": tool_name, "args": args}, sort_keys=True)
return hashlib.md5(serialized.encode("utf-8")).hexdigest()

def check_loop_safety(self, tool_name: str, args: Dict[str, Any]) -> None:
fp = self._compute_fingerprint(tool_name, args)
self.action_history.append(fp)

# 检查尾部连续相同动作次数
recent_matches = 0
for past_fp in reversed(self.action_history):
if past_fp == fp:
recent_matches += 1
else:
break

if recent_matches > self.max_repeated_actions:
raise LoopBreakerException(
f"检测到死循环风险:连续 {recent_matches} 次尝试执行相同工具及入参 [{tool_name}],执行已熔断。"
)

def external_verify(self, solution_artifact: str) -> Tuple[bool, str]:
# 模拟确定性外部断言(例如执行编译器编译或单元测试)
if "assert result == 42" in solution_artifact:
return True, "All tests passed."
return False, "AssertionError: Expected 42, but got invalid output."

def run(self, user_goal: str) -> Dict[str, Any]:
print(f"[Loop Engine] 启动任务循环: {user_goal}")
messages: List[Dict[str, Any]] = [
{"role": "user", "content": user_goal}
]

current_step = 0
while current_step < self.max_steps:
current_step += 1
print(f"\n--- [Step {current_step}/{self.max_steps}] ---")

# 模拟模型在第 1 步产出错误方案,在第 2 步收到验证失败反馈后纠偏成功
if current_step == 1:
model_proposal = "def solve(): return 0 # assert result == 0"
tool_call = ("run_eval", {"code": model_proposal})
else:
model_proposal = "def solve(): return 42 # assert result == 42"
tool_call = ("run_eval", {"code": model_proposal})

# 1. 死循环指纹拦截
try:
self.check_loop_safety(tool_call[0], tool_call[1])
except LoopBreakerException as e:
print(f"[熔断拦截]: {e}")
return {"status": "FAILED", "reason": "LOOP_DETECTED"}

# 2. 外部确定性验证驱动退出
passed, feedback = self.external_verify(tool_call[1]["code"])

if passed:
print(f"[外部验证通过]: 业务断言成功,允许收敛。")
return {"status": "SUCCESS", "step": current_step, "solution": model_proposal}
else:
print(f"[外部验证驳回]: {feedback}")
# 将真实的验证器错误写入上下文,驱动下一轮自我修复
messages.append({
"role": "system",
"content": f"外部验证失败反馈:\n{feedback}\n请根据上述断言失败信息调整代码方案。"
})

return {"status": "FAILED", "reason": "STEP_BUDGET_EXCEEDED"}

if __name__ == "__main__":
engine = AgentLoopEngine()
result = engine.run("编写求解核心指标的算法函数并确保满足断言")
print(f"\n最终执行状态: {result}")

实践指南:构建自收敛循环的关键指标

在优化 Agent 执行循环时,重点关注以下三项效能指标:

  1. 一次通过率(Pass@1)与重试后通过率(Pass@N):通过观察在给定第 轮循环内收敛的概率曲线,衡量自省与纠错逻辑的有效性。若曲线在第 3 轮后完全趋于平缓,说明超过 3 轮的重试属于无效消耗,应提前触发人工升级。
  2. 无效动作比率(Wasted Action Ratio):统计在任务全生命周期中,参数报错、被熔断拦截或无信息增益的工具调用占比。生产系统应将该比率压低至 以下。
  3. 退出真实性审计:定期采样抽检模型自称“已完成”的案例,交叉核对业务系统底层的数据状态,杜绝任何假性收敛。

系列导航与参考