开场三个预测三个全错上一课看的是事件怎么流这一课看循环怎么停。结论先摆桌上ReAct 循环有三种收场——正常收束、打满降级、用户中断前两种都以 RUN_FINISHED 结束RUN_ERROR 只留给流上真正的 Throwable。结束事件只报流崩没崩不报任务成没成。这话不是从文档抄来的是我拿三个全错的预测换的。给 workbench 里的 ReActAgent 把 maxIters 改成 1模型换成 fake:fake。fake:fake 是一条免 key 的假模型腿专门把模型这个变量摘出去单看框架自己的动作。然后发一句现在几点。发车前我在笔记里写下三行预测我理解的P1、RUN_ERROR。P2不会P3RUN_ERROR 。P2 押的是那句时间查询完成的文本回答不会出现。实测 0/312 条事件RUN_FINISHED 收场全程 0 ERROR。三发全偏还偏在同一个地方——我以为打满等于崩实际它体面收场、照交答案。边界先画清这是框架层怎么收场的事。收场长什么样是框架代码定的字节码为证模型和任务决定你会不会走到检查点不改变走到之后的收法。顺带把打满说死maxIters 份的常规推理轮数用完了模型还想开新一轮被检查点拦下——满的是轮数预算。至于打满为什么不算错往下几节全是它。循环怎么转引擎、上限与三个入口先看引擎本体。官方文档给这个循环的定义只有一句“the model reasons, calls tools and uses their results to continue until it can respond”——模型推理、调工具、拿结果接着推直到它能作答。拆开是两拍Reasoning推理拍把历史发给模型模型要么交最终回答要么交工具意图Acting执行拍框架把工具跑掉结果写回历史。两拍交替一圈算一轮。上一课抓的那 44 条事件序列其实就是这两拍转了两圈的完整快照——当时看热闹这一课看门道。检查点在每轮 Reasoning 的入口iter maxIters 就转 summarizing总结收尾不抛异常。字节码里这趟逻辑长在 executeIteration(iter) → reasoning(iter) 的入口上转圈的活由 ReActAgent$CallExecution 这个内部类管。入口这个位置是后面一连串现象的总开关循环是在推理开始前被拦下的不是工具跑到半道被掐死实验一里某条 warn 为什么缺席也由它解释。maxIters 数的是什么我一开始就理解偏了。它数常规推理轮数是一份 per-run 预算每次 call 从 0 起算不是工具个数也不是模型调用总次数——打满后那次 summary 收尾调用不占额度。这个数法实验一就会给反例下一节跑出来再验。默认值 10官方参数表一行 “maxIters|int|10| Max iterations of the ReAct main loop”javap 反编译里对着一句 bipush 10文档和字节码两处互证。还有个细节模型哪轮不要工具了循环立即收束没有再推一轮的宽限——所以一个 run 实际转几圈是模型说了算maxIters 只负责封顶。同一个循环官方开了三个入口。call(…)“Run the reasoning-acting loop and returnMonoMsg”——跑完只交最终 MsgstreamEvents(…)“Same loop, but emitsAgentEvents incrementally”——过程和结果都在流里workbench 的两条接入腿走的就是它observe(…)“Append messages to context without triggering reasoning”即往上下文里追加消息、不触发推理多 agent 的深用在后面的子代理课。差别只在事件流向谁、要不要最终 Msg。Mono 和 Flux 是 Reactor 的响应式类型前者≈单个最终值后者≈增量事件流。workbench 两条接入腿都挂在 streamEvents 上AG-UI 面板吃事件流OpenAI 兼容端点也吃事件流——同一个循环两种吃法。把出口画成图就是这张三收场地图圈体上 Reasoning⇄Acting 转圈开三个口——模型交出最终回答正常收束推理入口 iter 打满转 summarizing 降级用户中断interrupt本篇不展开。前两个口都汇进 RUN_FINISHED。RUN_ERROR 不在圈上它挂在圈外流上抛了 Throwable 才轮到它出场。并发语义顺手交代因为多用户共享一台引擎安不安全是写 Java 的都会问的一句。官方三句原话摆齐“Sharing an Agent instance is also supported. The execution engine is stateless, with session state addressed by identity.”——agent 实例无状态状态放在按 (userId, sessionId) 寻址的会话空间里“the execution gate serializes direct calls for the same user/session while different sessions can run concurrently”——同一会话自动串行不同会话完全并行。后面还跟半句边界“stateless does not mean lifecycle-free”无状态不等于没有生命周期stateStore 和冲突策略归后面的课。workbench 那道单一共享工厂、每请求新实例的配置题答案就在这三句里实例可以共享因为无状态会话不会串因为寻址加串行门。最后一句丑话官方 builder 参数表里的 reactConfig/modelConfig 聚合参数本地 2.0.3 的 Builder 上不存在ReactConfig 这个 record 在包里没接线重试和备用模型拆成了独立的 maxRetries/fallbackModel。文档描述能力面源码才是当前版本的契约——本课所有机制结论口径都是 javap 2.0.3。实验一12 条标本过堂设定先交代这是老师给的作业题maxIters(1)模型 fake:fake问题就一句现在几点外加一个查时间的工具。选假腿不光为免 key——真模型的脾气会把水搅浑这趟要看的是框架自己的动作变量越少越好。问题挑得也小有讲究现在几点足够小模型一轮就想到用工具绝无分叉查完必收工——两头钉死中间那道检查点就是唯一的看点。按这个设计第一轮该看到工具调用第二轮就该撞上检查点。笔记里那三行预测展开是P1 押结束事件 RUN_ERRORP2 押现在几点的文本回答不会再出现P3 是我跟标准答案抬杠的一问标准说 RUN_FINISHED我甩过去一句「P3应该是 RUN_ERROR 。」老师拿字节码四条证据维持 RUN_FINISHED——谁对谁错跑一遍见分晓。三行押的是两个直觉打满异常终止管 P1/P3“打满没答案管 P2。顺带说一句预测的写法必须落成具体选项不许写看情况”——有出入的那条才是金子这是后来两次实验一起攒下的教训。发车。12 条事件折叠骨架如下同类连续事件并成一行只画有戏的段落条数口径 12TOOL_CALL_START → TOOL_CALL_END ← 第一轮模型要工具 TOOL_CALL_RESULT ← 工具结果写回历史 RAW(EXCEED_MAX_ITERS) ← 第二轮推理入口被拦 REASONING TEXT ← summary 收尾时间查询完成: 2026-09-29 14:39:32 RUN_FINISHED ← 我赌输的收场逐段读。第一段工具全套 START/END/RESULT 走得干干净净正常。第二段等来的不是 REASONING是一条 RAW——预想中的异常没来来了件没人见过的马甲下一节开箱。第三段REASONING 和 TEXT 照样来了P2 错问题有回答还是句体面话把工具查到的时间报了出来。最后 RUN_FINISHEDP1、P3 一起错。12 条标本判完进历史的结论一句话打满不是错误路径是正常分支——实验二还要把这条路再走一遍。面板看完看日志预测、面板、日志三方对账1 条 DEBUG文本 “Maximum iterations reached. Generating summary…”时间戳比 RAW 载荷里的 createdAt 还早 1ms——框架内部先知道事件随后才到两边是同一件事。0 条 warn0 条 ERROR。从序列数模型调用第一轮推理加收尾摘要两次——数法对上了按调用次数就乱按推理轮数才对。账单事件CUSTOMtoken_usage一条也没有——fake 这条腿不报 usage整条事件不发第 1 课的结论复核成立。对完账两个直觉点名枪毙「打满异常终止」错在把循环当会 throw 的代码块「打满没答案」错在以为没轮次就没产出。拿结果去对答案老师给 0/3 的判词是这是本次作业最好的结果。全错比全对值钱——错的每一条都指着一个我以为理所当然、其实没验证过的假设。这两条直觉从此记进我的错误清单清单还有第三条到实验二自己跳出来。RAW一件没人认领的马甲那条 RAW 单独开箱。载荷按面板原样逐字抄{type:EXCEED_MAX_ITERS, maxIters:1, currentIter:1, createdAt:2026-09-29T06:39:32Z}type 自报家门EXCEED_MAX_ITERS打满事件。maxIters 和 currentIter 一个 1 一个 1预算与耗尽对上号。createdAt 是 UTC换算过来就是本地 14:39:32。还有个细节指给你看replyId 是空串——这事件没挂到任何一轮回复上像没登记的访客。它为什么顶着 RAW 的帽子出场先说转换器是干嘛的框架内部事件五花八门面板只认标准事件类型谁翻成什么转换器链上各管一段。我把这条链清点了一遍——12 个转换器挨个问没有一个是接 EXCEED_MAX_ITERS 的。RUN_STARTED、RUN_FINISHED 有生命周期专属翻译token_usage 有自己的 CUSTOM 通道唯独打满事件无人认领最后被链尾的 RawAgentEventConverter 兜底把原事件对象原样塞进载荷发了出去。“马甲说到这就收内容一个字没改只是没穿上框架备好的制服机制上就是没有专属转换器走了原始兜底”。这件马甲替框架说了句实话——打满在它眼里不是错误路径连专属翻译器都没混上。所以整齐的事件类型清单里看不见它面板安静不等于没发生。后来我看面板多了个习惯冒出 RAW 开头的事件先怀疑有内部事件没被翻译回去数转换器而不是当噪音略过。打满之后框架干了四件事RAW 只是框架那套动作里被面板递到我眼前的一件。字节码走读 summarizing()打满那一刻它按顺序干四件事。第一步extractPendingToolCalls把没执行完的工具调用捞出来。第二步给烂尾调用补 error 版 ToolResultBlock 写回历史——补的这份带着错误标记模型下轮读到就知道那次调用没成比悄悄抹掉记录诚实。这一步里藏着一条消息不变量每条 ToolUseBlock 必须配对一条 ToolResultBlock历史才合法、才能进下一轮请求。这是协议层的硬规矩一条悬空的 ToolUse 混进请求历史就不合格纯字符串拼接式的协议根本表达不出这种配对关系。第 1 课说块的类型化是两腿差异的根——这条配对不变量就是根上最硬的一枝字符串根本表达不出这条意图必须有人认领。第三步发 ExceedMaxItersEvent就是上一节那件 RAW 马甲。第四步发起 summary 收尾模型调用末尾还挂一个 onErrorResume摘要这步要是也失败吞掉兜成一条兜底 Msg 照样往前走。顺序有讲究先收拾现场捞烂尾、补配对再广播事件最后收尾——历史不先补合法后面两步都没根基。对照实验一四步里的第二步当时没活干唯一的工具调用早就配好对pending 列表是空的所以 0 warn——“pending tool calls” 那条 warn 只在真有烂尾调用时才出场。烂尾调用怎么来的说实话两次实验都没给我实例掐断发生在推理入口入口前那轮的意图早就配好了对。这条 warn 的真身留到后面课碰到权限拒绝那类收场再认——先记住它只在历史里躺着没配对的意图时出场。四步看完设计偏好也露出来了能变成结果喂回模型的就不让它变成异常。工具异常包成 ToolResultBlock.error烂尾调用补 error 配对摘要失败吞成兜底 Msg——一层层往下兜。兜不住的只剩一类模型调用本身失败它没法变成结果喂给模型因为问题恰恰出在模型那一步。这条线索先埋着讲 RUN_ERROR 那节挖。实验二真实模型三条账单实验一的毛病不用老师挑我自己先看出来了maxIters(1) 配单工具模型一轮就把工具用完、第二轮直接被拦——这种场景只能验证拦得住验证不了中途掐断长什么样。对答案时我把这层怀疑甩了出去反考了老师一句原话照录一个字没动进阶你这个maxIters结合实际代码是不是不行啊你这个场景就是调用一次tools能不能搞个稍微复杂的场景能多调用几次的我觉得才能测的出来。进阶。这话戳的不是一个点maxIters(1) 这个配置合不合理是一层单工具场景太薄、测不出中途的形状是一层要能多调几次、看循环被掐断时交什么答卷又是一层。三层都对实验推倒重搭三条货代工具串成严格的数据依赖链——query_vessel_schedule 查船期拿船期里的航次去 query_voyage_cargo 调舱单再拿舱单第一项货物的编号去 query_cargo_detail 查详情。任务句是钩着的MSC AURORA 是船名第一个航次 V-2609A 得从船期结果里拿舱单第一项货物 C-001 得从舱单结果里拿——前一步不出数后一步没得调工具没法并行抢跑循环的形状就被数据依赖钉死了。正常走完要 3 轮工具调用加 1 轮收尾maxIters 设 2正好掐在中途。模型这回动真格DeepSeek走 workbench 的 OpenAI 兼容端点。改代码重启是老师动手跑还是我来分工原话那你帮我修改代码重启服务我来跑。发给模型的任务句只有一句帮我查一下 MSC AURORA 的船期把它第一个航次的舱单调出来再把舱单里第一项货物的详情查给我。发车前照例先写预测「1、2条2、2组3、不会。4、RUN_FINISHED。」展开第 1 条押账单 2 条——两轮常规推理嘛第 2 条押工具调用配对出现 2 组第 3 条押货物详情不会出现在最终回答里第 4 条押 RUN_FINISHED吃一堑长一智。这次数的是 SSEServer-Sent Events服务端推送帧帧数全程 409 条。SSE 流会把回答切成一帧一帧原文整贴没有意义口径交底409 指帧数下面骨架里折叠后的行是逻辑条目别拿帧数去凑行数。骨架R1 REASONING → TOOL_CALL_*query_vessel_schedule船期到手 R2 REASONING → TOOL_CALL_*query_voyage_cargo舱单到手 RAWEXCEED_MAX_ITERSmaxIters:2, currentIter:2 REASONING TEXTsummary 收尾 RUN_FINISHED逐段读两轮工具调用都完整走完第三轮推理入口被拦——第三次工具调用从未发起流里没有第三个 TOOL_CALL_*query_cargo_detail 压根没出门。然后 RAW、summary、RUN_FINISHED跟实验一同一条路。日志里同样没有 “pending tool calls” 的 warn理由上一节刚讲过。R1、R2 两轮里模型的表现也顺带看一眼每轮只干一件事调一个工具拿到数就停等下一轮——ReAct 的一拍一事在这条链上看得最清楚。账单又栽了。CUSTOM 出来三条不是我押的两条。按轮次摆开表后点评。input 从 613 涨到 748涨的是历史第一轮的工具调用和结果进了上下文。R2 的 cached 512 是缓存命中——前缀原样复用才会命中按前缀缓存的一般行为推测命中的是系统提示加前一轮历史那截没动的部分。output 61 和 60模型说话很省。summary 那行先按住不表——三个数全反常下一节单独审。四条预测里第 2、3、4 条实测对上栽的是第 1 条对应直觉清单第三条「summary 不报 usage」。它报。规律实测到位每轮模型调用各报一条 token_usage含 summary 那轮replyId 与该轮调用一一对账——打满场景 常规轮数 1。数 CUSTOM 就是数模型调用这句先记下收口还要用。收尾摘要的内幕summary 那行三个数全反常。input 386比上轮的 748 少了小一半——历史明明在变长请求反而变小cached 0——前两轮焐热的缓存全失。最扎眼的是模型的回答里带着一句 “I hit max iterations… told to respond directly”它不光知道自己打满了还知道被要求直接回答——这指令是谁给它的三个反常指向同一个结论summary 不是再多推理一轮是换了请求形状。字节码两条实证summaryStream 给 ModelCallInput 传的工具表是 List.of()空列表说明书根本没发给模型prepareSummaryMessages 把前缀换成收尾指令。为什么抽工具表我的推断是轮数已经用完再留说明书模型没准还想上手干活说明书一抽它只剩说话一条路。——注意这只是推断字节码证明了没发没证明为什么。input 变小、缓存归零、模型念出框架的私房话三角互锁。![04-正常推理请求工具表q那句英文还有更有意思的半截模型说它 could still have called query_cargo_detail——还能调那个查详情的工具。可工具表根本不在它的请求里这个工具名是哪来的我的推断是按命名规律脑补的——工具表没发是字节码实锤名字不可能从请求里来但收尾指令的原文我手上没有不敢排除指令文本里提过一嘴。门外到底有没有人递条子证据不够先按脑补记标明是推断。再看 summary 交回来的活。船期表、舱单表都在最后一段是诚实交接第三步没做C-001 那条货物详情还没查让用户回复「继续」剩下的那步马上补。这个收尾要是按打满即失败的老直觉写该是抛异常或者一句冷冰冰的 error实际交出来的是两张表、一句差距说明、一个下一步动作。而那句继续为什么真能续上maxIters 是 per-run 预算回一句继续就是新 run计数从 0 起算剩下的那步马上补完。一个被打满的 run交出来的不是异常是带交接说明的半成品。RUN_ERROR 什么时候才出现铺垫到此正面回答我在笔记里追过的问题什么情况才有 RUN_ERROR答案藏在方法签名里。产出 RUN_ERROR 的方法签名是 errorEvents(threadId, runId, RunAgentInput, Throwable, boolean)——参数列表里就带着 Throwable。没有异常沿流抛上来这个方法根本不会被调到。所以 RUN_ERROR 不是任务失败事件是流崩了事件。拿老直觉对一对。RUN_FINISHED 像 HTTP 200状态行只承诺传输这趟走完了body 里照样可以写第三步没查到请回复继续RUN_ERROR 像 HTTP 500连接这层出事压根不保证 body 有货。排查线上问题时你不会因为 body 报了业务失败就去查网关——同理agent 回答里说还差一步时不必怀疑框架崩了反过来看到 RUN_ERROR先查的也不是 agent 逻辑。边界照例交代类比帮理解不是机制等价——HTTP 状态码管请求-响应那一层的成败这里管事件流里有没有 Throwable两层不是一回事别拿 500 硬套具体行为。RUN_ERROR 的最高频来源是模型调用失败key 无效、欠费、网络断、响应解析崩。这几个词值过班的都认识——不是你的代码崩了是你依赖的模型服务没应答。生产上它多半长这样某个凌晨 key 过期前端红字 RUN_ERROR日志里跟着模型服务的报错。工具异常反而很少把流干崩循环里有 3 处 onErrorResume外加 ToolResultBlock.error 兜底工具炸了多半被包成结果喂回模型正是第 5 节那句设计哲学。两次实验全程 0 ERROR也因为打满本来就不该往这上头想。排障指向由此定下来RUN_ERROR 出现先查 key、网络、额度再谈 agent 逻辑。这引擎凭什么值钱顺手把跟 Spring AI、LangChain4j 比有什么本质区别一并答了——这问题我自己问过还带着火气是不是太浅、没本质区别ReAct 推理加调工具主流框架都有手写也就百来行循环是商品不是壁垒。AgentScope 押的是循环守住的三样一消息不变量ToolUse 必配对 ToolResult历史才合法二可暂停的运行体interrupt 检查点、人在环挂起恢复、同会话串行、状态快照——那句请回复继续的握手就是雏形三每拍扩展点middleware 能挂在 onReasoning/onActing/onModelCall 上。demo 跑通和生产可用之间差的那几样——可观测、可干预、可恢复、可扩展——要靠这三样押注一样样去补。官方文档里还有一句给下一课留了扣子“BothReActAgentandHarnessAgentrun the same reasoning-acting loop for eachcall.” 同一循环不同机体。诚实边界单轮问答加调工具选谁都没差溢价要等长任务、人在环、多会话这些需求出现才收得回。收口带走三样临走三问带走三样。第一样收场地图正常收束、打满降级、用户中断前两种都以 RUN_FINISHED 结束RUN_ERROR 只在有 Throwable 沿流抛出时出现。配套的 maxIters 语义数常规推理轮数per-run 预算summary 不占额度默认 10。第二样口诀数账单就是数调用。CUSTOMtoken_usage每轮模型调用报一条打满场景等于常规轮数加 1。机理实验二那节讲完了口诀只是把机理压短。第三样排障指向见 RUN_ERROR先查 key、网络、额度见 RUN_FINISHED别默认任务全成——看看回答里有没有交接说明。自测搬走十分钟的事。在 workbench 里找到构建 ReActAgent 的那行 builder 代码把 maxIters 改成 1——注意这是代码参数不是配置键改完重启服务用--workbench.agent.modelfake:fake起免 key然后从 AG-UI 面板发一句现在几点。发车前写三个预测会不会冒出没见过的事件 type最终文本回答还出现吗结束事件是 RUN_FINISHED 还是 RUN_ERROR写完再跑。跑完对着面板数事件条数别只看个大概日志里搜 “Maximum iterations reached” 翻那 1 条 DEBUG对一对它和 RAW 的时间戳毫秒位。标准答案workbench 这套沙盘的口径12 条事件RAW 里躺着 EXCEED_MAX_ITERSsummary 照常给出回答RUN_FINISHED 收场日志 1 条 DEBUG、0 warn、0 ERROR。对不上的两种常见情况没见 RAW——检查点没撞上多半是 maxIters 没改生效蹦出 RUN_ERROR——回去查 key、网络、额度。测完把 maxIters 改回 10别把现场留给下一课。这套实验不花钱fake 腿不报 usage也就没有账单要数——注意力正好全放在事件形状上。先预测后实测的铁律移交给你——有出入的那条才是金子。临走三问只给场景不给答案那条 EXCEED_MAX_ITERS 为什么顶着 RAW 的 type 出现在面板上12 个转换器里谁最后接的手还是那条三工具链的任务把 maxIters 放开让模型正常收束CUSTOM 账单会出几条跟 maxIters(2) 掐断收尾的那次比多出来的是什么你接手一个生产 agent监控半夜报 RUN_ERROR你的前三个排查动作是什么三道都答对了这一课才算真的带走。第 1 课看事件怎么流这一课看循环怎么停下一课往上看一层官方 Quickstart 起手用的不是 ReActAgent是 HarnessAgent。上一节引过的官方原话——两者跑的是同一条 reasoning-acting 循环——已经把话放在那儿了。循环之上还盖着楼。AgentScope Java v2 第2课ReActAgent 的循环引擎2026.10.12 · 阅读约 14 分钟如果这篇文章对你有帮助欢迎转发给需要的同事和朋友。