大模型开发 - 零手写 AI Agent:深入理解 ReAct 模式与 Java 实现

文章目录

  • 引言
  • [一、什么是 ReAct 模式?](#一、什么是 ReAct 模式?)
    • [1.1 ReAct 的起源与核心思想](#1.1 ReAct 的起源与核心思想)
    • [1.2 ReAct 与思维链(CoT)的对比](#1.2 ReAct 与思维链(CoT)的对比)
    • [1.3 ReAct 的核心执行循环](#1.3 ReAct 的核心执行循环)
    • [1.4 一个具体的执行示例](#1.4 一个具体的执行示例)
  • 二、项目架构设计
    • [2.1 系统架构图](#2.1 系统架构图)
    • [2.2 核心组件职责](#2.2 核心组件职责)
    • [2.3 项目依赖](#2.3 项目依赖)
  • 三、核心代码实现详解
    • [3.1 模型配置(ModelConfig)](#3.1 模型配置(ModelConfig))
    • [3.2 工具注解系统(Tool / ToolParam)](#3.2 工具注解系统(Tool / ToolParam))
    • [3.3 工具描述生成器(ToolUtil)](#3.3 工具描述生成器(ToolUtil))
    • [3.4 实现具体工具(AgentTools)](#3.4 实现具体工具(AgentTools))
    • [3.5 Prompt 模板设计](#3.5 Prompt 模板设计)
    • [3.6 核心 Agent 循环(ReActAgent)](#3.6 核心 Agent 循环(ReActAgent))
    • [3.7 输出解析器](#3.7 输出解析器)
    • [3.8 工具执行器](#3.8 工具执行器)
  • 四、完整执行示例
    • [4.1 启动代码](#4.1 启动代码)
    • [4.2 执行过程输出](#4.2 执行过程输出)
    • [4.3 执行流程分析](#4.3 执行流程分析)
  • 五、工程化改进建议
    • [5.1 循环计数修复](#5.1 循环计数修复)
    • [5.2 未知工具处理](#5.2 未知工具处理)
    • [5.3 自动工具注册](#5.3 自动工具注册)
    • [5.4 参数校验增强](#5.4 参数校验增强)
    • [5.5 更多工具示例](#5.5 更多工具示例)
  • 六、与主流框架的对比
  • 七、进阶扩展方向
    • [7.1 多轮对话记忆](#7.1 多轮对话记忆)
    • [7.2 并行工具调用](#7.2 并行工具调用)
    • [7.3 流式输出](#7.3 流式输出)
    • [7.4 更高级的 Agent 架构](#7.4 更高级的 Agent 架构)
  • 八、总结
  • 参考资料

面向读者 :Java 后端开发者 / AI 工程师 / 技术爱好者
阅读收获:从零搭建一个"能思考、能调用工具、能闭环"的 ReAct Agent,深入理解 AI Agent 的核心工作原理

引言

在大语言模型(LLM)蓬勃发展的今天,AI Agent 已经成为最热门的技术方向之一。与传统的问答式 AI 不同,Agent 能够自主思考、规划任务、调用工具,并最终解决复杂问题。这种能力的核心在于让 AI 具备了"行动力"------它不再只是被动回答问题,而是能够主动采取行动来完成任务。

想象一下这样的场景:你对 AI 说"帮我把 1 到 10 的整数写入一个文件",传统的 ChatGPT 只能告诉你"你可以使用 Python 的文件操作来实现...",而一个真正的 AI Agent 会直接帮你创建文件、写入内容,然后告诉你"已完成,文件在 numbers.txt"。这就是 Agent 与传统 LLM 的本质区别。

在众多 Agent 架构中,ReAct(Reasoning + Acting) 模式因其简洁优雅而备受青睐。 其核心思想是让 LLM 交替进行推理(Reasoning)和行动(Acting),通过"思考-行动-观察"的循环来解决问题。

接下来让我们从零开始,使用纯 Java 代码手写一个完整的 ReAct Agent。我们不依赖 LangChain、Spring AI 等框架,而是直接使用 OpenAI 官方 Java SDK 与大模型交互,深入理解 Agent 的工作原理。


一、什么是 ReAct 模式?

1.1 ReAct 的起源与核心思想

ReAct 由 Yao 等人在 2022 年提出(ICLR 2023 发表),论文标题为《ReAct: Synergizing Reasoning and Acting in Language Models》。其核心洞察是:将推理(Reasoning)和行动(Acting)交织在一起,可显著提升 LLM 解决复杂任务的能力

传统的 Chain-of-Thought(CoT)只关注推理,让模型"一步步思考"来提升推理能力;传统的 Action-based 方法只关注行动,让模型直接调用工具。ReAct 的创新在于将两者合一:

java 复制代码
Thought → Action → Observation → Thought → Action → Observation → ... → Final Answer

这种设计模拟了人类解决问题的认知过程:我们不会一次性想清楚所有步骤,而是边思考边行动,根据行动的反馈调整下一步计划。

1.2 ReAct 与思维链(CoT)的对比

思维链(Chain of Thought, CoT)技术,ReAct 可以看作是 CoT 的增强版:

特性 CoT(思维链) ReAct
推理能力 ✅ 支持 ✅ 支持
外部工具调用 ❌ 不支持 ✅ 支持
信息获取 仅依赖模型已有知识 可从外部获取实时信息
任务执行 只能给出建议 可以实际执行任务
自我修正 较弱 较强(基于观察结果调整)

CoT 的局限在于,模型只能基于训练数据中的知识进行推理,无法获取实时信息或执行实际操作。而 ReAct 通过引入工具调用机制,让模型具备了与外部世界交互的能力。


1.3 ReAct 的核心执行循环

ReAct Agent 的工作流程可以概括为一个迭代循环:

  1. Reason(推理):分析当前状态,思考下一步应该做什么
  2. Action(行动):决定调用哪个工具
  3. Action Input(行动输入):生成工具调用参数
  4. Observation(观察):系统执行工具并返回结果
  5. 重复上述过程,直到模型输出最终答案(Final Answer)

用流程图表示:

jAva 复制代码
┌─────────────────────────────────────────────────────────────┐
│                    ReAct 执行循环                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   用户问题 ──→ 构建 Prompt ──→ 调用 LLM                      │
│                    ↑              │                         │
│                    │              ↓                         │
│            历史记录更新    解析 LLM 输出                      │
│                    │              │                         │
│                    │              ↓                         │
│              ┌─────┴─────┐   是 FinalAnswer?                │
│              │           │        │                         │
│              │  Observation       ├── 是 ──→ 返回最终答案    │
│              │           │        │                         │
│              └─────┬─────┘        ↓ 否                      │
│                    │         执行工具                        │
│                    │              │                         │
│                    └──────────────┘                         │
└─────────────────────────────────────────────────────────────┘

1.4 一个具体的执行示例

让我们通过一个具体例子来理解 ReAct 的执行流程。假设用户提出需求:"将 1 到 10 之间的所有整数写入文件"。

第一轮

复制代码
Reason: 用户需要将 1 到 10 的整数写入文件,我需要使用 writeFile 工具来完成这个任务。
        我需要指定文件路径和要写入的内容。
Action: writeFile
ActionInput: {"file_path": "numbers.txt", "content": "1\n2\n3\n4\n5\n6\n7\n8\n9\n10"}

系统执行工具后返回

复制代码
Observation: 写入成功

第二轮

复制代码
Reason: 文件已经成功写入,任务完成。
FinalAnswer: 我已经成功将 1 到 10 的所有整数写入了 numbers.txt 文件。

这个流程清晰地展示了 ReAct 的核心特点:LLM 负责思考和决策,而具体的文件写入操作则由外部工具完成。模型通过观察工具执行结果来决定下一步行动,形成完整的闭环。


二、项目架构设计

在动手编码之前,让我们先设计整体架构。一个完整的 ReAct Agent 需要以下核心组件。

2.1 系统架构图

jva 复制代码
┌─────────────────────────────────────────────────────────────┐
│                      ReAct Agent                             │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │   Prompt    │  │   LLM API   │  │   Output Parser     │  │
│  │  Template   │  │   Client    │  │                     │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────────┐│
│  │                    Tool System                          ││
│  │  ┌──────────┐  ┌──────────┐  ┌──────────────────────┐  ││
│  │  │  @Tool   │  │@ToolParam│  │     ToolUtil         │  ││
│  │  │Annotation│  │Annotation│  │  (Reflection-based)  │  ││
│  │  └──────────┘  └──────────┘  └──────────────────────┘  ││
│  └─────────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────────┐│
│  │                   Agent Tools                           ││
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐              ││
│  │  │writeFile │  │ readFile │  │  search  │  ...         ││
│  │  └──────────┘  └──────────┘  └──────────┘              ││
│  └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘

2.2 核心组件职责

组件 职责 关键类
模型配置 管理 API Key、模型名称、服务地址 ModelConfig
工具注解 声明式定义工具元信息 @Tool, @ToolParam
工具集合 实现具体的工具逻辑 AgentTools
工具工具类 通过反射生成工具描述 ToolUtil
Agent 核心 实现 ReAct 循环逻辑 ReActAgent

2.3 项目依赖

项目基于 Java 21,只依赖两个核心库:

xml 复制代码
<dependencies>
    <!-- OpenAI 官方 Java SDK -->
    <dependency>
        <groupId>com.openai</groupId>
        <artifactId>openai-java</artifactId>
        <version>0.32.0</version>
    </dependency>

    <!-- JSON 处理(工具实现中使用 Jackson,来自 SDK 传递依赖) -->
    <dependency>
        <groupId>com.alibaba.fastjson2</groupId>
        <artifactId>fastjson2</artifactId>
        <version>2.0.56</version>
    </dependency>
</dependencies>

为什么选择纯 Java 实现?

选择 OpenAI 官方 SDK 而非 Spring AI 等框架,是因为我们要能够理解 Agent 的底层实现原理,而非仅仅学会调用封装好的 API。这种方式的优势包括:

  1. 原理透明:每一行代码都可追溯,便于理解 Agent 工作机制
  2. 依赖精简:仅需 OpenAI SDK 和 JSON 库,启动快、包体小
  3. 灵活可控:可根据业务需求自由定制 Prompt、解析逻辑
  4. 学习价值:为后续使用框架打下坚实基础

三、核心代码实现详解

3.1 模型配置(ModelConfig)

首先,我们需要配置与大模型的连接。这里我们使用阿里云的 DashScope 服务(兼容 OpenAI API 格式):

java 复制代码
public class ModelConfig {
    // 从环境变量获取 API Key,安全且便于管理
    public static final String API_KEY = System.getenv("DASHSCOPE_API_KEY");

    // 使用 DashScope 的 OpenAI 兼容接口
    public static final String BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";

    // 使用通义千问 Max 模型
    public static final String LLM_NAME = "qwen-max";
}

设计要点

  • 环境变量读取 API Key:避免将敏感信息硬编码到代码中,这是安全最佳实践
  • OpenAI 兼容接口 :DashScope 提供了与 OpenAI API 兼容的接口,这意味着只需修改 BASE_URLAPI_KEY,即可无缝切换到 OpenAI、DeepSeek 等其他服务商
  • 模型选择qwen-max 是目前通义千问系列中推理能力最强的模型,适合需要复杂推理的 Agent 场景

3.2 工具注解系统(Tool / ToolParam)

为了让 Agent 能够识别和调用工具,我们设计了一套基于注解的工具系统。这套设计借鉴了 Spring 框架的声明式编程理念。

@Tool 注解:标记一个方法为可被 Agent 调用的工具

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Tool {
    String description();  // 工具功能描述,供 LLM 理解工具用途
}

@ToolParam 注解:描述工具参数

java 复制代码
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ToolParam {
    String description();  // 参数描述,帮助 LLM 理解如何构造输入
}

注解设计的核心价值

  1. 声明式定义:工具的元信息直接标注在方法上,代码即文档
  2. 运行时可见@Retention(RetentionPolicy.RUNTIME) 确保注解在运行时可被反射读取
  3. 自动化集成:无需手动维护工具列表,系统自动发现和注册工具

3.3 工具描述生成器(ToolUtil)

ToolUtil 类负责通过反射扫描工具类,自动生成供 LLM 使用的工具描述:

java 复制代码
public class ToolUtil {

    public static String getToolDescription(Class<?> clazz) {
        List<String> toolNameList = new ArrayList<>();
        List<String> formattedToolList = new ArrayList<>();

        // 遍历类中的所有方法
        for (Method declaredMethod : clazz.getDeclaredMethods()) {
            // 检查是否有 @Tool 注解
            if (declaredMethod.isAnnotationPresent(Tool.class)) {
                Tool toolAnnotation = declaredMethod.getAnnotation(Tool.class);
                String toolName = declaredMethod.getName();
                String toolDescription = toolAnnotation.description();

                // 获取参数描述
                String paramDescription = declaredMethod
                    .getParameters()[0]
                    .getAnnotation(ToolParam.class)
                    .description();

                // 格式化工具描述
                String formattedTool = String.format(
                    "- toolName=%s, toolDescription=%s, paramDescription=%s",
                    toolName, toolDescription, paramDescription
                );
                formattedToolList.add(formattedTool);
                toolNameList.add(toolName);
            }
        }

        return String.join("\n\n", formattedToolList);
    }
}
  1. 自动发现 :使用 Java 反射机制扫描所有带 @Tool 注解的方法
  2. 零配置:添加新工具只需在方法上加注解,无需修改任何配置文件
  3. LLM 友好:生成的描述格式清晰,便于模型理解工具用途和参数要求

生成的工具描述示例:

java 复制代码
- toolName=writeFile, toolDescription=将指定内容写入本地文件。, paramDescription=包含 'file_path' 和 'content' 的 JSON 字符串。

3.4 实现具体工具(AgentTools)

现在我们来实现一个具体的工具------文件写入工具:

java 复制代码
public class AgentTools {

    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 将指定内容写入本地文件。
     * @param jsonInput 一个包含 'file_path' 和 'content' 的 JSON 字符串。
     * @return 执行结果的描述字符串。
     */
    @Tool(description = "将指定内容写入本地文件。")
    public String writeFile(
        @ToolParam(description = "包含 'file_path' 和 'content' 的 JSON 字符串。")
        String jsonInput
    ) {
        try {
            // 解析 JSON 输入
            JsonNode rootNode = objectMapper.readTree(jsonInput);
            String filePath = rootNode.get("file_path").asText();
            String content = rootNode.get("content").asText();

            // 执行文件写入
            try (FileWriter writer = new FileWriter(filePath)) {
                writer.write(content);
                return "写入成功";
            } catch (IOException e) {
                return String.format("写入文件 '%s' 时发生错误: %s", filePath, e.getMessage());
            }
        } catch (Exception e) {
            return String.format("解析输入或执行工具时出错: %s", e.getMessage());
        }
    }
}

设计细节解析

  1. JSON 字符串作为输入:这与 LLM 生成的格式一致,避免复杂的类型转换
  2. 简洁的返回值:返回"写入成功"而非冗长的描述,让 LLM 能快速理解执行结果
  3. 完善的错误处理
    • JSON 解析失败时返回明确的错误信息
    • 文件写入失败时返回具体的异常原因
    • 这些信息会作为 Observation 反馈给 LLM,帮助它调整策略

3.5 Prompt 模板设计

Prompt 设计是 ReAct Agent 的灵魂。一个优秀的 Prompt 需要清晰地告诉 LLM:你是谁、你能做什么、你应该如何输出。

java 复制代码
private static final String REACT_PROMPT_TEMPLATE = """

    # 角色定义
    你是一个强大的 AI 助手,通过思考和使用工具来解决用户的问题。

    # 任务
    你的任务是尽你所能回答以下问题。你可以使用以下工具:
    {tools}

    # 规则
    - Action中只需要返回工具的名字,比如writeFile,不要返回以下格式toolName=writeFile
    - 每次只做一次Reason/Action/ActionInput或者FinalAnswer的输出过程,不要一次性都做了
    - 每次返回的过程中不要自己生成Observation的内容
    - 返回Reason/Action/ActionInput的时候不要生成并返回Observation的内容

    # 输出过程参考
    第一轮
    Reason: 你思考的过程
    Action: 你的下一步动作,你想要执行的工具是哪个,必须是{tools}中的一个
    ActionInput: 你要调用的工具的输入参数是什么

    第二轮
    Reason: 你思考的过程
    Action: 你的下一步动作
    ActionInput: 你要调用的工具的输入参数

    ...

    最后一轮
    FinalAnswer: 表示最终的答案,只需要最后输出就可以了

    # 用户需求
    Question: {input}

    # 历史聊天记录
    {history}
    """;

Prompt 设计要点详解

部分 作用 设计考量
角色定义 设定 AI 的身份和能力边界 让模型明确自己是一个"使用工具解决问题"的助手
工具清单 告知可用工具 {tools} 占位符会被替换为实际的工具描述
规则约束 控制输出格式 防止模型自行编造 Observation,确保每轮只输出一次
输出示例 引导输出格式 通过多轮对话示例展示期望的输出结构
历史记录 维持上下文 {history} 保存之前的推理过程,实现"记忆"能力

为什么规则约束如此重要?

没有严格的规则约束,LLM 可能会:

  • 一次性输出多轮的 Reason/Action/ActionInput
  • 自己编造 Observation 内容(而不是等待真实的工具执行结果)
  • 跳过工具调用直接给出答案(即使它无法完成任务)

通过明确的规则,我们确保了 LLM 的行为可预测、可控制。

3.6 核心 Agent 循环(ReActAgent)

现在让我们看看 ReAct Agent 的核心------主循环逻辑:

java 复制代码
public class ReActAgent {

    private OpenAIClient apiClient;

    public ReActAgent(OpenAIClient apiClient) {
        this.apiClient = apiClient;
    }

    public String run(String input) throws NoSuchMethodException {
        // 1. 注册可用工具
        HashMap<String, Method> tools = new HashMap<>();
        tools.put("writeFile", AgentTools.class.getMethod("writeFile", String.class));

        // 2. 初始化历史记录,用于维护推理上下文
        StringBuilder history = new StringBuilder();

        // 3. 最大循环次数,防止无限循环
        int i = 0;
        while (i < 10) {
            try {
                // 4. 构建完整 Prompt
                String prompt = buildPrompt(input, history.toString());

                // 5. 调用大模型
                ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
                        .addUserMessage(prompt)
                        .model(ModelConfig.LLM_NAME)
                        .build();
                ChatCompletion chatCompletion = apiClient.chat().completions().create(params);
                String rawLlmOutput = chatCompletion.choices().get(0).message().content().get();

                System.out.println("大模型原始输出:" + rawLlmOutput);

                // 6. 解析 LLM 输出
                ParsedOutput parsedOutput = parseLlmOutput(rawLlmOutput);

                // 7. 如果是最终答案,直接返回
                if (parsedOutput.type.equals("final_answer")) {
                    return parsedOutput.answer;
                }

                // 8. 执行工具并获取结果
                String observation = executeTool(parsedOutput, tools);
                System.out.println("工具执行结果:" + observation);

                // 9. 更新历史记录
                history.append("Reason: ").append(parsedOutput.reason).append("\n")
                        .append("Action: ").append(parsedOutput.action).append("\n")
                        .append("ActionInput: ").append(parsedOutput.actionInputStr).append("\n")
                        .append("Observation: ").append(observation).append("\n");

            } catch (Exception e) {
                e.printStackTrace();
                i++;
            }
        }

        return "达到了循环最大次数";
    }
}

核心流程逐步解析

  1. 工具注册:将可用工具及其方法引用存入 HashMap,便于后续通过名称查找和调用
  2. 历史记录初始化:使用 StringBuilder 累积每轮的执行记录
  3. 循环控制:设置最大循环次数为 10,防止 LLM 陷入无限循环
  4. Prompt 构建:将用户输入和历史记录填充到模板中
  5. LLM 调用:使用 OpenAI SDK 发送请求并获取响应
  6. 输出解析:区分"继续行动"和"最终答案"两种情况
  7. 终止判断:如果输出包含 FinalAnswer,则返回结果
  8. 工具执行:根据解析结果调用对应工具
  9. 记忆更新:将本轮的 Reason、Action、ActionInput、Observation 追加到历史记录

Prompt 构建方法

java 复制代码
private String buildPrompt(String input, String history) {
    String prompt = REACT_PROMPT_TEMPLATE.replace("{tools}", ToolUtil.getToolDescription(AgentTools.class));
    prompt = prompt.replace("{input}", input);
    prompt = prompt.replace("{history}", history);
    return prompt;
}

这个方法完成三个占位符的替换:

  • {tools} → 工具描述列表
  • {input} → 用户的原始问题
  • {history} → 之前的对话历史

3.7 输出解析器

LLM 的输出是自由文本,需要被解析为结构化数据以便程序处理:

java 复制代码
private ParsedOutput parseLlmOutput(String llmOutput) {
    // 1. 检查是否为最终答案
    if (llmOutput.contains("FinalAnswer: ")) {
        return new ParsedOutput(
            "final_answer",
            llmOutput.split("FinalAnswer: ")[1].strip(),
            null, null, null, null
        );
    }

    // 2. 使用正则表达式提取 Reason、Action、ActionInput
    Pattern actionPattern = Pattern.compile(
        "Reason:(.*?)Action:(.*?)ActionInput:(.*)",
        Pattern.DOTALL
    );
    Matcher matcher = actionPattern.matcher(llmOutput);

    if (matcher.find()) {
        String reason = matcher.group(1).trim();
        String action = matcher.group(2).trim();
        String actionInputStr = matcher.group(3).trim();

        // 3. 处理可能的 Markdown 代码块格式
        if (actionInputStr.startsWith("```json")) {
            actionInputStr = actionInputStr.substring(7);
        }
        if (actionInputStr.endsWith("```")) {
            actionInputStr = actionInputStr.substring(0, actionInputStr.length() - 3);
        }
        actionInputStr = actionInputStr.trim();

        return new ParsedOutput("action", null, reason, action, actionInputStr, null);
    }

    // 4. 解析失败
    return new ParsedOutput(
        "error", null, null, null, null,
        String.format("解析LLM输出失败: '%s'", llmOutput)
    );
}

// 使用 Java Record 定义解析结果数据结构
private record ParsedOutput(
    String type,           // 输出类型:final_answer / action / error
    String answer,         // 最终答案(当 type 为 final_answer 时)
    String reason,         // 推理过程
    String action,         // 要执行的工具名
    String actionInputStr, // 工具输入参数(JSON 字符串)
    String message         // 错误信息(当 type 为 error 时)
) {}

解析器设计要点

  1. 优先级判断 :首先检查是否包含 FinalAnswer:,这是终止循环的信号
  2. 正则匹配 :使用 Pattern.DOTALL 模式,使 . 能匹配换行符,处理多行输出
  3. 格式兼容 :处理 LLM 可能生成的 Markdown 代码块格式(如 ```````json```` 包裹的 JSON)
  4. 不可变数据:使用 Java Record 定义数据结构,简洁且线程安全

3.8 工具执行器

工具执行器通过反射调用实际的工具方法:

java 复制代码
private static String executeTool(
    ParsedOutput parsedOutput,
    HashMap<String, Method> tools
) throws IllegalAccessException, InvocationTargetException {

    String toolName = parsedOutput.action;
    String toolParams = parsedOutput.actionInputStr;

    // 根据工具名查找方法
    Method toolMethod = tools.get(toolName);

    // 通过反射调用工具方法
    Object observation = toolMethod.invoke(new AgentTools(), toolParams);

    return String.valueOf(observation);
}

反射调用的优势

  1. 动态调用:无需硬编码 switch-case 分支,根据工具名动态查找并调用
  2. 易于扩展:添加新工具只需注册到 HashMap,无需修改执行器代码
  3. 解耦设计:Agent 核心逻辑与具体工具实现完全分离

四、完整执行示例

4.1 启动代码

java 复制代码
public static void main(String[] args) throws Exception {
    // 1. 创建 OpenAI 客户端
    OpenAIClient apiClient = OpenAIOkHttpClient.builder()
            .apiKey(ModelConfig.API_KEY)
            .baseUrl(ModelConfig.BASE_URL)
            .build();

    // 2. 创建 ReAct Agent
    ReActAgent reActAgent = new ReActAgent(apiClient);

    // 3. 执行任务
    String result = reActAgent.run("将1到10中间的所有整数写到文件中");

    // 4. 输出结果
    System.out.println("最终结果:" + result);
}

4.2 执行过程输出

java 复制代码
大模型原始输出:Reason: 用户需要将1到10之间的所有整数写入到一个文件中。
我需要使用writeFile工具来完成这个任务,需要指定文件路径和内容。

Action: writeFile
ActionInput: {"file_path": "numbers.txt", "content": "1\n2\n3\n4\n5\n6\n7\n8\n9\n10"}

工具执行结果:写入成功

大模型原始输出:Reason: 文件已经成功写入,用户的任务已经完成。
FinalAnswer: 我已经成功将1到10之间的所有整数写入到了numbers.txt文件中。

最终结果:我已经成功将1到10之间的所有整数写入到了numbers.txt文件中。

4.3 执行流程分析

从输出可以清晰看到 ReAct 的工作过程:

轮次 阶段 内容
1 Reason 分析用户需求,决定使用 writeFile 工具
1 Action writeFile
1 ActionInput 构造 JSON 参数,包含文件路径和内容
1 Observation 系统执行工具后返回"写入成功"
2 Reason 观察到成功结果,判断任务完成
2 FinalAnswer 返回最终答案给用户

这个两轮对话完美地展示了 ReAct 的核心机制:思考→行动→观察→再思考→最终答案


五、工程化改进建议

当前实现是一个最小可行版本,适合学习和理解原理。在生产环境中,还需要进行以下改进:

5.1 循环计数修复

当前实现中,i 只在异常时递增,可能导致正常情况下无限循环:

java 复制代码
// 当前实现
while (i < 10) {
    try {
        // ... 正常逻辑
    } catch (Exception e) {
        i++;  // 只有异常时才递增
    }
}

// 建议改进
while (i < 10) {
    try {
        // ... 正常逻辑
    } catch (Exception e) {
        e.printStackTrace();
    }
    i++;  // 每轮都递增
}

5.2 未知工具处理

当模型输出未知工具名时,当前实现会触发 NullPointerException

java 复制代码
private static String safeExecuteTool(ParsedOutput parsedOutput, HashMap<String, Method> tools) {
    String toolName = parsedOutput.action;

    // 检查工具是否存在
    Method toolMethod = tools.get(toolName);
    if (toolMethod == null) {
        return "未知工具: " + toolName + "。请检查工具清单并重新选择。";
    }

    // ... 执行工具逻辑
}

5.3 自动工具注册

将手动注册改为反射扫描,添加新工具时无需修改 Agent 代码:

java 复制代码
private static HashMap<String, Method> registerTools(Class<?> toolClass) {
    HashMap<String, Method> tools = new HashMap<>();
    for (Method method : toolClass.getDeclaredMethods()) {
        if (method.isAnnotationPresent(Tool.class)) {
            tools.put(method.getName(), method);
        }
    }
    return tools;
}

// 使用
HashMap<String, Method> tools = registerTools(AgentTools.class);

5.4 参数校验增强

模型可能输出不合法的 JSON,建议增加基础校验:

java 复制代码
// 最小 JSON 校验
if (toolParams == null || !toolParams.trim().startsWith("{") || !toolParams.trim().endsWith("}")) {
    return "ActionInput 不是合法 JSON 对象,请输出形如 {\"key\":\"value\"} 的参数。";
}

5.5 更多工具示例

扩展工具集以支持更多场景:

java 复制代码
@Tool(description = "读取本地文件内容")
public String readFile(@ToolParam(description = "文件路径") String filePath) {
    try {
        return Files.readString(Path.of(filePath));
    } catch (IOException e) {
        return "读取文件失败: " + e.getMessage();
    }
}

@Tool(description = "执行网络搜索")
public String webSearch(@ToolParam(description = "搜索关键词的 JSON") String jsonInput) {
    // 调用搜索 API 实现
}

@Tool(description = "执行数学计算")
public String calculate(@ToolParam(description = "包含 expression 的 JSON") String jsonInput) {
    // 使用表达式求值库实现
}

六、与主流框架的对比

维度 手写实现 LangChain Spring AI
学习价值 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
灵活性 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
开发效率 ⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
生产就绪 ⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
社区生态 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐

建议:学习原理用手写实现,工程落地用成熟框架。理解了底层原理后,使用框架会更加得心应手。


七、进阶扩展方向

掌握了基础的 ReAct 实现后,可以进一步探索以下方向:

7.1 多轮对话记忆

当前实现在单次任务结束后会清空历史。可以引入持久化存储,实现跨会话记忆:

java 复制代码
public class ChatMemory {
    private final List<Message> messages = new ArrayList<>();

    public void addMessage(Message message) {
        messages.add(message);
        // 可选:持久化到数据库
    }

    public String getHistory() {
        return messages.stream()
            .map(Message::toString)
            .collect(Collectors.joining("\n"));
    }
}

7.2 并行工具调用

当多个工具之间没有依赖关系时,可以并行执行以提升效率:

java 复制代码
List<CompletableFuture<String>> futures = actions.stream()
    .map(action -> CompletableFuture.supplyAsync(() -> executeTool(action)))
    .toList();

List<String> results = futures.stream()
    .map(CompletableFuture::join)
    .toList();

7.3 流式输出

对于长时间运行的任务,可以使用流式输出提升用户体验:

java 复制代码
apiClient.chat().completions().createStream(params)
    .forEach(chunk -> {
        String content = chunk.choices().get(0).delta().content().orElse("");
        System.out.print(content);  // 实时输出
    });

7.4 更高级的 Agent 架构

  • Plan-and-Execute:先制定完整计划,再逐步执行
  • Tree of Thoughts:探索多条推理路径,选择最优解
  • Multi-Agent:多个 Agent 协作完成复杂任务
  • Self-Reflection:Agent 自我反思和错误修正

八、总结

我们从零开始实现了一个完整的 ReAct Agent,涵盖以下核心知识点:

主题 内容要点
ReAct 原理 推理与行动交替进行,通过观察结果迭代优化
工具系统 基于注解的声明式定义,反射机制自动发现
Prompt 工程 角色设定、规则约束、示例引导、历史记录
输出解析 正则匹配提取结构化信息,处理多种输出格式
循环控制 最大次数限制、终止条件判断、异常处理

ReAct 模式是构建 AI Agent 的基础范式,掌握其原理对于理解更复杂的 Agent 架构至关重要。通过纯 Java 实现,我们深入理解了 Agent 的工作机制------它不是魔法,而是精心设计的 Prompt + 循环 + 工具调用的组合。

AI Agent 是一个快速发展的领域,ReAct 只是众多架构之一。建议读者在掌握本文内容后,进一步探索 LangChain、Spring AI 等成熟框架,将原理知识转化为工程实践能力。


参考资料


相关推荐
翱翔的苍鹰2 小时前
法律问答机器人”技术方案”的实现
人工智能·rnn·深度学习·自然语言处理
m0_603888712 小时前
Structured Over Scale Learning Spatial Reasoning from Educational Video
人工智能·深度学习·机器学习·ai·论文速览
Bruk.Liu2 小时前
(LangChain实战4):LangChain消息模版PromptTemplate
人工智能·python·langchain
HyperAI超神经2 小时前
【TVM教程】设备/目标交互
人工智能·深度学习·神经网络·microsoft·机器学习·交互·gpu算力
应用市场2 小时前
#AI对话与AI绘画的底层原理:从概率预测到创意生成的完整解析
人工智能·ai作画
肾透侧视攻城狮2 小时前
《解锁 PyTorch 张量:多维数据操作与 GPU 性能优化全解析》
人工智能·numpy·张量的索引和切片·张量形状变换·张量数学运算操作·张量的gpu加速·张量与 numpy 的互操作
Tadas-Gao2 小时前
大模型幻觉治理新范式:SCA与[PAUSE]注入技术的深度解析与创新设计
人工智能·深度学习·机器学习·架构·大模型·llm
查无此人byebye2 小时前
从零解读CLIP核心源码:PyTorch实现版逐行解析
人工智能·pytorch·python·深度学习·机器学习·自然语言处理·音视频
PKUMOD2 小时前
论文导读 | 在长上下文及复杂任务中的递归式语言模型架构
人工智能·语言模型·架构