1. 从 LangChain 到 AgentScope为什么我们需要一个 Java 版的 ReAct Agent如果你最近在关注大模型应用开发尤其是智能体Agent领域那么“ReAct”这个范式你一定不陌生。它由“推理Reasoning”和“行动Acting”两个词组合而成核心思想是让大模型在解决问题时像人一样先思考再行动通过迭代的“思考-行动-观察”循环来完成任务。LangChain 的 ReAct Agent 一度是这个领域的标杆实现很多开发者都是通过它第一次体验到了智能体的强大。然而当我们把目光投向企业级应用和大型后端系统时一个现实问题就摆在了面前这些系统的技术栈绝大多数是 Java。无论是 Spring Boot 生态的微服务还是历史悠久的传统单体应用Java 都是当之无愧的“王者”。用 Python 写的 LangChain Agent 虽然原型开发快但如何与这些 Java 后端深度集成如何保证在高并发、长事务场景下的稳定性和性能如何复用现有的 Java 工具链和监控体系这些问题让很多团队在落地时犯了难。这正是 AgentScope-Java 出现的背景。它不是一个简单的“翻译”版本而是一个为 Java 技术栈量身定制的智能体开发框架。今天我们就来深入它的核心拆解其ReActAgent的代码实现。通过这篇文章你将不仅理解一个 Java Agent 是如何“思考”和“行动”的更能掌握如何在自己的 Java 项目中构建一个稳定、可扩展、易于集成的智能体。这对于那些正在为“如何将大模型能力无缝融入现有 Java 系统”而苦恼的架构师和开发者来说无疑是一份及时的实战指南。2. ReActAgent 的核心架构一个标准执行循环的 Java 实现在深入代码之前我们先从顶层视角理解ReActAgent的设计。一个标准的 ReAct 循环可以抽象为以下几个步骤规划Plan根据当前任务和目标决定下一步要做什么思考。行动Act调用一个工具Tool来执行具体操作。观察Observe获取工具执行的结果。评估Evaluate判断任务是否完成。若未完成回到步骤1。AgentScope-Java 的ReActAgent类就是对这个抽象循环的一个健壮、面向对象的 Java 实现。它的核心依赖通常包括大模型客户端LLM Client负责与 OpenAI、通义千问、DeepSeek 等模型 API 交互完成“思考”部分。工具注册表Tool Registry管理所有可用的工具Tools例如搜索、计算、数据库查询等。记忆系统Memory存储对话历史、工具执行结果为后续的推理提供上下文。解析器Parser负责解析大模型返回的文本将其结构化识别出“思考”内容和要调用的“工具”指令。下面我们通过一个简化的类图来理解它们之间的关系------------------- uses ---------------------- | ReActAgent |-----------------| LLM Client | |-------------------| |----------------------| | - memory: Memory | | call(prompt): String| | - tools: ListTool| ---------------------- | - maxIterations: int| ^ | - parser: Parser | | ------------------- | | uses | returns v | ------------------- uses ---------------------- | Tool |-----------------| Parser | |-------------------| |----------------------| | execute(args): | | parse(llmOutput): | | Object | | ActionThought | ------------------- ----------------------ReActAgent持有对内存、工具列表和解析器的引用。它的主要工作流方法例如run或execute会循环执行构造提示词 - 调用 LLM - 解析输出 - 执行工具 - 更新记忆直到任务完成或达到最大迭代次数。3. 环境搭建与核心依赖避开 Java 项目集成的第一个坑在开始编码之前正确的项目配置是成功的一半。AgentScope-Java 通常以 Maven 依赖或 Gradle 依赖的形式提供。假设我们使用 Maven在pom.xml中添加依赖是最关键的一步。错误的做法新手常犯直接在搜索引擎里找一个agentscope-core的版本号就填进去结果发现类找不到或者版本冲突。正确的做法首先确认你访问的是 AgentScope 的官方仓库或 Maven Central。由于 AgentScope 相对较新你可能需要显式添加其仓库地址。其次要理解其模块化设计。一个完整的智能体应用可能依赖多个模块。project !-- 首先可能需要添加仓库 -- repositories repository idagentscope-maven-repo/id urlhttps://repo.agentscope.ai/repository/maven-public//url /repository /repositories dependencies !-- 核心运行时依赖 -- dependency groupIdai.agentscope/groupId artifactIdagentscope-core/artifactId version2.0.0/version !-- 请使用最新稳定版 -- /dependency !-- 如果你使用 OpenAI 的模型 -- dependency groupIdai.agentscope/groupId artifactIdagentscope-openai/artifactId version2.0.0/version /dependency !-- 可能需要的工具集成例如网络请求 -- dependency groupIdai.agentscope/groupId artifactIdagentscope-tool-http/artifactId version2.0.0/version /dependency !-- 序列化如JSON处理 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.0/version /dependency !-- 日志框架 -- dependency groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId version2.0.7/version /dependency dependency groupIdch.qos.logback/groupId artifactIdlogback-classic/artifactId version1.4.11/version /dependency /dependencies /project注意版本号务必核对官方文档。agentscope-openai这类适配器模块的版本需要与agentscope-core主版本对齐否则可能出现不兼容的 API 调用。另外关于 JSON 处理虽然热词中提到了fastjson2.0和jackson但在与大多数开源生态包括 Spring Boot集成时强烈推荐使用 Jackson。它更标准社区支持更好序列化/反序列化行为更可预测能避免很多潜在的兼容性问题。配置好依赖后一个常见的启动错误是Java: OutOfMemoryError: Insufficient memory。这不一定是你系统内存不足而可能是 JVM 堆内存设置太小。对于运行大模型应用建议在启动时增加 JVM 参数java -Xmx4g -Xms2g -jar your-application.jar这里-Xmx4g设置最大堆内存为 4GB-Xms2g设置初始堆内存为 2GB根据你的模型大小和并发量调整。4. ReActAgent 的初始化与配置构造一个“会思考”的 Java 对象有了环境我们就可以开始创建ReActAgent实例了。在 AgentScope-Java 中初始化一个智能体通常采用建造者Builder模式或通过一个配置类AgentConfig进行这非常符合 Java 开发者的习惯。让我们看一个典型的初始化代码片段import ai.agentscope.agent.ReActAgent; import ai.agentscope.memory.SimpleMemory; import ai.agentscope.model.openai.OpenAIClient; import ai.agentscope.tool.Tool; import ai.agentscope.tool.builtin.WebSearchTool; import ai.agentscope.tool.builtin.CalculatorTool; import com.fasterxml.jackson.databind.ObjectMapper; public class ReActAgentDemo { public static void main(String[] args) { // 1. 初始化大模型客户端 (以OpenAI为例) String apiKey System.getenv(OPENAI_API_KEY); OpenAIClient llmClient new OpenAIClient.Builder() .apiKey(apiKey) .model(gpt-4-turbo-preview) // 或 gpt-3.5-turbo .temperature(0.1) // ReAct 任务需要较低随机性 .maxTokens(2000) .build(); // 2. 准备工具集 ListTool tools new ArrayList(); tools.add(new WebSearchTool()); // 假设这是一个内置的网络搜索工具 tools.add(new CalculatorTool()); // 内置计算器工具 // 3. 添加自定义工具下文会详细讲 tools.add(new DatabaseQueryTool()); // 3. 初始化记忆系统 SimpleMemory memory new SimpleMemory(); memory.setMaxTurns(10); // 保留最近10轮对话 // 4. 创建 ReActAgent ReActAgent agent new ReActAgent.Builder() .name(ResearchAssistant) .llmClient(llmClient) .tools(tools) .memory(memory) .maxIterations(8) // 防止无限循环 .verbose(true) // 打印详细执行日志调试时非常有用 .build(); // 5. 运行智能体 String task 请查询今天北京的温度并计算如果比昨天高5度那么昨天是多少度; String result agent.run(task); System.out.println(最终结果: result); } }关键配置项解析llmClient这是智能体的“大脑”。temperature设置为较低值如0.1是为了让模型在推理时更加确定性和一致减少天马行空的“幻觉”这对于需要精确执行步骤的 ReAct 循环至关重要。tools智能体的“手脚”。列表中的工具顺序有时会影响模型的调用偏好虽然不绝对。将最常用、最可靠的工具放在前面是个好习惯。memorySimpleMemory是一个基于内存的实现适用于单次会话。对于需要持久化或共享上下文的场景你需要实现或使用DatabaseMemory之类的扩展。maxTurns限制了上下文长度避免提示词过长导致 API 调用失败或成本激增。maxIterations这是一个至关重要的安全阀。大模型可能会陷入思考循环或者在一个无法解决的任务上不断尝试。设置一个合理的上限如8-15次可以防止无限循环消耗你的 API 额度。verbose在开发阶段务必设为true。它会打印出每一轮循环中模型生成的“思考”文本、调用的工具和返回的结果是调试智能体逻辑最直观的方式。5. 工具Tool的自定义与集成扩展智能体的能力边界内置工具如计算器、搜索虽然方便但真正的威力在于将智能体与你现有的业务系统连接起来。在 Java 中自定义一个工具通常意味着实现一个Tool接口或继承一个BaseTool抽象类。假设我们需要一个工具让智能体能查询公司内部的员工信息数据库。下面是一个详细的示例import ai.agentscope.tool.Tool; import ai.agentscope.tool.annotation.ToolAction; import ai.agentscope.tool.annotation.ToolParam; import com.fasterxml.jackson.databind.JsonNode; import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; /** * 自定义工具员工信息查询 */ public class EmployeeQueryTool implements Tool { private final DataSource dataSource; private final ObjectMapper objectMapper; public EmployeeQueryTool(DataSource dataSource, ObjectMapper objectMapper) { this.dataSource dataSource; this.objectMapper objectMapper; } Override public String getName() { return query_employee; } Override public String getDescription() { return 根据员工姓名或工号查询员工的基本信息。输入应为JSON字符串包含 key (姓名或工号) 和 type (name 或 id) 字段。; } /** * 工具的执行方法。 * param arguments 大模型生成的、调用此工具的参数通常是JSON字符串。 * return 查询结果的字符串表示。 */ ToolAction(name queryEmployeeInfo) public String execute(ToolParam(name arguments) String arguments) { try { // 1. 解析大模型传来的参数 JsonNode argsNode objectMapper.readTree(arguments); String key argsNode.get(key).asText(); String type argsNode.get(type).asText(); // 2. 构建并执行SQL查询 String sql; if (id.equalsIgnoreCase(type)) { sql SELECT id, name, department, email FROM employee WHERE id ?; } else if (name.equalsIgnoreCase(type)) { sql SELECT id, name, department, email FROM employee WHERE name LIKE ?; key % key %; } else { return 错误参数 type 必须是 id 或 name。; } try (Connection conn dataSource.getConnection(); PreparedStatement stmt conn.prepareStatement(sql)) { stmt.setString(1, key); ResultSet rs stmt.executeQuery(); // 3. 处理结果并格式化为自然语言 StringBuilder result new StringBuilder(); while (rs.next()) { result.append(String.format(工号: %s, 姓名: %s, 部门: %s, 邮箱: %s\n, rs.getString(id), rs.getString(name), rs.getString(department), rs.getString(email))); } if (result.length() 0) { return 未找到匹配的员工信息。; } return result.toString(); } } catch (Exception e) { // 4. 异常处理返回清晰的错误信息帮助智能体调整策略 return String.format(查询员工信息时发生错误%s。请检查查询参数格式是否正确。, e.getMessage()); } } }自定义工具的核心要点与避坑指南清晰的名称与描述getName()和getDescription()是给大模型看的“工具说明书”。描述必须极其精确说明输入格式、输出格式以及工具的功能边界。模糊的描述会导致模型错误调用。例如明确要求输入是 JSON 字符串并列出所有必需的字段。健壮的参数解析大模型生成的参数文本可能格式不完美多空格、换行、甚至轻微语法错误。使用Jackson的readTree比readValue更宽松容错性更好。务必进行参数校验对缺失或错误的参数返回友好的错误信息而不是抛出异常导致整个智能体崩溃。资源管理与安全工具中涉及数据库连接、HTTP 客户端等资源必须确保正确关闭使用 try-with-resources。永远不要相信大模型生成的参数直接拼接 SQL必须使用预编译语句PreparedStatement来防止 SQL 注入。这是企业级应用安全的生命线。格式化输出工具返回的字符串应该是易于大模型理解的自然语言或结构化文本。避免返回原始的、复杂的 JSON 或 HTML这可能会干扰模型的下一步推理。像上面例子那样用清晰的句子或列表呈现结果。错误处理工具执行失败时不要抛出未捕获的异常。应该捕获异常并返回一个描述性的错误字符串。这样智能体可以将“工具执行失败”作为一个观察结果并可能尝试其他策略例如修正参数后重试或换一个工具。将这个自定义工具添加到ReActAgent的工具列表中后智能体就具备了查询内部数据库的能力。当用户问“帮我找一下研发部的张三的联系方式”时模型可能会生成如下思考链思考用户需要找张三的联系方式。我需要先确定张三的部门然后查询他的邮箱。我可以使用 query_employee 工具根据姓名“张三”进行查询。 行动调用 query_employee参数{key: 张三, type: name} 观察工号: 1001, 姓名: 张三, 部门: 研发部, 邮箱: zhangsancompany.com 思考我已经找到了张三的信息。他的邮箱是 zhangsancompany.com。我可以直接把这个结果返回给用户。 最终答案张三在研发部他的邮箱是 zhangsancompany.com。6. 执行循环的代码级拆解一步步走进 ReAct 的思维过程理解了组件如何组装我们深入到ReActAgent.run()方法内部看看这个循环是如何在代码中流转的。以下是其核心逻辑的伪代码并附上关键点的解读public String run(String task) { // 初始化将用户任务存入记忆作为对话起点 memory.addMessage(new UserMessage(task)); String finalResult null; int iteration 0; // ReAct 主循环 while (iteration maxIterations finalResult null) { iteration; // 步骤1构建提示词 (Prompt Engineering 的核心) String prompt buildPrompt(memory.getRecentMessages(), tools); // prompt 通常包含系统指令、对话历史、工具描述、当前任务、输出格式要求 // 步骤2调用大模型进行“推理” String llmResponse llmClient.call(prompt); // 步骤3解析模型输出 ActionThought actionThought parser.parse(llmResponse); // ActionThought 通常包含两个字段thought (推理文本) 和 action (工具调用指令如 {name: tool_name, args: {...}}) // 步骤4判断并执行行动 if (actionThought.isFinalAnswer()) { // 模型认为任务已完成直接输出答案 finalResult actionThought.getFinalAnswer(); memory.addMessage(new AssistantMessage(finalResult)); } else if (actionThought.requiresAction()) { // 模型决定调用工具 ToolCall toolCall actionThought.getToolCall(); Tool tool findToolByName(toolCall.getName()); if (tool null) { // 工具未找到将错误信息作为观察 String errorMsg 错误工具 toolCall.getName() 不存在。; memory.addMessage(new SystemMessage(errorMsg)); continue; } // 执行工具 String toolResult tool.execute(toolCall.getArgs()); // 将“行动”和“观察”都存入记忆供下一轮推理使用 memory.addMessage(new AssistantMessage(llmResponse)); // 包含思考过程 memory.addMessage(new SystemMessage(工具 tool.getName() 返回: toolResult)); } else { // 解析失败或输出不符合预期 memory.addMessage(new SystemMessage(无法理解模型的响应格式。)); } } if (finalResult null) { finalResult 任务未在最大迭代次数( maxIterations )内完成。; } return finalResult; }循环中的关键细节与实战经验提示词构建 (buildPrompt)这是整个智能体性能的“开关”。一个优秀的提示词需要明确的系统角色例如“你是一个严谨的助理必须通过使用工具来获取信息回答问题。”严格的输出格式约束强制模型以如Thought: ... Action: {...}这样的固定格式输出这是Parser能正确解析的前提。在 Java 中可以用多行字符串清晰定义模板。工具描述的清晰呈现将每个工具的getName()和getDescription()以列表形式放入提示词。上下文窗口管理memory.getRecentMessages()不能无限制地返回所有历史需要根据 Token 数量进行截断否则会触发模型的长度限制。解析器 (Parser)它的鲁棒性直接决定了智能体的稳定性。模型输出可能会有细微的偏差比如多一个句号JSON 键名没加引号。一个成熟的Parser不能只依赖严格的 JSON 解析应该结合正则表达式和容错的 JSON 库如Jackson的JsonNodeFactory来提取关键信息。在开源实现中经常能看到对类似Action: None或Action: Finish[...]等边缘情况的处理。记忆 (Memory) 的更新策略并不是所有信息都需要存入记忆。通常只存储UserMessage,AssistantMessage(包含模型完整的输出即思考行动指令) 和SystemMessage(工具执行结果或错误)。过于冗长的记忆会挤占有效上下文。有些高级实现会引入“记忆摘要”功能将多轮对话压缩成一段摘要。迭代控制与超时除了maxIterations在生产环境中还应考虑总耗时控制。可以在循环开始记录时间如果超过一定阈值如30秒则主动中断并返回超时提示避免一个任务长时间阻塞线程。7. 调试与问题排查当你的 Java Agent 不按套路出牌时即使代码看似完美你的ReActAgent也可能表现怪异比如陷入循环、总是调用错误的工具、或者输出毫无意义的文本。别慌这是智能体开发的常态。下面是一个系统性的排查清单问题1智能体陷入无限循环反复调用同一个工具。可能原因1工具返回的结果无法让模型推导出下一步。比如工具返回了null或空字符串模型无法基于此做出新决策。解决检查工具实现确保在任何情况下包括无结果、出错都返回一个对模型有信息量的字符串。例如“未找到数据”比“”要好得多。可能原因2记忆上下文被污染。某次错误的工具调用及其结果留在了记忆里导致后续推理被带偏。解决开启verbose日志检查每一轮的输入输出。考虑在memory中实现一个过滤机制或者在构建提示词时选择性忽略某些无效的“观察”。可能原因3maxIterations设置过大且没有其他终止条件。解决合理设置maxIterations(5-10次对于简单任务足够)。在buildPrompt中明确告诉模型“如果你认为已经获得足够信息请直接给出最终答案”。问题2模型不调用工具总是试图直接回答问题“幻觉”。可能原因1系统指令不够强硬。模型没有被充分约束。解决强化提示词中的指令。例如“你必须使用提供的工具来获取信息。禁止基于已有知识直接猜测答案。如果你不知道就说不知道并尝试使用工具。”可能原因2工具描述太模糊或与问题不匹配。模型觉得没有合适的工具可用。解决优化工具的getDescription()使用更精准的关键词。确保工具的能力覆盖了用户可能问到的领域。问题3解析器频繁失败报“无法理解模型响应”。可能原因1模型输出格式不符合预期。可能是temperature设置过高导致输出随机性太大。解决将temperature降至 0.1 或 0.2。在提示词中用更醒目的方式如 强调输出格式。可能原因2Parser 逻辑过于脆弱。只能处理完美的 JSON。解决增强你的Parser。可以先尝试用正则表达式提取Action:后面的内容然后再用Jackson的JsonNode进行宽松解析。社区中有些库提供了JsonSurfer等工具来处理不规范的 JSON 片段。通用的调试技巧日志是生命线确保verbosetrue并把日志级别调到DEBUG。完整查看每一轮的 Prompt、LLM 响应、解析结果、工具调用和返回。单元测试工具在集成到 Agent 之前先为你的自定义工具编写单元测试用各种可能的输入包括错误输入验证其行为。简化场景如果复杂任务失败先用一个只需调用一次工具就能完成的简单任务测试确保基础链路是通的。检查 Token 数过长的提示词会被截断导致模型丢失关键信息如工具描述。在调用llmClient前可以估算一下 Prompt 的 Token 数量例如使用tiktoken库的 Java 移植版。8. 性能优化与生产就绪考量当一个ReActAgent在 demo 中跑通后要将其部署到生产环境还需要考虑更多异步与非阻塞llmClient.call()和工具执行尤其是网络 I/O 类工具可能是耗时的。在 Web 服务中同步调用会阻塞线程池。考虑将agent.run()改为返回CompletableFutureString使用异步 HTTP 客户端如 AsyncHttpClient调用 LLM API工具也实现为异步接口。连接池与资源管理自定义工具中的数据库连接、HTTP 客户端必须使用连接池如 HikariCP, OkHttp ConnectionPool避免频繁创建销毁连接的开销。限流与熔断对 LLM API 的调用必须实施限流防止意外流量打爆配额。为智能体设置超时和熔断机制当连续失败次数达到阈值时暂时禁用该智能体避免级联故障。可观测性集成监控指标Micrometer和分布式追踪OpenTelemetry。记录每次任务的迭代次数、总耗时、工具调用分布、Token 消耗等。这些数据对于优化成本和性能至关重要。配置外部化将模型类型、API Key、温度、最大迭代次数等参数移到配置文件如application.yml或配置中心便于不同环境开发、测试、生产的切换和动态调整。将 AgentScope-Java 的 ReActAgent 集成到你的 Spring Boot 应用中本质上就是创建一个受管理的 Bean。你可以通过Configuration类来组装它并通过Service对外提供智能体服务。这样你的智能体就能像其他业务服务一样享受依赖注入、事务管理、AOP 拦截等 Spring 生态的全部便利。从 LangChain 的原型验证到 AgentScope-Java 的生产落地这条路的核心在于理解智能体范式的本质并用 Java 工程师熟悉的、稳健的方式去实现它。ReActAgent的代码实现拆开来看就是一个状态机驱动的服务循环其难点不在于算法多深奥而在于对边界情况的细致处理、对资源的妥善管理以及与现有庞大 Java 生态的平滑对接。希望这篇深入的代码讲解能帮你跨过从“玩具”到“生产工具”的关键一步。