深入理解 ReAct Agent:从原理到 Java 实战

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:

  1. Thought:LLM 在内部推理(对用户不可见的思维链),决定调用哪个工具
  2. Action :LLM 输出 tool_calls 字段,包含工具名 + 参数
  3. Observation :框架执行工具,将结果以 ToolResponseMessage 注入上下文
  4. 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 可能反复调用同一个工具,或者在两个工具之间来回切换。

解决方案

三层安全网:

  1. 最大轮数限制MAX_TOOL_CALL_TURNS = 10
  2. 总超时控制TOTAL_TIMEOUT_MS = 120_000(120秒)
  3. 单工具超时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 会介入。它会:

  1. 分析错误类型(超时、权限、网络、业务异常)
  2. 从 RAG 知识库中检索相关的降级方案
  3. 将降级方案注入到工具结果中

这样 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 的局限在于没有全局规划

比如用户说"帮我分析这周的练习数据并生成报告",这需要:

  1. 查询本周练习数据
  2. 查询用户能力画像
  3. 检索 NVC 评估标准
  4. 综合分析生成报告

步骤 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 来实现更精细的控制。

核心要点

  1. ReAct = Reasoning + Acting,LLM 交替进行推理和工具调用,是当前最主流的 Agent 范式
  2. Agent Loop 是 ReAct 的驱动引擎,通过 while 循环驱动 LLM 多轮迭代,每轮检查"是否还需要调用工具"
  3. 关闭自动工具执行internalToolExecutionEnabled=false)是关键,它把控制权从框架交到应用层
  4. 安全网不可少:最大轮数、总超时、单工具超时、客户端断开检测,四道防线防止失控
  5. 防幻觉是工程重点:空结果注入指令、工具结果截断、降级响应,都是实战中踩过的坑

相关推荐
甲维斯2 小时前
给Claude Code配上DeepSeek,调用kimicu控制电脑
人工智能·agent
阿里云云原生2 小时前
从创建到发布:一套基于 AgentLoop 的 AI Agent Skill 持续调优工程链路
agent
geminigoth2 小时前
Spring AI Alibaba 入门开发一(备份)
java·人工智能·spring
DigitalOcean2 小时前
AI 应用成本怎么算?从推理费用到完整 TCO 拆解
llm·agent
Hammer_Hans2 小时前
DFT笔记98
java·开发语言·数据库
古法安卓2 小时前
Android-DeviceStorageMonitorService 流程分析
java·面试·android studio
栩栩云生2 小时前
AI 最大的安全问题:它让”发送数据”看起来像”继续思考”
安全·github·agent
阿里云云原生2 小时前
OpenAgentPack 开源:让云端 Agent 像代码一样可管理、可迁移
agent
Java内核笔记2 小时前
Spring Boot 4 拥抱 Jackson 3:包名迁移、配置改名与自动配置源码剖析
java·后端