ReAct Agent深度解析:推理与行动的闭环如何在Java中实现
ReAct Agent深度解析:推理与行动的闭环如何在Java中实现
那个让我印象深刻的线上事故
2025年11月的一个周三下午,小李盯着监控屏幕,脸色越来越难看。
他们公司刚上线了一个AI客服系统,用的是最简单的Function Calling方案——用户问问题,AI调工具,返回结果。理论上很完美。
但这天,一个用户问:"我的订单1234567什么时候能到?"
AI调了queryOrder工具,查出来订单状态是"物流异常"。
然后呢?AI直接回复用户:"您的订单正在运输中,预计明天到达。"
用户等了两天,没等到,打来投诉电话。
小李回头查日志才发现:AI拿到"物流异常"这个状态,根本没有进一步推理——它没有意识到这个状态意味着什么,没有调查异常原因,没有给用户正确的信息。工具调用成功了,但推理失败了。
这就是普通Function Calling的死穴:它只能"做",不能"想"。
后来他们引入了ReAct框架,同样的问题,AI的处理流程变成了:
Thought:查询到物流异常,需要进一步了解异常原因 Action:调用queryShippingException工具,参数:订单1234567 Observation:异常原因是"目的地暴雪,航班停飞" Thought:物流延误是天气原因,用户需要知道实际到达时间 Action:调用estimateArrival工具,获取新的预计到达时间 Observation:预计延误3-5天 Thought:现在我有足够信息回复用户了
最终回复准确、及时,用户满意度从61%提升到了89%。
就这一个改变,小李的年终奖多了两万块。
先说结论(TL;DR)
- ReAct = Reasoning(推理)+ Acting(行动),两者交替循环,形成闭环
- 普通Function Calling:用户输入 → AI决策 → 调用工具 → 返回结果(单次、线性)
- ReAct:思考 → 行动 → 观察 → 再思考 → 再行动(多轮、循环,直到得出结论)
- Spring AI通过
ChatClient+ 自定义系统提示 + 循环调用实现ReAct - 关键特性:思维链可见、错误可自愈、推理过程可审计
- 防止无限循环:设置最大推理步骤数(建议5-10步)
ReAct原理:推理与行动的交替循环
从一篇论文说起
ReAct出自2022年普林斯顿+谷歌联合发表的论文《ReAct: Synergizing Reasoning and Acting in Language Models》。核心思想非常直白:让AI在行动之前先"说出"它的思考过程。
传统的Chain-of-Thought(CoT)只让AI推理,不让它行动。 传统的Function Calling只让AI行动,没有显式的推理过程。 ReAct把两者结合:先思考,再行动,观察结果,继续思考。
ReAct的标准格式
Thought: [AI当前的推理内容]
Action: [要调用的工具名称]
Action Input: [工具的输入参数]
Observation: [工具返回的结果]
... (重复Thought/Action/Observation)
Thought: [最终推理,我现在知道了...]
Final Answer: [给用户的最终回答]图解ReAct循环
推理步骤的内部状态机
与普通Function Calling的本质区别
本质差异对比
| 维度 | 普通Function Calling | ReAct Agent |
|---|---|---|
| 推理过程 | 隐式(黑盒) | 显式(可见Thought) |
| 错误恢复 | 无法自愈 | 可观察→推理→重试 |
| 多步骤 | 通常单步 | 天然多步循环 |
| 调试难度 | 难(只看到输入输出) | 易(每步都有日志) |
| Token消耗 | 少 | 多(思维链有成本) |
| 适用场景 | 简单、确定性任务 | 复杂、不确定性任务 |
代码层面的区别
普通Function Calling:
// 一次调用,AI自己决定调什么工具
ChatResponse response = chatClient
.prompt(userMessage)
.tools(toolList)
.call()
.chatResponse();
// 直接拿结果,不知道AI经历了什么ReAct方式:
// 循环执行,每步都能看到推理过程
ReActResult result = reactAgent.run(userMessage);
// result.getThoughtSteps() 包含完整推理链
// result.getActions() 包含每次工具调用
// result.getObservations() 包含每次工具结果Spring AI中ReAct的实现架构
整体架构图
依赖配置
<!-- pom.xml -->
<dependencies>
<!-- Spring AI Core -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Data JPA (用于演示工具) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2数据库 (演示用) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Jackson for JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>应用配置
# application.yml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o
temperature: 0.1 # ReAct需要低温度,保证推理稳定性
max-tokens: 4096
datasource:
url: jdbc:h2:mem:reactdb
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true
react:
agent:
max-steps: 10 # 最大推理步骤
thought-visible: true # 是否向用户展示思维链
retry-on-error: true # 工具调用失败时是否重试
retry-max-attempts: 3 # 最大重试次数
logging:
level:
com.laozhang.react: DEBUG完整实现:带思维链日志的ReAct Agent
核心数据模型
// ReActStep.java - 单个推理步骤
package com.laozhang.react.model;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Builder
public class ReActStep {
public enum StepType {
THOUGHT, // 推理步骤
ACTION, // 行动步骤
OBSERVATION, // 观察步骤
FINAL_ANSWER // 最终答案
}
private int stepNumber;
private StepType type;
private String content; // 步骤内容
private String toolName; // 如果是ACTION,记录工具名
private String toolInput; // 工具输入
private String toolOutput; // 工具输出
private boolean success; // 工具调用是否成功
private String errorMessage; // 失败时的错误信息
private long durationMs; // 该步骤耗时
private LocalDateTime timestamp;
}// ReActResult.java - 完整执行结果
package com.laozhang.react.model;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class ReActResult {
private String userQuery; // 用户原始问题
private String finalAnswer; // 最终答案
private List<ReActStep> steps; // 所有推理步骤
private int totalSteps; // 总步骤数
private long totalDurationMs; // 总耗时
private boolean success; // 是否成功完成
private String failureReason; // 失败原因(如果有)
private int toolCallCount; // 工具调用总次数
private int toolErrorCount; // 工具错误次数
/**
* 获取格式化的思维链文本,用于展示给用户
*/
public String getThoughtChainText() {
StringBuilder sb = new StringBuilder();
for (ReActStep step : steps) {
sb.append("**Step ").append(step.getStepNumber()).append("**\n");
switch (step.getType()) {
case THOUGHT:
sb.append("🤔 **Thought**: ").append(step.getContent()).append("\n\n");
break;
case ACTION:
sb.append("⚡ **Action**: 调用工具 `").append(step.getToolName()).append("`\n");
sb.append(" 输入: ").append(step.getToolInput()).append("\n\n");
break;
case OBSERVATION:
if (step.isSuccess()) {
sb.append("👁️ **Observation**: ").append(step.getContent()).append("\n\n");
} else {
sb.append("❌ **Error**: ").append(step.getErrorMessage()).append("\n\n");
}
break;
case FINAL_ANSWER:
sb.append("✅ **Final Answer**: ").append(step.getContent()).append("\n\n");
break;
}
}
return sb.toString();
}
}解析器:从LLM输出中提取结构
// ReActOutputParser.java
package com.laozhang.react.parser;
import com.laozhang.react.model.ParsedOutput;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
@Component
public class ReActOutputParser {
// 匹配 Thought: 内容
private static final Pattern THOUGHT_PATTERN =
Pattern.compile("Thought:\\s*(.+?)(?=\\nAction:|\\nFinal Answer:|$)", Pattern.DOTALL);
// 匹配 Action: 工具名
private static final Pattern ACTION_PATTERN =
Pattern.compile("Action:\\s*(.+?)(?=\\n|$)");
// 匹配 Action Input: JSON或文本
private static final Pattern ACTION_INPUT_PATTERN =
Pattern.compile("Action Input:\\s*(.+?)(?=\\nObservation:|\\nThought:|$)", Pattern.DOTALL);
// 匹配 Final Answer:
private static final Pattern FINAL_ANSWER_PATTERN =
Pattern.compile("Final Answer:\\s*(.+?)$", Pattern.DOTALL);
/**
* 解析LLM的输出,提取结构化的思维链
*/
public ParsedOutput parse(String llmOutput) {
log.debug("解析LLM输出:\n{}", llmOutput);
ParsedOutput result = new ParsedOutput();
result.setRawOutput(llmOutput);
// 检查是否有最终答案
Matcher finalAnswerMatcher = FINAL_ANSWER_PATTERN.matcher(llmOutput);
if (finalAnswerMatcher.find()) {
result.setFinalAnswer(finalAnswerMatcher.group(1).trim());
result.setHasFinalAnswer(true);
log.debug("找到最终答案: {}", result.getFinalAnswer());
return result;
}
// 提取Thought
Matcher thoughtMatcher = THOUGHT_PATTERN.matcher(llmOutput);
if (thoughtMatcher.find()) {
result.setThought(thoughtMatcher.group(1).trim());
log.debug("提取到Thought: {}", result.getThought());
}
// 提取Action
Matcher actionMatcher = ACTION_PATTERN.matcher(llmOutput);
if (actionMatcher.find()) {
result.setAction(actionMatcher.group(1).trim());
log.debug("提取到Action: {}", result.getAction());
}
// 提取Action Input
Matcher actionInputMatcher = ACTION_INPUT_PATTERN.matcher(llmOutput);
if (actionInputMatcher.find()) {
result.setActionInput(actionInputMatcher.group(1).trim());
log.debug("提取到Action Input: {}", result.getActionInput());
}
result.setHasFinalAnswer(false);
return result;
}
}// ParsedOutput.java
package com.laozhang.react.parser;
import lombok.Data;
@Data
public class ParsedOutput {
private String rawOutput;
private String thought;
private String action;
private String actionInput;
private String finalAnswer;
private boolean hasFinalAnswer;
public boolean hasAction() {
return action != null && !action.isEmpty();
}
}工具注册中心
// ToolRegistry.java
package com.laozhang.react.tool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@Slf4j
@Component
public class ToolRegistry {
private final Map<String, ReActTool> tools = new HashMap<>();
/**
* 注册工具
*/
public void register(ReActTool tool) {
tools.put(tool.getName(), tool);
log.info("注册工具: {} - {}", tool.getName(), tool.getDescription());
}
/**
* 执行工具
*/
public ToolExecutionResult execute(String toolName, String input) {
ReActTool tool = tools.get(toolName);
if (tool == null) {
log.error("工具不存在: {}", toolName);
return ToolExecutionResult.failure(
"工具 '" + toolName + "' 不存在,可用工具: " + getAvailableToolNames()
);
}
long startTime = System.currentTimeMillis();
try {
log.debug("执行工具 {}, 输入: {}", toolName, input);
String result = tool.execute(input);
long duration = System.currentTimeMillis() - startTime;
log.debug("工具 {} 执行成功,耗时 {}ms", toolName, duration);
return ToolExecutionResult.success(result, duration);
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
log.error("工具 {} 执行失败: {}", toolName, e.getMessage(), e);
return ToolExecutionResult.failure(e.getMessage(), duration);
}
}
/**
* 获取所有工具的描述,用于构建系统提示
*/
public String getToolsDescription() {
StringBuilder sb = new StringBuilder();
for (ReActTool tool : tools.values()) {
sb.append("- **").append(tool.getName()).append("**: ").append(tool.getDescription()).append("\n");
sb.append(" 用法示例: ").append(tool.getUsageExample()).append("\n");
}
return sb.toString();
}
public Set<String> getAvailableToolNames() {
return tools.keySet();
}
public int size() {
return tools.size();
}
}// ReActTool.java - 工具接口
package com.laozhang.react.tool;
public interface ReActTool {
String getName();
String getDescription();
String getUsageExample();
String execute(String input) throws Exception;
}// ToolExecutionResult.java
package com.laozhang.react.tool;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class ToolExecutionResult {
private boolean success;
private String output;
private String errorMessage;
private long durationMs;
public static ToolExecutionResult success(String output, long duration) {
return ToolExecutionResult.builder()
.success(true)
.output(output)
.durationMs(duration)
.build();
}
public static ToolExecutionResult success(String output) {
return success(output, 0);
}
public static ToolExecutionResult failure(String errorMessage, long duration) {
return ToolExecutionResult.builder()
.success(false)
.errorMessage(errorMessage)
.durationMs(duration)
.build();
}
public static ToolExecutionResult failure(String errorMessage) {
return failure(errorMessage, 0);
}
}核心:ReAct Agent主体
// ReActAgent.java
package com.laozhang.react.agent;
import com.laozhang.react.model.ReActResult;
import com.laozhang.react.model.ReActStep;
import com.laozhang.react.parser.ParsedOutput;
import com.laozhang.react.parser.ReActOutputParser;
import com.laozhang.react.tool.ToolExecutionResult;
import com.laozhang.react.tool.ToolRegistry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class ReActAgent {
private final ChatClient chatClient;
private final ToolRegistry toolRegistry;
private final ReActOutputParser outputParser;
@Value("${react.agent.max-steps:10}")
private int maxSteps;
@Value("${react.agent.retry-on-error:true}")
private boolean retryOnError;
@Value("${react.agent.retry-max-attempts:3}")
private int retryMaxAttempts;
/**
* 执行ReAct推理循环
*/
public ReActResult run(String userQuery) {
log.info("ReAct Agent开始处理: {}", userQuery);
long startTime = System.currentTimeMillis();
List<ReActStep> steps = new ArrayList<>();
List<Message> conversationHistory = new ArrayList<>();
// 构建系统提示
String systemPrompt = buildSystemPrompt();
conversationHistory.add(new SystemMessage(systemPrompt));
conversationHistory.add(new UserMessage(userQuery));
int stepCount = 0;
int toolCallCount = 0;
int toolErrorCount = 0;
try {
while (stepCount < maxSteps) {
stepCount++;
log.debug("=== 推理步骤 {} ===", stepCount);
// 调用LLM获取下一步推理
String llmResponse = callLLM(conversationHistory);
log.debug("LLM响应:\n{}", llmResponse);
// 解析LLM输出
ParsedOutput parsed = outputParser.parse(llmResponse);
// 记录Thought步骤
if (parsed.getThought() != null) {
ReActStep thoughtStep = ReActStep.builder()
.stepNumber(stepCount)
.type(ReActStep.StepType.THOUGHT)
.content(parsed.getThought())
.timestamp(LocalDateTime.now())
.build();
steps.add(thoughtStep);
log.info("[Step {}] Thought: {}", stepCount, parsed.getThought());
}
// 如果有最终答案,结束循环
if (parsed.isHasFinalAnswer()) {
ReActStep finalStep = ReActStep.builder()
.stepNumber(stepCount)
.type(ReActStep.StepType.FINAL_ANSWER)
.content(parsed.getFinalAnswer())
.timestamp(LocalDateTime.now())
.build();
steps.add(finalStep);
log.info("[Step {}] Final Answer: {}", stepCount, parsed.getFinalAnswer());
return ReActResult.builder()
.userQuery(userQuery)
.finalAnswer(parsed.getFinalAnswer())
.steps(steps)
.totalSteps(stepCount)
.totalDurationMs(System.currentTimeMillis() - startTime)
.success(true)
.toolCallCount(toolCallCount)
.toolErrorCount(toolErrorCount)
.build();
}
// 如果有Action,执行工具调用
if (parsed.hasAction()) {
toolCallCount++;
String toolName = parsed.getAction();
String toolInput = parsed.getActionInput();
// 记录Action步骤
ReActStep actionStep = ReActStep.builder()
.stepNumber(stepCount)
.type(ReActStep.StepType.ACTION)
.toolName(toolName)
.toolInput(toolInput)
.timestamp(LocalDateTime.now())
.build();
steps.add(actionStep);
log.info("[Step {}] Action: {} | Input: {}", stepCount, toolName, toolInput);
// 执行工具(带重试)
ToolExecutionResult toolResult = executeWithRetry(toolName, toolInput);
if (!toolResult.isSuccess()) {
toolErrorCount++;
}
// 记录Observation步骤
String observationContent = toolResult.isSuccess()
? toolResult.getOutput()
: "工具执行失败: " + toolResult.getErrorMessage();
ReActStep observationStep = ReActStep.builder()
.stepNumber(stepCount)
.type(ReActStep.StepType.OBSERVATION)
.content(observationContent)
.success(toolResult.isSuccess())
.errorMessage(toolResult.getErrorMessage())
.durationMs(toolResult.getDurationMs())
.timestamp(LocalDateTime.now())
.build();
steps.add(observationStep);
log.info("[Step {}] Observation: {}", stepCount, observationContent);
// 将LLM的输出和Observation加入对话历史
conversationHistory.add(new AssistantMessage(llmResponse));
conversationHistory.add(new UserMessage("Observation: " + observationContent));
} else {
// LLM没有给出Action也没有Final Answer,说明有问题
log.warn("LLM输出既没有Action也没有Final Answer,强制结束");
break;
}
}
// 超过最大步骤数
if (stepCount >= maxSteps) {
log.warn("达到最大推理步骤数 {},强制结束", maxSteps);
// 让LLM基于现有信息给出最终答案
conversationHistory.add(new UserMessage(
"你已经进行了足够多的推理步骤,请基于目前获得的信息,给出Final Answer。"
));
String finalResponse = callLLM(conversationHistory);
ParsedOutput finalParsed = outputParser.parse(finalResponse);
String finalAnswer = finalParsed.isHasFinalAnswer()
? finalParsed.getFinalAnswer()
: "基于已有信息,我无法完整回答这个问题。已收集的信息:" + summarizeObservations(steps);
return ReActResult.builder()
.userQuery(userQuery)
.finalAnswer(finalAnswer)
.steps(steps)
.totalSteps(stepCount)
.totalDurationMs(System.currentTimeMillis() - startTime)
.success(false)
.failureReason("达到最大推理步骤数限制: " + maxSteps)
.toolCallCount(toolCallCount)
.toolErrorCount(toolErrorCount)
.build();
}
} catch (Exception e) {
log.error("ReAct Agent执行异常", e);
return ReActResult.builder()
.userQuery(userQuery)
.finalAnswer("处理过程中发生错误: " + e.getMessage())
.steps(steps)
.totalSteps(stepCount)
.totalDurationMs(System.currentTimeMillis() - startTime)
.success(false)
.failureReason(e.getMessage())
.toolCallCount(toolCallCount)
.toolErrorCount(toolErrorCount)
.build();
}
return ReActResult.builder()
.userQuery(userQuery)
.finalAnswer("处理完成但未能生成有效答案")
.steps(steps)
.totalSteps(stepCount)
.totalDurationMs(System.currentTimeMillis() - startTime)
.success(false)
.toolCallCount(toolCallCount)
.toolErrorCount(toolErrorCount)
.build();
}
/**
* 构建ReAct系统提示
*/
private String buildSystemPrompt() {
return String.format("""
你是一个使用ReAct(推理+行动)框架的智能助手。
你必须严格按照以下格式进行推理和行动:
格式要求:
Thought: [你的推理过程,分析当前情况和下一步需要做什么]
Action: [工具名称,必须是可用工具之一]
Action Input: [工具的输入参数,JSON格式或纯文本]
当你获得足够信息后:
Thought: [最终推理,说明你已经有了足够的信息]
Final Answer: [给用户的完整、准确的最终答案]
可用工具:
%s
重要规则:
1. 每次只能调用一个工具
2. 观察工具结果后,必须继续推理
3. 如果工具调用失败,分析原因并尝试不同的方法
4. 不要在没有足够信息的情况下给出Final Answer
5. Final Answer必须直接回答用户的问题
""", toolRegistry.getToolsDescription());
}
/**
* 调用LLM
*/
private String callLLM(List<Message> messages) {
return chatClient.prompt()
.messages(messages)
.call()
.content();
}
/**
* 带重试的工具执行
*/
private ToolExecutionResult executeWithRetry(String toolName, String toolInput) {
if (!retryOnError) {
return toolRegistry.execute(toolName, toolInput);
}
ToolExecutionResult lastResult = null;
for (int attempt = 1; attempt <= retryMaxAttempts; attempt++) {
lastResult = toolRegistry.execute(toolName, toolInput);
if (lastResult.isSuccess()) {
return lastResult;
}
log.warn("工具 {} 第{}次执行失败: {}", toolName, attempt, lastResult.getErrorMessage());
if (attempt < retryMaxAttempts) {
try {
Thread.sleep(500L * attempt); // 指数退避
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
return lastResult;
}
/**
* 汇总已有的Observation
*/
private String summarizeObservations(List<ReActStep> steps) {
StringBuilder sb = new StringBuilder();
for (ReActStep step : steps) {
if (step.getType() == ReActStep.StepType.OBSERVATION && step.isSuccess()) {
sb.append("- ").append(step.getContent()).append("\n");
}
}
return sb.toString();
}
}工具调用失败时的推理与重试
失败推理的关键:错误信息要有意义
// SmartDatabaseTool.java - 带详细错误信息的数据库工具
package com.laozhang.react.tool.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.laozhang.react.tool.ReActTool;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
@RequiredArgsConstructor
public class SmartDatabaseTool implements ReActTool {
private final JdbcTemplate jdbcTemplate;
private final ObjectMapper objectMapper;
@Override
public String getName() {
return "query_database";
}
@Override
public String getDescription() {
return "执行SQL查询,获取数据库中的数据。支持SELECT语句。";
}
@Override
public String getUsageExample() {
return "Action Input: {\"sql\": \"SELECT * FROM orders WHERE user_id = 123 LIMIT 10\"}";
}
@Override
public String execute(String input) throws Exception {
Map<String, Object> params = objectMapper.readValue(input, Map.class);
String sql = (String) params.get("sql");
if (sql == null || sql.trim().isEmpty()) {
throw new IllegalArgumentException("SQL语句不能为空,请提供sql参数");
}
// 安全检查:只允许SELECT
if (!sql.trim().toUpperCase().startsWith("SELECT")) {
throw new SecurityException(
"只允许执行SELECT语句,不允许: " + sql.substring(0, Math.min(50, sql.length()))
);
}
try {
List<Map<String, Object>> results = jdbcTemplate.queryForList(sql);
if (results.isEmpty()) {
return "查询结果为空。提示:请检查查询条件是否正确,或者数据是否存在。";
}
return "查询成功,返回 " + results.size() + " 条记录:\n" +
objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(results);
} catch (Exception e) {
// 提供有帮助的错误信息,帮助AI推理如何修复
String errorMsg = e.getMessage();
if (errorMsg.contains("Table") && errorMsg.contains("not found")) {
throw new Exception("表不存在。请用list_tables工具查看可用的表名。原始错误: " + errorMsg);
} else if (errorMsg.contains("Column") && errorMsg.contains("not found")) {
throw new Exception("列不存在。请用describe_table工具查看表结构。原始错误: " + errorMsg);
}
throw new Exception("SQL执行错误: " + errorMsg);
}
}
}// ListTablesTool.java - 帮助AI自我修复的元数据工具
package com.laozhang.react.tool.impl;
import com.laozhang.react.tool.ReActTool;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@RequiredArgsConstructor
public class ListTablesTool implements ReActTool {
private final JdbcTemplate jdbcTemplate;
@Override
public String getName() {
return "list_tables";
}
@Override
public String getDescription() {
return "列出数据库中所有可用的表名,当不确定表名时使用此工具。";
}
@Override
public String getUsageExample() {
return "Action Input: {}";
}
@Override
public String execute(String input) throws Exception {
List<String> tables = jdbcTemplate.queryForList(
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'PUBLIC'",
String.class
);
return "数据库中的表: " + String.join(", ", tables);
}
}错误恢复的实际推理链
当工具失败时,AI会产生这样的推理链:
Step 1 - Thought: 用户想查询用户ID为123的订单,我需要查询数据库
Step 1 - Action: query_database
Step 1 - Action Input: {"sql": "SELECT * FROM order WHERE user_id = 123"}
Step 1 - Observation: 工具执行失败: 表不存在。请用list_tables工具查看可用的表名
Step 2 - Thought: 原来表名不是"order",我需要先查看所有可用的表名
Step 2 - Action: list_tables
Step 2 - Action Input: {}
Step 2 - Observation: 数据库中的表: USERS, ORDERS, ORDER_ITEMS, PRODUCTS
Step 3 - Thought: 表名是"orders"(复数),现在重新查询
Step 3 - Action: query_database
Step 3 - Action Input: {"sql": "SELECT * FROM ORDERS WHERE USER_ID = 123 LIMIT 10"}
Step 3 - Observation: 查询成功,返回 3 条记录: [...]
Step 4 - Thought: 已经获得了用户123的所有订单信息
Step 4 - Final Answer: 用户123共有3笔订单...这就是ReAct的自愈能力——它从错误中学习,调整策略,继续前进。
推理步骤的可见性:让用户看到AI的"思考过程"
流式输出思维链
// ReActStreamController.java
package com.laozhang.react.controller;
import com.laozhang.react.agent.ReActAgent;
import com.laozhang.react.model.ReActResult;
import com.laozhang.react.model.ReActStep;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RestController
@RequestMapping("/api/react")
@RequiredArgsConstructor
public class ReActStreamController {
private final ReActAgent reactAgent;
private final ExecutorService executorService = Executors.newCachedThreadPool();
/**
* SSE流式返回思维链
*/
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamReAct(@RequestParam String query) {
SseEmitter emitter = new SseEmitter(60_000L);
executorService.submit(() -> {
try {
// 通知前端开始处理
emitter.send(SseEmitter.event()
.name("start")
.data("{\"query\":\"" + query + "\"}"));
// 执行ReAct(这里为了演示,实际应该用观察者模式实时推送)
ReActResult result = reactAgent.run(query);
// 逐步推送思维链
for (ReActStep step : result.getSteps()) {
String eventType = step.getType().toString().toLowerCase();
String data = buildStepJson(step);
emitter.send(SseEmitter.event()
.name(eventType)
.data(data));
// 模拟实时感(实际实现中应该在Agent执行时推送)
Thread.sleep(100);
}
// 推送最终答案
emitter.send(SseEmitter.event()
.name("final_answer")
.data("{\"answer\":\"" + escapeJson(result.getFinalAnswer()) + "\","
+ "\"totalSteps\":" + result.getTotalSteps() + ","
+ "\"durationMs\":" + result.getTotalDurationMs() + "}"));
emitter.complete();
} catch (Exception e) {
try {
emitter.send(SseEmitter.event()
.name("error")
.data("{\"message\":\"" + e.getMessage() + "\"}"));
} catch (IOException ioException) {
// ignore
}
emitter.completeWithError(e);
}
});
return emitter;
}
/**
* 普通HTTP接口,返回完整结果
*/
@PostMapping("/query")
public ReActResult query(@RequestBody QueryRequest request) {
return reactAgent.run(request.getQuery());
}
private String buildStepJson(ReActStep step) {
return String.format(
"{\"step\":%d,\"type\":\"%s\",\"content\":\"%s\"}",
step.getStepNumber(),
step.getType(),
escapeJson(step.getContent() != null ? step.getContent() : "")
);
}
private String escapeJson(String text) {
if (text == null) return "";
return text.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r");
}
record QueryRequest(String query) {}
}ReAct vs CoT vs ToT 的对比
三种推理范式对比
| 特性 | CoT (思维链) | ReAct | ToT (思维树) |
|---|---|---|---|
| 推理方式 | 线性推理 | 推理+行动交替 | 树状探索多条路径 |
| 能否调用工具 | 否 | 是 | 是(复杂) |
| 能否自我纠错 | 部分 | 是(通过Observation) | 是(回溯) |
| Token消耗 | 中 | 高 | 非常高 |
| 适用场景 | 推理题、数学 | 需要工具的任务 | 创意、规划类任务 |
| 实现复杂度 | 低 | 中 | 高 |
| Spring AI支持 | 原生支持 | 需自定义 | 需大量自定义 |
何时选择ReAct?
需要调用外部工具 → 使用ReAct
任务步骤不确定 → 使用ReAct
需要根据中间结果调整策略 → 使用ReAct
纯推理/数学问题 → 使用CoT
创意写作/多方案探索 → 使用ToT实战:用ReAct实现自动化数据分析Agent
业务场景
用户输入自然语言:"分析过去30天的销售趋势,找出增长最快的产品类别"
数据分析工具集
// DataAnalysisToolConfig.java - 配置所有数据分析工具
package com.laozhang.react.config;
import com.laozhang.react.tool.ToolRegistry;
import com.laozhang.react.tool.impl.*;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class DataAnalysisToolConfig implements CommandLineRunner {
private final ToolRegistry toolRegistry;
private final SmartDatabaseTool databaseTool;
private final ListTablesTool listTablesTool;
private final StatisticsTool statisticsTool;
private final ChartGeneratorTool chartGeneratorTool;
@Override
public void run(String... args) {
toolRegistry.register(databaseTool);
toolRegistry.register(listTablesTool);
toolRegistry.register(statisticsTool);
toolRegistry.register(chartGeneratorTool);
}
}// StatisticsTool.java - 统计计算工具
package com.laozhang.react.tool.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.laozhang.react.tool.ReActTool;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.*;
@Component
@RequiredArgsConstructor
public class StatisticsTool implements ReActTool {
private final ObjectMapper objectMapper;
@Override
public String getName() {
return "calculate_statistics";
}
@Override
public String getDescription() {
return "对数据列表进行统计分析,计算增长率、趋势、排名等。";
}
@Override
public String getUsageExample() {
return "Action Input: {\"operation\": \"growth_rate\", \"data\": [{\"period\": \"2026-04\", \"value\": 1000}, ...]}";
}
@Override
public String execute(String input) throws Exception {
Map<String, Object> params = objectMapper.readValue(input, Map.class);
String operation = (String) params.get("operation");
List<Map<String, Object>> data = (List<Map<String, Object>>) params.get("data");
switch (operation) {
case "growth_rate":
return calculateGrowthRate(data);
case "trend":
return analyzeTrend(data);
case "top_n":
int n = (Integer) params.getOrDefault("n", 5);
return getTopN(data, n);
default:
throw new IllegalArgumentException("不支持的操作: " + operation + ",支持: growth_rate, trend, top_n");
}
}
private String calculateGrowthRate(List<Map<String, Object>> data) {
if (data.size() < 2) return "数据点不足,无法计算增长率";
StringBuilder result = new StringBuilder("增长率分析:\n");
for (int i = 1; i < data.size(); i++) {
double current = ((Number) data.get(i).get("value")).doubleValue();
double previous = ((Number) data.get(i - 1).get("value")).doubleValue();
double growthRate = previous == 0 ? 0 : ((current - previous) / previous * 100);
result.append(String.format(" %s: %.1f%%\n",
data.get(i).get("period"), growthRate));
}
return result.toString();
}
private String analyzeTrend(List<Map<String, Object>> data) {
if (data.isEmpty()) return "无数据";
// 简单线性趋势分析
double first = ((Number) data.get(0).get("value")).doubleValue();
double last = ((Number) data.get(data.size() - 1).get("value")).doubleValue();
double overallGrowth = first == 0 ? 0 : ((last - first) / first * 100);
String trend = overallGrowth > 10 ? "强劲上涨" :
overallGrowth > 0 ? "温和上涨" :
overallGrowth > -10 ? "轻微下跌" : "明显下跌";
return String.format("整体趋势: %s (%.1f%%)", trend, overallGrowth);
}
private String getTopN(List<Map<String, Object>> data, int n) {
data.sort((a, b) -> Double.compare(
((Number) b.get("value")).doubleValue(),
((Number) a.get("value")).doubleValue()
));
StringBuilder result = new StringBuilder("Top " + n + " 排名:\n");
for (int i = 0; i < Math.min(n, data.size()); i++) {
result.append(String.format(" %d. %s: %s\n",
i + 1, data.get(i).get("category"), data.get(i).get("value")));
}
return result.toString();
}
}实际执行的推理链
用户: 分析过去30天的销售趋势,找出增长最快的产品类别
Step 1 - Thought: 需要先了解数据库有哪些表,特别是销售相关的表
Step 1 - Action: list_tables
Step 1 - Observation: 数据库中的表: USERS, ORDERS, ORDER_ITEMS, PRODUCTS, CATEGORIES
Step 2 - Thought: 看到了ORDERS、ORDER_ITEMS、PRODUCTS、CATEGORIES,
应该能查出销售数据,先查询过去30天按类别的销售额
Step 2 - Action: query_database
Step 2 - Action Input: {
"sql": "SELECT c.name, SUM(oi.quantity * oi.price) as revenue,
DATE_FORMAT(o.created_at, '%Y-%m') as month
FROM CATEGORIES c
JOIN PRODUCTS p ON p.category_id = c.id
JOIN ORDER_ITEMS oi ON oi.product_id = p.id
JOIN ORDERS o ON o.id = oi.order_id
WHERE o.created_at >= DATEADD(DAY, -30, NOW())
GROUP BY c.name, month ORDER BY month, revenue DESC"
}
Step 2 - Observation: 查询成功,返回24条记录...
Step 3 - Thought: 有了分类月度数据,现在计算各类别的增长率
Step 3 - Action: calculate_statistics
Step 3 - Action Input: {
"operation": "growth_rate",
"data": [{"period": "2026-04", "value": 125000, "category": "手机"},
{"period": "2026-05", "value": 168000, "category": "手机"}]
}
Step 3 - Observation: 增长率分析:手机类别: +34.4%
Step 4 - Thought: 已有完整数据,生成最终分析报告
Step 4 - Final Answer: 过去30天销售趋势分析:
增长最快的品类是"手机",增长34.4%;其次是"平板电脑"增长22%...推理步骤数的控制:防止无限循环
循环风险的来源
// StepLimitGuard.java - 步骤控制器
package com.laozhang.react.guard;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.*;
@Slf4j
@Component
public class StepLimitGuard {
/**
* 检测是否进入无限循环(相同的Action重复出现)
*/
public boolean isLooping(List<String> actionHistory, int windowSize) {
if (actionHistory.size() < windowSize * 2) {
return false;
}
// 检查最近windowSize个Action是否重复
int size = actionHistory.size();
List<String> recent = actionHistory.subList(size - windowSize, size);
List<String> previous = actionHistory.subList(size - windowSize * 2, size - windowSize);
boolean isLoop = recent.equals(previous);
if (isLoop) {
log.warn("检测到循环!重复的Action序列: {}", recent);
}
return isLoop;
}
/**
* 检测工具调用是否持续失败
*/
public boolean isTooManyErrors(int errorCount, int totalCount, double threshold) {
if (totalCount < 3) return false;
double errorRate = (double) errorCount / totalCount;
boolean tooManyErrors = errorRate > threshold;
if (tooManyErrors) {
log.warn("工具错误率过高: {}/{} = {:.1f}%", errorCount, totalCount, errorRate * 100);
}
return tooManyErrors;
}
/**
* 生成循环检测报告
*/
public String generateLoopReport(List<String> actionHistory) {
Map<String, Integer> actionCounts = new HashMap<>();
for (String action : actionHistory) {
actionCounts.merge(action, 1, Integer::sum);
}
StringBuilder sb = new StringBuilder("工具调用统计:\n");
actionCounts.forEach((action, count) ->
sb.append(" ").append(action).append(": ").append(count).append("次\n")
);
return sb.toString();
}
}// 在ReActAgent中集成步骤控制
// 在while循环中添加:
if (parsed.hasAction()) {
actionHistory.add(parsed.getAction());
// 检测循环
if (stepLimitGuard.isLooping(actionHistory, 3)) {
log.warn("检测到推理循环,强制中断");
conversationHistory.add(new UserMessage(
"你似乎陷入了循环,一直在重复相同的操作。请分析当前的情况," +
"尝试不同的方法,或者基于现有信息直接给出Final Answer。"
));
}
// 检测过多错误
if (stepLimitGuard.isTooManyErrors(toolErrorCount, toolCallCount, 0.8)) {
log.warn("工具错误率过高,强制中断");
conversationHistory.add(new UserMessage(
"工具调用多次失败,请基于已有信息给出Final Answer," +
"并说明因数据获取问题导致分析不完整。"
));
}
}生产注意事项
1. Token成本控制
// TokenBudgetManager.java
@Component
public class TokenBudgetManager {
@Value("${react.agent.max-tokens-per-session:50000}")
private int maxTokensPerSession;
private int estimateTokens(String text) {
// 粗略估算:英文约4字符/token,中文约2字符/token
return text.length() / 3;
}
public boolean isBudgetExceeded(List<Message> history) {
int totalEstimated = history.stream()
.mapToInt(m -> estimateTokens(m.getText()))
.sum();
return totalEstimated > maxTokensPerSession;
}
// 对话历史压缩:只保留最近N轮
public List<Message> compressHistory(List<Message> history, int keepLastN) {
if (history.size() <= keepLastN + 1) return history;
List<Message> compressed = new ArrayList<>();
compressed.add(history.get(0)); // 保留系统提示
// 保留最近N轮
compressed.addAll(history.subList(history.size() - keepLastN, history.size()));
return compressed;
}
}2. 可观测性配置
# application.yml 追加
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
tags:
application: react-agent
# 自定义指标
react:
metrics:
enabled: true
track-tool-latency: true
track-step-count: true
alert-on-loop: true// ReActMetrics.java
@Component
@RequiredArgsConstructor
public class ReActMetrics {
private final MeterRegistry meterRegistry;
public void recordSession(ReActResult result) {
// 记录总步骤数
meterRegistry.summary("react.steps.count")
.record(result.getTotalSteps());
// 记录总耗时
meterRegistry.timer("react.session.duration")
.record(result.getTotalDurationMs(), TimeUnit.MILLISECONDS);
// 记录成功率
meterRegistry.counter("react.sessions.total",
"success", String.valueOf(result.isSuccess())).increment();
// 记录工具错误率
if (result.getToolCallCount() > 0) {
double errorRate = (double) result.getToolErrorCount() / result.getToolCallCount();
meterRegistry.gauge("react.tool.error.rate", errorRate);
}
}
}3. 安全防护
// ReActSecurityFilter.java
@Component
public class ReActSecurityFilter {
// 禁止询问系统内部信息
private static final List<String> FORBIDDEN_PATTERNS = List.of(
"system prompt", "系统提示", "ignore previous", "忽略之前",
"act as", "扮演", "jailbreak", "越狱"
);
public boolean isQuerySafe(String query) {
String lowerQuery = query.toLowerCase();
return FORBIDDEN_PATTERNS.stream()
.noneMatch(pattern -> lowerQuery.contains(pattern.toLowerCase()));
}
// 限制工具调用频率
private final Map<String, Integer> toolCallCounts = new ConcurrentHashMap<>();
public boolean isToolCallAllowed(String toolName, int maxCallsPerSession) {
int count = toolCallCounts.merge(toolName, 1, Integer::sum);
return count <= maxCallsPerSession;
}
}常见问题解答
Q1:ReAct Agent会消耗很多Token吗,有多少?
A:是的,ReAct比单次Function Calling贵。一般而言:
- 普通Function Calling:1次LLM调用,约500-2000 tokens
- ReAct Agent(5步):5次LLM调用,每次携带历史,合计约5000-20000 tokens
建议的成本控制策略:
- 设置合理的
max-steps(5-8步通常够用) - 定期压缩对话历史,只保留最近3-5轮
- 对于简单任务,仍然使用普通Function Calling
Q2:如何让ReAct的推理过程更可靠?
A:几个关键技巧:
- 温度设置低一些(0.0-0.2),减少随机性
- 系统提示要给出清晰的格式要求和示例
- 提供Few-shot示例,让AI学习正确的推理格式
- 对Thought的内容做格式校验,发现格式混乱时重新生成
Q3:如果AI一直调同一个工具陷入循环怎么办?
A:三层防护:
- 检测循环(连续3次相同Action触发警告)
- 注入提示词,告诉AI它在循环
- 强制给出Final Answer,结束循环 具体实现见
StepLimitGuard类。
Q4:Spring AI原生支持ReAct吗?
A:Spring AI 1.0版本提供了基础的工具调用支持,但ReAct模式需要自己实现循环逻辑和提示词工程。本文的实现方案已经封装成了完整的ReActAgent类,可以直接使用。Spring AI路线图中有Agent框架的计划,后续版本可能会有更原生的支持。
Q5:ReAct和LangGraph/LangChain的Agent有什么区别?
A:LangChain的AgentExecutor本质上就是ReAct的实现。主要区别:
- LangChain是Python生态,Java版本功能相对弱
- 本文实现更轻量,不引入额外依赖
- 对Spring Boot的集成更友好(利用IoC容器管理工具)
Q6:ReAct Agent适合用在哪些业务场景?
A:最适合的场景:
- 需要查多个数据源才能回答的复杂查询
- 数据分析类任务(查询→计算→汇总→报告)
- 需要根据中间结果动态调整查询策略的场景
- 客服系统中需要调查多个系统的问题排查
不适合的场景:
- 简单的单工具调用
- 对响应时间要求极高的场景(<1秒)
- 工具集不稳定(经常报错)的场景
总结
ReAct让AI从"工具执行器"升级成"问题解决者",核心价值在于:
可操作行动清单:
ReAct最大的价值不是技术本身,而是让AI的决策过程变得透明可审计。当用户看到AI一步步思考、一步步验证、一步步得出结论,信任感自然而然就建立起来了。
