主题
Agent 循环与终止控制
一句话:Agent 是“无状态概率模型 + Harness(工具、循环、状态、验证)”的组合,它把模型从“会说”推进到“会做”,代价是模型的错误也会被真实执行,并按步数放大。
适用:2026 年主流大模型 API 与 Agent 框架。循环机制与模型无关,具体 API、参数与框架能力以官方文档为准。
为什么需要
一个典型现象:demo 里模型能查资料、改文件、跑命令,看起来什么都会;上线后却反复调用同一个工具、在任务没完成时宣布完成,或者一次任务烧掉几十轮 token。排查时往往发现不是模型“变笨了”,而是团队把它当成了有状态、会执行、能自检的服务。
LLM 的实际形态要朴素得多:每次调用都是一次独立的函数调用,输入是一段 token 序列,输出是下一个 token 的概率分布采样。状态、执行、终止、重试都不在模型里,而在外面的工程结构里。Agent 循环就是把这四件事补上的最小结构;不理解这一点,后面的上下文、记忆、工具治理都会建在错误的前提上。
核心机制
模型边界:四个必须先接受的事实
- 概率生成,不是确定性计算。模型逐 token 采样,
temperature=0只是倾向于选概率最高的 token,并不保证同一输入永远同一输出。评测与回归要多次运行看分布。 - 上下文有限,且注意力不均匀。窗口是硬上限,成本随输入增长;长上下文里首尾信息更容易被抓住,中间段落更容易被忽略。把整库代码塞进去,不等于模型会用。
- 无状态。模型不记得上一轮说过什么。所谓“记得”,是框架把历史消息、工具结果重新拼进本次请求;会话一断,进程里什么都不剩。
- 幻觉是机制的一部分。模型优化的是“下一个 token 像不像训练数据里的合理续写”,不是“这句话有没有被验证”。所以“已完成”“测试通过”是声明,不是证据;证据必须来自工具输出或外部检查。模型也不执行工具,只输出调用意图,真正读文件、发请求、改数据库的是框架或工具网关;权限、超时、幂等、审计都必须在执行侧做。
Agent 四要素:把不可靠的推理包进可靠的结构
这张图里,模型只占一个节点,剩下全是工程结构。状态外置(消息、计划、预算、权限记录)让循环可以中断、恢复、审计;工具网关把“模型想做什么”和“系统允许做什么”分开。代价是每一轮都要重新组装并重发上下文:单轮输入随轮数线性增长,累计发送成本接近二次增长。
Harness:模型之外的责任边界
用一句话概括这层工程结构:Agent = Model + Harness。Model 负责理解、推理、生成和决策;Harness 负责每轮给模型什么、允许它做什么、任务如何推进、失败如何恢复、完成由什么证明。连同 Model 一起拆,责任落在五层:Model 理解与生成,Harness 编排,Runtime(调度、状态、工作区、恢复),Sandbox / Environment(限定影响范围),Agent Platform(身份、版本、配额、审计)。框架只是 Harness 的一种载体,不等于 Harness;托管运行也不等于把责任托管出去。
这条边界不是术语洁癖,而是故障归因的前提:模型答错、Harness 漏了验证、Runtime 恢复不完整、沙箱权限过宽,是四类不同的问题,修法也完全不同。判断一个应用是否真的 Agentic,可以看六点:以任务结果为中心、运行时决定部分路径、能作用于外部环境、跨步骤维持状态、自主行为受确定性机制约束、靠可观测与评估持续改进。自主性越强,越要配同等的边界、证据和验证;选型原则是最低充分架构,不是能力堆得越多越好。完整拆解见 Harness 与 Agent 参考架构。
三种循环:能力来源不同,代价也不同
| 循环 | 怎么运转 | 换来了什么 | 代价 |
|---|---|---|---|
| ReAct | 推理 → 调工具 → 看结果 → 再推理 | 能根据中间结果改策略,适合步骤事先说不清的任务 | 路径方差大,轮数和成本不可预测,容易原地打转 |
| Plan-Execute | 先产出结构化计划,校验后逐步执行 | 计划可审计、可分配预算、可人工确认 | 计划错了后面全错;开放任务里计划很快过时,重规划也要花钱 |
| Reflection | 生成结果 → 批判/检查 → 修订 | 能抓出一部分明显错误,尤其是有外部检查信号时 | 多一次或多次模型调用;模型自查与自身盲区相关,容易只改措辞不改事实 |
选择的分界不是“哪个更先进”,而是任务的不确定性在哪里:步骤不可预知用 ReAct,步骤可枚举且要审计用 Plan-Execute,结果可验证且错误代价高才值得 Reflection。三者可以嵌套:Plan 的每个步骤内部跑 ReAct,关键产物过一道 Reflection。还要区分路径控制方式与应用形态:Workflow 路径预设、Agent 动态决策、Hybrid 固定边界加局部判断,这是路径控制;单 Agent、长程 Agent、多 Agent 是应用形态。两者是两个维度,长程和多 Agent 也不是必经升级。
循环控制权四象限:谁决定下一步
三种循环回答的是“推理策略怎么选”;四象限回答的是“循环流转由谁控制”。横轴看控制权在模型还是框架,纵轴看控制流是显式还是隐式:模型控制加隐式流转是 Prompt 驱动,框架控制下再分显式拓扑、事件流和显式状态。两条轴不能混为一谈。
| 路线 | 谁决定下一步 | 典型形态 | 换来了什么 | 代价 |
|---|---|---|---|---|
| Prompt 驱动 | 模型在每轮输出里决定继续、调工具还是结束 | 通用智能体、开放域助手 | 加能力主要改提示词,灵活、迭代快 | 循环藏在运行时里,审计弱,终止更依赖模型输出与计数器 |
| 图编译 | 开发者预定义节点和条件边,运行时按图推进 | 工作流式 Agent、需要回放的编排 | 路径可审计、可回放,Checkpoint 显式 | 拓扑变更要改代码,开放任务容易僵化 |
| Reactive | 事件与 Hook 链驱动,模型输出作为事件流 | 高并发、非阻塞的 Agent 运行时 | 并发好、扩展点清晰 | 控制流分散在回调里,排障和时序推理更难 |
| 状态机 | 显式状态枚举和转移函数,模型只在特定状态做有限选择 | 生产级多 Agent、强治理场景 | 确定性最强,异常路径也能程序化 | 状态与迁移设计成本高,状态多了容易膨胀 |
两条轴的取舍本质是把多少决策权让渡给模型的概率输出:控制权越靠模型,加能力越便宜(改提示词)、行为越不可预测;越靠框架,审计回放越便宜,开放任务越容易僵化。选路线就是在这笔账上选方向,不是选先进落后。图里先按纵轴分格、再在格内按控制权分出两条路线,所以四个节点是四种并列选型,不是四个先后阶段。
HITL 不是第五种路线,而是一层横跨四格的暂停机制。无论哪条路线,都可以在敏感动作前发出中断:把执行快照落盘、释放线程、等待人工确认,再从断点恢复。路线仍然决定“下一步做什么”,HITL 只决定“这一步要不要先停”。生产级 11 状态与旁路不在本文展开,见 04/04。另外,Prompt 驱动的通用智能体通常没有显式图拓扑,也未必能看到直观的 for/while;循环仍由运行时的一个 query loop 驱动,停止条件主要交给模型输出与计数器。这是对某类实现的观察与推断,不是所有框架的规范。
流式四层:用户看到的,不等于框架执行的
| 层 | 流的是什么 | 谁消费 | 判定或边界 |
|---|---|---|---|
| ① 消息触发 | 用户消息到运行请求 | 运行时 | 到达即启动,不等确认 |
| ② 渲染流式 | token 增量 | 前端 | 首 Token 目标 <800ms,是体验目标 |
| ③ 执行流式 | tool_call 的增量 JSON | 框架 | WAITING → GATHERING → READY,闭合即入队 |
| ④ 事件驱动 | 工具与进度事件 | 编排层 | 可并发,驱动下一轮 |
②和③是同一股流上的两条并行线:token 可以继续回显,工具也可以已经开始执行;用户看到“字在蹦”,不代表框架还没开始干活,反过来也一样。执行流式的关键不是“等整个响应结束”,而是增量解析到参数 JSON 闭合就分派。用户看到的是渲染结果,框架执行的是另一条线,不能从界面状态推断执行状态。
图里只有一个分叉点:同一股 token 流从 S 分成渲染线与解析线,两条线互不等候,任何一条都不会等另一条先结束。虚线是旁注而不是依赖,它说明界面看起来还在输出、执行侧可能已经入队,反过来也一样。
终止、预算与失败升级
终止不能只靠模型说“做完了”。可靠的顺序是三层漏斗:语义判断 → 模式检测 → 预算/程序化上限。下面的 5/8/20/3 是某实现的设计值,不是通用常数。
漏斗的原理是每层只信上一层不会骗自己:L0 最便宜但模型既可能谎称完成也可能原地打转不停;L1 不看语义只看行为模式,能抓住 L0 抓不住的"自称还在干活其实鬼打墙",但计数器会被换参数绕过、指纹要付出计算与误判代价;L2 是纯程序上限,不聪明但绝不会被骗,所以是唯一的"必须存在"层。三层是成本与可靠性的交换序列,不是可选菜单——只留 L0 等于让模型给自己的终止签字,只留 L2 则烧完预算才发现跑偏。
- 语义判断(L0):模型自己觉得该停了。最便宜,也最不可靠;必须接证据门——完成声明要附带命令退出码、测试结果、文件 diff 或接口返回,没有证据按未完成处理。
- 模式检测·计数器(L1a):相同调用 ×5 警告,同一工具连续 ×8 终止。它只数次数,实现简单,但会被“换参数继续调”绕过。
- 模式检测·指纹(L1b):把 prompt、工具参数、上下文快照一起做 MD5,保留最近 20 条历史,相同指纹出现 ≥3 次判死。它能抓语义等价但字面不同的循环,代价是计算与存储更高;快照口径不统一会误判。
- 预算/程序化上限(L2):步数、模型调用数、工具调用数、token、墙钟时间,任一超限即停。生产框架通常提供程序化上限(如 recursion_limit、MAX_STEPS),名字和机制不同,但都必须存在;只设一个很大的硬上限,等于没有预算。
- 失败升级:可重试错误先退避重试;重试无效则重规划或换模型;仍失败则降级为只读建议、转人工,或带着部分结果和终止原因退出。终止原因要写进结果和日志,调用方才能区分“做完”和“被截断”。
| 机制 | 输入 | 判据 | 抓什么 | 漏什么 |
|---|---|---|---|---|
| 计数器 | 工具名 + 规范化参数,或仅工具名 | 相同调用 ×5 警告,同一工具连续 ×8 终止 | 原地重复 | 换参数绕行、上下文变了还在重复 |
| 指纹 | MD5(prompt + tool_params + context_snapshot) | 最近 20 条历史中相同指纹 ≥3 次 | 语义等价但字面不同的循环 | 计算与存储更贵,快照口径不一致会误判 |
两者不是一回事,也不能都叫“调用指纹”:计数器数的是次数,指纹比的是整段执行现场。成本上要有一个直觉:每轮都把累计历史重发一遍。设首轮上下文 B、每轮新增 D,第 n 轮单次输入约为 B+(n−1)D,这是 O(n);累计发送量约为 nB+D·n(n−1)/2,这是 O(n²)。按 B=2K、D=1K 估算,10 轮累计约 65K,20 轮约 230K;只多 10 轮,累计成本约 3.5 倍。增长来自累计重发,不是单轮 token 本身按平方增长。
为什么“能执行”会把错误放大
只回答问题时,错误停在文本里;能执行时,错误会变成副作用。一次误删、一次错误发布、一次重复扣款,都比一句错话难恢复。更隐蔽的是步数放大:假设每步独立正确率 0.9,5 步全部正确的概率约为 0.9^5≈0.59。单步看起来“九成靠谱”,串起来就接近一半会出错。
所以执行能力必须配套三件事:可逆优先(软删除、先写草稿、保留回滚点)、写操作先 dry-run 或审批(展示影响面再执行)、幂等(重试或恢复时不重复产生副作用)。这不是对模型的不信任,而是把“模型可能错”当成系统的基本假设。
实战场景
案例:谎报完成——Coding Agent 的“测试通过”其实没跑过测试
现象:支付对账服务修复任务,模型在第 14 轮宣布“问题已修复,测试全部通过”,用户上线后同类差错复现。复盘发现该 run 从头到尾没有调用过测试工具——“测试通过”只是最像合理续写的 token 序列,被 L0 语义判断直接采信为终止信号。
排查路径:按每轮日志(步数、工具签名、终止原因、证据引用)回放整个 run。该 run 终止原因记录为 model_final,证据引用字段为空;工具序列是 read → edit → final,“跑测试”这一步从未发生,而不是跑了但失败。归因落在循环缺证据门,不在模型能力。
修法:完成声明不能签字放行,要走“声明 → 匹配最近证据 → 程序化校验”:
python
EVIDENCE_RULES = {
"test_pass": lambda ev: ev.exit_code == 0 and "failed" not in ev.stdout.lower(),
"file_change": lambda ev: bool(ev.diff),
}
def accept_final(out, state):
claims = extract_claims(out.text) # 「已修复」「测试通过」→ 声明集合
for c in claims:
ev = state.latest_evidence(c.kind) # 绑定最近一次同类工具结果
if ev is None or not EVIDENCE_RULES[c.kind](ev):
state.append(hint(f"声明「{c.kind}」缺少证据,请实际执行验证或说明阻塞"))
return False
record(reason="verified", evidence=[c.kind for c in claims])
return True退出码、测试输出、文件 diff 三类信号都在工具网关侧采集,不要求模型自述。这个门只拦“谎报完成”,不拦“不肯承认完成”——后者由 L2 预算层兜底,这正是终止漏斗各层分工的意义。
案例:长任务 token 成本爆炸——运维排障 Agent 的 20 轮预算账
现象:运维排障 Agent 单任务平均成本两个月涨约 3.4 倍。日志显示平均轮数从 8 涨到 19(新接入的告警规则让任务变长),但步数上限与预算预警线从未按新分布核算,多数任务一路烧到接近硬上限才被截断。
排查路径:用正文的累计重发公式 nB + D·n(n−1)/2 做一次真实核算。当前分布首轮上下文 B≈6K(平台规则+工具清单+runbook),每轮新增 D≈2.5K(日志类工具结果为主):10 轮累计约 172K,20 轮约 595K——轮数翻倍、成本约 3.4 倍,与账单增幅吻合,确认增长来自累计重发而非单轮变贵。
修法:先定单任务 token 预算,再用公式反推 run 上限与预警线,而不是凭感觉设轮数:
yaml
budget:
max_total_input_tokens: 400000 # 经验值,需按任务价值与单价校准
max_steps: 14 # 公式反推:14 轮约 312K,16 轮已逼近 400K 上限
warn_at: 0.80 # 320K 触发预警:换更便宜模型或更激进压缩,不是只记日志
hard_stop: 1.00 # 终止并返回部分结果 + 终止原因,写进日志
per_round_estimate: {base: 6000, delta: 2500} # 每季度按真实轮次分布重新拟合场景:计数器抓不到的换参数死循环——客服退款工单
现象:退款工单任务里,order_query 与 kb_search 交替调用 30 多次不收敛:每次查询参数都略有不同(页码递增、时间格式轮换、订单号与物流单号交替)。计数器一条都没触发——“相同调用 ×5”被参数变化打散,“同一工具连续 ×8”被交替调用打散。
排查路径:按工具签名排序这 30 多次调用,发现返回内容哈希高度相似——模型一直在拿同一种空结果。根因是订单接口忽略 page 参数,模型把“空返回”归因为“参数没对”,于是系统性换参数重试。这就是正文表格里“换参数绕行、上下文变了还在重复”的实例:语义上已经在鬼打墙,字面上每次都是新调用。
修法:上指纹层(L1b),关键在口径设计——三个输入都必须规范化到可复现:
python
def fingerprint(goal_text, call, task_state, messages):
params = canonicalize(call.params) # 键排序、枚举归一、时间转 epoch、page/offset 折叠同值
snapshot = hash(task_state.facts, len(messages)) # 取事实集哈希,不取消息原文:原文每轮都在变
return md5(f"{goal_text}\n{call.name}\n{params}\n{snapshot}")误判防护三条,缺一不可:
| 防护 | 做法 | 防的是什么 |
|---|---|---|
| 口径统一 | prompt 项用“目标+当前步骤”文本,快照用 Task State 事实哈希 | 每轮都在变导致指纹永不重复,L1b 形同虚设;或口径漂移造成误判 |
| 作用域隔离 | 指纹只在同一 run 内比较,不跨任务、不跨会话 | 不同任务复用同一工具模式被误杀 |
| 新信息豁免 | 工具返回哈希变化时重置该指纹计数 | “同参数但拿到了新结果”的合理重试被截断 |
指纹比计数器贵(计算与存储),误判又会直接截断本可完成的任务,因此只在长任务或工具异常率高的队列上启用;命中后走失败升级链(退避重试 → 重规划/换模型 → 转人工),带终止原因退出,而不是抛“死循环”错误。
最小上手
一个最小循环只需要四样东西:消息状态、工具表、终止条件、预算。下面是显式循环的示意,便于讲清责任边界,不代表所有框架都这样写:
python
# 示意:最小 Agent 循环
state = {"messages": [system_prompt, user_task], "steps": 0, "seen": set()}
while True:
if state["steps"] >= MAX_STEPS or budget.exceeded():
return partial_result(state, reason="budget_or_steps")
out = llm.chat(state["messages"], tools=TOOLS)
if not out.tool_calls:
if verify(out.text, state): # 证据检查,不是模型自评
return out.text
state["messages"].append(hint("缺少证据,请继续或说明阻塞"))
continue
for call in out.tool_calls:
sig = signature(call) # 工具名 + 规范化参数
if sig in state["seen"]:
state["messages"].append(hint("该调用已重复,请换策略"))
continue
state["seen"].add(sig)
state["messages"].append(tool_msg(call, gateway.execute(call)))
state["steps"] += 1跑通判据:同一任务连续跑 3 次,都能在预算内结束;每次结束都能说清终止原因;重复调用会被拦截;完成声明能追到具体证据;把工具改成故意失败,循环会升级而不是空转。
生产动作
- 先设上限再调效果:常规任务步数上限按经验从 10–15 起步,token 与墙钟时间同时设;预算用到 80% 预警,100% 终止并返回部分结果。上限值要用真实任务分布校准,不要照抄。
- 记录可排障的字段:每轮记录步数、工具签名、输入/输出 token、累计成本、终止原因、证据引用。排障顺序是“工具结果是否可信 → 上下文是否缺关键信息 → 终止与预算是否误杀 → 才是模型能力”。
- 重试分类,写操作分级:超时、限流、5xx 可退避重试;参数错误、权限拒绝、业务校验失败不要重试,直接把可读原因回给模型。只读自动执行;低风险写执行但留痕;中风险先 dry-run 展示影响;高风险强制审批并限制爆炸半径。
- 评测看分布,恢复要演练:固定任务集多次运行,看成功率、步数分布、成本分位数和终止原因构成;状态存成可序列化检查点,进程重启或人工介入后能从断点继续,恢复时清理未完成的工具中间态。
边界
- 适合:步骤无法完全预先枚举、但每步结果可观察的任务,例如检索排障、代码修改、多源信息汇总。不适合:流程完全确定的任务,直接用代码或工作流更便宜、更可靠。
- 适合:错误可逆、可验证、有人工兜底的场景。不适合:不可逆的高风险动作在没有任何审批和 dry-run 的情况下交给循环自动执行。
- 适合:工具有明确输入输出和超时语义。不适合:工具本身不稳定且无法区分失败类型,这时循环只会把不确定性放大。
- 常见误区:以为轮数越多越聪明(多数时候只是更贵);以为上下文塞满就利用充分(中间信息反而更容易丢);以为
temperature=0就可复现;以为模型说“完成”就是完成。把提示词当权限边界也是误区,硬约束必须在工具网关和执行环境里。
讲清楚
Agent 的本质不是更聪明的模型,而是给一个无状态、会概率性出错的模型补上工具、循环和外部状态。模型只负责提出下一步,执行、鉴权、终止和恢复都在工程侧。ReAct 用中间结果换适应性,Plan 用前期规划换可审计,Reflection 用额外调用换一次纠错机会;而四象限问的是另一个问题:下一步由模型、图、事件还是状态机决定。HITL 横跨四格,只负责在该停的地方暂停。能执行会把错误变成副作用,也会按步数放大,所以语义判断、计数器、指纹、预算和失败升级要一起上;先保证最坏情况有界,再谈效果。
适用:2026 年主流大模型与 Agent 框架 | 初稿 2026-10