本文是一篇关于如何构建一个极简ReAct Agent的实践教程,使用Java语言实现。文章通过一个200行代码的示例,帮助读者深入理解ReAct模式中的“思考 - 行动 - 观察”循环机制,并借助实际场景(如补货计划单审批)演示了Agent的工作流程。此外,作者还分享了代码运行方式、核心思路及具体的执行过程,便于读者动手实践与调试。
前言
之前听说过ReAct Agent,也看过它的原理图,但Thought、Action、Observation到底代表什么,以及它们是如何实现的,并没有深入理解过。今天我们就来动手实践下,一步步弄清ReAct的核心思想和关键实现。
核心思想
ReAct核心就是“思考(Thought) - 行动(Action) - 观察(Observation)”的循环,用伪代码表示:
while (true) { // 1、(Thought)大模型调用,根据大模型输出,判断是否需要工具调用,调用哪个工具,入参是什么 if(无需工具调用) { break; }
// 2、(Action)工具调用
// 3、(Observation)拿到工具调用的执行结果,追加到prompt中,回到1,进行下一轮LLM调用}
代码实现
核心代码
public class ReActAgent {
private static final String AGENT_ACTION_TEMPLATE = "工具名称,必须是[{0}]中的一个";
// 在AI平台(如百炼、coze等)上,需要先注册工具,然后创建agent时关联工具,这样系统会自动帮你拼接到Prompt中了 // 此次作为demo,手写写死 private static final List<Tool> TOOLS = Arrays.asList( // 工具1:查询补货计划单详情 Tool.builder() .name("查询补货计划单详情") .desc("根据补货计划单号查询补货计划单详情") .parameters(Collections.singletonList( Tool.Parameter.builder() .name("orderCode") .desc("补货计划单号") .type("string") .required(true) .build() )) .build(), // 工具2:审批补货计划单 Tool.builder() .name("审批补货计划单") .desc("根据补货计划单号审批补货计划单") .parameters(Collections.singletonList( Tool.Parameter.builder() .name("orderCode") .desc("补货计划单号") .type("string") .required(true) .build() )) .build() );
private static final String USER_PROMPT = "# 角色设定\n" + "你是一位经验丰富的供应链智能助理,专注于补货计划单的审批,具备如下技能:\n" + "\n" + "## 技能1:从上下文提取补货计划单号\n" + "1. 识别用户提供的文本中的补货计划单号;\n" + "\n" + "## 技能2:查询补货计划单详情并审批\n" + "1. 根据提取到的单号查询补货计划单的状态;\n" + "2. 如果是待审批状态,则调用审批工具进行审批;\n" + "3. 如果不是待审批状态,则告知用户该状态无法进行审批。\n" + "\n" + "## 行为准则\n" + "- 只讨论与供应链管理和补货计划单审批相关的话题;\n" + "- 回复时根据内容选择最合适的展现方式;" ;
public static void main(String[] args) { if (args.length != 1) { System.out.println("ak未配置,结束!"); return; }
// LLM的AK,注意不要泄露,不要泄露,不要泄露 String ak = args[0];
// 创建 Scanner 对象用于接收用户输入 Scanner scanner = new Scanner(System.in); // 多轮会话的记忆 Memory memory = new Memory(); System.out.println("请提问,开始和AI的对话吧"); // 循环逻辑 while (true) { // 接收用户输入 String input = scanner.nextLine(); System.out.println("用户输入:" + input); // 判断是否满足退出条件 if ("exit".equalsIgnoreCase(input)) { System.out.println("检测到退出指令,对话结束!"); break; // 满足条件时退出循环 } // reAct StringBuilder latestInput = new StringBuilder(input); String output = reAct(ak, memory, latestInput); System.out.println("AI输出: " + output); memory.add(new Memory.ChatMsg(Memory.ChatMsg.USER, input)); memory.add(new Memory.ChatMsg(Memory.ChatMsg.AI, output)); // 开始下一轮对话 } // 关闭 Scanner scanner.close(); } private static String reAct(String ak, Memory memory, StringBuilder latestInput) { while (true) { String prompt = prompt(USER_PROMPT, TOOLS, memory, latestInput); String llmResult = LLM.llm(prompt, ak); /** * 可能的结果 * * Thought: 已收到补货计划单号,现需查询补货计划单详情。 * Action: 查询补货计划单详情 * Action Input: {"orderCode": "BH002"} * * Thought: 当前补货计划单处于待审批状态,可以进行审批操作。 * Action: 审批补货计划单 * Action Input: {"orderCode": "BH002"} * * 1、通过Action,可以判断要调用哪个工具 * 2、通过Action Input,知道工具入参是什么 * * 此处仅做示例,因此: * 1、大模型返回比较玄学,正确解析是个麻烦事,demo中Action解析逻辑写的比较简单,Action Input就不解析了 * 2、工具调用用本地函数做示例,真实场景会调用RPC接口(如Dubbo等)、HTTP等 */ // llm判断要调用工具:查询补货计划单详情 if (isQueryPlanOrderAction(llmResult)) { String toolResult = JSON.toJSONString(queryPlanOrder(null)); // 所谓的observation就是观察工具执行的结果,最终会追加到下一次llm调用的Prompt中 String observataion = toolResult; latestInput.append("\n").append(llmResult) .append("\n").append("Observation: ").append(observataion); } // llm判断要调用工具:审批补货计划单 else if (isAuditPlanOrderAction(llmResult)) { String toolResult = JSON.toJSONString(auditPlanOrder(null)); // 所谓的observation就是观察工具执行的结果,最终会追加到下一次llm调用的Prompt中 String observataion = toolResult; latestInput.append("\n").append(llmResult) .append("\n").append("Observation: ").append(observataion); } // 无需工具调用,本轮对话结束,回复用户 else { return llmResult; } } } private static String prompt(String userPrompt, List<Tool> tools, Memory memory, StringBuilder latestInput) { String prompt = "${{user_prompt}}\n" + "---------------------\n" + "# 工具列表\n" + "${{tool_definitions}}\n" + "\n" + "使用如下格式:\n" + "Thought: 思考并确定下一步的最佳行动方案\n" + "Action: ${{agent_action}}\n" + "Action Input: 工具参数,必须是 JSON 对象\n" + "Observation: 工具执行结果\n" + "... (Thought/Action/Action Input/Observation 可以重复N次)\n" + "\n" + "注意:\n" + "- 不使用工具时,回复中不要出现 Thought、Action、Action Input;\n" + "- 使用工具前,先检查是否缺少必要参数,缺少必要参数时直接向用户提问,不要出现 Thought、Action、Action Input;\n" + "- 工具执行遇到问题时,向用户寻求帮助;\n" + "- 需要执行同一个工具多次时,Action Input 可以出现多次;\n" + "\n" + "---------------------\n" + "# 对话记录\n" + "${{history_record}}\n" + "\n" + "# 最新输入\n" + "${{latest_input}}"; prompt = prompt.replace("${{user_prompt}}", userPrompt); prompt = prompt.replace("${{tool_definitions}}", JSON.toJSONString(tools)); prompt = prompt.replace("${{agent_action}}", MessageFormat.format(AGENT_ACTION_TEMPLATE, TOOLS.stream().map(Tool::getName).collect(Collectors.joining(",")))); prompt = prompt.replace("${{history_record}}", memory.getAll()); prompt = prompt.replace("${{latest_input}}", latestInput.toString()); return prompt; } /** * 工具1:查询补货计划单详情 * 一般对应我们的RPC接口(如Dubbo等),需要提前在AI平台上注册成工具。会通过泛化调用过去 */ private static PlanOrder queryPlanOrder(String orderCode) { PlanOrder order = new PlanOrder(); order.setOrderCode(orderCode); // 可审批的状态 order.setStatus("待审批"); return order; } /** * 工具2:审批补货计划单 * 一般对应我们的RPC接口(如Dubbo等),需要提前在AI平台上注册成工具。会通过泛化调用过去 */ private static AuditResult auditPlanOrder(String orderCode) { return new AuditResult("审批成功", true); } private static boolean isQueryPlanOrderAction(String llmResult) { return llmResult.contains("Action") && llmResult.contains("查询补货计划单详情"); } private static boolean isAuditPlanOrderAction(String llmResult) { return llmResult.contains("Action") && llmResult.contains("审批补货计划单"); }}
其他
// 工具@Data@Builderpublic class Tool { private String name; private String desc; private List<Parameter> parameters; @Data @Builder public static class Parameter { private String name; private String desc; private String type; private Boolean required; private List<Parameter> properties; }}
// 记忆@Datapublic class Memory { private List<ChatMsg> memories = new ArrayList<>(); public void add(ChatMsg chatMsg) { memories.add(chatMsg); } public String getAll() { StringBuilder sb = new StringBuilder(); for (ChatMsg chatMsg : memories) { sb.append(chatMsg.getRole()).append(": \n").append(chatMsg.getMsg()).append("\n"); } return sb.toString(); } @Data public static class ChatMsg { public static final String USER = "用户"; public static final String AI = "AI"; public String role; public String msg; public ChatMsg(String role, String msg) { this.role = role; this.msg = msg; } }}
// 大模型调用,这里可替换成千问、OpenAI等的调用public class LLM { private static final String model = "你的模型名称"; @Data static class GearsAIResult { private List<GearsAIChoice> choices; // 省略usage } @Data static class GearsAIChoice { private GearsAIMessage message; private Integer index; @JSONField(name = "finish_reason") private String finish_reason; } @Data static class GearsAIMessage { private String role; private String content; } public static String llm(String prompt, String ak) { OkHttpClient.Builder builder = new OkHttpClient.Builder(); builder.connectTimeout(60, TimeUnit.SECONDS); // 设置连接超时时间为60秒 builder.readTimeout(120, TimeUnit.SECONDS); // 设置读取超时时间为120秒 builder.writeTimeout(60, TimeUnit.SECONDS); // 设置写入超时时间为60秒 OkHttpClient client = builder.build(); MediaType mediaType = MediaType.parse("application/json; charset=utf-8"); JSONObject reqData = new JSONObject(); reqData.put("model", model); JSONObject reqBody = new JSONObject(); reqBody.put("role", "user"); reqBody.put("content", prompt); reqData.put("messages", Collections.singletonList(reqBody)); RequestBody body = RequestBody.create(mediaType, reqData.toJSONString()); // 创建请求 Request request = new Request.Builder() .url("https://你的API地址/api/openai/v1/chat/completions") .header("Authorization", "Bearer " + ak) // 增加请求头属性 .post(body) // 添加请求体 .build(); // 同步请求,demo没用流式 try { Response response = client.newCall(request).execute(); if (!response.isSuccessful()) { throw new RuntimeException("req llm exception, res : " + response.toString()); } String res = response.body().string(); GearsAIResult gearsAIResult = JSON.parseObject(res, GearsAIResult.class); return Optional.ofNullable(gearsAIResult) .map(GearsAIResult::getChoices) .map(e -> e.get(0)) .map(GearsAIChoice::getMessage) .map(GearsAIMessage::getContent) .orElseThrow(() -> new RuntimeException("调用llm异常")); } catch (Exception e) { throw new RuntimeException(e); } }}
// 补货计划单模型@Datapublic class PlanOrder { private String orderCode; private String status;}
// 审批结果@Datapublic class AuditResult { private String data; private Boolean success; public AuditResult(String data, Boolean success) { this.data = data; this.success = success; }}
示例场景
以补货计划单催审场景为例,商家创建了一个补货计划单,来催小二审批。小二通过某些决策判断能不能审批通过。现在AI变成了小二的角色,商家找AI催审。
我们将这个场景进行简化,变成demo中的样子:
工具:
查询补货计划单详情
审批补货计划单
执行过程
执行效果
第1轮对话(1次LLM调用)
用户输入:
帮我审批下补货计划单
对应的prompt:
# 角色设定你是一位经验丰富的供应链智能助理,专注于补货计划单的审批,具备如下技能:
## 技能1:从上下文提取补货计划单号1. 识别用户提供的文本中的补货计划单号;
## 技能2:查询补货计划单详情并审批1. 根据提取到的单号查询补货计划单的状态;2. 如果是待审批状态,则调用审批工具进行审批;3. 如果不是待审批状态,则告知用户该状态无法进行审批。
## 行为准则- 只讨论与供应链管理和补货计划单审批相关的话题;- 回复时根据内容选择最合适的展现方式;---------------------# 工具列表[{"desc":"根据补货计划单号查询补货计划单详情","name":"查询补货计划单详情","parameters":[{"desc":"补货计划单号","name":"orderCode","required":true,"type":"string"}]},{"desc":"根据补货计划单号审批补货计划单","name":"审批补货计划单","parameters":[{"desc":"补货计划单号","name":"orderCode","required":true,"type":"string"}]}]
使用如下格式:Thought: 思考并确定下一步的最佳行动方案Action: 工具名称,必须是[查询补货计划单详情,审批补货计划单]中的一个Action Input: 工具参数,必须是 JSON 对象Observation: 工具执行结果... (Thought/Action/Action Input/Observation 可以重复N次)
注意:- 不使用工具时,回复中不要出现 Thought、Action、Action Input;- 使用工具前,先检查是否缺少必要参数,缺少必要参数时直接向用户提问,不要出现 Thought、Action、Action Input;- 工具执行遇到问题时,向用户寻求帮助;- 需要执行同一个工具多次时,Action Input 可以出现多次;
---------------------# 对话记录
# 最新输入帮我审批下补货计划单
大模型输出:
对不起,由于我还没有接收到具体的补货计划单号,无法执行审批。请您告诉我具体的补货计划单号,并确保其状态为待审批下。
由于大模型判断不需要调用工具,因此直接把这句话返回给用户。
Agent输出:
对不起,由于我还没有接收到具体的补货计划单号,无法执行审批。请您告诉我具体的补货计划单号,并确保其状态为待审批下。
第2轮对话(3次LLM调用)
用户输入:
单号是:BH001
对应prompt:
# 角色设定你是一位经验丰富的供应链智能助理,专注于补货计划单的审批,具备如下技能:
## 技能1:从上下文提取补货计划单号1. 识别用户提供的文本中的补货计划单号;
## 技能2:查询补货计划单详情并审批1. 根据提取到的单号查询补货计划单的状态;2. 如果是待审批状态,则调用审批工具进行审批;3. 如果不是待审批状态,则告知用户该状态无法进行审批。
## 行为准则- 只讨论与供应链管理和补货计划单审批相关的话题;- 回复时根据内容选择最合适的展现方式;---------------------# 工具列表[{"desc":"根据补货计划单号查询补货计划单详情","name":"查询补货计划单详情","parameters":[{"desc":"补货计划单号","name":"orderCode","required":true,"type":"string"}]},{"desc":"根据补货计划单号审批补货计划单","name":"审批补货计划单","parameters":[{"desc":"补货计划单号","name":"orderCode","required":true,"type":"string"}]}]
使用如下格式:Thought: 思考并确定下一步的最佳行动方案Action: 工具名称,必须是[查询补货计划单详情,审批补货计划单]中的一个Action Input: 工具参数,必须是 JSON 对象Observation: 工具执行结果... (Thought/Action/Action Input/Observation 可以重复N次)
注意:- 不使用工具时,回复中不要出现 Thought、Action、Action Input;- 使用工具前,先检查是否缺少必要参数,缺少必要参数时直接向用户提问,不要出现 Thought、Action、Action Input;- 工具执行遇到问题时,向用户寻求帮助;- 需要执行同一个工具多次时,Action Input 可以出现多次;
---------------------# 对话记录用户: 帮我审批下补货计划单AI: 对不起,由于我还没有接收到具体的补货计划单号,无法执行审批。请您告诉我具体的补货计划单号,并确保其状态为待审批下。
# 最新输入单号是:BH001
大模型输出:
Thought: 用户提供了补货计划单号,我应该先查询该单号的补货计划单详情,看看该单的状态是否可以进行审批。Action: 查询补货计划单详情Action Input: {"orderCode": "BH001"}
经过用户的2次输入,大模型提取到了补货计划单号。并思考出了下一步该调用【查询补货计划单详情】这个工具,且入参是:{"orderCode": "BH001"}。这个过程就是Thought。
我们自己调用这个工具,工具输出:
{"status":"待审批"}
这个步骤就是Action。
现在,在prompt中加入工具调用的结果,这就是Observation。然后进入下一轮大模型调用,对应prompt:
# 角色设定你是一位经验丰富的供应链智能助理,专注于补货计划单的审批,具备如下技能:
## 技能1:从上下文提取补货计划单号1. 识别用户提供的文本中的补货计划单号;
## 技能2:查询补货计划单详情并审批1. 根据提取到的单号查询补货计划单的状态;2. 如果是待审批状态,则调用审批工具进行审批;3. 如果不是待审批状态,则告知用户该状态无法进行审批。
## 行为准则- 只讨论与供应链管理和补货计划单审批相关的话题;- 回复时根据内容选择最合适的展现方式;---------------------# 工具列表[{"desc":"根据补货计划单号查询补货计划单详情","name":"查询补货计划单详情","parameters":[{"desc":"补货计划单号","name":"orderCode","required":true,"type":"string"}]},{"desc":"根据补货计划单号审批补货计划单","name":"审批补货计划单","parameters":[{"desc":"补货计划单号","name":"orderCode","required":true,"type":"string"}]}]
使用如下格式:Thought: 思考并确定下一步的最佳行动方案Action: 工具名称,必须是[查询补货计划单详情,审批补货计划单]中的一个Action Input: 工具参数,必须是 JSON 对象Observation: 工具执行结果... (Thought/Action/Action Input/Observation 可以重复N次)
注意:- 不使用工具时,回复中不要出现 Thought、Action、Action Input;- 使用工具前,先检查是否缺少必要参数,缺少必要参数时直接向用户提问,不要出现 Thought、Action、Action Input;- 工具执行遇到问题时,向用户寻求帮助;- 需要执行同一个工具多次时,Action Input 可以出现多次;
---------------------# 对话记录用户: 帮我审批下补货计划单AI: 对不起,由于我还没有接收到具体的补货计划单号,无法执行审批。请您告诉我具体的补货计划单号,并确保其状态为待审批下。
# 最新输入单号是:BH001Thought: 用户提供了补货计划单号,我应该先查询该单号的补货计划单详情,看看该单的状态是否可以进行审批。Action: 查询补货计划单详情Action Input: {"orderCode": "BH001"}Observation: {"status":"待审批"}
大模型输出
Thought: 根据查询结果,该补货计划单当前是待审批状态,可以进行审批。Action: 审批补货计划单Action Input: {"orderCode": "BH001"}
这次调用【审批补货计划单】的工具,工具输出:
{"data":"审批成功","success":true}
在下一轮大模型调用,此时的prompt中加入了2次工具调用的结果作为Observation,且每次的思考过程也加入了进去,对应prompt:
# 角色设定你是一位经验丰富的供应链智能助理,专注于补货计划单的审批,具备如下技能:
## 技能1:从上下文提取补货计划单号1. 识别用户提供的文本中的补货计划单号;
## 技能2:查询补货计划单详情并审批1. 根据提取到的单号查询补货计划单的状态;2. 如果是待审批状态,则调用审批工具进行审批;3. 如果不是待审批状态,则告知用户该状态无法进行审批。
## 行为准则- 只讨论与供应链管理和补货计划单审批相关的话题;- 回复时根据内容选择最合适的展现方式;---------------------# 工具列表[{"desc":"根据补货计划单号查询补货计划单详情","name":"查询补货计划单详情","parameters":[{"desc":"补货计划单号","name":"orderCode","required":true,"type":"string"}]},{"desc":"根据补货计划单号审批补货计划单","name":"审批补货计划单","parameters":[{"desc":"补货计划单号","name":"orderCode","required":true,"type":"string"}]}]
使用如下格式:Thought: 思考并确定下一步的最佳行动方案Action: 工具名称,必须是[查询补货计划单详情,审批补货计划单]中的一个Action Input: 工具参数,必须是 JSON 对象Observation: 工具执行结果... (Thought/Action/Action Input/Observation 可以重复N次)
注意:- 不使用工具时,回复中不要出现 Thought、Action、Action Input;- 使用工具前,先检查是否缺少必要参数,缺少必要参数时直接向用户提问,不要出现 Thought、Action、Action Input;- 工具执行遇到问题时,向用户寻求帮助;- 需要执行同一个工具多次时,Action Input 可以出现多次;
---------------------# 对话记录用户: 帮我审批下补货计划单AI: 对不起,由于我还没有接收到具体的补货计划单号,无法执行审批。请您告诉我具体的补货计划单号,并确保其状态为待审批下。
# 最新输入单号是:BH001Thought: 用户提供了补货计划单号,我应该先查询该单号的补货计划单详情,看看该单的状态是否可以进行审批。Action: 查询补货计划单详情Action Input: {"orderCode": "BH001"}Observation: {"status":"待审批"}Thought: 根据查询结果,该补货计划单当前是待审批状态,可以进行审批。Action: 审批补货计划单Action Input: {"orderCode": "BH001"}Observation: {"data":"审批成功","success":true}
大模型输出:
补货计划单BH001已成功审批。有其他补货计划单需要我帮助审批吗?
Agent输出:
补货计划单BH001已成功审批。有其他补货计划单需要我帮助审批吗?
小结:在上述例子中,用户一共说了2句话,Agent回复了2句话,但大模型分别调用了1次、3次。这些数字差异正体现了ReAct的核心原理所在。除了代码debug,大家还可以直接将prompt复制到LLM中去执行,体验一下效果~
总结
通过一步步的实现和推演,我们终于认识了 ReAct 的核心——“思考 (Thought) - 行动 (Action) - 观察 (Observation)”循环的真正含义,同时也对 ReAct 的关键实现有了更深入的理解。需要注意的是,作为一个 demo,该实现省略了一些异常处理、工程设计和细节。如果大家感兴趣,可以进一步探索和学习更复杂的实用场景!
团队介绍
本文作者慕轶,来自淘天集团-供应链技术团队。本团队面向淘天自营业务、中小企业及产业带商家,专注于以技术驱动的供应链创新,致力于打造端到端的智能供应链生态,支撑了诸如天猫超市、天猫国际等全部淘天自营业务。依托淘宝APP亿级流量入口的核心购物场景,通过持续的技术突破与前沿AI科技深度融合,沉淀出GearsAI、供应链数字分身等AI时代研发与运营基础设施,让商家、小二实现更高效、低成本的经营。
¤ 拓展阅读 ¤