文章目录
- 引言
- [一、什么是 ReAct 模式?](#一、什么是 ReAct 模式?)
-
- [1.1 ReAct 的起源与核心思想](#1.1 ReAct 的起源与核心思想)
- [1.2 ReAct 与思维链(CoT)的对比](#1.2 ReAct 与思维链(CoT)的对比)
- [1.3 ReAct 的核心执行循环](#1.3 ReAct 的核心执行循环)
- [1.4 一个具体的执行示例](#1.4 一个具体的执行示例)
- 二、项目架构设计
-
- [2.1 系统架构图](#2.1 系统架构图)
- [2.2 核心组件职责](#2.2 核心组件职责)
- [2.3 项目依赖](#2.3 项目依赖)
- 三、核心代码实现详解
-
- [3.1 模型配置(ModelConfig)](#3.1 模型配置(ModelConfig))
- [3.2 工具注解系统(Tool / ToolParam)](#3.2 工具注解系统(Tool / ToolParam))
- [3.3 工具描述生成器(ToolUtil)](#3.3 工具描述生成器(ToolUtil))
- [3.4 实现具体工具(AgentTools)](#3.4 实现具体工具(AgentTools))
- [3.5 Prompt 模板设计](#3.5 Prompt 模板设计)
- [3.6 核心 Agent 循环(ReActAgent)](#3.6 核心 Agent 循环(ReActAgent))
- [3.7 输出解析器](#3.7 输出解析器)
- [3.8 工具执行器](#3.8 工具执行器)
- 四、完整执行示例
-
- [4.1 启动代码](#4.1 启动代码)
- [4.2 执行过程输出](#4.2 执行过程输出)
- [4.3 执行流程分析](#4.3 执行流程分析)
- 五、工程化改进建议
-
- [5.1 循环计数修复](#5.1 循环计数修复)
- [5.2 未知工具处理](#5.2 未知工具处理)
- [5.3 自动工具注册](#5.3 自动工具注册)
- [5.4 参数校验增强](#5.4 参数校验增强)
- [5.5 更多工具示例](#5.5 更多工具示例)
- 六、与主流框架的对比
- 七、进阶扩展方向
-
- [7.1 多轮对话记忆](#7.1 多轮对话记忆)
- [7.2 并行工具调用](#7.2 并行工具调用)
- [7.3 流式输出](#7.3 流式输出)
- [7.4 更高级的 Agent 架构](#7.4 更高级的 Agent 架构)
- 八、总结
- 参考资料

面向读者 :Java 后端开发者 / AI 工程师 / 技术爱好者
阅读收获:从零搭建一个"能思考、能调用工具、能闭环"的 ReAct Agent,深入理解 AI Agent 的核心工作原理
引言
在大语言模型(LLM)蓬勃发展的今天,AI Agent 已经成为最热门的技术方向之一。与传统的问答式 AI 不同,Agent 能够自主思考、规划任务、调用工具,并最终解决复杂问题。这种能力的核心在于让 AI 具备了"行动力"------它不再只是被动回答问题,而是能够主动采取行动来完成任务。
想象一下这样的场景:你对 AI 说"帮我把 1 到 10 的整数写入一个文件",传统的 ChatGPT 只能告诉你"你可以使用 Python 的文件操作来实现...",而一个真正的 AI Agent 会直接帮你创建文件、写入内容,然后告诉你"已完成,文件在 numbers.txt"。这就是 Agent 与传统 LLM 的本质区别。
在众多 Agent 架构中,ReAct(Reasoning + Acting) 模式因其简洁优雅而备受青睐。 其核心思想是让 LLM 交替进行推理(Reasoning)和行动(Acting),通过"思考-行动-观察"的循环来解决问题。
接下来让我们从零开始,使用纯 Java 代码手写一个完整的 ReAct Agent。我们不依赖 LangChain、Spring AI 等框架,而是直接使用 OpenAI 官方 Java SDK 与大模型交互,深入理解 Agent 的工作原理。
一、什么是 ReAct 模式?
1.1 ReAct 的起源与核心思想
ReAct 由 Yao 等人在 2022 年提出(ICLR 2023 发表),论文标题为《ReAct: Synergizing Reasoning and Acting in Language Models》。其核心洞察是:将推理(Reasoning)和行动(Acting)交织在一起,可显著提升 LLM 解决复杂任务的能力。
传统的 Chain-of-Thought(CoT)只关注推理,让模型"一步步思考"来提升推理能力;传统的 Action-based 方法只关注行动,让模型直接调用工具。ReAct 的创新在于将两者合一:
java
Thought → Action → Observation → Thought → Action → Observation → ... → Final Answer
这种设计模拟了人类解决问题的认知过程:我们不会一次性想清楚所有步骤,而是边思考边行动,根据行动的反馈调整下一步计划。
1.2 ReAct 与思维链(CoT)的对比
思维链(Chain of Thought, CoT)技术,ReAct 可以看作是 CoT 的增强版:
| 特性 | CoT(思维链) | ReAct |
|---|---|---|
| 推理能力 | ✅ 支持 | ✅ 支持 |
| 外部工具调用 | ❌ 不支持 | ✅ 支持 |
| 信息获取 | 仅依赖模型已有知识 | 可从外部获取实时信息 |
| 任务执行 | 只能给出建议 | 可以实际执行任务 |
| 自我修正 | 较弱 | 较强(基于观察结果调整) |
CoT 的局限在于,模型只能基于训练数据中的知识进行推理,无法获取实时信息或执行实际操作。而 ReAct 通过引入工具调用机制,让模型具备了与外部世界交互的能力。
1.3 ReAct 的核心执行循环
ReAct Agent 的工作流程可以概括为一个迭代循环:
- Reason(推理):分析当前状态,思考下一步应该做什么
- Action(行动):决定调用哪个工具
- Action Input(行动输入):生成工具调用参数
- Observation(观察):系统执行工具并返回结果
- 重复上述过程,直到模型输出最终答案(Final Answer)
用流程图表示:
jAva
┌─────────────────────────────────────────────────────────────┐
│ ReAct 执行循环 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户问题 ──→ 构建 Prompt ──→ 调用 LLM │
│ ↑ │ │
│ │ ↓ │
│ 历史记录更新 解析 LLM 输出 │
│ │ │ │
│ │ ↓ │
│ ┌─────┴─────┐ 是 FinalAnswer? │
│ │ │ │ │
│ │ Observation ├── 是 ──→ 返回最终答案 │
│ │ │ │ │
│ └─────┬─────┘ ↓ 否 │
│ │ 执行工具 │
│ │ │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
1.4 一个具体的执行示例
让我们通过一个具体例子来理解 ReAct 的执行流程。假设用户提出需求:"将 1 到 10 之间的所有整数写入文件"。
第一轮:
Reason: 用户需要将 1 到 10 的整数写入文件,我需要使用 writeFile 工具来完成这个任务。
我需要指定文件路径和要写入的内容。
Action: writeFile
ActionInput: {"file_path": "numbers.txt", "content": "1\n2\n3\n4\n5\n6\n7\n8\n9\n10"}
系统执行工具后返回:
Observation: 写入成功
第二轮:
Reason: 文件已经成功写入,任务完成。
FinalAnswer: 我已经成功将 1 到 10 的所有整数写入了 numbers.txt 文件。
这个流程清晰地展示了 ReAct 的核心特点:LLM 负责思考和决策,而具体的文件写入操作则由外部工具完成。模型通过观察工具执行结果来决定下一步行动,形成完整的闭环。
二、项目架构设计
在动手编码之前,让我们先设计整体架构。一个完整的 ReAct Agent 需要以下核心组件。
2.1 系统架构图
jva
┌─────────────────────────────────────────────────────────────┐
│ ReAct Agent │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Prompt │ │ LLM API │ │ Output Parser │ │
│ │ Template │ │ Client │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Tool System ││
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ ││
│ │ │ @Tool │ │@ToolParam│ │ ToolUtil │ ││
│ │ │Annotation│ │Annotation│ │ (Reflection-based) │ ││
│ │ └──────────┘ └──────────┘ └──────────────────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Agent Tools ││
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││
│ │ │writeFile │ │ readFile │ │ search │ ... ││
│ │ └──────────┘ └──────────┘ └──────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
2.2 核心组件职责
| 组件 | 职责 | 关键类 |
|---|---|---|
| 模型配置 | 管理 API Key、模型名称、服务地址 | ModelConfig |
| 工具注解 | 声明式定义工具元信息 | @Tool, @ToolParam |
| 工具集合 | 实现具体的工具逻辑 | AgentTools |
| 工具工具类 | 通过反射生成工具描述 | ToolUtil |
| Agent 核心 | 实现 ReAct 循环逻辑 | ReActAgent |
2.3 项目依赖
项目基于 Java 21,只依赖两个核心库:
xml
<dependencies>
<!-- OpenAI 官方 Java SDK -->
<dependency>
<groupId>com.openai</groupId>
<artifactId>openai-java</artifactId>
<version>0.32.0</version>
</dependency>
<!-- JSON 处理(工具实现中使用 Jackson,来自 SDK 传递依赖) -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.56</version>
</dependency>
</dependencies>
为什么选择纯 Java 实现?
选择 OpenAI 官方 SDK 而非 Spring AI 等框架,是因为我们要能够理解 Agent 的底层实现原理,而非仅仅学会调用封装好的 API。这种方式的优势包括:
- 原理透明:每一行代码都可追溯,便于理解 Agent 工作机制
- 依赖精简:仅需 OpenAI SDK 和 JSON 库,启动快、包体小
- 灵活可控:可根据业务需求自由定制 Prompt、解析逻辑
- 学习价值:为后续使用框架打下坚实基础
三、核心代码实现详解
3.1 模型配置(ModelConfig)
首先,我们需要配置与大模型的连接。这里我们使用阿里云的 DashScope 服务(兼容 OpenAI API 格式):
java
public class ModelConfig {
// 从环境变量获取 API Key,安全且便于管理
public static final String API_KEY = System.getenv("DASHSCOPE_API_KEY");
// 使用 DashScope 的 OpenAI 兼容接口
public static final String BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
// 使用通义千问 Max 模型
public static final String LLM_NAME = "qwen-max";
}
设计要点:
- 环境变量读取 API Key:避免将敏感信息硬编码到代码中,这是安全最佳实践
- OpenAI 兼容接口 :DashScope 提供了与 OpenAI API 兼容的接口,这意味着只需修改
BASE_URL和API_KEY,即可无缝切换到 OpenAI、DeepSeek 等其他服务商 - 模型选择 :
qwen-max是目前通义千问系列中推理能力最强的模型,适合需要复杂推理的 Agent 场景
3.2 工具注解系统(Tool / ToolParam)
为了让 Agent 能够识别和调用工具,我们设计了一套基于注解的工具系统。这套设计借鉴了 Spring 框架的声明式编程理念。
@Tool 注解:标记一个方法为可被 Agent 调用的工具
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Tool {
String description(); // 工具功能描述,供 LLM 理解工具用途
}
@ToolParam 注解:描述工具参数
java
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ToolParam {
String description(); // 参数描述,帮助 LLM 理解如何构造输入
}
注解设计的核心价值:
- 声明式定义:工具的元信息直接标注在方法上,代码即文档
- 运行时可见 :
@Retention(RetentionPolicy.RUNTIME)确保注解在运行时可被反射读取 - 自动化集成:无需手动维护工具列表,系统自动发现和注册工具
3.3 工具描述生成器(ToolUtil)
ToolUtil 类负责通过反射扫描工具类,自动生成供 LLM 使用的工具描述:
java
public class ToolUtil {
public static String getToolDescription(Class<?> clazz) {
List<String> toolNameList = new ArrayList<>();
List<String> formattedToolList = new ArrayList<>();
// 遍历类中的所有方法
for (Method declaredMethod : clazz.getDeclaredMethods()) {
// 检查是否有 @Tool 注解
if (declaredMethod.isAnnotationPresent(Tool.class)) {
Tool toolAnnotation = declaredMethod.getAnnotation(Tool.class);
String toolName = declaredMethod.getName();
String toolDescription = toolAnnotation.description();
// 获取参数描述
String paramDescription = declaredMethod
.getParameters()[0]
.getAnnotation(ToolParam.class)
.description();
// 格式化工具描述
String formattedTool = String.format(
"- toolName=%s, toolDescription=%s, paramDescription=%s",
toolName, toolDescription, paramDescription
);
formattedToolList.add(formattedTool);
toolNameList.add(toolName);
}
}
return String.join("\n\n", formattedToolList);
}
}
- 自动发现 :使用 Java 反射机制扫描所有带
@Tool注解的方法 - 零配置:添加新工具只需在方法上加注解,无需修改任何配置文件
- LLM 友好:生成的描述格式清晰,便于模型理解工具用途和参数要求
生成的工具描述示例:
java
- toolName=writeFile, toolDescription=将指定内容写入本地文件。, paramDescription=包含 'file_path' 和 'content' 的 JSON 字符串。
3.4 实现具体工具(AgentTools)
现在我们来实现一个具体的工具------文件写入工具:
java
public class AgentTools {
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 将指定内容写入本地文件。
* @param jsonInput 一个包含 'file_path' 和 'content' 的 JSON 字符串。
* @return 执行结果的描述字符串。
*/
@Tool(description = "将指定内容写入本地文件。")
public String writeFile(
@ToolParam(description = "包含 'file_path' 和 'content' 的 JSON 字符串。")
String jsonInput
) {
try {
// 解析 JSON 输入
JsonNode rootNode = objectMapper.readTree(jsonInput);
String filePath = rootNode.get("file_path").asText();
String content = rootNode.get("content").asText();
// 执行文件写入
try (FileWriter writer = new FileWriter(filePath)) {
writer.write(content);
return "写入成功";
} catch (IOException e) {
return String.format("写入文件 '%s' 时发生错误: %s", filePath, e.getMessage());
}
} catch (Exception e) {
return String.format("解析输入或执行工具时出错: %s", e.getMessage());
}
}
}
设计细节解析:
- JSON 字符串作为输入:这与 LLM 生成的格式一致,避免复杂的类型转换
- 简洁的返回值:返回"写入成功"而非冗长的描述,让 LLM 能快速理解执行结果
- 完善的错误处理 :
- JSON 解析失败时返回明确的错误信息
- 文件写入失败时返回具体的异常原因
- 这些信息会作为 Observation 反馈给 LLM,帮助它调整策略
3.5 Prompt 模板设计
Prompt 设计是 ReAct Agent 的灵魂。一个优秀的 Prompt 需要清晰地告诉 LLM:你是谁、你能做什么、你应该如何输出。
java
private static final String REACT_PROMPT_TEMPLATE = """
# 角色定义
你是一个强大的 AI 助手,通过思考和使用工具来解决用户的问题。
# 任务
你的任务是尽你所能回答以下问题。你可以使用以下工具:
{tools}
# 规则
- Action中只需要返回工具的名字,比如writeFile,不要返回以下格式toolName=writeFile
- 每次只做一次Reason/Action/ActionInput或者FinalAnswer的输出过程,不要一次性都做了
- 每次返回的过程中不要自己生成Observation的内容
- 返回Reason/Action/ActionInput的时候不要生成并返回Observation的内容
# 输出过程参考
第一轮
Reason: 你思考的过程
Action: 你的下一步动作,你想要执行的工具是哪个,必须是{tools}中的一个
ActionInput: 你要调用的工具的输入参数是什么
第二轮
Reason: 你思考的过程
Action: 你的下一步动作
ActionInput: 你要调用的工具的输入参数
...
最后一轮
FinalAnswer: 表示最终的答案,只需要最后输出就可以了
# 用户需求
Question: {input}
# 历史聊天记录
{history}
""";
Prompt 设计要点详解:
| 部分 | 作用 | 设计考量 |
|---|---|---|
| 角色定义 | 设定 AI 的身份和能力边界 | 让模型明确自己是一个"使用工具解决问题"的助手 |
| 工具清单 | 告知可用工具 | {tools} 占位符会被替换为实际的工具描述 |
| 规则约束 | 控制输出格式 | 防止模型自行编造 Observation,确保每轮只输出一次 |
| 输出示例 | 引导输出格式 | 通过多轮对话示例展示期望的输出结构 |
| 历史记录 | 维持上下文 | {history} 保存之前的推理过程,实现"记忆"能力 |
为什么规则约束如此重要?
没有严格的规则约束,LLM 可能会:
- 一次性输出多轮的 Reason/Action/ActionInput
- 自己编造 Observation 内容(而不是等待真实的工具执行结果)
- 跳过工具调用直接给出答案(即使它无法完成任务)
通过明确的规则,我们确保了 LLM 的行为可预测、可控制。
3.6 核心 Agent 循环(ReActAgent)
现在让我们看看 ReAct Agent 的核心------主循环逻辑:
java
public class ReActAgent {
private OpenAIClient apiClient;
public ReActAgent(OpenAIClient apiClient) {
this.apiClient = apiClient;
}
public String run(String input) throws NoSuchMethodException {
// 1. 注册可用工具
HashMap<String, Method> tools = new HashMap<>();
tools.put("writeFile", AgentTools.class.getMethod("writeFile", String.class));
// 2. 初始化历史记录,用于维护推理上下文
StringBuilder history = new StringBuilder();
// 3. 最大循环次数,防止无限循环
int i = 0;
while (i < 10) {
try {
// 4. 构建完整 Prompt
String prompt = buildPrompt(input, history.toString());
// 5. 调用大模型
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
.addUserMessage(prompt)
.model(ModelConfig.LLM_NAME)
.build();
ChatCompletion chatCompletion = apiClient.chat().completions().create(params);
String rawLlmOutput = chatCompletion.choices().get(0).message().content().get();
System.out.println("大模型原始输出:" + rawLlmOutput);
// 6. 解析 LLM 输出
ParsedOutput parsedOutput = parseLlmOutput(rawLlmOutput);
// 7. 如果是最终答案,直接返回
if (parsedOutput.type.equals("final_answer")) {
return parsedOutput.answer;
}
// 8. 执行工具并获取结果
String observation = executeTool(parsedOutput, tools);
System.out.println("工具执行结果:" + observation);
// 9. 更新历史记录
history.append("Reason: ").append(parsedOutput.reason).append("\n")
.append("Action: ").append(parsedOutput.action).append("\n")
.append("ActionInput: ").append(parsedOutput.actionInputStr).append("\n")
.append("Observation: ").append(observation).append("\n");
} catch (Exception e) {
e.printStackTrace();
i++;
}
}
return "达到了循环最大次数";
}
}
核心流程逐步解析:
- 工具注册:将可用工具及其方法引用存入 HashMap,便于后续通过名称查找和调用
- 历史记录初始化:使用 StringBuilder 累积每轮的执行记录
- 循环控制:设置最大循环次数为 10,防止 LLM 陷入无限循环
- Prompt 构建:将用户输入和历史记录填充到模板中
- LLM 调用:使用 OpenAI SDK 发送请求并获取响应
- 输出解析:区分"继续行动"和"最终答案"两种情况
- 终止判断:如果输出包含 FinalAnswer,则返回结果
- 工具执行:根据解析结果调用对应工具
- 记忆更新:将本轮的 Reason、Action、ActionInput、Observation 追加到历史记录
Prompt 构建方法:
java
private String buildPrompt(String input, String history) {
String prompt = REACT_PROMPT_TEMPLATE.replace("{tools}", ToolUtil.getToolDescription(AgentTools.class));
prompt = prompt.replace("{input}", input);
prompt = prompt.replace("{history}", history);
return prompt;
}
这个方法完成三个占位符的替换:
{tools}→ 工具描述列表{input}→ 用户的原始问题{history}→ 之前的对话历史
3.7 输出解析器
LLM 的输出是自由文本,需要被解析为结构化数据以便程序处理:
java
private ParsedOutput parseLlmOutput(String llmOutput) {
// 1. 检查是否为最终答案
if (llmOutput.contains("FinalAnswer: ")) {
return new ParsedOutput(
"final_answer",
llmOutput.split("FinalAnswer: ")[1].strip(),
null, null, null, null
);
}
// 2. 使用正则表达式提取 Reason、Action、ActionInput
Pattern actionPattern = Pattern.compile(
"Reason:(.*?)Action:(.*?)ActionInput:(.*)",
Pattern.DOTALL
);
Matcher matcher = actionPattern.matcher(llmOutput);
if (matcher.find()) {
String reason = matcher.group(1).trim();
String action = matcher.group(2).trim();
String actionInputStr = matcher.group(3).trim();
// 3. 处理可能的 Markdown 代码块格式
if (actionInputStr.startsWith("```json")) {
actionInputStr = actionInputStr.substring(7);
}
if (actionInputStr.endsWith("```")) {
actionInputStr = actionInputStr.substring(0, actionInputStr.length() - 3);
}
actionInputStr = actionInputStr.trim();
return new ParsedOutput("action", null, reason, action, actionInputStr, null);
}
// 4. 解析失败
return new ParsedOutput(
"error", null, null, null, null,
String.format("解析LLM输出失败: '%s'", llmOutput)
);
}
// 使用 Java Record 定义解析结果数据结构
private record ParsedOutput(
String type, // 输出类型:final_answer / action / error
String answer, // 最终答案(当 type 为 final_answer 时)
String reason, // 推理过程
String action, // 要执行的工具名
String actionInputStr, // 工具输入参数(JSON 字符串)
String message // 错误信息(当 type 为 error 时)
) {}
解析器设计要点:
- 优先级判断 :首先检查是否包含
FinalAnswer:,这是终止循环的信号 - 正则匹配 :使用
Pattern.DOTALL模式,使.能匹配换行符,处理多行输出 - 格式兼容 :处理 LLM 可能生成的 Markdown 代码块格式(如 ```````json```` 包裹的 JSON)
- 不可变数据:使用 Java Record 定义数据结构,简洁且线程安全
3.8 工具执行器
工具执行器通过反射调用实际的工具方法:
java
private static String executeTool(
ParsedOutput parsedOutput,
HashMap<String, Method> tools
) throws IllegalAccessException, InvocationTargetException {
String toolName = parsedOutput.action;
String toolParams = parsedOutput.actionInputStr;
// 根据工具名查找方法
Method toolMethod = tools.get(toolName);
// 通过反射调用工具方法
Object observation = toolMethod.invoke(new AgentTools(), toolParams);
return String.valueOf(observation);
}
反射调用的优势:
- 动态调用:无需硬编码 switch-case 分支,根据工具名动态查找并调用
- 易于扩展:添加新工具只需注册到 HashMap,无需修改执行器代码
- 解耦设计:Agent 核心逻辑与具体工具实现完全分离
四、完整执行示例
4.1 启动代码
java
public static void main(String[] args) throws Exception {
// 1. 创建 OpenAI 客户端
OpenAIClient apiClient = OpenAIOkHttpClient.builder()
.apiKey(ModelConfig.API_KEY)
.baseUrl(ModelConfig.BASE_URL)
.build();
// 2. 创建 ReAct Agent
ReActAgent reActAgent = new ReActAgent(apiClient);
// 3. 执行任务
String result = reActAgent.run("将1到10中间的所有整数写到文件中");
// 4. 输出结果
System.out.println("最终结果:" + result);
}
4.2 执行过程输出
java
大模型原始输出:Reason: 用户需要将1到10之间的所有整数写入到一个文件中。
我需要使用writeFile工具来完成这个任务,需要指定文件路径和内容。
Action: writeFile
ActionInput: {"file_path": "numbers.txt", "content": "1\n2\n3\n4\n5\n6\n7\n8\n9\n10"}
工具执行结果:写入成功
大模型原始输出:Reason: 文件已经成功写入,用户的任务已经完成。
FinalAnswer: 我已经成功将1到10之间的所有整数写入到了numbers.txt文件中。
最终结果:我已经成功将1到10之间的所有整数写入到了numbers.txt文件中。
4.3 执行流程分析
从输出可以清晰看到 ReAct 的工作过程:
| 轮次 | 阶段 | 内容 |
|---|---|---|
| 1 | Reason | 分析用户需求,决定使用 writeFile 工具 |
| 1 | Action | writeFile |
| 1 | ActionInput | 构造 JSON 参数,包含文件路径和内容 |
| 1 | Observation | 系统执行工具后返回"写入成功" |
| 2 | Reason | 观察到成功结果,判断任务完成 |
| 2 | FinalAnswer | 返回最终答案给用户 |
这个两轮对话完美地展示了 ReAct 的核心机制:思考→行动→观察→再思考→最终答案。
五、工程化改进建议
当前实现是一个最小可行版本,适合学习和理解原理。在生产环境中,还需要进行以下改进:
5.1 循环计数修复
当前实现中,i 只在异常时递增,可能导致正常情况下无限循环:
java
// 当前实现
while (i < 10) {
try {
// ... 正常逻辑
} catch (Exception e) {
i++; // 只有异常时才递增
}
}
// 建议改进
while (i < 10) {
try {
// ... 正常逻辑
} catch (Exception e) {
e.printStackTrace();
}
i++; // 每轮都递增
}
5.2 未知工具处理
当模型输出未知工具名时,当前实现会触发 NullPointerException:
java
private static String safeExecuteTool(ParsedOutput parsedOutput, HashMap<String, Method> tools) {
String toolName = parsedOutput.action;
// 检查工具是否存在
Method toolMethod = tools.get(toolName);
if (toolMethod == null) {
return "未知工具: " + toolName + "。请检查工具清单并重新选择。";
}
// ... 执行工具逻辑
}
5.3 自动工具注册
将手动注册改为反射扫描,添加新工具时无需修改 Agent 代码:
java
private static HashMap<String, Method> registerTools(Class<?> toolClass) {
HashMap<String, Method> tools = new HashMap<>();
for (Method method : toolClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(Tool.class)) {
tools.put(method.getName(), method);
}
}
return tools;
}
// 使用
HashMap<String, Method> tools = registerTools(AgentTools.class);
5.4 参数校验增强
模型可能输出不合法的 JSON,建议增加基础校验:
java
// 最小 JSON 校验
if (toolParams == null || !toolParams.trim().startsWith("{") || !toolParams.trim().endsWith("}")) {
return "ActionInput 不是合法 JSON 对象,请输出形如 {\"key\":\"value\"} 的参数。";
}
5.5 更多工具示例
扩展工具集以支持更多场景:
java
@Tool(description = "读取本地文件内容")
public String readFile(@ToolParam(description = "文件路径") String filePath) {
try {
return Files.readString(Path.of(filePath));
} catch (IOException e) {
return "读取文件失败: " + e.getMessage();
}
}
@Tool(description = "执行网络搜索")
public String webSearch(@ToolParam(description = "搜索关键词的 JSON") String jsonInput) {
// 调用搜索 API 实现
}
@Tool(description = "执行数学计算")
public String calculate(@ToolParam(description = "包含 expression 的 JSON") String jsonInput) {
// 使用表达式求值库实现
}
六、与主流框架的对比
| 维度 | 手写实现 | LangChain | Spring AI |
|---|---|---|---|
| 学习价值 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| 灵活性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 开发效率 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 生产就绪 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 社区生态 | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
建议:学习原理用手写实现,工程落地用成熟框架。理解了底层原理后,使用框架会更加得心应手。
七、进阶扩展方向
掌握了基础的 ReAct 实现后,可以进一步探索以下方向:
7.1 多轮对话记忆
当前实现在单次任务结束后会清空历史。可以引入持久化存储,实现跨会话记忆:
java
public class ChatMemory {
private final List<Message> messages = new ArrayList<>();
public void addMessage(Message message) {
messages.add(message);
// 可选:持久化到数据库
}
public String getHistory() {
return messages.stream()
.map(Message::toString)
.collect(Collectors.joining("\n"));
}
}
7.2 并行工具调用
当多个工具之间没有依赖关系时,可以并行执行以提升效率:
java
List<CompletableFuture<String>> futures = actions.stream()
.map(action -> CompletableFuture.supplyAsync(() -> executeTool(action)))
.toList();
List<String> results = futures.stream()
.map(CompletableFuture::join)
.toList();
7.3 流式输出
对于长时间运行的任务,可以使用流式输出提升用户体验:
java
apiClient.chat().completions().createStream(params)
.forEach(chunk -> {
String content = chunk.choices().get(0).delta().content().orElse("");
System.out.print(content); // 实时输出
});
7.4 更高级的 Agent 架构
- Plan-and-Execute:先制定完整计划,再逐步执行
- Tree of Thoughts:探索多条推理路径,选择最优解
- Multi-Agent:多个 Agent 协作完成复杂任务
- Self-Reflection:Agent 自我反思和错误修正
八、总结
我们从零开始实现了一个完整的 ReAct Agent,涵盖以下核心知识点:
| 主题 | 内容要点 |
|---|---|
| ReAct 原理 | 推理与行动交替进行,通过观察结果迭代优化 |
| 工具系统 | 基于注解的声明式定义,反射机制自动发现 |
| Prompt 工程 | 角色设定、规则约束、示例引导、历史记录 |
| 输出解析 | 正则匹配提取结构化信息,处理多种输出格式 |
| 循环控制 | 最大次数限制、终止条件判断、异常处理 |
ReAct 模式是构建 AI Agent 的基础范式,掌握其原理对于理解更复杂的 Agent 架构至关重要。通过纯 Java 实现,我们深入理解了 Agent 的工作机制------它不是魔法,而是精心设计的 Prompt + 循环 + 工具调用的组合。
AI Agent 是一个快速发展的领域,ReAct 只是众多架构之一。建议读者在掌握本文内容后,进一步探索 LangChain、Spring AI 等成熟框架,将原理知识转化为工程实践能力。
参考资料
- ReAct 论文 :ReAct: Synergizing Reasoning and Acting in Language Models
- OpenAI Java SDK :https://github.com/openai/openai-java
- 阿里云 DashScope :https://dashscope.aliyuncs.com/
