
1. 项目概述当Java遇上AgentScope的ReAct智能体最近在智能体开发领域AgentScope这个框架的热度是越来越高了。作为一个专注于多智能体应用开发的平台它让构建复杂的协作式AI应用变得前所未有的简单。而今天我想和大家深入聊聊的是其中一个非常核心且强大的设计模式——ReActReasoning and Acting智能体以及如何用我们最熟悉的Java语言来实现它。你可能已经看过很多用Python演示的ReAct Agent比如在LangChain里几行代码就能跑起来。但现实是很多成熟的企业级系统、高并发后台服务其技术栈的基石依然是Java。把前沿的AI智能体模式用Java这套久经考验的工业级语言实现出来不仅意味着更好的性能、更易维护的工程结构也代表着AI能力能更平滑、更可靠地集成到现有的生产环境中。这不仅仅是“翻译”代码更是对设计思想的一次深度落地和工程化实践。所以这篇内容我会带你从零开始拆解一个Java版本的ReActAgent。我们会抛开那些笼统的概念直接深入到代码层面看看一个能“思考-行动”的智能体其内部的状态机如何流转工具如何被调用记忆如何被管理以及如何优雅地处理各种边界情况。无论你是对AgentScope框架感兴趣还是想在自己的Java项目中引入ReAct模式我相信这些从一线实践中总结出来的代码和思路都能给你带来直接的参考价值。2. ReAct模式核心思想与Java实现的架构设计2.1 理解ReAct不只是链式调用而是有状态的推理循环在动手写代码之前我们必须先吃透ReAct到底在做什么。它的全称是“Reasoning and Acting”中文可以理解为“推理-行动”。这个模式的核心思想是模仿人类解决问题的方式先观察、思考Reasoning然后根据思考结果采取行动Acting再根据行动的结果进行新一轮的观察和思考如此循环直至问题解决。这听起来有点像简单的“if-else”循环但其精妙之处在于“推理”部分。智能体在每一步的“思考”中并不是随机猜测而是基于当前所有的观察包括初始问题、历史对话、工具执行结果等生成一段自然语言格式的推理过程。这段过程会明确分析现状、提出假设、并规划下一步行动。然后再从这个推理文本中解析出要执行的具体“动作”通常是调用某个工具并传入参数。所以一个典型的ReAct循环步骤是观察Observe获取当前状态包括用户输入和上一步工具执行的结果。思考Think基于所有观察让大语言模型LLM生成一段包含推理和下一步行动计划的文本。解析Parse从“思考”产生的文本中结构化地提取出要执行的action工具名和action_input工具参数。行动Act调用对应的工具并获取执行结果。更新观察将工具执行的结果作为新的“观察”进入下一轮循环。这个循环会一直持续直到LLM在“思考”步骤中明确输出代表任务结束的标记例如Final Answer:或者达到预设的最大迭代次数。在Java中实现这个模式我们不能把它写成一段简单的线性代码。它必须是一个有状态的、可管理的、可监控的状态机。这也是我们设计ReActAgent类的出发点。2.2 Java版ReActAgent的类结构设计基于上述理解我们可以勾勒出核心类的骨架。一个健壮的ReActAgent需要包含以下几个关键部分// 引入必要的包这里以常见的工具库为例 import java.util.*; import java.util.concurrent.*; public class ReActAgent { // 1. 核心依赖与大模型交互的客户端 private final LLMClient llmClient; // 2. 工具集智能体可以调用的所有能力 private final MapString, Tool tools; // 3. 记忆体保存对话历史和工具执行轨迹 private final ListMessage memory; // 4. 配置参数最大步数、推理模板等 private final int maxSteps; private final String promptTemplate; // 5. 当前执行状态 private String currentObservation; private int currentStep; // 构造函数 public ReActAgent(LLMClient llmClient, MapString, Tool tools, int maxSteps) { this.llmClient Objects.requireNonNull(llmClient, LLMClient must not be null); this.tools new HashMap(tools); this.memory new ArrayList(); this.maxSteps maxSteps; this.promptTemplate buildDefaultPromptTemplate(); this.currentStep 0; this.currentObservation ; } // 核心执行方法 public String run(String userInput) { // 初始化观察 this.currentObservation User: userInput; this.memory.add(new Message(user, userInput)); this.currentStep 0; // ReAct 主循环 while (currentStep maxSteps) { // 步骤1思考 (Think) String thought think(); // 步骤2解析思考获取行动指令 (Parse) Action action parseAction(thought); // 步骤3判断是否为最终答案 if (action.isFinalAnswer()) { return action.getAnswer(); } // 步骤4执行行动 (Act) String result act(action); // 步骤5更新观察进入下一轮 updateObservation(result); currentStep; } return 已达到最大执行步数( maxSteps )未能得出最终结论。; } // 其他私有方法think(), parseAction(), act(), updateObservation() 等将在下文详细实现 }这个设计有几个关键考量依赖注入LLMClient和Tool集合通过构造函数注入保证了类的可测试性和灵活性。你可以轻松替换不同的模型后端如OpenAI、通义千问、本地部署模型或工具集。状态封装将循环状态当前观察、当前步数和持久状态记忆封装在对象内部每次run方法调用都是一次独立的执行会话。清晰的流程run方法中的while循环清晰地对应了ReAct的步骤逻辑一目了然。注意这里的LLMClient和Tool是我们定义的接口Message和Action是简单的数据类Record。这样做是为了解耦让你可以根据自己的项目情况实现具体的HTTP客户端、工具逻辑和数据结构。3. 核心模块的代码实现与详解3.1 思考Think模块Prompt工程与LLM调用思考模块是整个智能体的“大脑”它的质量直接决定了智能体是否“聪明”。这里的关键在于构造一个能引导LLM进行有效推理的Prompt。private String think() { // 1. 构建完整的Prompt String prompt buildPrompt(); // 2. 调用LLM LLMResponse response llmClient.complete(prompt); // 3. 记录到记忆 memory.add(new Message(assistant, response.getContent())); return response.getContent(); } private String buildPrompt() { StringBuilder sb new StringBuilder(); // 第一部分系统指令定义角色和流程 sb.append(你是一个善于思考并解决问题的助手。请遵循以下步骤\n); sb.append(1. 观察基于之前的对话和工具返回的结果。\n); sb.append(2. 思考分析当前情况推理下一步该做什么。\n); sb.append(3. 行动如果需要使用工具请严格按照格式输出\n); sb.append( Action: 工具名称\n); sb.append( Action Input: 工具的输入参数JSON格式\n); sb.append(4. 如果问题已解决请输出\n); sb.append( Final Answer: 你的最终答案\n\n); // 第二部分可用工具描述 sb.append(你可以使用的工具有\n); for (Map.EntryString, Tool entry : tools.entrySet()) { sb.append(- ).append(entry.getKey()) .append(: ).append(entry.getValue().getDescription()).append(\n); } sb.append(\n); // 第三部分对话和工具执行历史记忆 sb.append(历史记录\n); for (Message msg : memory) { sb.append(msg.getRole()).append(: ).append(msg.getContent()).append(\n); } sb.append(\n); // 第四部分当前观察 sb.append(当前观察).append(currentObservation).append(\n\n); // 第五部分引导词 sb.append(现在请开始你的思考过程\nThought: ); return sb.toString(); }实操要点与避坑指南格式的强制性Prompt中必须明确、严格地规定输出格式如Action:和Final Answer:。LLM的“对齐”能力很强清晰的格式指令能极大提高输出解析的成功率。我通常会加粗或使用特殊符号强调格式。工具描述的清晰度工具描述不能只写名字必须包含其功能、输入参数的格式和示例、输出是什么。例如search_web: 用于搜索网络信息。输入应为包含‘query’键的JSON对象如 {\query\: \Java最新特性\}。返回搜索结果的摘要。历史记录的裁剪在实际应用中记忆memory可能会很长需要做裁剪或总结以避免超出模型的上下文长度限制。一个简单的策略是只保留最近N轮交互或者用一个单独的“总结智能体”来压缩历史。LLM调用的稳定性生产环境中llmClient.complete()必须包含重试、超时、熔断等机制。网络波动或模型服务暂时不可用是常态不能因为一次调用失败就导致整个智能体崩溃。3.2 解析Parse模块从自由文本到结构化指令LLM返回的是一段自由文本我们需要从中精准地提取出结构化指令。这里推荐使用正则表达式它比简单的字符串查找更健壮能处理一些格式上的微小变异。private Action parseAction(String thought) { // 先检查是否是最终答案 Pattern finalAnswerPattern Pattern.compile(Final Answer:\\s*(.*?)(?\\n\\n|\\nAction:|$), Pattern.DOTALL); Matcher finalMatcher finalAnswerPattern.matcher(thought); if (finalMatcher.find()) { String answer finalMatcher.group(1).trim(); return Action.finalAnswer(answer); // 返回一个标记为最终答案的Action对象 } // 解析工具调用指令 Pattern actionPattern Pattern.compile(Action:\\s*(\\w)); Pattern inputPattern Pattern.compile(Action Input:\\s*(\\{.*?\\}), Pattern.DOTALL); Matcher actionMatcher actionPattern.matcher(thought); Matcher inputMatcher inputPattern.matcher(thought); if (actionMatcher.find() inputMatcher.find()) { String toolName actionMatcher.group(1).trim(); String inputJson inputMatcher.group(1).trim(); try { // 使用如Jackson、Gson等库解析JSON ObjectMapper mapper new ObjectMapper(); MapString, Object params mapper.readValue(inputJson, new TypeReferenceMapString, Object() {}); return Action.toolAction(toolName, params); } catch (JsonProcessingException e) { // 如果JSON解析失败将错误信息作为观察让LLM在下轮修正 return Action.finalAnswer(解析Action Input时出错输入不是有效的JSON格式。请重新思考并确保Action Input是合法的JSON。); } } // 如果既不是最终答案也没找到有效指令则视为需要继续思考 return Action.finalAnswer(未能从你的回复中识别出有效的‘Action’或‘Final Answer’格式。请严格按照要求的格式回复。); }注意事项正则的贪婪与非贪婪在匹配Action Input的JSON时我们使用了\\{.*?\\}非贪婪模式这可以防止匹配到多个JSON块或文本末尾。Pattern.DOTALL标志让.也能匹配换行符因为JSON可能跨行。健壮的JSON解析LLM生成的JSON有时会有格式问题如尾随逗号、注释。使用严格的解析器如Jackson会直接抛异常。这里我们选择捕获异常并将错误信息反馈给LLM让它自我修正这比直接让智能体崩溃更友好。Action数据类设计Action类最好设计成不可变的使用Record并包含一个类型字段来区分是工具调用还是最终答案。例如public record Action(ActionType type, String toolName, MapString, Object input, String finalAnswer) { public static Action toolAction(String name, MapString, Object input) { return new Action(ActionType.TOOL, name, input, null); } public static Action finalAnswer(String answer) { return new Action(ActionType.FINAL, null, null, answer); } public boolean isFinalAnswer() { return type ActionType.FINAL; } } enum ActionType { TOOL, FINAL }3.3 行动Act模块工具执行与结果处理行动模块是智能体与外部世界交互的“手”。它的职责是安全、高效地执行工具调用。private String act(Action action) { if (action.type() ! ActionType.TOOL) { return 内部错误尝试执行一个非工具类型的Action。; } String toolName action.toolName(); MapString, Object input action.input(); Tool tool tools.get(toolName); if (tool null) { String availableTools String.join(, , tools.keySet()); return String.format(错误工具‘%s’不存在。可用工具有[%s]。, toolName, availableTools); } try { // 执行工具并设置超时防止工具卡死 CompletableFutureString future CompletableFuture.supplyAsync(() - tool.execute(input)); String result future.get(30, TimeUnit.SECONDS); // 设置30秒超时 // 记录工具调用和结果到记忆 memory.add(new Message(tool_call, String.format(Called %s with input: %s, toolName, input))); memory.add(new Message(tool_result, result)); return result; } catch (TimeoutException e) { return String.format(错误工具‘%s’执行超时30秒。, toolName); } catch (InterruptedException | ExecutionException e) { return String.format(错误工具‘%s’执行失败。原因%s, toolName, e.getCause() ! null ? e.getCause().getMessage() : e.getMessage()); } } private void updateObservation(String result) { this.currentObservation Tool Result: result; }核心经验与技巧工具接口设计Tool接口应该非常简单例如只有一个execute(MapString, Object input)方法。具体的工具实现如搜索、计算、查询数据库再去实现这个接口。这符合“依赖倒置”原则。超时控制是必须的任何外部调用网络IO、复杂计算都必须设置超时。这里用CompletableFuture.get(timeout)是一种方式。在生产环境中你可能需要更复杂的线程池和断路器模式。结果格式化工具返回的结果应该是简洁、信息丰富且格式化的文本。避免返回原始的、冗长的JSON或HTML。最好在工具内部就做好结果的处理和摘要方便LLM在下轮思考时理解。副作用与安全性对于会修改数据的工具如写入数据库、发送邮件必须进行严格的权限校验和参数验证。智能体不应该拥有不受限制的“写”权限。可以在Tool.execute方法内部或通过一个代理层来实现。4. 工程化进阶让Java ReActAgent更健壮、更易用一个能跑通的Demo和一個能在生产环境使用的组件之间隔着许多工程细节。下面我们来完善它。4.1 配置化与可观测性硬编码的Prompt模板和参数不利于维护。我们可以引入一个AgentConfig类通过配置文件或环境变量来管理。ConfigurationProperties(prefix agent.react) // 如果你用Spring Boot public class ReActAgentConfig { private int maxSteps 10; private String systemPrompt; private long toolTimeoutSeconds 30; private boolean enableMemorySummary false; private int maxMemoryLength 20; // ... getters and setters }同时可观测性Observability对于调试和监控AI应用至关重要。我们需要在关键节点埋点。import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ReActAgent { private static final Logger logger LoggerFactory.getLogger(ReActAgent.class); private final MeterRegistry meterRegistry; // 假设使用Micrometer private String think() { long start System.currentTimeMillis(); String prompt buildPrompt(); logger.debug(Generated prompt for step {}: \n{}, currentStep, prompt); LLMResponse response llmClient.complete(prompt); long duration System.currentTimeMillis() - start; // 记录指标 meterRegistry.timer(agent.think.time).record(duration, TimeUnit.MILLISECONDS); meterRegistry.counter(agent.think.calls).increment(); logger.info(Step {} Think completed in {}ms, currentStep, duration); memory.add(new Message(assistant, response.getContent())); return response.getContent(); } private String act(Action action) { // ... 工具执行 ... meterRegistry.counter(agent.tool.calls, tool, toolName).increment(); if (!success) { meterRegistry.counter(agent.tool.errors, tool, toolName).increment(); } // ... } }记录日志和指标可以帮助你分析每一步的耗时、统计工具调用成功率、在出错时快速定位是Prompt问题、工具问题还是模型问题。4.2 记忆管理与上下文优化随着对话轮数增加记忆会越来越长最终会突破LLM的上下文窗口限制。我们需要一个记忆管理策略。滑动窗口只保留最近N条消息。简单有效但可能丢失关键的长程依赖信息。总结性记忆这是更高级的策略。当记忆达到一定长度时触发一个“总结智能体”让它用一段话总结之前的对话历史和工具执行结果然后用这个总结替换掉旧的历史记录。private void manageMemory() { if (memory.size() config.getMaxMemoryLength()) { if (config.isEnableMemorySummary()) { summarizeMemory(); } else { // 简单裁剪保留最近的系统消息、用户消息和助理消息对 int toRemove memory.size() - config.getMaxMemoryLength(); // 实现一个更智能的裁剪逻辑避免剪掉关键的工具结果 memory.subList(0, toRemove).clear(); } } } private void summarizeMemory() { // 构建一个请求让LLM总结之前的对话 String summaryPrompt 请将以下对话历史简要总结成一段话保留关键的事实、决策和结果\n getRecentMemoryText(); String summary llmClient.complete(summaryPrompt).getContent(); // 用一条新的系统消息存储总结并清除大部分旧记忆 Message summaryMsg new Message(system, 对话历史总结 summary); // 清空旧记忆但保留最近一两轮和总结 memory.clear(); memory.add(summaryMsg); // 可以选择性地再保留最近一轮完整交互 }4.3 工具的动态注册与发现在大型应用中工具可能由不同的团队或模块开发。我们可以设计一个工具注册中心。public class ToolRegistry { private final ConcurrentHashMapString, Tool toolMap new ConcurrentHashMap(); public void register(String name, Tool tool) { toolMap.put(name, tool); } public void registerAll(MapString, Tool tools) { toolMap.putAll(tools); } public OptionalTool getTool(String name) { return Optional.ofNullable(toolMap.get(name)); } public MapString, String getToolDescriptions() { return toolMap.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, e - e.getValue().getDescription())); } } // 在Agent中注入ToolRegistry public ReActAgent(LLMClient llmClient, ToolRegistry registry, ReActAgentConfig config) { this.llmClient llmClient; this.toolRegistry registry; this.config config; // ... } private String buildPrompt() { // ... sb.append(你可以使用的工具有\n); toolRegistry.getToolDescriptions().forEach((name, desc) - { sb.append(- ).append(name).append(: ).append(desc).append(\n); }); // ... }这样新的工具可以通过Spring的PostConstruct、监听应用启动事件等方式动态注册进来Agent无需重启即可感知新能力。5. 实战构建一个简单的问答智能体并排查问题让我们用一个具体的例子把上面的代码串起来。假设我们要构建一个能回答“今天天气如何”和进行简单计算的智能体。第一步定义工具Component // 假设使用Spring管理Bean public class WeatherTool implements Tool { Override public String getDescription() { return 获取指定城市的当前天气。输入应为包含‘city’键的JSON对象如 {\city\: \北京\}。返回天气概况。; } Override public String execute(MapString, Object input) { String city (String) input.get(city); if (city null) { return 错误缺少‘city’参数。; } // 这里模拟一个API调用 return String.format(%s的天气是晴温度22-28°C微风。, city); } } Component public class CalculatorTool implements Tool { Override public String getDescription() { return 执行数学计算。输入应为包含‘expression’键的JSON对象如 {\expression\: \3 5 * 2\}。支持加减乘除和括号。返回计算结果。; } Override public String execute(MapString, Object input) { String expr (String) input.get(expression); // 警告实际项目中切勿直接用ScriptEngine等执行未经净化的用户输入此处仅为演示。 try { ScriptEngineManager mgr new ScriptEngineManager(); ScriptEngine engine mgr.getEngineByName(JavaScript); Object result engine.eval(expr); return 计算结果: result.toString(); } catch (ScriptException e) { return 计算错误: e.getMessage(); } } }第二步组装并运行AgentSpringBootApplication public class ReActDemoApplication implements CommandLineRunner { Autowired private LLMClient llmClient; // 假设已配置好 Autowired private WeatherTool weatherTool; Autowired private CalculatorTool calculatorTool; public static void main(String[] args) { SpringApplication.run(ReActDemoApplication.class, args); } Override public void run(String... args) { ToolRegistry registry new ToolRegistry(); registry.register(get_weather, weatherTool); registry.register(calculator, calculatorTool); ReActAgentConfig config new ReActAgentConfig(); config.setMaxSteps(5); ReActAgent agent new ReActAgent(llmClient, registry, config); String question1 上海今天天气怎么样; System.out.println(Q: question1); String answer1 agent.run(question1); System.out.println(A: answer1); System.out.println(-----); // 重置Agent状态或新建一个实例进行下一个问题 agent new ReActAgent(llmClient, registry, config); String question2 如果北京温度是25度上海比北京高3度那么上海温度是多少; System.out.println(Q: question2); String answer2 agent.run(question2); System.out.println(A: answer2); } }预期执行流程对于问题2LLM思考“用户想知道上海温度。已知北京25度上海高3度。这是一个计算问题我需要用计算器。先计算253。”输出Action: calculatorAction Input: {expression: 25 3}计算器返回“计算结果: 28”新观察“Tool Result: 计算结果: 28”LLM思考“计算得到上海是28度。这是最终答案。”输出Final Answer: 上海的温度是28度。5.1 常见问题排查与调试技巧在实际运行中你肯定会遇到各种问题。下面是一个快速排查指南问题现象可能原因排查步骤与解决方案LLM不输出Action格式Prompt指令不清晰或LLM没对齐。1.检查Prompt确保格式指令非常醒目如用###包裹。2.在思考步骤后加示例在Prompt末尾加一个完整的示例循环。3.调整温度temperature尝试调低温度如0.1以获得更确定性的输出。解析JSON失败LLM生成的JSON格式有误如缺少引号、尾随逗号。1.增强解析器使用JsonNode或更宽松的解析模式。2.在Prompt中强调“Action Input必须是严格、有效的JSON不能包含注释或尾随逗号。”3.让LLM自我修正像我们代码里做的那样将解析错误信息反馈给下一轮思考。智能体陷入死循环工具结果无法满足终止条件或LLM推理出现逻辑循环。1.设置最大步数这是最基本的保护。2.检查工具输出工具是否返回了错误或模糊信息导致LLM无法理解确保工具输出清晰。3.引入循环检测记录历史动作序列如果发现重复调用相同工具和参数则强制终止或返回错误。工具执行超时或出错工具依赖的外部服务不稳定或工具本身有Bug。1.查看日志在act方法中详细记录工具调用的开始、结束和结果。2.实现熔断机制如果某个工具连续失败暂时将其禁用。3.提供友好的错误反馈工具返回的错误信息应能帮助LLM理解问题如“网络超时请稍后再试”比“IOException”更好。上下文长度超限对话轮次太多记忆过长。1.实现记忆管理如上文所述采用滑动窗口或总结机制。2.选择更长上下文的模型。一个关键的调试技巧记录完整的思维链。不要只记录最终输入输出。在开发阶段把每一轮的Prompt、LLM回复Thought、解析出的Action、工具结果都打印或记录到日志文件中。这就像飞机的黑匣子能让你完整复盘智能体的“心路历程”是定位问题最有效的方法。最后我想分享一点个人体会。用Java实现ReAct Agent最大的挑战不是语法而是将非确定性的LLM输出与确定性的程序逻辑可靠地结合。这要求我们的代码必须有极高的鲁棒性和可观测性。每一个与LLM交互的边界都要做好“防御性编程”假设任何奇怪的输出都可能出现并为之设计降级或修正路径。当你看到自己编写的智能体能够像人类一样一步步推理、调用工具、最终解决问题时那种成就感是非常独特的。这不仅仅是完成了一个功能更像是赋予了一段代码自主思考和行动的能力。希望这篇详细的实现讲解能帮助你顺利跨出这一步。