
简介:Loop 的控制权一旦交给概率模型,自动化会把缺陷放大成灾难;微观 Agent Loop 和宏观 Loop Engineering 必须共享同一套 5 条工程宪法
Loop Engineering 与它的暗面
简介:让循环系统自动发现工作、分配任务、交叉检查,前提是底层 Agent 本身可控
- 背景
- Loop Engineering:不再直接写 Prompt,而是设计一个闭环系统
- 自动发现工作、分配任务、交叉检查、持久记忆
- 由循环去提示模型、决定下一步做什么
- 搭好自动分诊 + 多 Agent 并行处理,第二天可能看到
- Token 账单炸了
- 只读工具被组合调用,导致全库数据泄露
- 两个 Agent 并发修改同一份文件,数据全没了
- 查日志却一切正常:每步都有输出,工具调用标记为绿色,响应时间也合理
- Loop Engineering:不再直接写 Prompt,而是设计一个闭环系统
- 根因
- Loop 的控制权交给概率模型,自动化会以更高的效率把缺陷放大为灾难
- 底层 Agent 会撒谎、会死循环、会越权,再完美的顶层调度也只是在用十倍速制造事故
- 业界讨论多停在"为什么要设计 Loop",很少进入"Loop 内部怎么设计、哪里会出事"
- 两个层次
| 层次 | 是什么 | 解决的问题 | 比喻 |
|---|---|---|---|
| Agent Loop(微观执行层) | 包裹在大模型外围的 while (true) 逻辑 |
单个 Agent 怎么稳定跑完一次任务 | 发动机 |
| Loop Engineering(宏观编排层) | 多 Agent 调度、外部工具对接、长期状态记忆的系统架构 | Agent 体系怎么脱离人工值守、长期自主运转 | 整车工程 |
- 两层面对的是同一组核心挑战
- 大模型是概率性的,生产系统要求确定性
- 大模型是无状态的,长期运行要求有记忆
- 大模型是单次推理的,业务闭环要求可恢复
- 多实例并发要求状态同步与隔离,模型天生缺乏协调能力
- 结论:两层共享同一套工程宪法(5 大通用设计模式)
- 微观上不遵守:死循环空转 Token、盲目越权毁坏数据
- 宏观上不遵守:多 Agent 互相踩踏、状态撕裂、静默失效
宪法一:硬边界熔断与独立裁决
简介:绝不依赖模型自觉停下,终止权交给代码硬边界和独立验证逻辑
- 两种失控
- 死循环:连续遇到模糊错误,不断尝试同一种方案
- 幻觉终止:明明没做完,却宣布做完了
- 微观层:硬预算熔断
- 设定轮数、时间、Token、金额四个上限,每轮强制检查
java
/** 硬预算:每轮循环最先检查,任何一项越界立即熔断 */
public record HardBudget(
int maxTurns, // 最多多少轮
int maxSeconds, // 超时
long maxTokens, // Token 预算
double maxCostUsd // 金额预算
) {
public static HardBudget defaults() {
return new HardBudget(50, 600, 200_000, 5.0);
}
}
- 微观层:动作指纹去重与进度哈希
- 死循环:同一工具 + 同一参数的调用次数超过阈值
- 幽灵循环:连续多轮上下文哈希不变,判定为空转
java
public static Optional<String> detectInfiniteRetry(AgentRun run, ToolCall call, int repeatThreshold) {
// 指纹 = 工具名 + 按 key 排序后的参数,参数完全一致才算同一个动作
String fp = sha256(call.name() + ":" + toSortedJson(call.params()));
int count = run.getFingerprintCounter().merge(fp, 1, Integer::sum);
if (count > repeatThreshold) {
return Optional.of("Dead loop: '%s' called %d×".formatted(call.name(), count));
}
return Optional.empty();
}
/** 幽灵循环检测:内部维护上下文 hash 的滑动窗口,连续不变则抛异常 */
public class ProgressMonitor {
public void check(AgentRun run) { /* ... */ }
}
- 独立裁决(防幻觉终止)
- 模型说做完不算,系统验证才算
- 微观层:一个
goalAchieved(run)校验函数 - 宏观层:Claude Code 的
/goal命令通过 Stop Hook 调用另一个专门的小模型,判断 Goal 是否达成- 本质是把终止权从"执行模型"交给"校验模型"(Maker/Checker 分离)
- 校验模型点头,循环才算真正终止
- 宏观层
- 全局任务硬约束:超出单日总预算(如 Claude Code 的
maxBudgetUsd)直接熔断 - 自动化任务必须定义可验证的终止条件,由独立验证节点判定
- 全局任务硬约束:超出单日总预算(如 Claude Code 的
- 本质:概率模型会自欺欺人,终止权必须交给有确定逻辑的守门员
宪法二:提议-裁决分离与权限门禁
简介:模型只负责提出下一步做什么,Loop 负责判断能不能做、要不要做
- 工具执行永远不是第一步,而是层层门禁之后的最后一步
- 微观层:拦截工具幻觉
- 执行前必须过注册表强校验,参数类型必须匹配
java
public static ValidationResult validateToolCall(ToolCall call, ToolRegistry registry) {
if (!registry.contains(call.name())) {
return ValidationResult.fail("工具 " + call.name() + " 不存在");
}
// ... 参数类型强校验略
return ValidationResult.ok();
}
- 微观层:审批拦截与权限上下文注入
- 模型不知道当前用户的 RBAC 权限,只会为了完成任务穷举路径
- Loop 在执行前拦截高风险操作,并把用户的数据权限范围强制注入工具参数
- 防止 Agent "忠诚地越权"
java
public record ToolCallPolicy(Set<String> highRiskTools) {
public Decision decide(ToolCall call, UserContext userContext) {
if (highRiskTools.contains(call.name())) {
return Decision.ASK; // 高风险:挂起,等人工审批
}
// 权限上下文注入:强制追加当前用户的数据权限范围
call.params().put("scope", userContext.dataScope());
return Decision.ALLOW;
}
}
- 宏观层:子 Agent 产出校验
- 写代码的和查代码的必须是两个 Agent(Maker/Checker 分离)
- 绝不采信子 Agent 的"已完成"声明,产出必须经校验节点通过
- 本质:把模型从"决策者"打回"提议者",工程系统才是最终裁判和权限守门员
宪法三:显式状态机与上下文防挤压
简介:拒绝隐式的 while(true),所有运行阶段都要有可序列化的状态定义,同时防止长上下文导致"指令失忆"
- 微观层:拆出明确的运行状态和阶段
- 阶段:PREPARE、THINK、DECIDE、EXECUTE、DONE(流转关系见文末状态机图)
java
public enum RunState { RUNNING, SUSPENDED, COMPLETED, FAILED, CANCELLED }
public enum RunPhase {
PREPARE, // 组装上下文(L0-L3 分层防挤压)
THINK, // 等模型输出
DECIDE, // 独立裁决意图
EXECUTE, // 执行工具(串行/并行由 ToolRunner 决定)
DONE // 目标达成,循环终止
}
- 防挤压认知
- PREPARE 阶段把上下文分成 L0-L3:系统指令 > 当前状态 > 知识 > 历史
- 系统区永不压缩,防止最关键的规则被长历史冲走
- 宏观层
- 任务全生命周期状态化,状态存在外部系统(Markdown、Linear)
- 随时可查当前卡在哪一步
- 本质
- 状态显式 = 可观测 = 可调试
- 上下文分层 = 防失忆 = 不越界
宪法四:持久化可恢复
简介:OOM、断网、API 限流是常态,挂了要能接着跑,绝不从头再来
- 不可恢复的循环都是玩具
- Checkpoint 不等于聊天记录,必须能完整恢复运行现场
- 微观层:关键节点写 Checkpoint,并保证写入的原子性
java
public interface CheckpointStore {
CompletableFuture<Void> save(AgentRun run); // 框架保证原子写入
CompletableFuture<Optional<AgentRun>> load(String runId);
}
// 在 AgentLoop 构造时注入;每轮 EXECUTE 结束后自动触发
AgentLoop loop = AgentLoop.builder()
.checkpointWriter(store::save)
.checkpointLoader(store::load)
.build();
- 宏观层
- 全局进度持久化到外部存储
- 系统重启或次日运行时,读取未完成的任务进度继续,而不是重跑全流程
- 本质:Checkpoint 粒度决定恢复精度
- 存聊天记录:只能让模型从头重新理解
- 存执行位置 + 快照:能直接续跑
宪法五:同步与隔离
简介:多个 Loop 实例并发操作共享状态时,用隔离和同步防止数据竞争与静默覆盖
- 微观层:写入串行化
- 模型一次输出多个工具调用时
- 只读工具:可受控并发
- 写入/删除工具:必须按顺序串行执行,涉及共享资源时加锁
- 模型一次输出多个工具调用时
- 宏观层:工作空间隔离
- 用 Git Worktree 隔离(如 Claude Code 的
--worktree参数),每个子 Agent 在独立分支干活- 防止代码冲突
- 确保 Checker 审查的基准不被 Maker 篡改
- 任务分派支持去重,同一项工作不会重复派发
- 用 Git Worktree 隔离(如 Claude Code 的
- 本质:没有隔离的并行是相互毁灭,没有同步的共享是定时炸弹
- 在概率性的模型输出之上,必须构建确定性的协调机制
终局代码:5 条宪法焊进 Agent Loop
简介:单 Agent 的微观循环不再是单薄的 while(true),而是一台有明确状态转换的执行引擎
- AgentRun:所有运行状态的唯一容器(宪法三)
java
@Getter
@Setter
public class AgentRun {
private final String runId;
private final String task;
private RunState state = RunState.RUNNING;
private RunPhase phase = RunPhase.PREPARE;
private int turnCount;
private long inputTokens;
private long outputTokens;
private double costUsd;
private final List<Message> messages = new ArrayList<>();
private final List<ToolCall> toolHistory = new ArrayList<>();
private ToolCall pendingToolCall;
private final Map<String, Integer> fingerprintCounter = new HashMap<>(); // 死循环检测
private final Map<String, Integer> retryCounter = new HashMap<>(); // 红绿灯重试计数
private String faultClass; // 崩溃现场
private String lastError;
}
- AgentLoop:5 条宪法的汇聚点
java
public class AgentLoop {
private final LlmClient llm;
private final HardBudget budget;
private final ContextManager contextManager;
private final ToolRunner runner; // 宪法五:串行/并行路由 + 红绿灯重试
private final ProgressMonitor progress; // 宪法一:幽灵循环检测
private final CheckpointWriter checkpointWriter;
public void loop(AgentRun run, EventSink events) {
while (true) {
// 宪法一:硬边界熔断 + 幽灵循环检测(每轮最先执行,越界必断)
BudgetGuard.check(run, budget);
progress.check(run);
// 宪法三:上下文分层防挤压(系统指令 > 当前状态 > 知识 > 历史)
// ContextManager 负责:超长时摘要压缩历史、系统区永不压缩
run.setPhase(RunPhase.PREPARE);
PreparedContext ctx = contextManager.prepare(run);
// 宪法三:THINK 阶段,把组装好的上下文送给 LLM
run.setPhase(RunPhase.THINK);
LlmResponse response = llm.complete(ctx.messages(), ctx.system(), tools());
run.setTurnCount(run.getTurnCount() + 1);
run.addTokens(response.usage());
// 宪法一 & 宪法二:独立裁决(防幻觉终止)
run.setPhase(RunPhase.DECIDE);
if (response.stopReason() == StopReason.END_TURN) {
// 铁律:模型说做完不算,goalAchieved 验证才算
// 宏观上 Claude Code 的 Stop Hook 同理:终止权交给校验模型(Maker/Checker 分离)
if (goalAchieved(run)) {
run.setState(RunState.COMPLETED);
events.emit(new RunCompletedEvent(run));
return;
}
run.getMessages().add(Message.user("目标尚未达成,请继续。"));
continue;
}
// 宪法二 & 宪法五:批量执行(死循环检测、幂等注入、审批拦截、并行/串行、红绿灯重试)
run.setPhase(RunPhase.EXECUTE);
runner.runBatch(run, response.toolCalls(), events);
// 宪法四:每轮执行结束后写 Checkpoint(挂了接着跑)
if (checkpointWriter != null) {
checkpointWriter.write(run);
}
}
}
}
- 生产级 Agent Loop 状态机

- 图中红色是 Loop 的两个裁决点,橙色是失败终态,绿色是唯一的成功终态
- 代码逐行的通俗注释和例子走查见文末「拓展:通俗读懂终局代码」
大模型是油门,Loop 才是刹车系统
简介:顶级 Agent 框架都遵循这 5 条宪法,差别只在各自的权衡偏好
| 维度 | Claude Code | OpenAI Agents SDK | LangGraph | 5 条工程宪法 |
|---|---|---|---|---|
| 熔断与裁决 | 美元预算 + 轮数 + /goal 校验 |
轮数 + 超时 + Guardrail | 图深度 + 递归限制 + 拓扑终态 | 硬边界熔断 + 独立裁决 |
| 裁决与权限 | canUseTool + hooks |
三层 Guardrail | interrupt_before/after |
提议-裁决分离 + 权限门禁 |
| 状态与防挤压 | 层次化(会话 + 循环) | 类型化(RunResult 子类) | 图快照链 | 显式状态机 + 上下文防挤压 |
| 恢复 | 会话级持久化 | 需自行实现 | 一等公民 Checkpoint | 持久化可恢复 |
| 同步与隔离 | Worktree 隔离 + 文件锁 | 并发控制 | 节点级管控 + 状态锁 | 同步与隔离 |
- 范式转移
- 从 Prompt Engineering 到 Context Engineering,再到 Loop Engineering
- 本质是把控制权从"概率性的大模型"剥离,交还给"确定性的工程系统"
- 分工
- 大模型是油门:提供智能与创意
- Loop 是刹车与方向盘:兜底、纠偏、导航
- 5 条宪法既适用于单 Agent 的微观循环,也是多 Agent 宏观编排的底层铁律
延伸思考:Maker/Checker 如何防"共谋"
简介:执行 Agent 为尽快完成任务伪造"测试通过"的证据,校验模型也被骗时,怎么设计更健壮的 Maker/Checker
- 问题
- Maker 假装跑了测试,或生成假的成功日志,传给 Checker
- Checker 只看 Maker 交来的材料,就会跟着误判
- 思路
- 别让 Checker 读 Maker 写的成功日志,那等于让被告自己写判决书
- Maker 只提交可复核的引用
- 产物引用、commit hash、测试命令和参数
- Checker 用独立身份在干净环境拉取固定版本,重新跑测试
- 查询 CI、对象存储、工单和目标系统的真实状态
- 证据必须由这些外部系统签发
- 能用退出码、哈希、状态码判定的,就不交给模型判断
- 语义质量这类没法机验的部分,才交给裁判模型
- 低温度、与 Maker 不同模型族,按 rubric 打分
- 随机化材料顺序,保留分歧
- 注意点
- 主观任务可以多评委,但仍有误判概率,不能包装成零风险
面试/考试记忆点
- Loop 的控制权交给概率模型,自动化只会以十倍速放大缺陷
- Agent Loop 是发动机(单 Agent 稳定跑完任务),Loop Engineering 是整车工程(体系长期自主运转),两者共享 5 条宪法
- 宪法一:终止权交给硬预算(轮数/时间/Token/金额)+ 独立裁决,模型说做完不算
- 宪法二:模型是提议者,Loop 是决策者;高风险工具挂起审批,用户权限范围强制注入参数
- 宪法三:显式状态机 + L0-L3 上下文分层,系统区永不压缩
- 宪法四:Checkpoint 存执行位置 + 快照而不是聊天记录,粒度决定恢复精度
- 宪法五:只读可受控并发、写入必须串行;宏观用 Worktree 隔离,防止 Maker 篡改 Checker 的审查基准
- 防 Maker/Checker 共谋:Checker 不读 Maker 的日志,自己在干净环境复跑,并查外部系统的真实状态
拓展:通俗读懂终局代码
简介:用一个清理过期订单的例子走一遍状态机,再给 AgentRun 和 AgentLoop 逐行加上大白话注释
- 一句话看懂
- 模型只负责提建议,Loop 负责拍板
- 模型说"我要调工具""我做完了",都要先过 Loop 这一关
- AgentRun 是档案袋:所有会变的数据都在里面,存档存它、恢复读它
- AgentLoop 是流水线:自己不存任务数据,只拿着一堆帮手一轮一轮干活
- 模型只负责提建议,Loop 负责拍板
- 状态机节点说明
| 节点 | 做什么 |
|---|---|
| AgentRun | 用户发消息或定时器、事件触发,创建一次任务 |
| Prepare | 按 L0-L3 拼上下文,太长就压缩历史,系统指令永不压缩 |
| Thinking | 调用 LLM,模型只回两种:final(做完了)或 tool(要调工具) |
| Decide | final 先跑独立校验,通过到 Finished,驳回回 Prepare;tool 去审批 |
| Wait_Approval | 策略裁决 Allow / Ask / Deny,同时把用户权限范围注入工具参数 |
| Suspended | Ask 时写 Checkpoint 挂起,人工批准去执行,拒绝到 Failed |
| 执行阶段 | Execute(只读并发、写入串行)→ Observe(结果写回对话)→ Compact(压缩 + 存档) |
| 虚线熔断 | 预算越界随时判失败,管的是整次运行,不属于某个具体步骤 |
- 图里的两个回路
- 小回路 Decide → Prepare:模型说做完了,校验没通过被打回
- 大回路 执行阶段 → Prepare:正常干活的每一轮
- 例子走一遍:清理 30 天前的过期订单
| 轮次 | 模型提议 | Loop 裁决 | 走向 |
|---|---|---|---|
| 1 | 调 query_orders(status=expired) |
只读低风险,Allow,注入当前用户的门店范围 | 查出 120 条写回对话,存档,进入下一轮 |
| 2 | 调 delete_orders(...) |
高风险,Ask | 写 Checkpoint 挂起,主管批准后恢复并执行删除 |
| 3 | 说"已完成" | goalAchieved 查库发现还剩 3 条(分页漏了),驳回 |
塞入"目标尚未达成,请继续",回到 Prepare |
| 4~5 | 删完后再说"已完成" | 校验通过 | Finished |
| 任意一轮 | 同参数反复调用,或花费超过 5 美元 | 熔断 | Failed |
- 代码与图的对应
| 代码 | 图上节点 |
|---|---|
BudgetGuard.check / progress.check |
虚线熔断 |
contextManager.prepare |
Prepare |
llm.complete |
Thinking |
if (END_TURN) + goalAchieved |
Decide 的 final 分支 |
return |
Finished |
塞入"目标尚未达成" + continue |
校验驳回 → Prepare |
runner.runBatch |
Decide 的 tool 分支 → Wait_Approval → Suspended → Execute → Observe |
checkpointWriter.write |
Compact 里的 Checkpoint |
回到 while 开头 |
进入下一轮 |
- 图里 Compact 的"压缩"在下一轮
contextManager.prepare里做,checkpointWriter.write只负责存档 - AgentRun 通俗注释版
java
// 一次任务的"档案袋":这次任务里所有会变的数据都装在这里
// 存档时把它整个存下来,程序挂了读回来就能接着跑
@Getter // Lombok:自动给每个字段生成 getXxx()
@Setter // Lombok:自动给非 final 字段生成 setXxx(),final 字段只能读不能改
public class AgentRun {
// ===== 身份:这是哪一次任务 =====
private final String runId; // 任务编号,存档和恢复时靠它找回这次任务
private final String task; // 任务目标,比如"清理 30 天前的过期订单",创建后不再变
// ===== 状态:现在进行到哪了(宪法三)=====
// 整个任务的大状态:RUNNING 进行中 / SUSPENDED 挂起等审批 / COMPLETED 成功 / FAILED 失败 / CANCELLED 取消
private RunState state = RunState.RUNNING;
// 这一轮走到哪一步:PREPARE 准备 / THINK 等模型 / DECIDE 判断 / EXECUTE 执行 / DONE 结束
// 打个比方:state 像"病人住院中还是已出院",phase 像"今天的检查做到第几项"
private RunPhase phase = RunPhase.PREPARE;
// ===== 账本:花了多少(宪法一)=====
private int turnCount; // 跑了几轮,也就是问了模型几次
private long inputTokens; // 累计发给模型的 Token
private long outputTokens; // 累计模型回复的 Token
private double costUsd; // 累计花了多少美元
// 每轮开头的预算检查就拿这几个数和上限比,超了直接停
// ===== 记忆:发生过什么 =====
// 和模型的完整对话,每轮发给模型看的就是它
// 工具结果也追加在这里,模型下一轮才知道"上一步查到了 120 条订单"
private final List<Message> messages = new ArrayList<>();
// 调用过的所有工具,事后查账、排查问题用
private final List<ToolCall> toolHistory = new ArrayList<>();
// 集合加 final 只表示"盒子不能换",盒子里照样能放东西,所以能写 getMessages().add(...)
// ===== 挂起现场(宪法二)=====
// 高风险操作等人审批时,暂存那个还没执行的调用,比如 delete_orders(...)
// 主管批准后从这里取出来执行
private ToolCall pendingToolCall;
// ===== 防原地打转(宪法一)=====
// key:工具名 + 排序后参数算出的指纹,value:调用次数
// 同一个查询调了 10 次还在调,说明模型卡住了,判定死循环
private final Map<String, Integer> fingerprintCounter = new HashMap<>();
// 每个调用重试了几次(红绿灯重试)
// 绿灯成功直接返回;红灯永久失败(如参数格式错)不重试;临时错误(如超时)最多重试 3 次
private final Map<String, Integer> retryCounter = new HashMap<>();
// ===== 出事现场 =====
private String faultClass; // 挂掉时属于哪类故障,比如预算超限、死循环、审批被拒
private String lastError; // 最后一条报错信息,排查和恢复时看
}
- AgentLoop 通俗注释版
java
// Agent 的"发动机 + 刹车":负责一轮一轮推进任务
// 自己不保存任务数据(都在 AgentRun 里),只拿着下面 6 个帮手干活
public class AgentLoop {
private final LlmClient llm; // 调大模型:发问题、收回答
private final HardBudget budget; // 预算上限:50 轮、600 秒、20 万 Token、5 美元(宪法一)
private final ContextManager contextManager; // 拼好发给模型的材料,太长就压缩(宪法三)
private final ToolRunner runner; // 真正执行工具:审批、排队、重试(宪法二、五)
private final ProgressMonitor progress; // 发现模型在原地空转(宪法一)
private final CheckpointWriter checkpointWriter; // 存档(宪法四)
// run:这次任务的档案袋
// events:广播口,把"任务完成""工具返回"这类消息推给前端或日志
public void loop(AgentRun run, EventSink events) {
while (true) { // 转一圈 = 一轮:查预算 → 准备 → 问模型 → 判断 → 执行 → 存档
// ===== 第 1 步:查预算(图里的虚线熔断)=====
// 每轮第一件事:轮数、时间、Token、钱有没有超,超了直接抛异常跳出循环
BudgetGuard.check(run, budget);
// 连续几轮对话内容都没变化,说明模型在空转,同样直接停
progress.check(run);
// ===== 第 2 步:准备材料(图里的 Prepare)=====
run.setPhase(RunPhase.PREPARE); // 在档案上记一笔:现在在准备
// 拼成"系统规矩 > 当前进度 > 知识 > 历史"
// 太长就把老历史压成摘要,系统规矩永远保留原文,防止模型忘了规矩
PreparedContext ctx = contextManager.prepare(run);
// ===== 第 3 步:问模型(图里的 Thinking)=====
run.setPhase(RunPhase.THINK);
// 把材料和可用工具清单发给模型
// 模型只会回两种:A "我做完了";B "我要调某个工具,参数是......"
LlmResponse response = llm.complete(ctx.messages(), ctx.system(), tools());
run.setTurnCount(run.getTurnCount() + 1); // 记账:轮数 +1
run.addTokens(response.usage()); // 记账:这次用掉的 Token 加到账本
// ===== 第 4 步:判断模型说了什么(图里的 Decide)=====
run.setPhase(RunPhase.DECIDE);
if (response.stopReason() == StopReason.END_TURN) {
// 走进这里 = 模型说"我做完了"(回答 A)
// 模型的话不能直接信,它可能没做完却以为做完了(幻觉终止)
// 由 goalAchieved 自己去核实,比如真去库里查还有没有过期订单
if (goalAchieved(run)) {
// 核实通过 → 任务成功(图里的 Finished)
run.setState(RunState.COMPLETED);
events.emit(new RunCompletedEvent(run)); // 广播:任务完成
return; // 退出循环
}
// 核实没通过 → 往对话里塞一句话,推模型继续干
run.getMessages().add(Message.user("目标尚未达成,请继续。"));
// 直接回到循环开头(图里"校验驳回 → Prepare")
// 开头又要先查预算,所以模型一直嘴硬说做完了,也会被轮数上限截住
continue;
}
// ===== 第 5 步:执行工具(图里的 Wait_Approval + 执行阶段)=====
// 走到这里 = 模型要调工具(回答 B),一次可能要调好几个,所以叫 runBatch
run.setPhase(RunPhase.EXECUTE);
// runner 内部依次做:
// 1. 查死循环:同工具、同参数是不是调太多次了
// 2. 查真伪:工具存不存在、参数类型对不对
// 3. 审批:低风险放行,高风险挂起等人,禁止的直接拒绝
// 4. 加权限:放行时把用户能动的范围强行塞进参数,比如只能删自己门店的订单
// 5. 排队:只读工具可以一起跑,写和删必须一个一个排队跑
// 6. 重试:工具报错时按红绿灯规则决定重不重试
// 7. 回写:每个工具的结果写回 messages,下一轮模型就能看到
runner.runBatch(run, response.toolCalls(), events);
// ===== 第 6 步:存档(图里 Compact 的 Checkpoint)=====
// 这一轮干完,把整个档案袋存一份,下一轮崩了从这里接着跑
if (checkpointWriter != null) { // 没配存档器就跳过,比如本地测试
checkpointWriter.write(run);
}
// 回到 while 开头,进入下一轮
}
}
}
- 失败与恢复在循环外处理
- 循环体里没有
setState(FAILED),熔断、空转、拒绝、挂起都跳出循环,交给调用loop()的外层处理 - 恢复时按
runId读回 AgentRun,再交给loop()接着跑
- 循环体里没有
java
// 示意代码:调用 loop() 的外层,异常类名仅为示意
try {
agentLoop.loop(run, events);
} catch (SuspendedException e) {
// 高风险操作在等审批:档案已存好,程序先退出,批完再来
} catch (Exception e) {
// 预算超限、死循环、审批被拒都走到这里(图里的 Failed)
run.setState(RunState.FAILED);
run.setFaultClass(e.getClass().getSimpleName()); // 记下故障类型
run.setLastError(e.getMessage()); // 记下报错信息
}
// 恢复:主管批准了或服务器重启了,按任务编号读回档案袋,交给 loop() 接着跑
// 挂起时暂存的 pendingToolCall 也在档案袋里,批准后取出来执行
AgentRun saved = store.load(run.getRunId()).join().orElseThrow();
agentLoop.loop(saved, events);
- 注意点
- state 和 phase 不是一回事:state 是整次任务的状态,phase 是这一轮走到哪一步
- 模型一直嘴硬说做完了也不会无限循环 :
continue回到开头仍要先过预算检查,最终被轮数上限截住 - final 集合只锁引用不锁内容,所以能
getMessages().add(...)