Agent 为什么需要循环
普通大模型调用像一次咨询:给出问题,得到回答,调用结束。
Agent 面对的却常常是另一类任务:先查看项目结构,再定位代码,修改文件,运行测试,根据失败结果继续调整,最后验证是否完成。模型不可能在第一次调用前就知道所有工具结果,因此必须在“观察—判断—行动—再观察”之间循环。
驱动这个过程的就是 Harness 第三个核心部分:Agent Loop。
它看起来像一个 while 循环,但工程上的 Agent Loop 更接近一台有预算、有状态、有错误语义、可以被用户打断的状态机。
本文是《把 Harness 讲清楚:Agent 背后的运行系统》八个核心部分的第三篇。
最小循环只有几行代码
最容易理解的版本是:
messages = [user_request]
while True:
response = model.generate(messages, tools)
messages.append(response)
if response.has_no_tool_calls():
return response.text
for call in response.tool_calls:
result = execute_tool(call)
messages.append(result)
这段代码已经具备 Agent 的基本形状:模型决定动作,环境返回观察,模型再根据新信息继续判断。
但它没有回答更困难的问题:
- 模型连续调用同一个失败工具怎么办?
- 多个工具可以并行吗?
- 工具执行一半时用户取消怎么办?
- 模型已经给出答案,但任务真的完成了吗?
- 上下文超长、预算耗尽或模型服务失败怎么办?
- 新消息在运行中到达,应该进入当前步骤还是下一轮?
这些问题决定了 Loop 能不能从 Demo 走向真实系统。
Turn、Step 和 Tool Call
理解 Agent Loop,最好先区分三个层级。
Turn
一次由用户输入或系统事件唤醒的完整处理过程。它从领取任务开始,到本轮不再有待处理工作为止。
Step
一次模型调用,以及这次调用要求执行的工具。模型读取工具结果后再次请求模型,就进入下一个 Step。
Tool Call
某个 Step 中的一次具体环境操作,例如读取文件、搜索网页或执行测试。
它们的关系是:
Turn 1
├─ Step 1
│ ├─ 调用模型
│ ├─ Tool Call:搜索文件
│ └─ Tool Call:读取配置
├─ Step 2
│ ├─ 调用模型
│ └─ Tool Call:修改代码
├─ Step 3
│ ├─ 调用模型
│ └─ Tool Call:运行测试
└─ Step 4
└─ 调用模型并生成最终答复
分层以后,预算、日志、重试和恢复才能说清楚。例如模型请求失败可以重试当前 Step,工具失败可能让模型进入下一 Step 自我修正,而用户发来新任务通常应该开启新的 Turn。
一个稳健循环的完整流程
领取输入
↓
检查任务契约与当前状态
↓
组装上下文和可用工具
↓
请求模型
├─ 模型服务错误 → 按策略重试、降级或终止
↓
解析模型输出
├─ 无工具调用 → 进入完成验证
└─ 有工具调用
↓
权限检查与工具调度
↓
记录结果并更新状态
↓
检查预算、取消和停止条件
├─ 可以继续 → 下一 Step
└─ 不可继续 → 终止或请求用户介入
Loop 本身不应包办上下文检索、安全策略和工具实现。它更像交通调度中心:按顺序调用各个模块,并确保状态转换完整发生。
停止条件比循环条件更重要
很多 Agent 问题不是不会开始,而是不知道什么时候停。
一个 Loop 至少需要考虑这些停止条件:
- 模型没有请求工具,并给出最终答复;
- 任务完成标准已被验证;
- 达到最大 Step 数、token、时间或费用预算;
- 用户取消任务;
- 策略层阻止继续执行;
- 同类错误重复出现,继续尝试价值很低;
- 缺少必须由用户提供的信息;
- 外部服务不可恢复地失败。
“模型说完成了”只能是候选停止信号,不能代替结果验证。代码任务至少应检查文件确实变化、相关测试确实运行并通过。
防止 Agent 原地打转
循环最常见的退化是重复:反复读取同一个文件、用相同参数调用失败工具,或者在两个方案之间来回切换。
可以为最近动作建立指纹:
def action_fingerprint(tool_name, arguments, state_revision):
return hash(tool_name, normalize(arguments), state_revision)
if fingerprint in recent_failed_actions:
repeated_count += 1
if repeated_count >= 3:
inject_feedback("相同动作已连续失败,请改变方案或请求帮助")
if repeated_count >= 5:
stop(reason="repeated_no_progress")
不能只比较工具名称。修改文件后再次运行同一个测试是合理行为,因为状态已经变化;在状态不变时重复同一失败调用,才更像死循环。
预算不是只有 token
真实任务的成本来自多个维度:
| 预算 | 防止的问题 |
|---|---|
| 最大 Step 数 | 无限推理循环 |
| 最大工具调用数 | 高频无效操作 |
| token 上限 | 上下文与生成成本失控 |
| 挂钟时间 | 任务长期占用资源 |
| 金额上限 | 模型和外部 API 费用超标 |
| 外部配额 | 搜索、邮件、数据库被打爆 |
预算耗尽时,Harness 应清楚告诉模型或用户哪个预算已用完、已经完成什么、还差什么,而不是只返回“任务失败”。
模型错误与工具错误要分开
模型服务超时、限流和内容被截断,属于请求层错误。命令退出码非零、文件不存在和接口返回 403,属于工具层结果。
两者的处理方式不同:
- 临时模型限流可以指数退避后重试同一 Step;
- 参数错误应该反馈给模型,让它修正调用;
- 权限拒绝不应自动重试;
- 有副作用工具的结果未知时,不能盲目重放;
- 预算错误通常需要调整任务或请求用户决定。
如果所有问题都被包装成一条普通文本“执行失败”,模型和运维人员都无法采取正确行动。
取消不是抛出一个异常就结束
用户点击停止时,Loop 需要完成一套有序退出:
收到取消信号
↓
停止发起新的模型请求和工具调用
↓
向正在执行的能力传播取消
↓
等待已启动操作结算或进入明确未知状态
↓
持久化已获得的结果
↓
关闭当前 Step 和 Turn
↓
向用户报告已完成、已取消和结果未知的部分
尤其是外部写入,网络连接中断不等于操作没有发生。Harness 必须把“未执行”“执行失败”和“结果未知”区分开。
运行中到达的新消息怎么处理
用户可能在 Agent 工作时补充:“不要修改数据库。”这条消息不能等任务全部完成后才读取。
常见设计会提供不同入口:
- 当前步骤注入:只加入不改变安全边界的补充上下文;
- 下一步骤消息:当前工具结束后,让模型立即看到;
- 下一轮消息:当前 Turn 完整收尾后处理;
- 取消并重开:新要求推翻原任务时停止当前运行。
选择哪一种取决于消息语义。新的禁止条件应尽快生效,而一句“完成后顺便总结”可以排入后续步骤。
更完整的 Loop 伪代码
async def run_turn(agent, task):
state.open_turn()
try:
while not state.should_stop():
state.check_budgets()
cancel_signal.throw_if_cancelled()
context = context_engine.build(task, state)
response = await llm.call(context, tool_gateway.visible_tools(task))
state.record_assistant(response)
if not response.tool_calls:
verdict = evaluator.check(task, state, response)
if verdict.passed:
return response
state.inject(verdict.feedback)
continue
results = await scheduler.execute(response.tool_calls, task)
state.record_tool_results(results)
state.detect_no_progress()
except Cancelled:
await scheduler.drain_started_calls()
state.mark_cancelled()
finally:
state.close_turn()
这里每个模块都有独立职责,Loop 只负责编排和状态推进。
常见的失败方式
把 Loop 写成无法观察的大函数
上下文、模型调用、工具执行和错误处理混在一起,任何策略变化都要修改核心循环。应该通过明确事件或接口连接各模块。
自动重试所有错误
权限拒绝、参数校验失败和未知副作用都不适合无脑重试。重试策略必须理解错误类型和动作语义。
只有最大轮数,没有进展检测
Agent 可以在预算内重复几十次无效操作。除了硬上限,还应判断状态是否发生有意义变化。
取消后立即丢弃所有结果
已经完成的工具结果仍然是事实,应当落盘;否则恢复时系统不知道外部世界发生了什么。
用最终文本代替完成验证
“测试已通过”是一句话,退出码为 0 的真实测试记录才是证据。
Agent Loop 检查清单
- 是否明确区分 Turn、Step 和 Tool Call?
- 每个状态转换是否可以记录和恢复?
- 是否有 Step、token、时间、金额和调用次数预算?
- 是否能识别重复失败和无进展循环?
- 模型错误、工具错误和策略拒绝是否分类处理?
- 取消是否会停止新工作并妥善结算已启动操作?
- 运行中新消息是否有明确的进入时机?
- 停止前是否检查任务完成标准?
- 失败时能否说明已经完成什么、还缺什么?
最后理解 Agent Loop
Agent Loop 不是让模型“一直想”,而是让推理与现实反馈有秩序地交替发生。
好的 Loop 不以轮数多为荣。它应该让每一步都有新信息或真实进展,在需要行动时安全行动,在证据不足时继续验证,在预算耗尽或风险上升时及时停下。
一句话总结:循环的价值不在于不停运行,而在于每一轮都让任务更接近可验证的完成。