Java实现ReAct智能体:从设计模式到工程实践
1. 项目概述当Java遇上AgentScope的ReAct智能体最近在智能体开发领域AgentScope这个框架的热度是越来越高。作为一个旨在简化多智能体应用开发的平台它最近推出的2.0版本更是带来了不少新特性。我看到很多朋友在搜索“AgentScope Java”、“ReActAgent 代码实现”这些关键词说明大家对这个结合点很感兴趣。今天我就以一个Java后端开发者的视角来拆解一下如何在Java环境中理解和实现一个类似AgentScope中ReActAgent核心思想的智能体。这不仅仅是调用一个API而是深入到设计模式、流程控制和与LLM交互的层面让你真正掌握其精髓并能应用到自己的项目中无论是构建一个智能客服助手、一个自动化数据分析工具还是一个复杂的决策系统。简单来说ReActReasoning Acting是一种让大语言模型LLM具备“思考-行动”循环能力的范式。模型不只是直接给出最终答案而是会先“推理”出下一步该做什么比如调用哪个工具、查询什么信息然后“执行”那个动作观察结果再基于结果进行下一轮推理如此循环直到解决问题。AgentScope框架原生支持Python但其设计思想是语言无关的。我们的目标就是用Java构建一个具备同样核心能力的智能体骨架。这对于那些核心业务栈是Java又想引入AI能力的企业或项目来说具有非常实际的参考价值。2. 核心架构与设计模式解析在动手写代码之前我们必须先理清思路。一个ReAct智能体不是一堆if-else的堆砌它需要一个清晰、可扩展的架构。从搜索热词如“java成熟分类”、“java接口自动化测试框架”可以看出Java开发者对良好的结构和设计模式有很高的要求。我们的实现也将遵循这一原则。2.1 ReAct范式的工作流分解一个标准的ReAct循环可以分解为以下几个核心步骤这构成了我们代码的主干逻辑任务解析与初始化接收用户查询Query初始化上下文Context。这个上下文将贯穿整个循环记录历史对话、工具调用结果和智能体的内部思考。推理Reason这是核心。智能体基于当前上下文分析现状决定下一步要做什么。输出通常是一个结构化的“思考”文本以及一个明确的“动作”指令例如我需要查询天气应该调用get_weather工具参数是{city: 北京}。行动Act根据推理出的动作指令找到并执行对应的工具Tool。工具可以是任何东西调用一个外部API、执行一段数据库查询、运行一个计算函数甚至是让智能体暂停等待人工输入。观察Observe获取工具执行后的结果。这个结果可能成功也可能失败例如API返回错误、数据库无记录。结果整合与循环判断将观察到的结果整合到上下文中。然后判断问题是否已经解决如果解决了则生成最终答案Final Answer并结束循环如果没解决或者出现了新问题则回到第2步“推理”开始下一轮循环。这个循环可能因为工具调用失败、信息不足或达到最大循环次数而终止。我们的Java代码需要优雅地处理这些边界情况。2.2 关键组件与接口设计基于上述工作流我们可以抽象出几个核心的Java接口这是实现高内聚、低耦合系统的关键。1.Agent接口这是智能体的统一门面。它定义了一个process方法接收用户输入返回最终输出。ReActAgent将是它的一个具体实现。public interface Agent { String process(String userInput); }2.Tool接口代表智能体可以调用的工具。每个工具必须有唯一的名称、清晰的描述用于让LLM理解它的用途以及一个执行方法。public interface Tool { String getName(); String getDescription(); String execute(String arguments) throws ToolExecutionException; // arguments 通常是JSON字符串 }例如一个计算器工具CalculatorTool其getDescription()可以是“用于执行基础数学运算如加()、减(-)、乘(*)、除(/)”。LLM会根据这个描述来决定是否以及如何调用它。3.LLMService接口封装与大语言模型的交互。这层抽象至关重要它让我们可以灵活切换不同的LLM提供商如OpenAI、通义千问、本地部署的模型而无需修改核心的ReAct逻辑。public interface LLMService { String generateResponse(String prompt); // 更高级的可以支持消息列表、温度等参数 // ListChatMessage generateResponse(ListChatMessage messages, double temperature); }4.ReActEngine核心引擎这是协调整个循环的“大脑”。它持有LLMService的引用、一个Tool的注册表MapString, Tool并控制着“推理-行动-观察”的循环流程。它负责构建给LLM的提示词Prompt解析LLM的回复调用工具并管理循环状态如当前上下文、已执行步骤、最大步数限制。注意提示词Prompt工程是成败关键。给LLM的提示词必须清晰定义输出格式。例如我们可以要求LLM严格按以下格式回复思考这里写下你的推理过程 动作工具名称 动作输入JSON格式的参数或者当任务完成时思考最终推理 最终答案给用户的答案在Java中我们需要编写稳健的解析器来从LLM的非结构化文本中提取出“动作”和“动作输入”这些结构化信息。这里正则表达式或简单的字符串分割会很有用但也要做好LLM不按格式输出的错误处理。3. 核心代码实现与分步讲解理论讲完了我们进入实战环节。我会分模块展示核心代码并解释每一部分的设计考量。假设我们正在构建一个能查询天气和进行简单计算的智能体。3.1 工具Tool的实现与注册首先我们实现两个简单的工具。工具的实现要简单、健壮做好参数验证和异常处理。WeatherTool.javaimport com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class WeatherTool implements Tool { private static final String NAME get_weather; private static final String DESCRIPTION 获取指定城市的当前天气情况。需要参数{\city\: \城市名\}; private final HttpClient httpClient; private final ObjectMapper objectMapper; // 假设我们使用一个模拟天气API private static final String API_URL https://api.weatherapi.mock/v1/current.json?keydemoq%s; public WeatherTool() { this.httpClient HttpClient.newHttpClient(); this.objectMapper new ObjectMapper(); } Override public String getName() { return NAME; } Override public String getDescription() { return DESCRIPTION; } Override public String execute(String arguments) throws ToolExecutionException { try { JsonNode params objectMapper.readTree(arguments); String city params.get(city).asText(); if (city null || city.trim().isEmpty()) { throw new ToolExecutionException(参数 city 不能为空); } // 构建请求实际项目请替换为真实的API和鉴权 String url String.format(API_URL, city); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(url)) .GET() .build(); HttpResponseString response httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() ! 200) { throw new ToolExecutionException(天气API请求失败状态码 response.statusCode()); } // 简化处理直接返回响应体。实际应解析JSON提取所需信息。 return response.body(); } catch (Exception e) { throw new ToolExecutionException(执行天气查询工具失败: e.getMessage(), e); } } }CalculatorTool.javaimport com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; public class CalculatorTool implements Tool { private static final String NAME calculator; private static final String DESCRIPTION 执行数学表达式计算。需要参数{\expression\: \数学表达式如 35*2\}; private final ObjectMapper objectMapper new ObjectMapper(); private final ScriptEngine engine; public CalculatorTool() { ScriptEngineManager mgr new ScriptEngineManager(); this.engine mgr.getEngineByName(JavaScript); // 用于简单计算 } Override public String getName() { return NAME; } Override public String getDescription() { return DESCRIPTION; } Override public String execute(String arguments) throws ToolExecutionException { try { JsonNode params objectMapper.readTree(arguments); String expr params.get(expression).asText(); if (expr null || expr.trim().isEmpty()) { throw new ToolExecutionException(参数 expression 不能为空); } // 安全警告在生产环境中直接使用ScriptEngine执行用户提供的表达式极其危险 // 这里仅为演示。实际应使用安全的数学表达式解析库如 exp4j。 Object result engine.eval(expr); return String.format(表达式 %s 的计算结果是: %s, expr, result.toString()); } catch (ScriptException e) { throw new ToolExecutionException(计算表达式失败请检查格式: e.getMessage()); } catch (Exception e) { throw new ToolExecutionException(执行计算工具失败: e.getMessage(), e); } } }实操心得工具设计的两个关键点。描述Description要精准这是LLM理解工具功能的唯一依据。描述应简洁说明功能、输入格式和输出预期。像“需要参数{city: 城市名}”这样的提示能极大提高LLM调用工具的准确性。异常处理要友好工具执行可能失败网络、参数错误等。抛出的ToolExecutionException应包含足够的信息以便ReActEngine能将其作为“观察”结果反馈给LLM让LLM知道“行动”失败了并可能触发新一轮“推理”来纠正。工具实现后需要在引擎启动时进行注册public class ToolRegistry { private MapString, Tool toolMap new HashMap(); public void registerTool(Tool tool) { toolMap.put(tool.getName(), tool); } public Tool getTool(String name) { return toolMap.get(name); } public String getToolsDescription() { // 生成供LLM参考的工具列表描述 StringBuilder sb new StringBuilder(); for (Tool tool : toolMap.values()) { sb.append(- ).append(tool.getName()).append(: ).append(tool.getDescription()).append(\n); } return sb.toString(); } }3.2 LLM服务层LLMService的抽象与实现为了不绑定特定厂商我们实现一个基于OpenAI API的示例但保留切换能力。OpenAIService.javaimport java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; public class OpenAIService implements LLMService { private final String apiKey; private final String model; private final HttpClient httpClient; private final ObjectMapper objectMapper; private static final String API_ENDPOINT https://api.openai.com/v1/chat/completions; public OpenAIService(String apiKey, String model) { this.apiKey apiKey; this.model model; this.httpClient HttpClient.newHttpClient(); this.objectMapper new ObjectMapper(); } Override public String generateResponse(String prompt) { try { ObjectNode requestBody objectMapper.createObjectNode(); requestBody.put(model, this.model); requestBody.putArray(messages).addObject() .put(role, user) .put(content, prompt); requestBody.put(temperature, 0.1); // 低温度让输出更确定更遵循格式 String requestBodyString objectMapper.writeValueAsString(requestBody); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(API_ENDPOINT)) .header(Content-Type, application/json) .header(Authorization, Bearer apiKey) .POST(HttpRequest.BodyPublishers.ofString(requestBodyString)) .build(); HttpResponseString response httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() ! 200) { throw new RuntimeException(LLM API调用失败: response.body()); } JsonNode rootNode objectMapper.readTree(response.body()); return rootNode.path(choices).get(0).path(message).path(content).asText(); } catch (Exception e) { throw new RuntimeException(生成LLM响应时出错, e); } } }如果你后续想切换为通过HTTP调用本地部署的模型比如一些搜索热词中提到的特定模型只需创建另一个LLMService实现类修改请求的URL和报文格式即可核心的ReActEngine代码完全不用动。3.3 ReActEngine循环控制的核心这是最复杂也最核心的部分。ReActEngine需要维护对话状态并驱动循环。ReActEngine.java(核心片段)public class ReActEngine { private final LLMService llmService; private final ToolRegistry toolRegistry; private final int maxSteps; private final ObjectMapper objectMapper new ObjectMapper(); public ReActEngine(LLMService llmService, ToolRegistry toolRegistry, int maxSteps) { this.llmService llmService; this.toolRegistry toolRegistry; this.maxSteps maxSteps; } public String run(String userQuery) { StringBuilder context new StringBuilder(); context.append(用户问题).append(userQuery).append(\n\n); String toolsDescription toolRegistry.getToolsDescription(); for (int step 1; step maxSteps; step) { // 1. 构建本轮Prompt String prompt buildPrompt(userQuery, context.toString(), toolsDescription, step); System.out.println( Step step 推理 Prompt ); System.out.println(prompt); // 2. 调用LLM进行推理 String llmResponse llmService.generateResponse(prompt); System.out.println( LLM 响应 ); System.out.println(llmResponse); // 3. 解析LLM响应 ParsedResponse parsed parseLlmResponse(llmResponse); context.append(步骤).append(step).append( - 思考).append(parsed.thought).append(\n); // 4. 判断是否为最终答案 if (parsed.finalAnswer ! null) { context.append(最终答案).append(parsed.finalAnswer); System.out.println(任务完成); return parsed.finalAnswer; } // 5. 执行动作 if (parsed.action ! null parsed.actionInput ! null) { Tool tool toolRegistry.getTool(parsed.action); if (tool null) { String error 错误未知工具 parsed.action 。; context.append(error).append(\n); continue; // 工具不存在将错误信息加入上下文进入下一轮 } try { String observation tool.execute(parsed.actionInput); context.append(动作).append(parsed.action) .append( 输入).append(parsed.actionInput) .append(\n观察结果).append(observation).append(\n\n); } catch (ToolExecutionException e) { context.append(动作).append(parsed.action) .append( 输入).append(parsed.actionInput) .append(\n观察结果错误).append(e.getMessage()).append(\n\n); } } else { // LLM没有输出有效动作可能是格式错误将整个响应作为观察 context.append(警告LLM响应未解析出有效动作。原始响应).append(llmResponse).append(\n\n); } } return 达到最大步骤数( maxSteps )未能解决问题。最后上下文\n context; } private String buildPrompt(String query, String context, String toolsDesc, int step) { // 这是一个简化的Prompt模板。实际应用中需要精心设计。 return String.format( 你是一个ReAct智能体通过思考(Thought)、行动(Action)、观察(Observation)的循环来解决问题。\n 你可以使用以下工具\n%s\n 工具调用格式必须严格为\n Thought: 你的推理过程\n Action: 工具名\n Action Input: JSON格式的输入参数\n 或者当你认为可以给出最终答案时\n Thought: 你的最终推理\n Final Answer: 给用户的答案\n\n 当前是第%d步。已有的对话上下文\n%s\n 用户问题%s\n\n 请开始你的回应, toolsDesc, step, context, query ); } private ParsedResponse parseLlmResponse(String response) { ParsedResponse parsed new ParsedResponse(); String[] lines response.split(\n); for (String line : lines) { if (line.startsWith(Thought:)) { parsed.thought line.substring(Thought:.length()).trim(); } else if (line.startsWith(Action:)) { parsed.action line.substring(Action:.length()).trim(); } else if (line.startsWith(Action Input:)) { // 处理可能的多行JSON简化处理实际需更健壮 parsed.actionInput line.substring(Action Input:.length()).trim(); } else if (line.startsWith(Final Answer:)) { parsed.finalAnswer line.substring(Final Answer:.length()).trim(); } } return parsed; } // 内部类用于存储解析结果 private static class ParsedResponse { String thought; String action; String actionInput; String finalAnswer; } }3.4 组装与运行主程序入口最后我们把所有部件组装起来形成一个可运行的ReActAgent。ReActAgent.javapublic class ReActAgent implements Agent { private final ReActEngine engine; public ReActAgent(LLMService llmService, int maxSteps) { ToolRegistry registry new ToolRegistry(); registry.registerTool(new WeatherTool()); registry.registerTool(new CalculatorTool()); // 可以注册更多工具... this.engine new ReActEngine(llmService, registry, maxSteps); } Override public String process(String userInput) { return engine.run(userInput); } public static void main(String[] args) { // 1. 初始化LLM服务此处API Key需从安全配置读取 LLMService llm new OpenAIService(your-openai-api-key, gpt-3.5-turbo); // 2. 创建智能体设置最大步数为10 Agent agent new ReActAgent(llm, 10); // 3. 处理用户查询 String answer agent.process(请先计算一下(1527)*2等于多少然后告诉我北京现在的天气。); System.out.println(\n 智能体最终回复 ); System.out.println(answer); } }运行这个程序你将在控制台看到完整的ReAct思考链。对于上面的查询它可能会先调用计算器工具得到84然后将结果和“北京”作为上下文的一部分在下一轮推理中调用天气查询工具最后整合信息给出最终答案。4. 高级优化与生产级考量上面的代码是一个教学原型展示了核心思想。但要用于实际生产还需要在以下几个方面进行深度优化和加固。4.1 提示词Prompt工程的精细化原型的Prompt比较简单。一个健壮的ReAct智能体需要更精细的Prompt控制系统角色设定在消息列表开头设定一个清晰的system角色定义智能体的行为准则和输出格式要求。少样本示例Few-Shot在Prompt中提供1-2个完整的ReAct循环示例能极大提高LLM遵循格式的能力。错误恢复指令明确告诉LLM如果工具调用失败或返回意外结果它应该如何处理例如“如果工具返回错误分析错误原因并尝试另一种方法”。上下文长度管理随着循环进行上下文会越来越长。需要设计策略来裁剪或总结过长的历史防止超出LLM的令牌限制。一种常见方法是只保留最近N轮交互和关键的初始信息。4.2 状态管理、持久化与异步处理会话状态在Web应用中每个用户的对话是一个独立会话。需要为每个会话维护独立的ReActEngine实例或上下文状态。这通常涉及会话ID和某种形式的状态存储如Redis、数据库。异步与非阻塞LLM调用和工具调用尤其是网络IO可能是耗时的。在生产环境的Web服务中绝不能阻塞HTTP请求线程。必须将agent.process()设计为异步方法返回CompletableFutureString或使用反应式编程模型如Project Reactor。流式输出为了更好的用户体验可以考虑支持流式输出Streaming即边思考边输出而不是等待整个循环结束。这需要LLM服务和支持流式传输的协议如SSE、WebSocket。4.3 工具生态的扩展与管理动态工具注册我们的ToolRegistry是启动时静态注册的。更高级的系统可以支持动态注册和卸载工具无需重启服务。工具发现与描述自动化对于大型工具库可以要求每个工具提供更结构化的描述如OpenAPI Schema并自动生成给LLM的说明。工具权限与安全不是所有工具对所有用户或所有问题都可用。需要引入工具调用权限控制在ToolRegistry.getTool()或工具execute()方法中进行校验。4.4 可观测性与调试支持这是开发复杂智能体应用时最容易忽视但至关重要的一环。结构化日志不要只用System.out.println。集成SLF4J等日志框架为每一步推理、每一次工具调用、每一次观察记录结构化的日志JSON格式最佳并包含会话ID、步骤号等信息。这便于后续用ELK等工具进行分析。链路追踪集成OpenTelemetry等追踪系统为一次用户查询的完整ReAct链路生成追踪ID可视化每个环节的耗时和状态快速定位性能瓶颈或错误点。交互历史存储将完整的“思考-行动-观察”链持久化到数据库。这不仅是审计的需要更是后续进行效果分析、Prompt优化和模型微调的宝贵数据来源。5. 常见问题、排查技巧与性能调优在实际编码和调试过程中你肯定会遇到各种问题。这里我总结了一些典型场景和解决思路。5.1 LLM不按格式输出怎么办这是初期最常见的问题。我们的解析器parseLlmResponse依赖于严格的格式。症状解析失败action或finalAnswer为null导致循环逻辑出错。排查首先打印出LLM的原始响应检查是否包含“Thought:”、“Action:”等关键词。很可能LLM在自由发挥。解决强化Prompt在Prompt中使用更强烈的语气如“你必须严格按照以下格式回复不要添加任何额外解释。”并给出更清晰的少样本示例。降低温度Temperature像我们在OpenAIService中设置temperature0.1让模型输出更确定、更可预测。使用函数调用Function Calling如果使用的LLM如GPT-4支持函数调用强烈建议改用此功能。它本质上是让LLM输出一个结构化的JSON来调用“函数”即我们的工具格式问题由API底层保障比文本解析稳定得多。这是生产环境的推荐做法。后处理与重试在解析器中增加容错逻辑。如果第一次解析失败可以尝试用正则表达式匹配或者将解析失败的响应连同错误信息一起作为新的“观察”输入给LLM要求它纠正格式。但要注意控制重试次数避免死循环。5.2 智能体陷入死循环或无效循环症状智能体反复调用同一个工具或者在不同工具间来回切换始终得不出最终答案直到达到最大步数。原因工具能力不足现有工具无法解决当前问题LLM在“穷举”所有可能。上下文信息丢失或混乱过长的上下文导致关键信息被淹没或者错误的观察结果污染了上下文。Prompt引导不足没有明确告诉LLM“何时停止”。解决设置合理的最大步数这是最后的防线防止资源耗尽。在Prompt中明确终止条件例如“如果你已经获得了足够的信息来直接回答用户问题或者所有可用工具都无法推进问题解决请直接给出最终答案。”优化上下文管理实现上文提到的上下文总结或裁剪策略确保LLM看到的是最相关、最简洁的历史。引入“最终答案”工具可以设计一个虚拟的final_answer工具当LLM调用它时引擎将其参数直接作为最终答案返回并结束循环。这有时比依赖文本解析更可靠。5.3 工具调用失败或超时症状ToolExecutionException被抛出观察结果是错误信息。排查网络问题检查工具依赖的外部API是否可达、网络策略是否正确。参数错误检查LLM生成的actionInputJSON格式是否正确参数名和类型是否符合工具预期。可以在工具execute方法入口打印日志。权限/配额问题检查API密钥是否有效、配额是否用完。解决工具层做好防御如WeatherTool所示进行参数校验和友好的错误封装。引擎层优雅处理如ReActEngine所示将错误信息作为“观察”反馈给LLM让它有机会自我纠正例如“查询天气API失败错误原因是城市参数为空。请确认用户问题中是否包含了城市信息。”。5.4 性能瓶颈分析与优化当智能体处理变慢时需要定位瓶颈。测量各部分耗时在ReActEngine的run方法中以及LLMService.generateResponse和每个Tool.execute方法中加入耗时统计。常见瓶颈点LLM API调用延迟这是主要瓶颈。考虑使用更高性能的模型如GPT-4 Turbo、设置合理的超时、或使用本地部署的轻量级模型处理简单推理。同步阻塞调用如前所述改为异步非阻塞架构。工具响应慢优化工具自身逻辑或为其设置独立的超时和熔断机制。上下文过长导致Prompt构建和LLM处理都变慢。必须实施上下文裁剪策略。通过实现这样一个Java版的ReActAgent核心框架你不仅深入理解了AgentScope这类框架背后的思想更获得了一套可以在自己Java项目中复用的、可扩展的智能体基础设施。从简单的工具调用开始逐步加入状态管理、异步流式输出、复杂的工具编排你就能构建出越来越强大的智能体应用。记住关键在于清晰的架构设计、稳健的错误处理以及持续迭代的Prompt工程。