大模型开发 - 用纯Java手写一个多功能AI Agent:01 从零实现类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 项目结构

java 复制代码
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等重型框架,采用"最小依赖"原则:

依赖 版本 用途
OkHttp3 4.12.0 HTTP客户端,调用LLM API
Jackson 2.16.1 JSON序列化/反序列化
Lombok 1.18.30 减少样板代码
docker-java 3.3.6 Docker容器管理
langchain4j-tavily 0.36.2 Tavily网页搜索
Playwright 1.55.0 浏览器自动化

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

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

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

2.1 BaseAgent:循环骨架

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

java 复制代码
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循环的单步逻辑:

java 复制代码
@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方法中:

java 复制代码
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实现,负责注册工具和配置系统提示词:

java 复制代码
public class ManusAgent extends ToolCallAgent {

    private final static 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 四种角色的消息设计

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

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

java 复制代码
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中,多模态消息的转换逻辑如下:

java 复制代码
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的通义千问模型):

java 复制代码
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标准格式:

java 复制代码
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 相关性过滤接口

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

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

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

4.2 LLM相关性过滤器

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

java 复制代码
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;
}

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

java 复制代码
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返回数字或文本描述两种情况:

java 复制代码
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评估其与当前查询的相关性:

java 复制代码
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 工具接口与基类

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

java 复制代码
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参数定义的辅助方法:

java 复制代码
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支持三种返回形态------纯文本、错误信息、带图片的多模态结果:

java 复制代码
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沙箱:安全的代码执行

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

容器配置强调安全隔离

java 复制代码
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方式传递代码,避免了复杂的字符转义问题:

java 复制代码
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 等待元素出现或页面加载

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

java 复制代码
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可以截图后让大模型"看到"页面内容:

java 复制代码
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封装实现网页搜索:

java 复制代码
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的执行过程:

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

执行流程如下

java 复制代码
┌─────────────────────────────────────────────────────────┐
│ Step 0: 初始化                                           │
│  Memory: [SystemMessage, UserMessage]                    │
└──────────────────────────┬──────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────┐
│ Step 1: LLM推理 → 决定调用 write_file 工具               │
│  Action: write_file(path="workspace/test_page.html",    │
│          content="<html>...</html>")                     │
│  Memory: + [AssistantMsg(tool_calls), ToolMsg(成功)]      │
└──────────────────────────┬──────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────┐
│ Step 2: LLM推理 → 决定调用 browser.navigate              │
│  Action: browser(action="navigate",                     │
│          url="file:///path/to/workspace/test_page.html") │
│  Memory: + [AssistantMsg(tool_calls), ToolMsg(导航成功)]  │
└──────────────────────────┬──────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────┐
│ Step 3: LLM推理 → 决定调用 browser.screenshot            │
│  Action: browser(action="screenshot")                   │
│  Memory: + [AssistantMsg(tool_calls),                    │
│             ToolMsg(截图成功 + base64Image)]              │
└──────────────────────────┬──────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────┐
│ Step 4: 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为决策引擎、以工具为执行手段、以消息链为上下文的自动化循环系统。理解了这个本质,你就能根据自己的业务需求,构建出真正有用的智能体应用。


相关推荐
doiito1 天前
【Agent Harness】Gliding Horse 工具结果压缩体系:如何用“指针”驯服上下文膨胀
ai·rust·架构设计·系统设计·ai agent
doiito2 天前
【Agent Harness】Gliding Horse 上下文动态感知与智能压缩:让 Agent 真正“听得进”每一句话
ai·rust·架构设计·系统设计·ai agent
doiito3 天前
【Agent Harness】Gliding Horse 记忆系统深度剖析:像 CPU 一样思考的 AI 记忆架构
ai·rust·架构设计·系统设计·ai agent
doiito4 天前
【Agent Harness】Gliding Horse 给 Agent OS 装上双曲空间引擎与默克尔树边云同步
ai·rust·架构设计·系统设计·ai agent
doiito5 天前
【Agent Harness】Gliding Horse 本体论系统设计:给 AI Agent 装上“语义大脑”
ai·rust·架构设计·系统设计·ai agent
doiito6 天前
【Agent Harness】为什么我把 JSON‑LD “编译成 DAG” 后,整个 Agent 平台立刻聪明了
ai·rust·架构设计·系统设计·ai agent
xiezhr6 天前
折腾半小时,终于让AI 能直接帮我写飞书文档了
ai·飞书·ai agent·飞书cli·飞书文档
Super Scraper11 天前
如何批量抓取 TikTok 数据而不被封锁?完整指南
爬虫·ai·自动化·抖音·tiktok·ai agent
DogDaoDao11 天前
【GitHub】CL4R1T4S:AI 系统提示词的透明革命
人工智能·python·ai·大模型·github·ai agent·cl4r1t4s
Mininglamp_271812 天前
Vibe Coding 之后是 Vibe Operating?
后端·开源·多智能体·ai agent·mano-p