200行极简demo - 学习如何手搓一个ReAct Agent

发布于:2025-07-20 ⋅ 阅读:(22) ⋅ 点赞:(0)

图片

本文是一篇关于如何构建一个极简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时代研发与运营基础设施,让商家、小二实现更高效、低成本的经营。

¤ 拓展阅读 ¤

3DXR技术 | 终端技术 | 音视频技术

服务端技术 | 技术质量 | 数据算法


网站公告

今日签到

点亮在社区的每一天
去签到