尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

ReAct 模式实战:让 Agent 学会思考的 Java 实现

ReAct 模式实战:让 Agent 学会思考的 Java 实现 之前做一个内部知识问答工具的时候我直接用大模型接口 长提示词让模型自己回答问题。效果嘛简单问题还行一旦碰到需要查数据库、算数据、调外部接口的问题模型就开始一本正经地胡说八道。后来我看到了 ReActReasoning Acting的概念才发现问题出在“只让模型想没让它动”。这篇文章我会用 Java 从零实现一个最小的 ReAct Agent演示怎么让模型在思考的同时调用外部工具最终更可靠地解决问题。先说清楚这里的 ReAct 不是 React 前端框架ReAct来自论文《ReAct: Synergizing Reasoning and Acting in Language Models》核心是把推理轨迹Thought和动作Action交替结合模型推理当前状态决定要调用哪个工具然后观察结果继续推理直到得出最终答案。这种循环让模型不再是“闭卷考试”而是能像人一样查资料、验算再下结论。一个典型场景模型需要查数据和计算假设产品让 Agent 回答上海今天气温 20 度明天 15 度温差多少如果你直接用 prompt 让大模型算它可能直接输出一个数字但它根本不知道真实天气更没法保证算对。如果我们给它两个工具get_city_temperature和calculate让模型先查温度再算温差结果就靠谱多了。为什么不用现成框架我知道有 LangChain4j、Spring AI 可以直接封装 Agent。但为了理解核心机制我选择用HttpClientJackson手写一个最小实现。工作中你当然可以用框架但明白底层循环是怎么转的排错时才不会抓瞎。实现设计整个 Agent 是一个循环流程如下用户问题 - 调用 LLM - 解析输出 ├─ 如果是 Final Answer返回给用户 ├─ 如果是 Thought Action执行工具 └─ 把观察结果加入上下文继续调用 LLM为了让模型输出可解析我在提示词里明确要求它输出 JSON 格式{thought: 我需要知道上海的当前温度, action: get_city_temperature, action_input: 上海}或者回答{thought: 我已经知道温差了, action: Final Answer, action_input: 温差为 5 度}工具接口与实现首先定义一个统一的工具接口public interface Tool { String getName(); String getDescription(); String execute(String input) throws Exception; }然后是温度查询工具真实项目里会接 API这里用 mock 数据public class TemperatureTool implements Tool { Override public String getName() { return get_city_temperature; } Override public String getDescription() { return 获取指定城市的当前温度输入参数为城市名称例如北京; } Override public String execute(String input) { // 真实项目中调用天气服务这里直接返回 mock 数据 if (input.contains(上海)) return 20; if (input.contains(北京)) return 25; return 未知城市; } }再写一个简单的计算器工具用来算温差public class CalculatorTool implements Tool { Override public String getName() { return calculate; } Override public String getDescription() { return 计算数学表达式例如20 - 15; } Override public String execute(String input) { try { // 简单处理加减乘除实际工程建议用表达式引擎 if (input.contains(-)) { String[] parts input.split(-); double a Double.parseDouble(parts[0].trim()); double b Double.parseDouble(parts[1].trim()); return String.valueOf(a - b); } // 其他运算省略只演示减法 return 无法计算; } catch (Exception e) { return 表达式错误; } } }注意工具描述要写清楚“参数是什么、返回什么”。模型靠描述决定什么时候调用它好的描述能显著提高成功率。ReAct Agent 核心循环下面是最关键的代码。我用了 OpenAI 的chat/completions接口模型设置为 GPT-4开启了response_format为 JSON Object 让输出更容易解析。import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.*; import java.util.*; public class ReActAgent { private final String apiKey; private final HttpClient httpClient HttpClient.newHttpClient(); private final ObjectMapper mapper new ObjectMapper(); private final MapString, Tool tools new HashMap(); private final ListMapString, String messages new ArrayList(); public ReActAgent(String apiKey) { this.apiKey apiKey; registerTool(new TemperatureTool()); registerTool(new CalculatorTool()); } private void registerTool(Tool tool) { tools.put(tool.getName(), tool); } public String run(String userQuestion) throws Exception { // 构建系统提示词 String systemPrompt buildSystemPrompt(); messages.add(Map.of(role, system, content, systemPrompt)); messages.add(Map.of(role, user, content, userQuestion)); int maxSteps 5; for (int step 0; step maxSteps; step) { String response callLlm(); JsonNode result mapper.readTree(response); String action result.get(action).asText(); String actionInput result.get(action_input).asText(); if (Final Answer.equals(action)) { return actionInput; } // 执行工具 Tool tool tools.get(action); String observation; if (tool null) { observation 没有找到名为 action 的工具; } else { try { observation tool.execute(actionInput); } catch (Exception e) { observation 工具执行出错: e.getMessage(); } } // 把模型上一轮输出和工具观察结果追加到对话中 messages.add(Map.of(role, assistant, content, response)); messages.add(Map.of(role, user, content, 观察结果: observation)); } return 达到最大步数未能得到最终答案; } private String buildSystemPrompt() { StringBuilder sb new StringBuilder(); sb.append(你是一个能使用工具解决问题的智能体。\n); sb.append(你必须严格按照以下 JSON 格式输出\n); sb.append({\thought\: \你的推理过程\, \action\: \工具名或Final Answer\, \action_input\: \工具输入或最终答案\}\n); sb.append(可用的工具如下\n); for (Tool tool : tools.values()) { sb.append(- ).append(tool.getName()) .append(: ).append(tool.getDescription()) .append(\n); } sb.append(如果已经得到答案请使用 Final Answer。\n); return sb.toString(); } private String callLlm() throws Exception { String body mapper.writeValueAsString(Map.of( model, gpt-4, messages, messages, response_format, Map.of(type, json_object) )); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(https://api.openai.com/v1/chat/completions)) .header(Authorization, Bearer apiKey) .header(Content-Type, application/json) .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponseString httpResponse httpClient.send(request, HttpResponse.BodyHandlers.ofString()); JsonNode root mapper.readTree(httpResponse.body()); // 取第一个回复的内容 return root.path(choices).get(0).path(message).path(content).asText(); } }调用方式很简单public class Main { public static void main(String[] args) throws Exception { String apiKey System.getenv(OPENAI_API_KEY); ReActAgent agent new ReActAgent(apiKey); String answer agent.run(上海今天20度明天15度温差是多少); System.out.println(answer); } }一次真实的运行过程示意假如模型的推理过程如下这就是“思考”和“行动”的结合{thought: 用户想知道温差我需要先获取上海明天的温度但我已经知道今天20度明天需查, action: get_city_temperature, action_input: 上海}我返回观察结果: 15模型继续生成{thought: 今天20度明天15度温差为20-155度, action: calculate, action_input: 20 - 15}观察结果5模型最终输出{thought: 已经计算出温差, action: Final Answer, action_input: 温差为5度}整个过程中模型把大问题拆成了两步每一步都依赖工具返回的事实而不是凭空猜测。工程化还要注意这些坑1. 输出格式不是百分百稳定即使设置了 JSON 模式模型偶尔还会输出多余文字。我在初版代码里直接asText()然后readTree结果经常解析失败。后来加了预处理String content content.trim(); if (content.startsWith() content.endsWith()) { content content.substring(3, content.length() - 3).trim(); } // 再去掉可能的“json”标识 if (content.startsWith(json)) { content content.substring(4).trim(); }更稳健的方案是用函数调用Function Calling功能让模型直接输出结构化参数但本文为了演示原理保留了 JSON 解析。2. 上下文长度会快速膨胀每多一步对话里就会多两条消息。如果 Agent 需要十几次工具调用token 消耗不小。我通常在工程里只保留最近几轮的工具调用结果或者对历史消息做摘要。3. 工具要能优雅地处理异常如果工具抛异常不能让 Agent 卡死。我在上面的代码里try-catch了工具执行并把错误字符串作为观察结果返回给模型。这样模型可能会换一种方式或者道歉。4. 限制最大步数这是必须的。如果没有限制模型可能进入死循环。我一般设 5~10 步超过后返回固定提示。5. 不要暴露太多内部信息工具描述是给模型看的但同时也是用户输入可能注入的入口。记得对action_input做必要校验避免模型意外调用危险操作。真实项目中工具权限、频率限制、日志审计都不能少。为什么说 ReAct 让 Agent“学会思考”传统的 prompt 能引导模型回答但模型无法感知外部世界也没有办法验证自己的推理。ReAct 通过“思考-行动-观察”循环把推理过程和外部反馈打通了。模型每走一步都会基于新信息重新推理自然比“一次性输出”更接近人类的思考方式。当然ReAct 不是银弹。对于需要复杂规划的长时间任务可能还需要加上子目标分解Tree of Thought或记忆机制。但理解 ReAct 是入手 Agent 开发最扎实的一步。总结这篇文章我手写了一个 Java 版最小 ReAct Agent核心就一句话让模型输出结构化的“想法 动作”执行工具后把结果反馈回去循环直到得出答案。虽然比直接调 API 麻烦但换来的是更高的准确性和可解释性。你可以把这里的温度工具换成搜索引擎、数据库查询、代码解释器一个更通用的 Agent 就会慢慢成型。但记住工具越稳定描述越清晰Agent 的表现才会越好。
返回列表