← AI Agent

ReAct 的执行循环是怎样的?每一轮模型和代码各做什么?

高频 中等 ReAct 循环 · 第 1 / 2 问 更新于 2026/09/29
AI AgentReAct工具调用Function Calling循环控制
本题落地项目AI Agent 智慧医院智能导诊就诊系统

简化版

ReAct 是「推理(Reason)+ 行动(Act)」交替进行的循环:每一轮,代码把系统提示、任务、到目前为止的完整对话历史和可用工具清单一起发给模型;模型决定这一轮是调用工具(给出工具名和参数),还是认为信息够了直接给出最终答案;如果要调工具,代码按工具名找到真实的执行方法去执行,把结果作为 Observation(工具消息)追加到历史里,进入下一轮。模型不再请求工具时循环结束,这一轮的正文就是最终答案。分工上,模型负责「下一步做什么」,代码负责「真的去做、把结果交回去、控制最多转几圈、处理模型给错的东西」。

详细版

messages = [system, user]
for round in 1..最大轮数:
    response = 模型(messages, 工具清单)
    messages += 模型这一轮的回复(含工具调用请求)
    if 没有工具调用请求:
        return response.正文            ← 最终答案
    for 每个工具调用请求:
        结果 = 执行器[工具名](参数)       ← 找不到工具名:把错误作为结果回给模型
        messages += 工具消息(对应请求, 结果)   ← Observation
抛出「达到最大轮数仍未得出结论」
环节谁负责做什么
组织输入代码系统提示放角色和规则,用户消息放任务数据,附上工具规格
决定下一步模型调哪个工具、传什么参数,或者给出最终答案
执行动作代码按工具名路由到真实方法,查库、调接口
回传观察代码把执行结果作为工具消息拼回历史
判断结束模型 + 代码模型不再调工具即结束;代码设最大轮数兜底
处理异常代码工具名不存在、参数错误、执行失败,决定回给模型还是中止

完整版教学

一、为什么需要「边想边做」

普通问答是一次「文本进、文本出」:把问题发给模型,模型凭训练记忆回答。一旦答案依赖系统里的实时数据,这种方式就会出问题:

患者:上腹隐痛三天,还有点恶心,挂哪个科?
模型:建议挂消化内科。     ← 没查任何数据:这个科室存在吗?今天有医生出诊吗?

要回答得准,模型必须先查数据,而且查什么、查几次,取决于前面查到了什么:症状指向哪个科室,才知道要核实哪个科室;科室确认了,才知道去查哪个科室的排班。这种「下一步取决于上一步结果」的任务,没法事先写死步骤,于是把「查什么、查几次」的决定权交给模型,代码只负责执行和兜底,这就是 ReAct。

二、一轮循环里消息是怎么流动的

ReAct 在工程上就是一个消息列表不断变长的循环。以一次分诊为例,前两轮之后消息列表是这样的:

[system]    你是分诊助手,只输出挂号科室建议……
[user]      患者档案ID=12,主诉:上腹隐痛三天,伴恶心
[assistant] 调用 symptom_tag_search(query="上腹隐痛 恶心")          ← 第 1 轮模型回复
[tool]      [{"symptomName":"上腹疼痛","deptId":1,"weight":8}, …]    ← 第 1 轮 Observation
[assistant] 调用 department_search(deptName="消化内科")               ← 第 2 轮模型回复
[tool]      {"matches":[{"deptId":1,"deptName":"消化内科",…}]}         ← 第 2 轮 Observation

两个细节必须做对。第一,模型这一轮的回复要原样追加进历史,包括它的工具调用请求,否则下一轮工具消息无法和调用请求对上。第二,一轮里模型可能同时请求好几个工具,要逐个执行、逐条回填,每条工具消息对应各自的调用请求。

三、模型做什么,代码做什么

ReAct 常被理解成「模型自己在跑」,其实每一轮真正干活的是代码:

问题模型能做必须由代码做
这一轮该调哪个工具?✓
参数传什么?✓(给出 JSON)解析、校验参数
工具真的执行✓ 查库、调接口
数据是否真实✓ 结果来自数据库,模型只读
最多转几圈✓ 最大轮数、调用次数
什么时候结束✓ 不再调工具✓ 到上限强制结束

模型的输出只是「意图」:它说要调某个工具,并不等于这个工具存在,更不等于执行成功。把意图变成动作、把动作结果变成下一轮输入,全是代码的职责。

记忆钩子:模型出主意,代码跑腿、记账、踩刹车。

四、循环怎么结束

结束有三种方式,处理方式不同:

结束方式判断结果
模型给出最终答案这一轮回复里没有工具调用请求取这一轮的正文作为结论,继续做校验和落库
模型调用「提交」类工具约定一个专门的提交工具,调用即结束提交的参数就是结构化结果
达到上限轮数或调用次数超过上限判失败,写明原因,不把半截结果当结论

第一种最常见,但要注意:模型说「够了」只代表它认为够了。拿到最终答案之后,还要检查它是不是真的查过数据(比如数一数实际调用了几次工具),结论里的关键字段还要回数据库核对。具体怎么核对,见「Agent 给出的最终结论能直接落库吗?怎么让它可信?」。

五、模型给错东西时怎么办

模型会犯几类错误,代码要分别处理:

错误例子推荐处理
编造工具名请求一个清单里没有的 check_insurance不中断,把「工具不存在,请改用清单里的工具」作为结果回给模型
参数查不到数据传了一个不存在的 ID工具返回「未找到」,让模型决定下一步
参数格式错参数不是合法 JSON回给模型让它重发,反复写坏由次数上限兜住
执行真的失败数据库异常视业务而定:可以回给模型,也可以终止整次运行

原则是:模型能自己改正的,就把错误当成 Observation 回给它;改不了、或者继续下去会造成错误结果的,就终止并记录原因。直接抛异常中断,会让前面几轮已经查到的信息全部白费。

六、每一轮都在付全量历史的钱

ReAct 每一轮都要把完整历史重新发给模型,输入 token 随轮数线性增长。用一组示意数字算一下:

首轮输入(系统提示 + 任务 + 工具规格):1500 token
每多一轮,历史增加约 600 token(模型回复 + 工具结果)

第 1 轮 1500 → 第 2 轮 2100 → 第 3 轮 2700 → 第 4 轮 3300 → 第 5 轮 3900
5 轮累计输入 = 1500 + 2100 + 2700 + 3300 + 3900 = 13500 token

5 轮的输入总量是单次调用的 9 倍。这就是为什么 ReAct 必须有最大轮数、工具结果要精简(只返回下一步决策需要的字段)、统计成本时要把每一轮的用量累加,而不是只看最后一轮。

七、自己写循环还是交给框架

上面的循环可以自己写,也可以交给框架:Spring AI 的 ChatClient、LangChain4j 的 AiServices 都能在一次调用里跑完整个循环。自己写的好处是每一步都在手里,能在工具执行前后写步骤记录、做次数检查、处理未知工具名;交给框架代码更少,但循环对业务是黑盒,记录和拦截要借助框架的扩展点。两者的取舍见「手写 Function Calling 循环和框架托管有什么区别?怎么选?」。

另外,界面上展示 ReAct 的过程时,展示的应该是可审计的事实:调了哪个工具、传了什么参数、返回了什么,而不是去伪造或还原模型的「思考过程」。

八、常见误区与追问

  • 误区:ReAct 就是模型自己在循环。 循环是代码写的,模型每一轮只给出意图,执行、回传、上限都由代码负责。
  • 误区:只把工具结果回填就行。 模型那一轮的回复(含工具调用请求)也要原样追加,否则下一轮工具结果对不上请求。
  • 误区:模型给了个不存在的工具名就该报错中断。 把错误作为结果回给模型,它通常能改用正确的工具,中断会浪费前面的所有轮次。
  • 误区:模型说够了,结论就可信。 要检查它是否真的调用过工具,关键字段还要回库核对。
  • 误区:轮数多一点无所谓。 每一轮都发全量历史,输入 token 随轮数累加,轮数失控就是成本失控。
  • 追问:一轮里有多个工具调用请求怎么处理? 按模型给出的顺序逐个执行,每个结果单独作为一条工具消息回填,和各自的请求对应。
  • 追问:最大轮数设多少? 看正常任务一般几轮收敛,留出余量;跑满上限的运行几乎都是在原地打转,应当判失败并记录原因。

九、加强记忆

ReAct 记「一轮四步、两个结束、三类错误」:一轮四步是代码发出历史和工具清单、模型给出工具调用或最终答案、代码按工具名执行、把结果作为工具消息追加回历史,模型的回复本身也要原样追加,一轮多个调用就逐个执行逐条回填。结束有两类:模型不再调工具(或调用提交工具)是正常结束,到达轮数上限是失败结束,正常结束后还要核对是否真的查过数据。模型给错东西时,编造工具名、查不到数据、参数写坏都尽量作为 Observation 回给模型,改不了的才终止。每一轮都付全量历史的钱,所以要有上限、结果要精简、成本要逐轮累加。

项目实战落地

项目里怎么做的

《AI Agent 智慧医院智能导诊就诊系统》的分诊 Agent 是手写的 ReAct 循环,在 AI 统一出口 HospitalAiService.chatWithTools 里:

  • 起点:system 放分诊模板(角色与规则),user 放患者档案 ID、主诉、档案摘要、对话背景和 JSON 输出格式要求;工具池从工具中心按「启用」现查现建,每个工具是「工具规格 + 执行器」一对,另按工具名建一张执行器索引;
  • 每一轮:把完整历史和全部工具规格发给 LangChain4j 的 ChatModel,模型这一轮的回复先追加进历史;没有工具执行请求就说明取证够了,这一轮的正文就是最终结论;
  • 执行工具:模型一轮里可能要调好几个工具,逐个按名字找执行器执行,结果以 ToolExecutionResultMessage 拼回历史,作为下一轮的 Observation;
  • 编造的工具名:执行器索引里找不到时不中断整次运行,把「工具不存在或已停用,请改用工具清单里列出的工具」作为这次工具的结果回给模型;
  • 轮数上限:MAX_REACT_ROUNDS = 12,分诊正常 3 到 6 轮就收敛,跑满几乎都是模型在原地打转,抛出「推理轮数达到上限,仍未得出结论」。

《AI Agent岗位匹配与求职规划系统》用的是框架托管的 ReAct:ChatClient 带上由工具表构建的 toolCallbacks、把运行 ID 放进 toolContext 后一次 call(),循环在 Spring AI 内部完成,每次工具调用进入 ToolCallback.call 时写一条步骤记录。

为什么这样取舍

  • 分诊把 ReAct 放进统一出口:业务 Service 调模型只准走 HospitalAiService,带工具的循环和普通对话共用同一套截断检测、报错翻译和调用日志。
  • 查什么交给模型:患者一句话和五句话需要查的信息量完全不同,查数据的动作写不死在代码里。

面试官还会追问

  • 工具规格里每个参数的类型是怎么从 input_schema 这种「字段名 → 说明」的简写推出来的?
  • 分诊结论要求模型输出什么格式?模型把 JSON 包在代码块里时,代码怎么把它取出来?
  • 每一步的步骤号为什么取「这次运行已有步骤数 + 1」,而不是在内存里自己计数?

学完《AI Agent 智慧医院智能导诊就诊系统》,上面这些追问你都会迎刃而解。

本题落地项目地狱锤炼AI Agent 智慧医院智能导诊就诊系统项目简介:基于医学知识库 RAG、向量检索、数据库驱动工具中心、Spring Boot + LangChain4j、Function Calling,实现文档切分与 Embedding 向量化、按库收窄的内存余弦 topK 检索、SSE 真流式多轮预问诊、数据库驱动 ReAct 分诊 Agent 多轮自主取证、工具注册表真开关与调用日志、科室医生查库核验、挂号乐观锁扣减、Agent 执行时间线与 AI 调用观测、就诊运营,覆盖从预问诊、智能分诊、在线挂号到接诊写病历与运营复盘的完整就诊闭环。SpringbootLangChain4JAgentMCPRAG源码+SQL喂饭学习教程配套面试文档环境安装文档项目运行文档 学习这个项目 也可以学AI Agent岗位匹配与求职规划系统地狱锤炼 查看项目