大模型 · 2024-12-31 0

Java 开发大模型 Agent 的 ReAct 模式及 langchain4j 使用

一、ReAct 架构

在早期的大语言模型(LLM)应用中,提示工程(Prompt Engineering)是连接用户意图与模型输出的核心手段。然而,纯提示驱动的方案在多步推理、工具调用和动态环境交互中常显不足。ReAct框架(Reasoning+Acting)应运而生,通过将链式推理(Reasoning) 与环境行动(Acting) 结合,构建出能主动思考、决策并执行复杂任务的智能体(Agent)。

ReAct(Reasoning and Action)是一个框架,其概念来源于一篇论文,其核心思想,就是通过思维链的方式,引导模型将复杂问题进行拆分,一步一步地进行推理(Reasoning)和行动(Action),同时还引入了观察(Observation)环节,在每次执行(Action)之后,都会先观察(Observation)当前现状,然后再进行下一步的推理(Reason)。

ReAct这个框架,就是要让LLM,进行推理,然后采取行动与外界环境互动。

ReAct这个框架,就是要让开发者一步步引导LLM进行推理,然后根据推理的结果,判断采取哪个行动。

ReAct 架构:AI 代理的“推理-执行”闭环。

ReAct(Reasoning + Action)并非简单的技术模块,而是为大语言模型(LLM)赋予自主任务解决能力的核心范式。区别于传统 LLM 直接输出答案的“黑箱模式”,ReAct 强制代理在“思考”与“行动”之间形成循环闭环,具体表现为两步交替执行:

推理(Reasoning):基于当前任务目标与已有信息,分析缺口、规划下一步操作方向,相当于代理的“大脑决策”环节;
行动(Action):根据推理结论调用外部工具(如搜索引擎、计算器),获取新数据或执行特定操作,是代理与外部环境交互的“执行环节”。

Thought → Act → Observation → Thought → ... → Final Answer

ReAct框架的核心组件包括:

  • 思维链(Chain of Thought):将一个大的复杂任务进行拆解,拆解成多个思维步骤。
  • 推理(Reasoning):负责分析和处理输入的数据,生成有效的决策。
  • 行动(Action):执行具体的操作,比如搜索、执行代码,或者其余自定义的行动。
  • 观察(Observation):监控和收集环境反馈的数据,为下一步的推理和行动提供依据。

核心组件拆解

模块 功能说明 实现示例
LLM Core 生成推理与行动指令 GPT-4、Claude 3、Llama 3
Tool Engine 工具调度与执行 LangChain Tools, LlamaIndex
Memory 存储历史观察与推理链 Redis、向量数据库
Parser 解析LLM输出为结构化操作 Pydantic + 正则表达式

二、Java 开发实现 ReActAgent

1.prompt

你需要解决一个问题。为此,你需要将问题分解为多个步骤。对于每个步骤,首先使用 <thought> 思考要做什么,然后使用可用工具之一决定一个 <action>。接着,你将根据你的行动从环境/工具中收到一个 <observation>。持续这个思考和行动的过程,直到你有足够的信息来提供 <final_answer>。

所有步骤请严格使用以下 XML 标签格式输出:
- <question> 用户问题
- <thought> 思考
- <action> 采取的工具操作
- <observation> 工具或环境返回的结果
- <final_answer> 最终答案

⸻

例子 1:

<question>埃菲尔铁塔有多高?</question>
<thought>我需要找到埃菲尔铁塔的高度。可以使用搜索工具。</thought>
<action>get_height("埃菲尔铁塔")</action>
<observation>埃菲尔铁塔的高度约为330米(包含天线)。</observation>
<thought>搜索结果显示了高度。我已经得到答案了。</thought>
<final_answer>埃菲尔铁塔的高度约为330米。</final_answer>

⸻

请严格遵守:
- 你每次回答都必须包括两个标签,第一个是 <thought>,第二个是 <action> 或 <final_answer>
- 输出 <action> 后立即停止生成,等待真实的 <observation>,擅自生成 <observation> 将导致错误
- 如果 <action> 中的某个工具参数有多行的话,请使用 \n 来表示,如:<action>write_to_file("/tmp/test.txt", "a\nb\nc")</action>
- 工具参数中的文件路径请使用绝对路径,不要只给出一个文件名。比如要写 write_to_file("/tmp/test.txt", "内容"),而不是 write_to_file("test.txt", "内容")

⸻

本次任务可用工具:
- writeToFile(String, String): 工具方法
- readFile(String): 工具方法
- runTerminalCommand(String): 工具方法

⸻

环境信息:
操作系统:Linux
工作目录在:/home/zxm/test

2.pom

<properties>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
    <dependency>
        <groupId>org.apache.httpcomponents.client5</groupId>
        <artifactId>httpclient5</artifactId>
        <version>5.3</version>
    </dependency>

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.16.1</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.38</version>
        <optional>true</optional>
    </dependency>
</dependencies>

3.工具类

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * 工具函数集合,提供文件操作和终端命令执行功能
 */
public class ToolFunctions {

    /**
     * 读取文件内容
     * @param filePath 文件路径
     * @return 文件内容
     */
    public static String readFile(String filePath) {
        try {
            Path path = Paths.get(filePath);
            return Files.readString(path);
        } catch (IOException e) {
            return "读取文件错误:" + e.getMessage();
        }
    }

    /**
     * 将指定内容写入指定文件
     * @param filePath 文件路径
     * @param content 要写入的内容
     * @return 操作结果
     */
    public static String writeToFile(String filePath, String content) {
        try {
            Path path = Paths.get(filePath);
            Files.createDirectories(path.getParent());
            Files.writeString(path, content.replace("\\n", "\n"));
            return "写入成功";
        } catch (IOException e) {
            return "写入文件错误:" + e.getMessage();
        }
    }

    /**
     * 执行终端命令
     * @param command 要执行的命令
     * @return 执行结果
     */
    public static String runTerminalCommand(String command) {
        try {
            ProcessBuilder processBuilder = new ProcessBuilder();

            // 根据操作系统选择shell
            String os = System.getProperty("os.name").toLowerCase();
            if (os.contains("win")) {
                processBuilder.command("cmd.exe", "/c", command);
            } else {
                processBuilder.command("sh", "-c", command);
            }

            Process process = processBuilder.start();

            // 读取输出
            StringBuilder output = new StringBuilder();
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    output.append(line).append("\n");
                }
            }

            // 读取错误输出
            StringBuilder error = new StringBuilder();
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    error.append(line).append("\n");
                }
            }

            int exitCode = process.waitFor();

            if (exitCode == 0) {
                return "执行成功\n" + output.toString();
            } else {
                return "执行失败\n" + error.toString();
            }
        } catch (Exception e) {
            return "工具执行错误:" + e.getMessage();
        }
    }
}
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ToolUtils {

    private final static Map<String, Method> tools;

    static {
        tools = new HashMap<>();
        try {
            tools.put("readFile", ToolFunctions.class.getMethod("readFile", String.class));
            tools.put("writeToFile", ToolFunctions.class.getMethod("writeToFile", String.class, String.class));
            tools.put("runTerminalCommand", ToolFunctions.class.getMethod("runTerminalCommand", String.class));
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("无法找到工具方法", e);
        }
    }

    /**
     * 执行工具方法
     */
    public static String executeTool(String toolName, List<String> args) throws Exception {
        Method method = tools.get(toolName);
        if (method == null) {
            throw new IllegalArgumentException("未知工具: " + toolName);
        }

        Object[] methodArgs = new Object[args.size()];
        Class<?>[] paramTypes = method.getParameterTypes();

        for (int i = 0; i < args.size(); i++) {
            String arg = args.get(i);
            Class<?> paramType = paramTypes[i];

            if (paramType == String.class) {
                methodArgs[i] = arg;
            } else if (paramType == int.class || paramType == Integer.class) {
                methodArgs[i] = Integer.parseInt(arg);
            } else if (paramType == long.class || paramType == Long.class) {
                methodArgs[i] = Long.parseLong(arg);
            } else if (paramType == double.class || paramType == Double.class) {
                methodArgs[i] = Double.parseDouble(arg);
            } else if (paramType == boolean.class || paramType == Boolean.class) {
                methodArgs[i] = Boolean.parseBoolean(arg);
            } else {
                methodArgs[i] = arg;
            }
        }

        Object result = method.invoke(null, methodArgs);
        return result != null ? result.toString() : "null";
    }

    /**
     * 生成工具列表字符串
     */
    public static String getToolList() {
        List<String> toolDescriptions = new ArrayList<>();
        for (Method method : tools.values()) {
            String name = method.getName();
            String signature = getMethodSignature(method);
            toolDescriptions.add("- " + name + signature + ": 工具方法");
        }
        return String.join("\n", toolDescriptions);
    }

    /**
     * 获取方法签名
     */
    private static String getMethodSignature(Method method) {
        StringBuilder sb = new StringBuilder("(");
        Class<?>[] paramTypes = method.getParameterTypes();
        for (int i = 0; i < paramTypes.length; i++) {
            if (i > 0) sb.append(", ");
            sb.append(paramTypes[i].getSimpleName());
        }
        sb.append(")");
        return sb.toString();
    }
}
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.util.Timeout;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class CallUtils {

    public static String callQwenAPI(List<Map<String, String>> messages,
                                     String apiUrl, String apiModel, String apiKey) {
        RequestConfig config = RequestConfig.custom()
                .setConnectionKeepAlive(Timeout.ofSeconds(600))
                .setConnectTimeout(Timeout.ofSeconds(600))
                .setConnectionRequestTimeout(Timeout.ofSeconds(600))
                .setResponseTimeout(Timeout.ofSeconds(600))
                .build();

        try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(config).build()) {
            HttpPost httpPost = new HttpPost(apiUrl);

            // 设置请求头
            httpPost.setHeader("Content-Type", "application/json");
            httpPost.setHeader("Authorization", "Bearer " + apiKey);

            // 构建请求体
            Map<String, Object> requestBody = new HashMap<>();
            requestBody.put("model", apiModel);
            requestBody.put("messages", messages);
            requestBody.put("temperature", 0.7);
            requestBody.put("max_tokens", 2000);

            // 使用Jackson序列化JSON
            ObjectMapper objectMapper = new ObjectMapper();
            String jsonRequest = objectMapper.writeValueAsString(requestBody);

            // 设置请求体
            StringEntity entity = new StringEntity(jsonRequest, ContentType.APPLICATION_JSON);
            httpPost.setEntity(entity);

            // 发送请求
            try (CloseableHttpResponse response = httpClient.execute(httpPost, (HttpContext) null)) {
                int statusCode = response.getCode();

                if (statusCode == 200) {
                    // 读取响应
                    String responseBody = new String(response.getEntity().getContent().readAllBytes());

                    // 使用Jackson解析JSON响应
                    JsonNode rootNode = objectMapper.readTree(responseBody);
                    JsonNode choicesNode = rootNode.get("choices");
                    JsonNode messageNode = choicesNode.get(0).get("message");
                    return messageNode.get("content").asText();
                } else {
                    System.err.println("API请求失败,响应码: " + statusCode);
                    return null;
                }
            }
        } catch (Exception e) {
            System.err.println("调用通义千问API失败: " + e.getMessage());
            return null;
        }
    }
}

4.ReActAgent

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ReActAgent {

    private final String apiKey;

    private final String apiUrl;

    private final String apiModel;

    public ReActAgent(String apiKey, String apiUrl, String apiModel) {
        this.apiKey = apiKey;
        this.apiUrl = apiUrl;
        this.apiModel = apiModel;
    }

    public String run(String task) {
        System.out.println("开始执行任务: " + task);

        List<Map<String, String>> messages = new ArrayList<>();
        messages.add(Map.of("role", "system", "content", getDefaultSystemPrompt()));
        messages.add(Map.of("role", "user", "content", "<question>" + task + "</question>"));

        // 防止无限循环
        int maxIterations = 10;
        int iteration = 0;

        while (iteration < maxIterations) {
            iteration++;
            System.out.println("--- 第 " + iteration + " 轮 ---");

            long begin = System.currentTimeMillis();
            String content = CallUtils.callQwenAPI(messages, apiUrl, apiModel, apiKey);
            messages.add(Map.of("role", "assistant", "content", content));

            long end = System.currentTimeMillis();
            System.out.println(String.format("大模型调用耗时:%s 秒", (end - begin) / 1000));

            if (content == null) {
                return "API调用失败";
            }

            // 检测 Thought
            Pattern thoughtPattern = Pattern.compile("<thought>(.*?)</thought>", Pattern.DOTALL);
            Matcher thoughtMatcher = thoughtPattern.matcher(content);
            if (thoughtMatcher.find()) {
                String thought = thoughtMatcher.group(1);
                System.out.println("Thought: " + thought);
            }

            // 检测是否输出 Final Answer
            if (content.contains("<final_answer>")) {
                Pattern finalAnswerPattern = Pattern.compile("<final_answer>(.*?)</final_answer>", Pattern.DOTALL);
                Matcher finalAnswerMatcher = finalAnswerPattern.matcher(content);
                if (finalAnswerMatcher.find()) {
                    String finalAnswer = finalAnswerMatcher.group(1);
                    System.out.println("Final Answer: " + finalAnswer);
                    return finalAnswer;
                }
            }

            // 检测 Action
            Pattern actionPattern = Pattern.compile("<action>(.*?)</action>", Pattern.DOTALL);
            Matcher actionMatcher = actionPattern.matcher(content);
            if (!actionMatcher.find()) {
                System.out.println("模型未输出有效的action");
                return "执行失败:模型未输出有效的action";
            }

            String action = actionMatcher.group(1);
            ActionResult actionResult = parseAction(action);

            System.out.println("Action: " + actionResult.getToolName() + "(" +
                    String.join(", ", actionResult.getArgs()) + ")");

            try {
                String observation = ToolUtils.executeTool(actionResult.getToolName(), actionResult.getArgs());
                System.out.println("Observation: " + observation);
                messages.add(Map.of("role", "user", "content", "<observation>" + observation + "</observation>"));
            } catch (Exception e) {
                String observation = "工具执行错误:" + e.getMessage();
                System.out.println("Observation: " + observation);
                messages.add(Map.of("role", "user", "content", "<observation>" + observation + "</observation>"));
            }
        }

        return "执行超时:达到最大迭代次数";
    }

    /**
     * 默认系统提示
     */
    private String getDefaultSystemPrompt() {
        return """
                你需要解决一个问题。为此,你需要将问题分解为多个步骤。对于每个步骤,首先使用 <thought> 思考要做什么,然后使用可用工具之一决定一个 <action>。接着,你将根据你的行动从环境/工具中收到一个 <observation>。持续这个思考和行动的过程,直到你有足够的信息来提供 <final_answer>。

                所有步骤请严格使用以下 XML 标签格式输出:
                - <question> 用户问题
                - <thought> 思考
                - <action> 采取的工具操作
                - <observation> 工具或环境返回的结果
                - <final_answer> 最终答案

                ⸻

                例子 1:

                <question>埃菲尔铁塔有多高?</question>
                <thought>我需要找到埃菲尔铁塔的高度。可以使用搜索工具。</thought>
                <action>get_height("埃菲尔铁塔")</action>
                <observation>埃菲尔铁塔的高度约为330米(包含天线)。</observation>
                <thought>搜索结果显示了高度。我已经得到答案了。</thought>
                <final_answer>埃菲尔铁塔的高度约为330米。</final_answer>

                ⸻

                请严格遵守:
                - 你每次回答都必须包括两个标签,第一个是 <thought>,第二个是 <action> 或 <final_answer>
                - 输出 <action> 后立即停止生成,等待真实的 <observation>,擅自生成 <observation> 将导致错误
                - 如果 <action> 中的某个工具参数有多行的话,请使用 \\n 来表示,如:<action>write_to_file("/tmp/test.txt", "a\\nb\\nc")</action>
                - 工具参数中的文件路径请使用绝对路径,不要只给出一个文件名。比如要写 write_to_file("/tmp/test.txt", "内容"),而不是 write_to_file("test.txt", "内容")

                ⸻

                本次任务可用工具:
                ${tool_list}

                ⸻

                环境信息:
                操作系统:${operating_system}
                工作目录在:${work_path}
                """.replace("${tool_list}", ToolUtils.getToolList())
                .replace("${operating_system}", System.getProperty("os.name"))
                .replace("${work_path}", "/home/zxm/test");
    }

    /**
     * 解析Action字符串
     */
    private ActionResult parseAction(String codeStr) {
        Pattern pattern = Pattern.compile("(\\w+)\\((.*)\\)", Pattern.DOTALL);
        Matcher matcher = pattern.matcher(codeStr);

        if (!matcher.find()) {
            throw new IllegalArgumentException("Invalid function call syntax");
        }

        String funcName = matcher.group(1);
        String argsStr = matcher.group(2).trim();

        List<String> args = parseArguments(argsStr);
        return new ActionResult(funcName, args);
    }

    /**
     * 解析参数列表
     */
    private List<String> parseArguments(String argsStr) {
        List<String> args = new ArrayList<>();
        StringBuilder currentArg = new StringBuilder();
        boolean inString = false;
        char stringChar = 0;
        int parenDepth = 0;

        for (int i = 0; i < argsStr.length(); i++) {
            char c = argsStr.charAt(i);

            if (!inString) {
                if (c == '"' || c == '\'') {
                    inString = true;
                    stringChar = c;
                    currentArg.append(c);
                } else if (c == '(') {
                    parenDepth++;
                    currentArg.append(c);
                } else if (c == ')') {
                    parenDepth--;
                    currentArg.append(c);
                } else if (c == ',' && parenDepth == 0) {
                    args.add(parseSingleArg(currentArg.toString().trim()));
                    currentArg = new StringBuilder();
                } else {
                    currentArg.append(c);
                }
            } else {
                currentArg.append(c);
                if (c == stringChar && (i == 0 || argsStr.charAt(i - 1) != '\\')) {
                    inString = false;
                    stringChar = 0;
                }
            }
        }

        if (currentArg.length() > 0) {
            args.add(parseSingleArg(currentArg.toString().trim()));
        }

        return args;
    }

    /**
     * 解析单个参数
     */
    private String parseSingleArg(String argStr) {
        argStr = argStr.trim();

        // 如果是字符串字面量
        if ((argStr.startsWith("\"") && argStr.endsWith("\"")) ||
                (argStr.startsWith("'") && argStr.endsWith("'"))) {
            String innerStr = argStr.substring(1, argStr.length() - 1);
            // 处理常见的转义字符
            innerStr = innerStr.replace("\\\"", "\"").replace("\\'", "'");
            innerStr = innerStr.replace("\\n", "\n").replace("\\t", "\t");
            innerStr = innerStr.replace("\\r", "\r").replace("\\\\", "\\");
            return innerStr;
        }

        return argStr;
    }

    /**
     * Action解析结果
     */
    @Getter
    @AllArgsConstructor
    private static class ActionResult {

        private final String toolName;

        private final List<String> args;
    }
}

4.测试类

public class Main {

    private static final String QWEN_API_KEY = "sk-123456";

    private static final String QWEN_API_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";

    private static final String QWEN_MODEL = "qwen-turbo";

    private static final String TASK = "帮我创建一个简单的Hello World Java程序,并运行它";

    public static void main(String[] args) {
        System.out.println("任务: " + TASK);

        try {
            // 创建Agent
            ReActAgent agent = new ReActAgent(QWEN_API_KEY, QWEN_API_URL, QWEN_MODEL);
            // 执行任务
            String result = agent.run(TASK);
            System.out.println("最终结果: " + result);
        } catch (Exception e) {
            System.err.println("执行失败: " + e.getMessage());
        }
    }
}

5.结论

调用步骤:

 第一次发送:
      1) role: system
         content:你需要解决一个问题。为此,你需要将问题分解为多个步骤...
      2) role: user
         content:<question>帮我创建一个简单的Hello World Java程序,并运行它</question>
 第一次返回:
      3) role: assistant
         content:<thought>我需要先创建一个Java源文件,然后编译并运行它。首先使用writeToFile工具创建HelloWorld.java文件。</thought>
                  <action>writeToFile("/home/zxm/test/HelloWorld.java", "public class HelloWorld")</action>
 第二次发送:
      1) 2) 3)
      4) role: user
         content:<observation>写入成功</observation>
 第二次返回:
      5) role: assistant
         content: <thought>现在需要编译并运行Java程序。使用runTerminalCommand工具执行编译和运行命令。</thought>
                  <action>runTerminalCommand("cd /home/zxm/test && javac HelloWorld.java && java HelloWorld")</action>
 第三次发送:
      1) 2) 3) 4) 5)
      6) role: user
         content:<observation>执行成功 Hello World </observation>
 第三次返回:
      7) role: assistant
         content: <thought>程序已成功编译并运行,输出了"Hello World"。任务完成。</thought>
                  <final_answer>Java程序已创建并成功运行,输出结果为:Hello World</final_answer>

三、langchain4j 框架实现 ReActAgent

1.pom

<properties>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <langchain4j.version>1.7.1</langchain4j.version>
</properties>

<dependencies>
    <!-- LangChain4j Core -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j</artifactId>
        <version>${langchain4j.version}</version>
    </dependency>

    <!-- LangChain4j OpenAI -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-open-ai</artifactId>
        <version>${langchain4j.version}</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.38</version>
        <optional>true</optional>
    </dependency>
</dependencies>

2.工具类

import dev.langchain4j.agent.tool.Tool;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * 工具函数集合,提供文件操作和终端命令执行功能
 * 使用 langchain4j @Tool 注解标记为工具函数
 */
public class ToolFunctions {

    /**
     * 读取文件内容
     * @param filePath 文件路径
     * @return 文件内容
     */
    @Tool("读取指定路径的文件内容")
    public static String readFile(String filePath) {
        try {
            Path path = Paths.get(filePath);
            return Files.readString(path);
        } catch (IOException e) {
            return "读取文件错误:" + e.getMessage();
        }
    }

    /**
     * 将指定内容写入指定文件
     * @param filePath 文件路径
     * @param content 要写入的内容
     * @return 操作结果
     */
    @Tool("将内容写入指定路径的文件")
    public static String writeToFile(String filePath, String content) {
        try {
            Path path = Paths.get(filePath);
            Files.createDirectories(path.getParent());
            Files.writeString(path, content.replace("\\n", "\n"));
            return "写入成功";
        } catch (IOException e) {
            return "写入文件错误:" + e.getMessage();
        }
    }

    /**
     * 执行终端命令
     * @param command 要执行的命令
     * @return 执行结果
     */
    @Tool("执行终端命令并返回结果")
    public static String runTerminalCommand(String command) {
        try {
            ProcessBuilder processBuilder = new ProcessBuilder();

            // 根据操作系统选择shell
            String os = System.getProperty("os.name").toLowerCase();
            if (os.contains("win")) {
                processBuilder.command("cmd.exe", "/c", command);
            } else {
                processBuilder.command("sh", "-c", command);
            }

            Process process = processBuilder.start();

            // 读取输出
            StringBuilder output = new StringBuilder();
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    output.append(line).append("\n");
                }
            }

            // 读取错误输出
            StringBuilder error = new StringBuilder();
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    error.append(line).append("\n");
                }
            }

            int exitCode = process.waitFor();

            if (exitCode == 0) {
                return "执行成功\n" + output.toString();
            } else {
                return "执行失败\n" + error.toString();
            }
        } catch (Exception e) {
            return "工具执行错误:" + e.getMessage();
        }
    }
}

3.ReActAgent

LangChain4j 实现了 function calling,可以把工具信息提供给 LangChain4j 即可。

import dev.langchain4j.model.openai.internal.chat.ChatCompletionRequest;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.model.openai.OpenAiChatModel;

import java.io.IOException;
import java.time.Duration;

public class ReActAgent {

    private final AiService aiService;

    /**
     * @see dev.langchain4j.model.openai.internal.DefaultOpenAiClient#chatCompletion(ChatCompletionRequest)
     */
    public ReActAgent(String baseApiUrl, String apiModel, String apiKey) throws IOException {
        OpenAiChatModel chatModel = OpenAiChatModel.builder()
                .apiKey(apiKey)
                .baseUrl(baseApiUrl)
                .modelName(apiModel)
                .temperature(0.7)
                .maxTokens(2000)
                .timeout(Duration.ofMinutes(10))
                .build();

        String systemPrompt = String.format("工作目录在:/home/zxm/test;当前操作系统是:%s;", System.getProperty("os.name"));
        this.aiService = AiServices.builder(AiService.class)
                .chatModel(chatModel)
                .tools(new ToolFunctions())
                .systemMessageProvider(chatRequest -> systemPrompt)
                .build();
    }

    public String chat(String userQuestion) {
        try {
            return aiService.generateResponse(userQuestion);
        } catch (Exception e) {
            return "处理问题时发生错误: " + e.getMessage();
        }
    }

    // 定义 AI 服务接口
    public interface AiService {
        String generateResponse(String userQuestion);
    }
}

4.测试类

public class Main {

    public static void main(String[] args) {
        try {
            // 创建 ReAct Agent 服务
            ReActAgent agentService = new ReActAgent(
                    "https://dashscope.aliyuncs.com/compatible-mode/v1",
                    "qwen-turbo",
                    "sk-123456");

            // 固定的测试问题
            String question = "帮我创建一个简单的Hello World Java程序,并运行它";
            System.out.println("测试问题: " + question);

            String response = agentService.chat(question);
            System.out.println("结果:" + response);
        } catch (Exception e) {
            System.err.println("处理问题时发生错误: " + e.getMessage());
        }
    }
}

5.结论

调用步骤:

发起大模型的请求如下:
{
  "model" : "qwen-turbo",
  "messages" : [ {
    "role" : "system",
    "content" : "工作目录在:/home/zxm/test;当前操作系统是:Linux;"
  }, {
    "role" : "user",
    "content" : "帮我创建一个简单的Hello World Java程序,并运行它"
  }, {
    "role" : "assistant",
    "tool_calls" : [ {
      "id" : "call_684add391e414f1683631d",
      "type" : "function",
      "function" : {
        "name" : "writeToFile",
        "arguments" : "{\"arg0\": \"/home/zxm/test/HelloWorld.java\", \"arg1\": \"public class HelloWorld {\\n    public static void main(String[] args) {\\n        System.out.println(\\\"Hello, World!\\\");\\n    }\\n}\"}"
      }
    } ]
  }, {
    "role" : "tool",
    "tool_call_id" : "call_684add391e414f1683631d",
    "content" : "写入成功"
  }, {
    "role" : "assistant",
    "tool_calls" : [ {
      "id" : "call_1b4f157945474fb7bc28ea",
      "type" : "function",
      "function" : {
        "name" : "runTerminalCommand",
        "arguments" : "{\"arg0\": \"javac /home/zxm/test/HelloWorld.java && java -cp /home/zxm/test HelloWorld\"}"
      }
    } ]
  }, {
    "role" : "tool",
    "tool_call_id" : "call_1b4f157945474fb7bc28ea",
    "content" : "执行成功\nHello, World!\n"
  } ],
  "temperature" : 0.7,
  "stream" : false,
  "max_tokens" : 2000,
  "tools" : [ {
    "type" : "function",
    "function" : {
      "name" : "writeToFile",
      "description" : "将内容写入指定路径的文件",
      "parameters" : {
        "type" : "object",
        "properties" : {
          "arg0" : {
            "type" : "string"
          },
          "arg1" : {
            "type" : "string"
          }
        },
        "required" : [ "arg0", "arg1" ]
      }
    }
  }, {
    "type" : "function",
    "function" : {
      "name" : "runTerminalCommand",
      "description" : "执行终端命令并返回结果",
      "parameters" : {
        "type" : "object",
        "properties" : {
          "arg0" : {
            "type" : "string"
          }
        },
        "required" : [ "arg0" ]
      }
    }
  }, {
    "type" : "function",
    "function" : {
      "name" : "readFile",
      "description" : "读取指定路径的文件内容",
      "parameters" : {
        "type" : "object",
        "properties" : {
          "arg0" : {
            "type" : "string"
          }
        },
        "required" : [ "arg0" ]
      }
    }
  } ]
}

现在关注messages:
第一次发送:
     1) role: system
        content:工作目录在:/home/zxm/test
     2) role: user
        content:帮我创建一个简单的Hello World Java程序,并运行它
第一次返回:
    3) role: assistant
       content: ""
       tool_calls: id: call_fcbb88e694c54b619df4aa
                   type: function
                   function: arguments: "{\"arg0\": \"/home/zxm/test/HelloWorld.java\", \"arg1\": \"public class HelloWorld {\\n    public static void main(String[] args) {\\n        System.out.println(\\\"Hello, World!\\\");\\n    }\\n}\"}"
                             name: writeToFile
第二次发送:
    1) 2) 3)
    4) role: tool
       content: 写入成功
       tool_call_id: call_fcbb88e694c54b619df4aa
第二次返回:
    5) role: assistant
       content: ""
       tool_calls: id: call_bf240bb2c6164d01ad4042
                   type: function
                   function: arguments: "{\"arg0\": \"javac /home/zxm/test/HelloWorld.java && java -cp /home/zxm/test HelloWorld\"}"
                             name: runTerminalCommand
第三次发送:
   1) 2) 3) 4) 5)
   6) role: tool
      tool_call_id: call_bf240bb2c6164d01ad4042
      content: "执行成功\nHello, World!\n"
第三次返回:
   7) role: assistant
      content: "以下为您创建的简单 Hello World Java 程序的运行结果:\n\n```\nHello, World!\n```\n\n程序已成功编译并运行,输出了 \"Hello, World!\"。如果您需要进一步的帮助,请随时告诉我!"