跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
JavaAIjava算法

纯 Java 手写多功能 AI Agent:从零实现类 Manus 智能体架构

纯 Java 实现多功能 AI Agent,基于 ReAct 模式构建核心循环。项目不依赖 Spring 框架,通过 BaseAgent 与 ToolCallAgent 分层设计,实现推理与行动交替执行。集成文件读写、Docker 沙箱代码执行、浏览器自动化及网页搜索工具。采用 LLM 驱动的相关性过滤管理上下文记忆,支持多模态消息交互。完整展示从消息封装、工具注册到安全执行的底层逻辑,为理解 Agent 架构提供清晰参考。

无尘发布于 2026/3/23更新于 2026/4/253 浏览
纯 Java 手写多功能 AI Agent:从零实现类 Manus 智能体架构

引言

2024 年以来,AI Agent(智能体)成为大模型应用领域最炙手可热的方向。从 OpenAI 的 GPT-4 with Tools,到 Anthropic 的 Claude Computer Use,再到国内 Manus、AutoGLM 等产品,"让大模型不只是聊天,而是真正地做事"已经成为行业共识。

然而,大多数开发者对 AI Agent 的理解还停留在概念层面:知道它能调用工具、能自主决策,但对其内部运作机制缺乏深入了解。市面上的 Agent 框架(如 LangChain、Spring AI)虽然降低了开发门槛,但也隐藏了大量实现细节。

本文将通过一个完全不依赖 Spring 框架的纯 Java 项目——ai-manus,带你从零理解 AI Agent 的核心架构。这个项目实现了一个功能完整的多工具智能体,具备文件读写、Docker 沙箱代码执行、网页搜索、浏览器自动化等能力,并采用了 ReAct(Reasoning + Acting)推理模式和 LLM 驱动的上下文记忆管理。通过逐层拆解其代码实现,你将真正理解一个 AI Agent 是如何"思考"和"行动"的。

一、项目全景:架构与技术选型

1.1 项目结构

ai-manus/
├── pom.xml
└── src/main/java/com/artisan/
    ├── ManusApplication.java # 启动入口
    ├── agent/
    │   ├── BaseAgent.java # Agent 基类(循环控制)
    │   ├── ToolCallAgent.java # 工具调用 Agent(ReAct 核心)
    │   └── ManusAgent.java # 具体 Agent 实现(注册工具)
    ├── model/
    │   ├── ModelConfig.java # 模型配置
    │   ├── OpenAIClient.java # LLM API 客户端
    │   ├── Message.java # 消息模型(支持多模态)
    │   ├── Memory.java # 记忆管理
    │   ├── RelevanceFilter.java # 相关性过滤接口
    │   ├── LLMRelevanceFilter.java # 基于 LLM 的相关性过滤
    │   ├── ModelResponse.java # 模型响应
    │   ├── Role.java # 角色枚举
    │   ├── ToolCall.java # 工具调用模型
    │   ├── ToolDefinition.java # 工具定义模型
    │   └── Function.java # 函数调用模型
    └── tools/
        ├── Tool.java # 工具接口
        ├── BaseTool.java # 工具基类
        ├── ToolCollection.java # 工具注册中心
        ├── ToolResult.java # 工具执行结果
        └── impl/
            ├── FileWriterTool.java # 文件写入
            ├── FileReaderTool.java # 文件读取
            ├── SandboxTool.java # Docker 沙箱执行
            ├── DockerSandbox.java # Docker 容器管理
            ├── TavilySearchTool.java # 网页搜索
            └── BrowserTool.java 
# 浏览器自动化

1.2 技术选型

项目刻意避开了 Spring 等重型框架,采用"最小依赖"原则:

依赖版本用途
OkHttp34.12.0HTTP 客户端,调用 LLM API
Jackson2.16.1JSON 序列化/反序列化
Lombok1.18.30减少样板代码
docker-java3.3.6Docker 容器管理
langchain4j-tavily0.36.2Tavily 网页搜索
Playwright1.55.0浏览器自动化

这种选择的好处是:你看到的每一行代码都是 Agent 逻辑本身,没有框架魔法的干扰,非常适合学习和理解 Agent 的运作原理。

二、Agent 核心循环:ReAct 模式的实现

ReAct(Reasoning + Acting)是当前 AI Agent 最主流的推理范式。其核心思想是:大模型在每一步先进行推理(Thought),然后决定执行什么动作(Action),观察执行结果(Observation),再进入下一轮推理。这个项目通过三层 Agent 继承体系优雅地实现了这一模式。

2.1 BaseAgent:循环骨架

BaseAgent是所有 Agent 的抽象基类,它定义了 Agent 执行的基本骨架——一个有限步数的循环:

public abstract class BaseAgent {
    protected final Memory memory;
    private final int maxStep;
    protected String systemPrompt;

    public String run(String prompt) {
        // 1. 初始化:将系统提示词和用户输入加入记忆
        memory.addMessage(Message.systemMessage(systemPrompt));
        memory.addMessage(Message.userMessage(prompt));
        
        int currentStep = 0;
        StringBuilder allStepResult = new StringBuilder();

        // 2. 核心循环:最多执行 maxStep 步
        while (currentStep < maxStep) {
            StepResult stepResult = step(prompt); // 子类实现
            allStepResult.append(stepResult.output).append("\n");
            if (!stepResult.isShouldContinue()) {
                break; // Agent 认为任务完成,退出循环
            }
            currentStep++;
        }
        return allStepResult.toString();
    }

    // 由子类实现的单步执行逻辑
    protected abstract StepResult step(String currentQuery);
}

这里有几个关键设计决策:

  • 最大步数限制(maxStep=10):防止 Agent 陷入无限循环,这是所有 Agent 系统必备的安全阀。
  • StepResult 双字段设计:output记录当前步的输出,shouldContinue标识是否需要继续执行。大模型通过返回 finish_reason="stop"来告知 Agent 任务已完成。
  • Memory 贯穿全程:所有消息(系统提示、用户输入、助手回复、工具结果)都存入 Memory,保证上下文连贯性。

2.2 ToolCallAgent:ReAct 的核心引擎

ToolCallAgent是整个系统的灵魂所在,它实现了 ReAct 循环的单步逻辑:

@Override
protected StepResult step(String currentQuery) {
    // 1. 从记忆中获取上下文消息(带相关性过滤)
    List<Message> contextMessages = memory.getMessages(currentQuery);
    // 2. 获取与当前查询相关的工具定义(带相关性过滤)
    List<ToolDefinition> toolDefinitions = toolCollection.getRelevantToolDefinitions(currentQuery);
    // 3. 调用大模型,传入上下文消息和可用工具
    ModelResponse modelResponse = openAIClient.chat(contextMessages, toolDefinitions);

    // 4. 大模型决定调用工具
    if (modelResponse.hasToolCalls()) {
        Message assistantMessage = Message.assistantMessage(modelResponse.getContent());
        assistantMessage.setToolCalls(convertToToolCalls(modelResponse.getToolCalls()));
        memory.addMessage(assistantMessage);
        // 执行工具并返回结果
        return handleToolCalls(modelResponse.getToolCalls());
    }

    // 5. 大模型不调用工具,直接返回文本
    if (modelResponse.getContent() != null && !modelResponse.getContent().isBlank()) {
        memory.addMessage(Message.assistantMessage(modelResponse.getContent()));
    }

    // 6. 判断是否结束
    if (modelResponse.getFinishReason().equals("stop")) {
        return StepResult.builder()
                .shouldContinue(false)
                .output("大模型认为任务已经执行结束")
                .build();
    }
    return StepResult.builder()
            .shouldContinue(true)
            .output(modelResponse.getContent())
            .build();
}

让我们拆解这个方法中蕴含的设计智慧:

上下文管理:每次调用 LLM 前,先通过 memory.getMessages(currentQuery) 获取经过相关性过滤的消息,避免上下文窗口溢出。

工具动态筛选:不是每次都把所有 5 个工具都传给大模型,而是通过 getRelevantToolDefinitions根据当前查询动态选择最相关的工具,减少干扰提升决策质量。

消息链维护:严格遵循 OpenAI Tool Calling 协议——先将 assistant 的 tool_calls 消息存入记忆,再存入对应的 tool 执行结果消息。这个消息链的完整性是大模型正确理解上下文的关键。

工具执行的核心逻辑在 handleToolCalls方法中:

private StepResult handleToolCalls(List<Object> toolCalls) {
    StringBuilder allResults = new StringBuilder();
    for (Object toolCallObj : toolCalls) {
        try {
            JsonNode toolCallNode = objectMapper.valueToTree(toolCallObj);
            String toolCallId = toolCallNode.get("id").asText();
            String toolName = toolCallNode.get("function").get("name").asText();
            String argumentsJson = toolCallNode.get("function").get("arguments").asText();

            // 解析参数并执行工具
            Map<String, Object> arguments = objectMapper.readValue(argumentsJson, Map.class);
            ToolResult result = toolCollection.executeTool(toolName, arguments);

            // 将工具结果封装为 toolMessage 存入记忆
            String resultContent = result.hasError() ? "Error: " + result.getError() : result.getOutput().toString();
            Message toolMessage = Message.toolMessage(resultContent, toolName, toolCallId, result.getBase64Image());
            memory.addMessage(toolMessage);
        } catch (Exception e) {
            // 错误也要存入记忆,让大模型知道发生了什么
            Message errorMessage = Message.toolMessage(
                    "工具执行失败:" + e.getMessage(),
                    "unknown",
                    UUID.randomUUID().toString());
            memory.addMessage(errorMessage);
        }
    }
    return StepResult.builder().shouldContinue(true).output(allResults.toString()).build();
}

注意这里的一个细节:工具执行完成后,shouldContinue始终为true。这意味着工具执行只是 Agent 的一个中间步骤,执行完工具后需要将结果反馈给大模型,由大模型决定下一步做什么——可能继续调用其他工具,也可能认为任务已完成。这就是 ReAct 循环的精髓。

2.3 ManusAgent:具体 Agent 的组装

ManusAgent是最终面向用户的 Agent 实现,负责注册工具和配置系统提示词:

public class ManusAgent extends ToolCallAgent {
    private static final String SYSTEM_PROMPT = """
        # 角色定义
        你是 Manus,一个多功能的 AI 代理,能够使用可用的工具处理各种任务。
        # 规则
        - 工作目录:{workspace}
        - Sandbox 里面不使用工作目录
        - 利用 Sandbox 执行代码时,直接把代码内容传给 Sandbox,而不是把代码脚本文件传给 Sandbox
        - 一次只能执行一个工具
        """;

    public ManusAgent(OpenAIClient openAIClient) {
        super(openAIClient, null, null);
        // 注册 5 个工具
        ToolCollection toolCollection = new ToolCollection();
        toolCollection.addTool(new FileWriterTool());
        toolCollection.addTool(new FileReaderTool());
        toolCollection.addTool(new SandboxTool());
        toolCollection.addTool(new TavilySearchTool());
        toolCollection.addTool(new BrowserTool());
        this.toolCollection = toolCollection;

        // 创建工作区目录并注入到系统提示词
        Path workspaceRoot = getProjectRoot().resolve("workspace");
        Files.createDirectories(workspaceRoot);
        this.systemPrompt = SYSTEM_PROMPT.replace("{workspace}", workspaceRoot.toString());
    }
}

系统提示词的设计值得关注:它明确了 Agent 的角色定位、工作目录规范以及工具使用规则。特别是"一次只能执行一个工具"这条规则,简化了工具执行的并发复杂度。

三、消息系统:多模态对话的基石

3.1 四种角色的消息设计

public enum Role {
    SYSTEM("system"),     // 系统提示词
    USER("user"),         // 用户输入
    ASSISTANT("assistant"), // 大模型回复(含 tool_calls)
    TOOL("tool")          // 工具执行结果
}

Message类是整个消息系统的核心,它需要兼容 OpenAI Chat Completions API 的消息格式:

public class Message {
    private Role role;
    private String content;
    private List<ToolCall> toolCalls; // assistant 消息携带的工具调用
    private String name;              // tool 消息的工具名
    private String toolCallId;        // tool 消息关联的调用 ID
    private String base64Image;       // 多模态图片数据
}

这里有一个精妙的设计——base64Image字段使得消息天然支持多模态。当浏览器工具截图后,截图数据以 Base64 编码附在 toolMessage中传回;大模型在下一轮接收到这个多模态消息时,就能"看到"截图内容并进行分析。

在 OpenAIClient中,多模态消息的转换逻辑如下:

if (message.getBase64Image() != null) {
    // 构造多模态 content 数组
    List<Map<String, Object>> content = new ArrayList<>();
    content.add(Map.of("type", "text", "text", message.getContent()));
    content.add(Map.of("type", "image_url", "image_url", 
            Map.of("url", "data:image/jpeg;base64," + message.getBase64Image())));
    apiMessage.put("content", content);
} else {
    apiMessage.put("content", message.getContent());
}

3.2 LLM API 的封装

OpenAIClient使用 OkHttp3 直接调用 OpenAI 兼容 API(本项目实际对接的是阿里云 DashScope 的通义千问模型):

public class OpenAIClient {
    private final OkHttpClient httpClient;

    public OpenAIClient(ModelConfig modelConfig) {
        this.httpClient = new OkHttpClient.Builder()
                .connectTimeout(Duration.ofSeconds(30))
                .readTimeout(Duration.ofMinutes(5)) // 长超时,等待大模型推理
                .writeTimeout(Duration.ofMinutes(5))
                .build();
    }

    public ModelResponse chat(List<Message> messages, List<ToolDefinition> tools) {
        Map<String, Object> requestBody = new HashMap<>();
        requestBody.put("model", modelConfig.getModel());
        requestBody.put("messages", convertMessagesToApiFormat(messages));
        // 仅在有工具时传入 tools 参数
        if (tools != null && !tools.isEmpty()) {
            requestBody.put("tools", convertToolsToApiFormat(tools));
        }
        // ... 发送请求并解析响应
    }
}

工具定义的转换遵循 OpenAI Function Calling 标准格式:

private List<Map<String, Object>> convertToolsToApiFormat(List<ToolDefinition> tools) {
    return tools.stream().map(tool -> Map.of(
            "type", "function",
            "function", Map.of(
                    "name", tool.getName(),
                    "description", tool.getDescription(),
                    "parameters", tool.getParameters() // JSON Schema 格式
            )
    )).toList();
}

四、记忆管理:LLM 驱动的上下文过滤

随着 Agent 执行步骤增多,消息历史会快速膨胀。如果不加处理,很快就会超出大模型的上下文窗口。本项目提供了一个创新的解决方案——用 LLM 自身来评估消息的相关性。

4.1 相关性过滤接口

public interface RelevanceFilter {
    List<Message> filter(List<Message> messages, String currentQuery, int maxMessages);
    double calculateRelevance(Message message, String currentQuery);
}

Memory类在获取消息时会自动应用过滤:

public List<Message> getMessages(String currentQuery) {
    if (relevanceFilter != null) {
        return relevanceFilter.filter(messages, currentQuery, 5); // 最多保留 5 条
    }
    return messages;
}

4.2 LLM 相关性过滤器

LLMRelevanceFilter是这个机制的核心实现。它对每条非系统消息调用 LLM 进行语义相关性评分:

public List<Message> filter(List<Message> messages, String currentQuery, int maxMessages) {
    // 系统消息始终保留
    List<Message> systemMessages = messages.stream()
            .filter(msg -> msg.getRole() == Role.SYSTEM)
            .toList();

    // 为每条非系统消息计算相关性得分
    List<MessageScore> scoredMessages = nonSystemMessages.stream()
            .map(msg -> new MessageScore(msg, calculateRelevance(msg, currentQuery)))
            .sorted((a, b) -> Double.compare(b.score, a.score))
            .toList();

    // 保留系统消息 + 得分最高的 N 条消息
    List<Message> result = new ArrayList<>(systemMessages);
    int remainingSlots = maxMessages - systemMessages.size();
    result.addAll(scoredMessages.stream()
            .limit(remainingSlots)
            .map(ms -> ms.message)
            .toList());
    return result;
}

评分时构造的提示词非常讲究,提供了明确的评分标准:

private String buildRelevancePrompt(String messageContent, String query) {
    return String.format(
            "请评估以下消息内容与查询的相关性,返回 0.0 到 1.0 之间的数字评分:\n\n" +
            "查询:%s\n\n消息内容:%s\n\n" +
            "评估标准:\n" +
            "1.0 - 高度相关:消息直接回答查询或包含查询的核心信息\n" +
            "0.7-0.9 - 相关:消息与查询主题相关,包含有用信息\n" +
            "0.4-0.6 - 部分相关:消息与查询有一定关联\n" +
            "0.1-0.3 - 微弱相关:消息与查询只有很少关联\n" +
            "0.0 - 不相关:消息与查询完全无关\n\n" +
            "请只返回数字评分,不要包含其他文字说明:",
            query, messageContent);
}

同时,代码还实现了健壮的评分解析逻辑,能处理 LLM 返回数字或文本描述两种情况:

private double parseRelevanceScore(String content) {
    // 优先尝试解析数字
    String numberStr = trimmed.replaceAll("[^0-9.]..*", "");
    if (!numberStr.isEmpty()) {
        return Double.parseDouble(numberStr);
    }
    // 回退:从文本关键词推断
    if (lowerContent.contains("高度相关")) return 0.9;
    if (lowerContent.contains("相关")) return 0.7;
    if (lowerContent.contains("部分")) return 0.5;
    if (lowerContent.contains("微弱")) return 0.2;
    if (lowerContent.contains("不相关")) return 0.0;
    return 0.0;
}

4.3 工具的动态过滤

同样的相关性过滤机制也应用于工具选择。ToolCollection会将每个工具的"名称 + 描述"包装成消息,让 LLM 评估其与当前查询的相关性:

public List<ToolDefinition> getRelevantToolDefinitions(String query) {
    List<ToolScore> scoredTools = allTools.stream()
            .map(tool -> {
                String toolText = tool.getName() + " " + tool.getDescription();
                Message message = Message.assistantMessage(toolText);
                double relevance = relevanceFilter.calculateRelevance(message, query);
                return new ToolScore(tool, relevance);
            })
            .filter(ts -> ts.score >= 0.3) // 相关性阈值
            .sorted((a, b) -> Double.compare(b.score, a.score))
            .toList();

    // 安全兜底:如果没有工具超过阈值,返回全部
    if (relevantTools.isEmpty()) {
        return allTools;
    }
    return relevantTools;
}

这个设计有一个重要的安全阈值——0.3。当没有任何工具的相关性超过 0.3 时,系统会回退到返回所有工具,避免 Agent"无工具可用"的死锁状态。

五、工具系统:可插拔的能力扩展

5.1 工具接口与基类

工具系统采用经典的接口 - 抽象类 - 实现类三层设计:

public interface Tool {
    String getName();
    String getDescription();
    Map<String, Object> getParametersSchema(); // JSON Schema
    ToolResult execute(Map<String, Object> parameters);
    default ToolDefinition toDefinition() {
        return new ToolDefinition(getName(), getDescription(), getParametersSchema());
    }
}

BaseTool提供了构建 JSON Schema 参数定义的辅助方法:

public abstract class BaseTool implements Tool {
    // 参数 Schema 构建器
    protected Map<String, Object> stringParam(String description) { ... }
    protected Map<String, Object> boolParam(String description) { ... }
    protected Map<String, Object> intParam(String description) { ... }
    protected Map<String, Object> enumParam(String description, List<String> values) { ... }

    // 参数安全提取
    protected String getString(Map<String, Object> parameters, String key) { ... }
    protected Boolean getBoolean(Map<String, Object> parameters, String key) { ... }
    protected Integer getInteger(Map<String, Object> parameters, String key) { ... }

    // Schema 组装
    protected Map<String, Object> buildSchema(Map<String, Map<String, Object>> properties, List<String> required) { ... }
}

ToolResult支持三种返回形态——纯文本、错误信息、带图片的多模态结果:

public class ToolResult {
    private final Object output;
    private final String error;
    private final String base64Image; // 支持截图等多模态输出

    public static ToolResult success(Object output) { ... }
    public static ToolResult success(Object output, String base64Image) { ... }
    public static ToolResult error(String error) { ... }
}

5.2 Docker 沙箱:安全的代码执行

SandboxTool和 DockerSandbox配合实现了在 Docker 容器内安全执行用户代码的能力。

容器配置强调安全隔离:

public static class SandboxSettings {
    public static String image = "python:3.12-slim";
    public static String workDir = "/workspace";
    public static String memoryLimit = "512m"; // 内存限制 512MB
    public static double cpuLimit = 1.0; // CPU 限制 1 核
    public static int timeout = 300; // 超时 5 分钟
    public static boolean networkEnabled = false; // 禁用网络访问
}

禁用网络是一个关键的安全决策——防止恶意代码外传数据或发起攻击。

支持多语言代码执行,使用 Heredoc 方式传递代码,避免了复杂的字符转义问题:

private String buildCodeExecutionCommand(String code, String language) {
    switch (language.toLowerCase()) {
        case "python": return buildHeredocCommand(code, "python3");
        case "bash": return code; // Bash 直接执行
        case "node": return buildHeredocCommand(code, "node");
        case "java": // Java 需要先写文件、编译、再执行
            String javaHeredoc = buildHeredocToFile(code, "/tmp/Main.java");
            return javaHeredoc + " && cd /tmp && javac Main.java && java Main";
        default: throw new IllegalArgumentException("Unsupported language: " + language);
    }
}

private String buildHeredocCommand(String code, String interpreter) {
    String delimiter = "OPENMANUS_CODE_EOF_" + System.currentTimeMillis();
    return String.format("%s << '%s'\n%s\n%s", interpreter, delimiter, code, delimiter);
}

使用时间戳生成唯一的 Heredoc 分隔符(OPENMANUS_CODE_EOF_1718...),确保分隔符不会与代码内容冲突。

5.3 浏览器自动化:Playwright 驱动

BrowserTool基于 Playwright 实现了完整的浏览器操作能力,支持 7 种操作:

操作说明
navigate导航到 URL,等待网络空闲
click通过 CSS 选择器点击元素
type在输入框中输入文本
screenshot全页面截图,返回 Base64 编码
get_content提取页面标题、URL 和文本内容
scroll上下左右滚动页面
wait等待元素出现或页面加载

浏览器采用懒加载模式,只在首次使用时初始化:

public ToolResult execute(Map<String, Object> parameters) {
    // 首次使用时才创建浏览器实例
    if (browser == null) {
        initializeBrowser();
    }
    // ...
}

private void initializeBrowser() {
    Playwright playwright = Playwright.create();
    browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false)); // 非无头模式
    context = browser.newContext(new Browser.NewContextOptions()
            .setViewportSize(1920, 1080)
            .setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."));
    currentPage = context.newPage();
}

截图功能是多模态能力的关键入口——Agent 可以截图后让大模型"看到"页面内容:

private ToolResult handleScreenshot(Map<String, Object> parameters) {
    byte[] screenshot = currentPage.screenshot(new Page.ScreenshotOptions().setFullPage(true).setType(ScreenshotType.PNG));
    String base64Screenshot = Base64.getEncoder().encodeToString(screenshot);
    return ToolResult.success("截图成功", base64Screenshot); // 携带 Base64 图片
}

5.4 网页搜索:Tavily 集成

TavilySearchTool通过 langchain4j 的 Tavily 封装实现网页搜索:

public ToolResult execute(Map<String, Object> parameters) {
    String query = getString(parameters, "query");
    WebSearchResults results = searchEngine.search(query);
    List<Map<String, Object>> searchResults = results.results().stream()
            .map(result -> Map.of(
                    "title", result.title(),
                    "url", result.url(),
                    "snippet", result.snippet()
            )).toList();
    return ToolResult.success(response);
}

六、完整执行流程:一个真实的例子

让我们通过入口程序的示例任务,完整追踪 Agent 的执行过程:

String prompt = """
    1. 创建一个名为'test_page.html'的 HTML 文件并添加内容
    2. 使用 file://协议在浏览器中打开本地文件
    3. 给打开的页面截图
    4. 告诉截图中的内容
    """;
manusAgent.run(prompt);

执行流程如下:

┌─────────────────────────────────────────────────────────┐
│ Step0: 初始化                                            │
│ Memory:[SystemMessage, UserMessage]                     │
└──────────────────────────┬──────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────┐
│ Step1: LLM 推理 → 决定调用 write_file 工具                 │
│ Action: write_file(path="workspace/test_page.html",      │
│           content="<html>...</html>")                   │
│ Memory:+[AssistantMsg(tool_calls), ToolMsg(成功)]       │
└──────────────────────────┬──────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────┐
│ Step2: LLM 推理 → 决定调用 browser.navigate               │
│ Action: browser(action="navigate",                       │
│           url="file:///path/to/workspace/test_page.html")│
│ Memory:+[AssistantMsg(tool_calls), ToolMsg(导航成功)]   │
└──────────────────────────┬──────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────┐
│ Step3: LLM 推理 → 决定调用 browser.screenshot             │
│ Action: browser(action="screenshot")                    │
│ Memory:+[AssistantMsg(tool_calls),                      │
│          ToolMsg(截图成功 + base64Image)]               │
└──────────────────────────┬──────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────┐
│ Step4: LLM 推理 → 分析截图内容,返回文本描述              │
│ Response:"截图中显示了一个 HTML 页面,内容包括..."       │
│ finish_reason:"stop" → 任务完成,退出循环               │
└─────────────────────────────────────────────────────────┘

在这个过程中,Agent 展现了以下能力:

  1. 任务规划:将复合任务自动拆解为多个步骤
  2. 工具选择:根据当前步骤选择最合适的工具
  3. 结果反馈:每次工具执行后将结果传回 LLM 进行下一步推理
  4. 多模态理解:能够"看到"截图内容并进行描述
  5. 自主终止:完成所有子任务后主动结束

七、设计模式总结

回顾整个项目,我们可以提炼出以下核心设计模式:

设计模式应用位置作用
模板方法BaseAgent.run() + step()固定执行骨架,子类实现单步逻辑
策略模式RelevanceFilter接口可插拔的上下文过滤策略
工厂方法Message.userMessage()等统一消息创建
注册表模式ToolCollection集中管理工具的注册和查找
懒加载BrowserTool/DockerSandbox按需初始化重量级资源
适配器模式OpenAIClient将内部模型适配为 OpenAI API 格式

八、进一步思考

这个项目虽然是学习性质,但其架构设计完全可以作为生产级 Agent 系统的基础。以下是一些可以进一步优化的方向:

  1. 流式输出:当前使用同步 HTTP 调用,可改为 SSE 流式响应,提升用户体验。
  2. 并行工具执行:当前系统提示词限制"一次只能执行一个工具",实际上 OpenAI API 支持在一次响应中返回多个 tool_calls,可以并行执行。
  3. 持久化记忆:当前 Memory 是内存级别的,可引入向量数据库实现长期记忆。
  4. 更细粒度的错误恢复:当工具执行失败时,可以引入重试机制或备选方案。
  5. 安全增强:对 FileWriter/FileReader 添加路径白名单限制,防止任意文件读写。

结语

通过这个纯 Java 实现的 AI Agent 项目,我们深入理解了 Agent 的三个核心机制:

  • ReAct 循环:推理与行动的交替执行,是 Agent"思考 - 行动 - 观察"的基本范式
  • 工具调用协议:大模型通过 Function Calling 标准接口与外部工具交互
  • 上下文管理:在有限的上下文窗口内,通过相关性过滤保留最有价值的信息

AI Agent 不是魔法,它本质上是一个以 LLM 为决策引擎、以工具为执行手段、以消息链为上下文的自动化循环系统。理解了这个本质,你就能根据自己的业务需求,构建出真正有用的智能体应用。

目录

  1. 引言
  2. 一、项目全景:架构与技术选型
  3. 1.1 项目结构
  4. 1.2 技术选型
  5. 二、Agent 核心循环:ReAct 模式的实现
  6. 2.1 BaseAgent:循环骨架
  7. 2.2 ToolCallAgent:ReAct 的核心引擎
  8. 2.3 ManusAgent:具体 Agent 的组装
  9. 三、消息系统:多模态对话的基石
  10. 3.1 四种角色的消息设计
  11. 3.2 LLM API 的封装
  12. 四、记忆管理:LLM 驱动的上下文过滤
  13. 4.1 相关性过滤接口
  14. 4.2 LLM 相关性过滤器
  15. 4.3 工具的动态过滤
  16. 五、工具系统:可插拔的能力扩展
  17. 5.1 工具接口与基类
  18. 5.2 Docker 沙箱:安全的代码执行
  19. 5.3 浏览器自动化:Playwright 驱动
  20. 5.4 网页搜索:Tavily 集成
  21. 六、完整执行流程:一个真实的例子
  22. 七、设计模式总结
  23. 八、进一步思考
  24. 结语
  • 💰 8折买阿里云服务器限时8折了解详情
  • 💰 8折买阿里云服务器限时8折购买
  • 🦞 5分钟部署阿里云小龙虾了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 动态规划经典题解:按摩师、打家劫舍、删除点数与粉刷房子
  • 高鋒集團合夥人黃俊瑯:以資本與生態賦能傳統企業 Web3 轉型
  • 数据结构与算法:随机链表复制的三步解法
  • VR+ 具身智能 + 人形机器人:通往现实世界的智能接口
  • RoVer:机器人奖励模型作为 VLA 模型的测试时验证器
  • C++ 堆数据结构原理与实现详解
  • AR 健身教练实践:基于 Rokid CXR-M SDK 的落地实现
  • 基于 JSP 的志愿者管理系统设计与实现
  • 钢条切割与饼干分发算法设计详解
  • 大模型 RAG 技术深度解析:从入门到进阶
  • Llama-3.2-3B 部署优化:Ollama 配置上下文窗口与 Token 限制
  • 宇树 G1 机器人强化学习训练环境搭建与奖励函数解析
  • HarmonyOS Next DevEco Studio 同步云端代码至工程指南
  • Spatial Joy 2025 全球 AR&AI 开发大赛参赛指南与赛道解析
  • 力扣 Hot 100 算法解题思路总结
  • LeetCode 链表专题:分割、相交及环形链表 C++ 解法
  • Flutter modular_core 鸿蒙适配:微服务化架构与依赖注入实践
  • 高鋒集團合夥人兼 Web3Labs 行政總裁黃俊瑯:以資本與生態賦能傳統企業 Web3 轉型
  • C++ std::map 容器详解:键值对存储与操作
  • YOLO26n-Pose 在 LSP 姿势估计数据集的训练预测流程(Python/C++)

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online