ReAct Agent 执行循环原理:从理论到Java实战
"什么是 ReAct Agent?"
你真的理解这个问题吗?
是背诵"Reasoning + Acting"的定义,还是能结合项目讲清楚它的工作原理、工程实现、踩过的坑?
这篇博客就是帮你搞定后者。
先搞懂一个基本问题:Agent 到底在干嘛?
你有没有想过,当你跟 ChatGPT 对话的时候,它其实只能做一件事------根据你给的上下文,预测下一个 token。
但 Agent 不一样。Agent 能做事情。
它能搜索网页、查数据库、调 API、写代码...它能行动。
那问题来了:Agent 怎么知道该做什么?怎么决定先做什么后做什么?
这就是 ReAct 要解决的问题。
ReAct = Re asoning + Acting
翻译过来就是:推理 + 行动
它的核心思想特别简单:让 LLM 交替进行"思考"和"行动",每一步行动后观察结果,再决定下一步。
┌──────────────────────────────────────────────┐
│ ReAct Loop │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Thought │──▶│ Action │──▶│Obervation│ │
│ │ (推理) │ │ (工具) │ │ (观察) │ │
│ └──────────┘ └──────────┘ └────┬─────┘ │
│ ▲ │ │
│ └──────────────────────────────┘ │
│ │
│ 终止条件:Thought 不再需要 Action │
│ (直接输出最终回答) │
└──────────────────────────────────────────────┘
这跟人类解决问题的方式一模一样。
比如你遇到一个不懂的问题,你不会凭空编答案,而是会想:"嗯,这个问题我不太确定,让我想想我之前学过什么相关的..."
这就是 Thought。
然后你会想:"我记得有个公式/原理可以解释这个,让我试试看。"
这就是 Action。
你试完之后发现对不对,然后继续调整。
这就是 Observation。
关键点 :LLM 本身不执行 Action,它只决定 要执行什么 Action。执行由外部的 Agent Loop(Agent 执行循环) 完成。这就是为什么 ReAct 需要一个"循环"------LLM 每次只走一步,由外部编排器驱动多轮迭代。
ReAct 和其他 Agent 范式有什么区别?
ReAct 不是唯一的 Agent 范式。在它之后,又出了好几种不同的思路。
但 ReAct 仍然是最通用的。
为什么?
| 范式 | 核心思路 | 优点 | 缺点 |
|---|---|---|---|
| ReAct | 思考 → 行动 → 观察,逐轮推进 | 可解释性强、可控、适合工具调用 | 串行执行,速度较慢 |
| Plan-and-Execute | 先规划完整计划,再逐步执行 | 适合复杂多步任务 | 计划可能偏离,需重规划 |
| Reflexion | 执行后自我反思,从失败中学习 | 自纠错能力强 | 多轮开销大 |
选型建议:
- 大多数生产环境用 ReAct(简单、成熟、够用)
- 复杂任务用 Plan-and-Execute(多步依赖、需要全局规划)
- 高风险任务用 Reflexion(金融、医疗、代码生成,错误代价高)
- 实际项目中往往是混合范式:以 ReAct 为主体,在关键节点加入规划和反思
ReAct 是最通用的 Agent 范式,也是当前绝大多数 LLM 应用框架(LangChain、Spring AI、AutoGen)的默认实现方式。
Function Calling 是 ReAct 的技术实现
很多人把 ReAct 和 Function Calling 混为一谈。
其实它们是范式 和实现的关系。
ReAct 是一种思想------推理 → 行动 → 观察的循环。
Function Calling 是一种技术------让 LLM 能输出结构化的工具调用请求。
在没有 Function Calling 的年代,ReAct 通过 Prompt Engineering 让 LLM 输出特定格式的 Action(如 Action: search[query]),应用层解析这个格式来执行工具。
有了 Function Calling 之后,LLM 直接返回结构化的工具调用 JSON,不需要解析文本,更可靠。
延伸阅读 :关于 Function Calling 的详细原理和 Schema 设计,可以参考 Function Calling 原理与 Schema 设计。
ReAct 论文原文:ReAct: Synergizing Reasoning and Acting in Language Models
现代 LLM(GPT-4、Claude、DeepSeek)通过 Function Calling / Tool Use 原生支持 ReAct:
- Thought:LLM 在内部推理(对用户不可见的思维链),决定调用哪个工具
- Action :LLM 输出
tool_calls字段,包含工具名 + 参数 - Observation :框架执行工具,将结果以
ToolResponseMessage注入上下文 - LLM 收到观察结果,继续推理,直到不再需要调用工具,输出最终文本
核心配置 :internalToolExecutionEnabled = false------关闭框架的自动工具执行,由自己的 Agent Loop 控制流程。
Java 实战:自建 Agent Loop
我在做 NVC 练习平台的时候,需要一个 Agent 来引导用户练习 NVC(非暴力沟通)。
最开始用的是 Spring AI 的默认工具执行------LLM 返回 tool_calls 后,框架自动执行并注入结果。
延伸阅读 :如果你对 Spring AI 还不熟悉,可以先看看 Spring AI 架构与核心概念。
但我很快发现这样不行。
问题:
- 没法在工具执行前后插入自定义逻辑(限流、缓存、日志)
- 没法做 SSE 实时推送(前端看不到"正在调用 xxx 工具")
- 没法精确控制超时、重试、降级
解决方案:
关闭 Spring AI 的自动工具执行,自己写一个 while 循环来驱动 ReAct。
核心代码:
java
// AgentLoop.java - 核心循环
int turn = 0;
while (turn < MAX_TOOL_CALL_TURNS) { // 最多 10 轮
// 安全检查
if (sink.isCancelled()) break; // 客户端断开
if (System.currentTimeMillis() - startTime > TOTAL_TIMEOUT_MS) break;
// 调用 LLM(含 Fallback 降级)
ChatResponse response = fallbackHandler.executeWithFallback(...);
if (assistantMessage.hasToolCalls()) {
// Thought + Action:LLM 决定调用工具
List<ToolCallResult> results = toolExecutor.execute(toolCalls, userId, conversationId);
// Observation:将工具结果注入上下文
messages.add(assistantMessage);
messages.add(ToolResponseMessage.builder()
.responses(toolResponses).build());
turn++;
} else {
// Thought 无 Action → 输出最终回答,循环结束
sink.next(AgentEvent.content(extractContent(response)));
break;
}
}
关键配置:
java
.options(OpenAiChatOptions.builder()
.toolCallbacks(toolCallbacks)
.internalToolExecutionEnabled(false) // 关键!不让框架自动执行
.build())
这是 ReAct 模式的核心开关。Spring AI 默认会自动执行 LLM 返回的工具调用,但我们关闭了它,自己在 while 循环中控制执行流程。这样做的好处:
- 可以在工具执行前后插入 Hook 链(限流、缓存、日志、权限)
- 可以实现 SSE 实时推送(前端可以看到"正在调用 xxx 工具")
- 可以精确控制 超时、重试、降级
#mermaid-svg-aUPYRJKZ0rm2wsg4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aUPYRJKZ0rm2wsg4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .error-icon{fill:#552222;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .marker.cross{stroke:#333333;}#mermaid-svg-aUPYRJKZ0rm2wsg4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aUPYRJKZ0rm2wsg4 p{margin:0;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .cluster-label text{fill:#333;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .cluster-label span{color:#333;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .cluster-label span p{background-color:transparent;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .label text,#mermaid-svg-aUPYRJKZ0rm2wsg4 span{fill:#333;color:#333;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .node rect,#mermaid-svg-aUPYRJKZ0rm2wsg4 .node circle,#mermaid-svg-aUPYRJKZ0rm2wsg4 .node ellipse,#mermaid-svg-aUPYRJKZ0rm2wsg4 .node polygon,#mermaid-svg-aUPYRJKZ0rm2wsg4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .rough-node .label text,#mermaid-svg-aUPYRJKZ0rm2wsg4 .node .label text,#mermaid-svg-aUPYRJKZ0rm2wsg4 .image-shape .label,#mermaid-svg-aUPYRJKZ0rm2wsg4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .rough-node .label,#mermaid-svg-aUPYRJKZ0rm2wsg4 .node .label,#mermaid-svg-aUPYRJKZ0rm2wsg4 .image-shape .label,#mermaid-svg-aUPYRJKZ0rm2wsg4 .icon-shape .label{text-align:center;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .node.clickable{cursor:pointer;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .arrowheadPath{fill:#333333;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aUPYRJKZ0rm2wsg4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aUPYRJKZ0rm2wsg4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aUPYRJKZ0rm2wsg4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .cluster text{fill:#333;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .cluster span{color:#333;}#mermaid-svg-aUPYRJKZ0rm2wsg4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aUPYRJKZ0rm2wsg4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .icon-shape,#mermaid-svg-aUPYRJKZ0rm2wsg4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .icon-shape p,#mermaid-svg-aUPYRJKZ0rm2wsg4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .icon-shape .label rect,#mermaid-svg-aUPYRJKZ0rm2wsg4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aUPYRJKZ0rm2wsg4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aUPYRJKZ0rm2wsg4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aUPYRJKZ0rm2wsg4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 命中
未命中
YES
NO
用户消息
IntentRouter 预路由
直接执行工具
AgentLoop
调用 LLM
有 tool_calls?
ToolExecutor.execute
beforeToolCall Hook
Tool.execute
afterToolCall Hook
结果注入上下文
提取文本
SSE 推送
安全网设计:如何防止无限循环?
ReAct 循环最怕的就是死循环。
LLM 可能反复调用同一个工具,或者在两个工具之间来回切换。
解决方案:
三层安全网:
- 最大轮数限制 :
MAX_TOOL_CALL_TURNS = 10 - 总超时控制 :
TOTAL_TIMEOUT_MS = 120_000(120秒) - 单工具超时 :
TOOL_TIMEOUT_MS = 30_000(30秒)
代码实现:
java
// 每轮循环开始前检查
if (sink.isCancelled()) break; // 客户端断开
if (System.currentTimeMillis() - startTime > TOTAL_TIMEOUT_MS) break; // 总超时
额外防护:
同一个工具连续调用 3 次且参数相似时,直接返回提示:"你已经检索过相关内容,以下是已有的结果,请基于这些结果回答用户。"
这样就打破了循环。
防幻觉:空结果时 LLM 会编造
LLM 在收到空的工具返回结果时,有很高的概率会"幻觉"------编造不存在的内容来填补空白。
延伸阅读 :关于 LLM 幻觉的更多讨论,可以参考 什么是 hallucination(幻觉)?为什么会发生?。
解决方案:
在工具结果为空时,向上下文注入明确的系统指令。
代码实现:
java
if (result.success() && isEmptyResult(result.result())) {
responseText = result.result()
+ "\n\n[系统指令] 搜索结果为空。请如实告知用户未找到相关内容,"
+ "并询问是否要尝试其他搜索词。不要调用其他无关工具,不要编造内容。";
}
为什么有效:
这比依赖 LLM 的"自律"要可靠得多。明确的系统指令会覆盖 LLM 的默认行为。
这是一个工程上的重要细节。LLM 在 Observation 为空时,容易"幻觉"出不存在的内容。通过注入明确的系统指令,强制 LLM 诚实回答。
意图预路由:跳过 ReAct 的优化
ReAct 循环的一个问题是:即使是简单的意图(如"开始新练习"),也要走一轮 LLM 推理。
延迟 2-3 秒,用户体验差。
延伸阅读 :意图路由的更多实现细节,可以参考 意图路由(Intent Router)实现原理。
解决方案:
在 AgentLoop 前增加一层 IntentRouter,通过正则匹配和关键词检测,在 LLM 调用之前识别用户的明确意图。
代码实现:
java
// AgentLoop 入口
public AgentResult execute(PracticeContext context) {
// 1. 意图预路由(跳过 ReAct 循环)
IntentMatch match = intentRouter.match(context.getUserMessage());
if (match != null && match.getConfidence() >= 0.8) {
// 高置信度意图 → 直接执行工具,不走 LLM
return toolExecutor.executeDirectly(match.getToolName(), match.getArgs());
}
// 2. 低置信度 → 进入 ReAct 循环
return reactLoop(context);
}
效果:
延迟从 2-3 秒降到 500 毫秒以内。
这是对 ReAct 范式的工程优化:对于高置信度的确定性意图,跳过 ReAct 的 Thought-Action 循环,直接执行 Action。
为什么自建 Agent Loop 而不用 Spring AI 默认?
对比表格:
| 维度 | Spring AI 默认 | 自建 AgentLoop |
|---|---|---|
| 工具执行 | 框架自动执行 | 自己的 while 循环控制 |
| Hook 链 | 不支持 | 7 层异步 Hook |
| SSE 推送 | 不支持 | 全链路实时事件 |
| 超时控制 | 框架级 | 精细到每个工具 |
| Fallback | 不支持 | 多级降级 |
| 意图优化 | 无 | IntentRouter 跳过 LLM |
核心价值:
自建 Agent Loop 的核心价值是控制力。
你可以在工具执行的全链路上插入自定义逻辑:限流、缓存、权限校验、评估触发、持久化...
这些都是框架默认行为无法满足的。
延伸思考
如果工具调用失败了怎么办?
前面讲的都是正常流程,但实际项目中,工具调用可能会失败------网络超时、API 报错、权限不足...
这时候 ReAct 循环会怎么处理?
答案是:ErrorEnhanceHook。
在 ToolExecutor 执行工具时,如果捕获到异常,afterToolCall Hook 链中的 ErrorEnhanceHook 会介入。它会:
- 分析错误类型(超时、权限、网络、业务异常)
- 从 RAG 知识库中检索相关的降级方案
- 将降级方案注入到工具结果中
这样 LLM 收到的不是冷冰冰的错误码,而是:"这个工具暂时不可用,但你可以试试这个替代方案..."
这就是为什么我们要自建 Agent Loop 而不是用框架默认------错误处理的精细度决定了用户体验的下限。
如果 LLM 反复调用同一个工具呢?
有时候 LLM 会"钻牛角尖"------第一次搜索结果不够详细,它就再搜一次,再搜一次...
这就是死循环的一种表现。
前面讲了三层安全网(最大轮数、总超时、单工具超时),但那只是"兜底"。更好的做法是主动检测:
java
// 检测同一个工具连续调用次数
if (toolCallHistory.isRepeated(toolName, args, 3)) {
return "你已经检索过相关内容,以下是已有的结果,请基于这些结果回答用户。";
}
这个逻辑在 beforeToolCall Hook 中实现。当检测到同一个工具连续调用 3 次且参数相似时,直接返回提示,打断循环。
如果用户的问题很简单,也要走完整 ReAct 循环吗?
不一定。
这就是意图预路由的价值。
比如用户说"查看我的练习记录",这是一个明确的意图,不需要 LLM 推理。IntentRouter 会通过正则匹配直接识别,跳过 ReAct 循环,直接执行工具。
延迟从 2-3 秒降到 500 毫秒。
但如果用户说"我想练习 NVC 的观察步骤",这个意图不够明确,需要 LLM 来理解。这时候才进入 ReAct 循环。
核心思想:能用规则解决的,不要用 LLM;能用小模型解决的,不要用大模型。
如果需要多步依赖的任务呢?
ReAct 的局限在于没有全局规划。
比如用户说"帮我分析这周的练习数据并生成报告",这需要:
- 查询本周练习数据
- 查询用户能力画像
- 检索 NVC 评估标准
- 综合分析生成报告
步骤 1、2、3 可以并行,但步骤 4 依赖前面三个的结果。
ReAct 会怎么做?它会一步一步来:先调用工具 1,拿到结果;再调用工具 2,拿到结果;再调用工具 3,拿到结果;最后生成报告。
这没问题,但效率不高。
如果任务更复杂------比如步骤 3 失败了,需要重新规划------ReAct 就显得力不从心了。
这时候需要 Plan-and-Execute 范式:先生成完整计划,再逐步执行,失败时重新规划。
但在我的项目中,大多数对话都是短周期交互(1-2 轮工具调用),ReAct 够用。只有在生成报告这种复杂任务时,才会用 Plan-and-Execute。
Function Calling 和 ReAct 到底是什么关系?
很多人把这两个概念混为一谈。
其实它们是范式 和实现的关系。
ReAct 是一种思想------推理 → 行动 → 观察的循环。它描述的是 Agent 的执行模式。
Function Calling 是一种技术------让 LLM 能输出结构化的工具调用请求。它描述的是 LLM 的能力。
在没有 Function Calling 的年代,ReAct 通过 Prompt Engineering 让 LLM 输出特定格式的 Action(如 Action: search[query]),应用层解析这个格式来执行工具。这种方式不稳定,容易解析失败。
有了 Function Calling 之后,LLM 直接返回结构化的工具调用 JSON,不需要解析文本,更可靠。
所以,Function Calling 是 ReAct 范式中"Action"环节的技术实现方式。它让 ReAct 从"理论"变成了"工程实践"。
总结
ReAct 是当前最主流的 Agent 范式,核心是 Thought → Action → Observation 循环。
在 Java 项目中,可以通过自建 Agent Loop 来实现更精细的控制。
核心要点:
- ReAct = Reasoning + Acting,LLM 交替进行推理和工具调用,是当前最主流的 Agent 范式
- Agent Loop 是 ReAct 的驱动引擎,通过 while 循环驱动 LLM 多轮迭代,每轮检查"是否还需要调用工具"
- 关闭自动工具执行 (
internalToolExecutionEnabled=false)是关键,它把控制权从框架交到应用层 - 安全网不可少:最大轮数、总超时、单工具超时、客户端断开检测,四道防线防止失控
- 防幻觉是工程重点:空结果注入指令、工具结果截断、降级响应,都是实战中踩过的坑