大模型 · 2024-12-31 0

Java 开发大模型 Agent 的 Plan-And-Execute 模式及 langchain4j 使用

一、Plan-And-Execute 架构

当任务变得更加复杂、步骤繁多时,「规划-执行」架构开始展现威力。

这种模式将 Agent 的流程明确分为两个阶段:先规划(Plan),再执行(Execute) 先让 Agent 想出一整套方案,然后按照方案逐步落实。

与 ReAct 不同,Plan-and-Execute 会强制 LLM 做全局思考。它通常涉及两个子 Agent 或子模块:一个 Planner(规划者)和一个Executor(执行者)。两者分工如下:

Planner:由一个 LLM 来承担,它的任务是分析目标,产出详细的执行计划。Planner 会接收用户的最终任务描述,然后以列表形式生成需要完成的子任务序列。
Planner 在这一步可以充分利用 LLM 的链式思考能力,将模糊的目标细化为可执行的步骤,并考虑步骤间的依赖、先后顺序等 。
Executor(s):执行者负责按照 Planner 给出的每个子任务,逐条执行 。Executor 本质上也是一个 Agent,可以针对不同子任务切换工具或 API,也可以调用一个内部 ReAct Agent 来完成。
Executor 会读取任务清单的某一条,比如「第1步:搜索 X 信息」,然后实际调用对应的工具完成它,将结果记录下来,再执行下一步。

二、langchain4j 框架实现 Plan-And-Execute

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>
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j</artifactId>
        <version>${langchain4j.version}</version>
    </dependency>

    <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.Tool

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;
import java.nio.file.StandardOpenOption;

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

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

    /**
     * 将指定内容写入指定文件
     */
    @Tool("将内容写入指定路径的文件")
    public static String writeToFile(String filePath, String content) {
        try {
            Path path = Paths.get(filePath);
            // 如果目录不存在,创建目录
            Path parent = path.getParent();
            if (parent != null && !Files.exists(parent)) {
                Files.createDirectories(parent);
            }
            // 写入文件(覆盖模式)
            Files.writeString(path, content, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
            return "写入成功";
        } catch (IOException e) {
            return "写入文件失败:" + e.getMessage();
        }
    }

    /**
     * 执行终端命令
     */
    @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.PlanAndExecuteAgent

PlannerService 作为任务规划器
ExecutorService 作为任务执行器


import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.UserMessage;

import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class PlanAndExecuteAgent {

    private final PlannerService plannerService;

    private final ExecutorService executorService;

    private final int maxReplanAttempts;

    public PlanAndExecuteAgent(String baseApiUrl, String apiModel, String apiKey) throws IOException {
        this.maxReplanAttempts = 3;

        OpenAiChatModel chatModel = OpenAiChatModel.builder()
                .baseUrl(baseApiUrl)
                .modelName(apiModel)
                .apiKey(apiKey)
                .temperature(0.7)
                .maxTokens(2000)
                .timeout(Duration.ofMinutes(10))
                .build();

        // 规划器:负责将复杂任务分解为多个步骤
        this.plannerService = AiServices.builder(PlannerService.class)
                .chatModel(chatModel)
                .systemMessageProvider(request ->
                        """
                        你是一个任务规划器。请将用户的复杂任务分解为一系列清晰的步骤。

                        可用工具:
                        - readFile(path): 读取文件内容
                        - writeToFile(path, content): 将内容写入文件
                        - runTerminalCommand(command): 执行终端命令

                        规划原则:
                        1. 每个步骤应该具体可执行
                        2. 步骤按顺序排列
                        3. 步骤之间相互依赖
                        4. 如果需要读取/写入文件或执行命令,请明确标注需要使用哪个工具

                        请用以下格式输出计划:
                        目标:[总体目标]
                        步骤1: [具体操作]
                        步骤2: [具体操作]
                        ...
                        """)
                .build();

        // 执行器:负责执行每个步骤
        this.executorService = AiServices.builder(ExecutorService.class)
                .chatModel(chatModel)
                .tools(new ToolFunctions())
                .systemMessageProvider(request ->
                        """
                        你是一个任务执行器。使用提供的工具函数执行步骤。

                        你可以使用以下工具:
                        - readFile(文件路径): 读取文件内容
                        - writeToFile(文件路径, 内容): 写入文件
                        - runTerminalCommand(命令): 执行终端命令

                        请仔细执行每个步骤,需要时使用工具。

                        执行结果要求:
                        - 如果步骤执行成功,返回执行结果
                        - 如果步骤执行失败(工具调用失败、命令执行出错、文件操作失败等),返回以"失败:"开头的错误信息
                        """)
                .build();
    }

    /**
     * 执行任务:先制定计划,然后逐步执行,必要时重新规划
     */
    public String execute(String task) {
        return executeWithReplan(task, task, 0);
    }

    /**
     * 带重新规划功能的执行方法
     */
    private String executeWithReplan(String originTask, String task, int replanCount) {
        if (replanCount > maxReplanAttempts) {
            return " 达到最大重新规划次数限制(%d),停止执行。 ".formatted(maxReplanAttempts);
        }

        try {
            // 第一步:制定计划
            String plan = plannerService.createPlan(task);

            // 解析计划为步骤列表
            List<String> steps = parsePlan(plan);

            // 第二步:执行计划
            StringBuilder results = new StringBuilder();

            for (int i = 0; i < steps.size(); i++) {
                String step = steps.get(i);
                String result = executorService.executeStep(step);

                results.append("""
                        步骤 %d: %s
                        结果: %s

                        """.formatted(i + 1, step, result));

                // 如果需要重新规划
                if (result.trim().toUpperCase().startsWith("失败")) {
                    String updatedTask = """
                            原始任务:%s

                            之前的执行失败了,需要你重新制定一个完整的计划。

                            之前的规划步骤:
                            %s

                            已执行的步骤及结果:
                            %s

                            请分析失败原因,制定一个新计划。新计划应该:
                            1. 避免之前失败的错误
                            2. 可以采用不同的方法或工具
                            3. 如果需要,可以调整步骤的顺序或添加额外的步骤
                            4. 确保新计划能够完成原始任务
                            """.formatted(task, plan, results);

                    return executeWithReplan(originTask, updatedTask, replanCount + 1);
                }
            }

            return """
                    计划:
                    %s

                    执行结果:
                    %s
                    """.formatted(originTask, results.toString());

        } catch (Exception e) {
            return "执行任务时发生错误: %s ".formatted(e.getMessage());
        }
    }

    /**
     * 解析计划文本,提取步骤列表
     */
    private List<String> parsePlan(String plan) {
        List<String> steps = new ArrayList<>();
        // 匹配每个步骤:从步骤标题到下一个步骤标题之间的所有内容
        Pattern pattern = Pattern.compile("^\\s*(?:步骤\\d+|Step\\s*\\d+|\\d+)[.::]\\s*(.*?)(?=^\\s*(?:步骤\\d+|Step\\s*\\d+|\\d+)[.::]|\\z)", Pattern.MULTILINE | Pattern.DOTALL);
        Matcher matcher = pattern.matcher(plan);

        while (matcher.find()) {
            String stepContent = matcher.group(1).trim();
            if (!stepContent.isEmpty()) {
                steps.add(stepContent);
            }
        }

        return steps.isEmpty() ? List.of(plan) : steps;
    }

    // 规划器接口
    public interface PlannerService {
        String createPlan(@UserMessage String task);
    }

    // 执行器接口
    public interface ExecutorService {
        String executeStep(@UserMessage String step);
    }
}

4.测试类

public class Main {

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

            // 固定的测试问题
            String question = "帮我创建一个简单的Hello World Java程序,并运行它";

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