用十倍速制造事故:Loop Engineering 的暗面

简介:Loop 的控制权一旦交给概率模型,自动化会把缺陷放大成灾难;微观 Agent Loop 和宏观 Loop Engineering 必须共享同一套 5 条工程宪法

Loop Engineering 与它的暗面

简介:让循环系统自动发现工作、分配任务、交叉检查,前提是底层 Agent 本身可控

  • 背景
    • Loop Engineering:不再直接写 Prompt,而是设计一个闭环系统
      • 自动发现工作、分配任务、交叉检查、持久记忆
      • 由循环去提示模型、决定下一步做什么
    • 搭好自动分诊 + 多 Agent 并行处理,第二天可能看到
      • Token 账单炸了
      • 只读工具被组合调用,导致全库数据泄露
      • 两个 Agent 并发修改同一份文件,数据全没了
    • 查日志却一切正常:每步都有输出,工具调用标记为绿色,响应时间也合理
  • 根因
    • 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)直接熔断
    • 自动化任务必须定义可验证的终止条件,由独立验证节点判定
  • 本质:概率模型会自欺欺人,终止权必须交给有确定逻辑的守门员

宪法二:提议-裁决分离与权限门禁

简介:模型只负责提出下一步做什么,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 篡改
    • 任务分派支持去重,同一项工作不会重复派发
  • 本质:没有隔离的并行是相互毁灭,没有同步的共享是定时炸弹
    • 在概率性的模型输出之上,必须构建确定性的协调机制

终局代码: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 是流水线:自己不存任务数据,只拿着一堆帮手一轮一轮干活
  • 状态机节点说明
节点 做什么
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(...)
相关推荐
6098440661 小时前
AI Agent 长跑必踩的坑:上下文压缩导致信息流失的根因分析与解法
agent
qinglongcn2 小时前
Agent 可靠性的数学——为什么编程是第一个跑通的赛道
agent·ai编程
用户3126874877202 小时前
Gemini 4 Argon 技术拆解:100 万输出 token 的"长活"模型,谷歌押注的三个战场
agent
.Bryce.2 小时前
在智能体中使用A2A工具:让大模型自己决定“该找谁帮忙“
android·java·服务器·redis·后端·agent·a2a
崎岖Qiu2 小时前
MCP 的架构、协议分层与通信过程
后端·ai·架构·agent·mcp
Percy_kk2 小时前
AI Agent 开发实践:从手写 ReAct 到框架选型
agent·设计
方方洛2 小时前
ai-agent教程-07-从零实现一个工程化AI-Agent
人工智能·llm·agent
方方洛2 小时前
ai-agent教程-08-多智能体协作
人工智能·llm·agent
simon_1233 小时前
它验证过才删的:agent 的 dry-run 是干净的,线上文件还是没了
agent