跳到正文

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 则烧完预算才发现跑偏。

  1. 语义判断(L0):模型自己觉得该停了。最便宜,也最不可靠;必须接证据门——完成声明要附带命令退出码、测试结果、文件 diff 或接口返回,没有证据按未完成处理。
  2. 模式检测·计数器(L1a):相同调用 ×5 警告,同一工具连续 ×8 终止。它只数次数,实现简单,但会被“换参数继续调”绕过。
  3. 模式检测·指纹(L1b):把 prompt、工具参数、上下文快照一起做 MD5,保留最近 20 条历史,相同指纹出现 ≥3 次判死。它能抓语义等价但字面不同的循环,代价是计算与存储更高;快照口径不统一会误判。
  4. 预算/程序化上限(L2):步数、模型调用数、工具调用数、token、墙钟时间,任一超限即停。生产框架通常提供程序化上限(如 recursion_limit、MAX_STEPS),名字和机制不同,但都必须存在;只设一个很大的硬上限,等于没有预算。
  5. 失败升级:可重试错误先退避重试;重试无效则重规划或换模型;仍失败则降级为只读建议、转人工,或带着部分结果和终止原因退出。终止原因要写进结果和日志,调用方才能区分“做完”和“被截断”。
机制输入判据抓什么漏什么
计数器工具名 + 规范化参数,或仅工具名相同调用 ×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

把 AI 工程讲到既懂也会用