Appearance
第9章 让工作真正循环起来——Agent Loop
小a给 Agent 接上了工具,却发现它只会“干一步”:让它修一个测试,它读完失败信息就停住了,安静地等着下一步指示。
“它不是有工具了吗?为什么不会自己往下干?”小a问。
“工具给了模型受限的行动入口,但一次调用只能提出一次请求。”老z说,“真实任务从来不是一步——读取、判断、执行、再验证,是一轮接一轮的往复。把它串起来,才是 Agent。”
本章讨论 Agent Loop、消息流、ReAct、停止条件和并发边界;不把循环等同于自主或可靠。
先把名字对齐
| 中文术语 | 英文全称或缩写 | 本书工作定义 | 不等于什么 |
|---|---|---|---|
| Agent 循环 | Agent Loop | 反复请求模型、处理工具结果、更新状态的控制流 | 模型自动拥有执行权 |
| 回合 | turn | 一次模型响应及其后续工具结果的状态单元 | 一个用户输入必然只请求一次模型 |
| ReAct | Reasoning and Acting | 推理轨迹与动作交替的研究方法 | 所有 Agent 的强制实现规范 |
| 停止条件 | stop condition | 预算、完成、失败、取消等终止转移 | 模型输出“完成”这句话 |
| 事件/状态机 | event/state machine | 可观测事实和允许转移的控制模型 | 仅用于 UI 的日志 |
| 取消 | cancellation | 通过 signal/控制器请求停止当前工作 | 已发生副作用的自动回滚 |
9.1 循环的最小职责
“那这个‘串起来’最少要做什么?”小a问。
“Agent Loop 维护消息状态,调用模型,执行获准的工具,并把结果作为下一轮输入。模型决定候选下一步,程序决定是否执行及何时停止。”老z说,“你可以把它想成一次手术的巡回:医生(模型)提出下一步操作,麻醉与护理团队(程序)确认、执行、记录,再回到医生那里决定下一步。”
text
初始化消息
↓
请求模型 ── 无工具请求 ──> 结束或请求澄清
↓ 有工具请求
校验、审批、执行工具
↓
将结果写入消息状态 ──> 下一轮请求模型“就这么简单?”小a问。
“这是简化示意。生产实现还需处理流、取消、持久化、预算和事件订阅。”
“那模型第一次开口前,先要准备什么?”小a问。
“至少三样:系统规则、工具清单(含签名和描述)、以及已有的消息历史。”老z说,“这些要在第一轮请求前就装配好。工具清单如果和真实执行器对不上——比如签名写错、描述过时——模型要么不调用,要么调用后校验失败,第一轮就卡住。装配上下文是一层独立的代码,它的职责是‘这一轮给模型看什么’,循环本体的职责是‘这一轮之后往哪走’,两者经常被写进同一个函数,排障时反而分不清是谁的问题。”
“那怎么判断该往哪走?”
“看这一轮输出里有没有工具调用。”老z说,“有,进入校验、审批、执行、回写;没有,就检查停止条件。这两条分支的成本差很多:只走模型的一条分支,花的是生成费用;带工具的分支还要叠上执行开销和副作用。以‘有没有工具请求’作为轮次分界,比让模型用自然语言说‘我还要继续’可靠得多。”
“那为什么不把整条任务一次想完,非要一圈圈转?”小a问。
“因为模型一次能看到和执行的是有限的一步。”老z说,“多步任务里,后面几步依赖前面工具的真实结果——文件改完长什么样、测试跑完通过没有,这些只有执行之后才知道。想提前‘在脑子里’把这些全部算完,等于让模型在没有任何外部证据的情况下编造后续,正是幻觉最容易冒出来的地方。循环的本质不是重复,而是让每一步决策都基于上一步的真实结果。”
“那一圈圈转,时间不是更长吗?”
“确实更长。”老z说,“每轮至少一次模型往返,加上工具执行时间,一个五步的任务比一次问答慢很多。换来的是可审计、可中断、可恢复——任务可以在任意一圈停下来而不丢失进展。这个取舍要提前算清楚:适合循环的是需要外部验证的多步任务,简单一步能答完的,硬套循环反而绕远路。”
比喻:手术巡回
模型提方案,程序把方案变成受控的动作;每一轮都记录,下一次决策基于真实发生的状态,而不是模型嘴上说的“完成了”。
9.2 消息是唯一可审计的状态
“那每一轮的结果都记在哪?”小a问。
“常见消息包含系统规则、用户输入、模型响应和工具结果。”老z说,“具体角色名称与字段因协议而异,但必须保留谁提出了什么调用、哪个执行器返回了哪个结果、结果是否成功或被拒绝的关联关系。不要只把自然语言摘要留在历史中,否则无法可靠恢复或审计副作用。”
“模型不是已经看过工具结果了吗,为什么还要保存?”小a问。
“因为下一轮、重启恢复和事故审计都需要同一事实记录。模型上下文是临时工作台,应用状态才是控制循环的依据。”
“那保存的是原始结果还是摘要?”小a追问。
“两者都要,但要分开存。”老z说,“原始记录用于审计和排障——它告诉你‘当时到底发生了什么’;进入模型上下文的是受限的摘要或截断版,用于控制预算。不能用摘要替代原始记录,因为摘要可能丢失‘失败原因’‘截断标记’这些排障时最需要的信息;也不能把原始记录全塞进上下文,那会撑爆预算。”
“那如果上下文被截断了,历史还能对得上吗?”
“这正是消息状态和上下文要分开的原因。”老z说,“消息状态是完整的、持久的,不随上下文截断而丢失;上下文是消息状态的一个受限视图,会被截断或压缩。截断上下文,不等于删除历史——历史还在消息状态里,只是这一轮没让它进入模型的视野。”
“那每条消息至少得记哪些字段?”小a问。
“够还原事件的就够了。”老z说,“角色或来源、内容、工具调用标识、对应结果、结果状态(成功、失败、拒绝)、时间戳和所用模型版本。标识关联是命门:没有调用标识,就分不清哪条结果属于哪个请求;没有状态字段,就审计不出‘当时到底执行没有’。”
| 字段 | 作用 | 缺失时的问题 |
|---|---|---|
| 角色或来源 | 区分系统、用户、模型、工具 | 无法重建对话脉络 |
| 工具调用标识 | 关联请求与对应结果 | 分不清结果属于哪个请求 |
| 结果状态 | 记录成功、失败、拒绝 | 审计时还原不了事实 |
| 时间戳与模型版本 | 定位事故、确认重放基线 | 说不清当时用了哪个模型 |
“那这些记录会占用多少空间?”
“和上下文预算不是一回事。”老z说,“持久化消息通常落文件或数据库,成本远低于上下文 token;真正贵的是把全文喂给模型。所以两者分开存:消息状态按审计需要存,上下文视图按预算需要裁。五十轮对话的完整日志可能几千行,进入模型的只有摘要加最近几轮原文。”
“那存原始结果会不会有泄露风险?”
“会。工具结果可能含密钥、内部路径或敏感数据。”老z说,“所以消息状态和日志要分层授权、脱敏或加密:给模型看的摘要在装配时处理敏感字段,审计库保留原始记录但限制访问。把敏感信息直接写进上下文,相当于把权限交给模型输出,想回收就晚了。”
“那这些消息状态能不能用来‘重放’?”小a问。
“能,重放是审计最常用的一招。”老z说,“把消息状态按时间顺序导出来,逐条核对:‘这一轮模型请求了什么、工具返回了什么、宿主批准没有、结果写回没有’。如果状态里缺了某一条,重放就会在那一环断掉——断点所在,往往就是问题所在。重放的价值不在跑一遍,而在让缺口显形。”
“那消息状态记录到哪个粒度?”小a问。
“记录到‘动作’级就够了。”老z说,“每个工具调用、每次审批决定、每轮模型请求都算一个动作。粒度再细到字符级没有意义;粒度再粗,比如只记‘这轮完成了’,恢复时就不知道完成了什么。粒度跟着审计和恢复的需求走,两者需要什么就记什么。”
9.3 ReAct 是理解框架,不是强制流程
“我听说有个叫 ReAct 的东西,是必须照做的吗?”小a问。
“不是。ReAct 原始论文研究推理轨迹与任务动作的交替,并让动作从外部来源取得额外信息。”老z说,“它可帮助理解工具结果如何改变下一步,但论文不能证明每一轮都必须输出可见‘思考’,也不能推出所有 Agent 都遵循同一顺序。计划驱动、工作流路由和人机协作都可以嵌入或替代这类循环。”
“那 ReAct 到底给了我们什么?”小a追问。
“一个理解循环形态的框架:观察(工具结果)如何影响下一步行动(模型决策)。”老z说,“它让你意识到,每一轮不是孤立的——上一轮的工具结果,是这一轮模型决策的输入。但‘理解循环形态’和‘必须按 ReAct 格式输出’是两回事。很多循环根本不输出可见的‘思考文本’,它们的推理藏在模型内部,外部只看到动作。”
“那不输出思考,会不会更不可控?”
“可见性是另一个维度。”老z说,“有些模型支持把推理过程显式输出(思维链),有些不支持。输出可见推理有助于审查,但不改变‘停止判定权在宿主’这个分工——不管模型想没想、想得对不对,何时算完成都由宿主控制。”
“那除了 ReAct,还有哪些循环形态?”小a问。
“常见的至少四种,别把它们当成同一件事。”老z列了一张表:
| 形态 | 关键特征 | 典型代价 |
|---|---|---|
| ReAct 式 | 观察工具结果后交替决策 | 每轮一次模型请求,token 随轮数线性涨 |
| 计划驱动 | 先产出计划,再逐步执行并校验 | 计划可能与现实脱节,需中途重规划 |
| 工作流路由 | 按阶段把任务路由到固定流程 | 阶段间信息交接容易丢细节 |
| 人机协作 | 关键节点停下等人确认 | 延迟高,副作用风险最低 |
“那能混着用吗?”
“能,而且是常态。”老z说,“很多系统把工作流路由当骨架,把 ReAct 式循环嵌在某一阶段里,再把高风险动作交给人机协作收口。选哪种形态取决于任务的稳定性:步骤固定的任务适合路由,探索性任务适合观察—行动往复,不可逆动作留给人确认。‘必须照 ReAct 输出’不是设计原则,只是多种可能中的一种。”
“那 ReAct 式循环最容易在哪一步失控?”小a问。
“在‘行动’和‘观察’之间的缝隙。”老z说,“模型提了一个动作,工具执行了,但结果被忽略或没写回——下一轮模型还当它没发生,就会重复请求同一个动作。ReAct 的循环本身不会自动修复这种断开,把工具结果完整写回消息状态,比让模型‘记得’更重要。”
“那输出可见思考有没有成本?”
“有。思考文本也要算 token,越长越贵;而且在循环里,模型上一轮写下的可见推理会进入下一轮上下文,占用空间,也可能引导后续步骤往同一个方向走。”老z说,“可见性是一种可选的工程选择,不是默认的免费增益。”
9.4 一个正常路径与一个失败路径
“那一个真实任务在循环里长什么样?”小a问。
“下面是文件修复任务的简化事件序列。”老z敲出一段:
text
用户:修复测试失败
模型:请求运行测试
工具:返回失败摘要
模型:请求读取相关文件
工具:返回受限文件内容
模型:请求修改并再次测试
工具:返回修改结果和测试通过
模型:报告完成“那如果读文件失败了呢?”小a问。
“若读取失败,正确路径不是伪造内容,而是返回‘路径不存在’或‘权限拒绝’,让模型改路径、请求帮助或停止。”老z说,“若同一失败重复出现,循环应识别重复状态并终止或升级给人,而非继续消耗资源。”
“怎么判断‘重复’?”小a追问。
“看状态是否在原地踏步。”老z说,“如果连续几轮模型请求的都是同一个工具、同样的参数、得到同样的失败,这就是重复状态。检测它需要一个简单的计数器或状态指纹——比较‘这一轮的请求+结果’和上一轮是否实质相同。重复检测的价值不是惩罚模型,而是止损:一个陷入重复的循环,继续转下去只会烧预算,不会产出结果。”
“那‘升级给人’具体指什么?”
“把控制权交还给人。”老z说,“循环暂停,记录‘已尝试 N 轮,连续 K 次相同失败’,等待人决定是修改任务、调整工具,还是放弃。升级不是失败,是一种有控制的停止——它比无限循环或静默出错都更可取。”
“除了‘路径不存在’,还有哪些典型的失败?”小a追问。
“至少四类,表现和处理各不相同。”老z列了一张反例表:
| 失败类型 | 表面现象 | 应如何处理 |
|---|---|---|
| 目标不存在 | 反复返回“路径不存在” | 计数并停止或升级,别无限重试 |
| 权限拒绝 | 读取或写入被拒 | 换路径、换身份或请求审批,避免循环重试 |
| 内容异常 | 文件读到了,但内容不是预期的 | 校验内容再使用,别把“读到了”当“读对了” |
| 超时或中断 | 工具没在时限内返回 | 标记未完成,决定重试、降级或停止 |
“那‘读到了文件’算不算成功?”小a问。
“算工具成功,不算任务成功。”老z说,“工具成功只表示执行器完成了动作,比如返回了内容;任务成功还要模型校验内容是否符合预期。这两层经常被混成一个状态——工具返回正常就被当作任务完成,排障时日志里全是‘成功’,却说不清成功在哪一步失效了。”
“那正常路径上还有没有要留意的?”
“有。正常路径也要防两件事。”老z说,“一是模型报告的‘完成’未经验证——它说测试通过了,但循环没去确认测试结果,就把任务标成 completed;二是工具返回了部分数据——比如只读了文件的前 100 行,模型把‘读到了’当成‘读全了’,结论建立在残缺输入上。失败路径要处理异常,正常路径也要校验‘正常’是不是真的正常。”
9.5 停止与并发是安全条件
“怎么防止它无限循环下去?”小a问。
“循环至少应限制总轮数、总 token 或费用、单轮超时、连续失败次数和用户取消。”老z说,“达到任一限制时,记录明确停止原因与已完成动作。模型的‘完成’文本不能覆盖这些外部约束。”
“那一次多调几个工具,是不是更快?”小a问。
“并发工具调用也不是默认优化。”老z说,“独立只读操作可并发;共享文件写入、数据库变更、部署和外部通信需要串行、锁、幂等策略或审批。并发前先画出资源和副作用,而不是只看模型一次提出了几个调用。”
“那每个停止条件,最后落到什么状态?”小a问。
“每个停止原因都应映射到一个明确的退出状态和一句可记录的说明。”老z列了一张表:
| 停止原因 | 建议的退出状态 | 循环留下什么记录 |
|---|---|---|
| 任务完成 | completed | 已完成动作列表 + 最终消息摘要 |
| 预算耗尽 | budget_exhausted | 用掉多少 token/费用、完成到哪一步 |
| 超时 | timeout | 超时前最后的动作与时间点 |
| 连续失败 | failed | 失败次数、最后一轮的请求与结果 |
| 用户取消 | cancelled | 取消来源、取消时正在执行的动作 |
“那用户取消是怎么生效的?”
“协作式,不是强杀。”老z说,“宿主向正在运行的调用或子进程发出 signal,请求它停下来;工具是否响应、多久响应,取决于工具的实现。取消是‘请求停止’而不是‘保证停止’——工具不响应时,宿主只能等待、设置宽限时间或最后强制终止,并把实际结果如实记录。”
“那记录里该写什么?”
“写两件事:‘取消已请求’和‘实际停止时间’。”老z说,“时间线对不上,审计就会误判——是工具响应了取消,还是它自己跑完的,全靠记录区分。示意图如下:”
text
宿主发出取消 ──> 工具收到 signal
├─ 及时响应:执行中断,记录 cancelled
└─ 不响应:等待宽限 ──> 强制终止 ──> 记录 force_terminated“那‘资源独立才能并发’具体怎么判断?”小a问。
“看读写对象。”老z列了一张矩阵:
| 操作组合 | 是否可并发 | 需要什么保障 |
|---|---|---|
| 读文件 + 读文件 | 可并发 | 无(只读) |
| 写文件 + 写同一文件 | 不可并发 | 串行或文件锁 |
| 写库 + 写同一张表 | 不可并发 | 事务或锁 |
| 外部通信 + 外部通信 | 视目标而定 | 幂等或审批 |
| 读 + 写同一资源 | 不可并发 | 按序执行或版本检查 |
“这矩阵是死的吗?”
“是判断的起点,不是结论。”老z说,“同名文件、同一条数据库记录才是真正的冲突点;不同文件、不同记录之间通常互不相干。画资源依赖图时,粒度要落到具体对象,而不是工具名字。”
“那停止条件和预算冲突时听谁的?”
“听先到的那一个。”老z说,“比如任务完成了,但 token 也刚好快耗尽——按‘完成’走 completed,同时把剩余预算一并记录;反过来预算先耗尽、任务没完成,就按 budget_exhausted 走。两个条件都触发时,状态机只转移一次,别让两套逻辑同时写状态。”
要点
循环必须有停止条件和并发边界。前者防失控,后者防冲突——都不来自模型“自觉”。
9.6 循环应被建模为状态机
“我怎么保证循环不乱转?”小a问。
“把循环写成状态机,能避免‘文本到了就继续’的隐式控制流。”老z说,“最小状态可包括 idle、requesting_model、awaiting_approval、running_tools、completed、failed 与 cancelled;只允许预先定义的转移。”
text
idle → requesting_model → running_tools → requesting_model
│ │
├→ completed ├→ awaiting_approval → running_tools
└→ failed └→ failed / cancelled“状态机有什么好处?”小a问。
“状态机的代价是实现和事件记录更复杂;收益是可以定义恢复规则、测试非法转移,并准确说明一个任务在中断时已经做了什么。”老z说,“并发工具应由资源依赖图决定,而不是由模型是否一次请求多个调用决定。”
“那状态机本身会出什么错?”小a问。
“三类常见错误。”老z说,“一是非法转移——工具还没执行完就跳进 completed,通常源于把‘模型说要完成’当成了转移条件;二是漏事件——某个工具返回没有触发任何转移,循环卡在原地;三是状态陈旧——并发工具的结果晚到,用的还是旧快照。前两类靠定义转移表堵住,第三类靠给每个事件带上来源和顺序标记。”
“那状态是不是越少越好?”
“没有定论,看恢复需求。”老z说,“状态少了,实现简单,但中断时说不清做到哪一步;状态多了,每条转移都要测试。一个务实的起点是本章那张最小集合,缺的再按任务补。”
“那每次转移之前要不要查预算?”
“要,检查点放在转移上,而不是放在文本里。”老z说,“比如 requesting_model → running_tools 之前查剩余预算,running_tools → requesting_model 之前查总轮数。预算检查挂在转移上,就不会出现‘这一轮超了但循环还在转’的情况。”
“那这张状态机能画得更细一点,带上守卫条件吗?”小a问。
“可以。”老z画了一版带检查点的:
text
idle ──> requesting_model(校验工具清单、预算 ≥ 0)
requesting_model ── 有工具请求 ──> validating_tools ──> running_tools
│ 无工具请求
├─> 检查停止条件 ──> completed / failed
running_tools ── 结果写回 ──> requesting_model(轮数 +1)
└─ 用户取消 ──> cancelled“这个版本多了什么?”
“多了转移条件和每个状态的可观察产物。”老z说,“validating_tools 专管校验——签名对不对、权限够不够、是否要审批;校验结果本身也是一条事件,进审计。状态机不是越复杂越好,但该有的检查点缺失,比状态机复杂更危险。”
示意: 用户取消发生在工具运行中时,系统先请求中止,再记录“取消请求已发出”;若子进程不能立刻停止,不能伪称任务已取消。类似地,模型返回最终文本后仍需确认没有待处理的工具调用,才进入
completed。
9.7 一个受预算约束的状态机
“能不能给我一张带预算的版本?”小a问。
“可以。”老z画了一张:
text
idle → requesting_model → awaiting_tool_decision → running_tools
↑ │ │ │
└─────────────┴────────────────────┴───────────────────┘
requesting_model → completed | failed | cancelled | budget_exhausted“那修复测试的正常路径和反例呢?”小a问。
“工作场景中,修复测试的正常路径是读取失败摘要、选择文件、修改、重跑验证,所有 tool result 都进入下一回合。”老z说,“反例一是连续返回同一‘路径不存在’:状态机应计数并停止或升级,而不是无限循环。反例二是取消发生在子进程执行时:记录取消已请求;若进程不能立即停,不能伪称已经回滚。”
“预算只有 token 一种吗?”小a问。
“不是。token/费用、轮数、墙钟时间和连续失败次数是不同的 budget;任一耗尽都应形成明确 stop condition。”老z说,“事件流让 UI、持久化和审计看到相同转移,代价是并发、取消和恢复实现更复杂。多个调用只有在资源独立时才可并发;同一文件写入或外部事务要串行、加锁或用幂等策略。”
“那这几种预算各自怎么耗?”小a问。
“触发条件不同,消耗节奏也不同。”老z列了一张表:
| 预算类型 | 什么会耗尽它 | 耗尽的典型后果 |
|---|---|---|
| token/费用 | 长输入、多轮重试、长输出 | 请求失败或成本失控 |
| 轮数 | 陷入循环、来回试探 | 任务迟迟不收敛 |
| 墙钟时间 | 单轮等待、工具执行慢 | 用户等不到结果 |
| 连续失败次数 | 同一错误反复出现 | 资源被反复烧掉 |
“那预算应该在什么粒度上检查?”
“至少两个粒度。”老z说,“每轮开始前查总预算,工具调用前后查本次额度;budget_exhausted 对 UI、审计和恢复逻辑应是同一个可观测事实,而不是各自在日志里猜。把预算当成一等状态,而不是顺带的 if 判断,循环的边界才说得清。”
老z画了一条完整时间线:
text
t0 idle
t1 requesting_model(查总预算,通过)
t2 running_tools(工具 A 执行,返回结果)
t3 requesting_model(轮数 +1,仍有余量)
t4 running_tools(工具 B 超时,进入 failed)
t5 记录 stop reason=timeout,动作清单写入消息状态“如果每一步都留下事件,恢复时能还原到什么程度?”
“能还原‘走到哪一步、为什么停、还剩什么没做’。”老z说,“要完全重放当时的中间状态,还要额外保存工具调用前后的一致性快照;没有快照,恢复只能从最后一条持久化消息继续。审计是知道发生了什么,恢复是能重新接上——这两个层次别混淆。”
“那恢复有没有优先级?”
“有,按副作用排。”老z说,“已经执行的写操作、已经发出去的外部通信,优先级高于尚未执行的计划——恢复时先记录‘哪些副作用已经发生’,再决定下一步是从断点重跑还是整体回退。恢复要回答的永远是‘已经发生了什么’,而不是‘模型打算做什么’。”
“那持久化本身放在循环的哪个位置?”
“放在每个状态转移完成之后,而不是每轮结束才写。”老z说,“工具执行完、结果写回消息状态,紧接着就该落一次持久化;模型还在请求中时不需要写。持久化的频率决定了断电能恢复到哪一圈——越频繁,丢得越少,代价是 IO 越多,两者要按任务价值取舍。”
小结
Agent Loop 把"模型输出、工具执行、消息更新"串成一条能自我延续的反馈回路,而单次问答算不上 Agent——循环才是它区别于普通聊天机器人的本质。一个回合是一次模型响应加上它触发的所有工具结果;循环就在回合之间反复推进,直到某个停止条件被满足。停止条件不是可有可无的装饰:预算耗尽、任务完成、发生失败、外部取消,任何一种都会让循环停下来。一个没有明确停止条件的循环,最现实的下场就是在死循环里烧掉账单。事件和状态机让循环变得可观测、可调试,取消则通过 signal 协作式地请求停止——注意是"请求",如果工具不响应 signal,取消就不会真的生效。
这一章最关键的认知纠偏是:别信模型自称的"完成"。ReAct 的"观察—行动"往复有助于理解循环的形态,但循环的可靠性来自外部状态、失败路径设计、预算控制和并发管理,而不是模型说一句"我做完了"。模型说自己完成了,不代表它真的完成了——何时算完成,要由宿主的停止判定来控制。把这个判定权交给模型,等于把循环的刹车也交了出去,这是 Agent 设计里最危险的让步。
消息状态是循环唯一的审计依据,上下文只是它的一份受限视图。截断上下文不等于删除历史:完整记录仍留在消息状态里,只是没进入这一轮模型的视野。审计需要的是"谁提出了哪个调用、哪个执行器返回了什么、结果成功与否"的关联关系,连同时间戳和模型版本;原始记录与进入上下文的摘要要分开存,敏感字段还要单独授权或脱敏。ReAct 也只是循环形态中的一种,计划驱动、工作流路由、人机协作各有适用场景,多数系统把它们组合使用——选哪种取决于任务本身的稳定性,而不是模型输出了什么格式。
停止条件要映射成明确的退出状态,预算要分类型记账:token/费用、轮数、墙钟时间、连续失败次数各自有各自的耗尽节奏,检查点挂在状态机转移上,预算事件本身进事件流。并发只在资源独立时才成立,共享写入要串行、加锁或走幂等策略。取消是协作式请求:信号发出去,工具未必立刻响应,记录里要写"取消已请求"和"实际停止时间"两件事,不能把两者混成同一个"已取消"。模型说"做完了"、工具返回"正常",都不等于任务真的完成——工具成功只代表执行器完成了动作,任务成功还需要宿主对结果做验证。
动手核验
为一个两工具的玩具 Agent 记录每轮事件:输入、模型请求、校验结果、工具结果和停止原因。刻意让工具返回“路径不存在”,确认它被写回状态且循环在达到重复失败阈值后退出。再尝试两个写同一文件的调用,确认执行器不会并发写入。