当你的 Agent 需要同时调用数据库、HTTP API、文件系统和 Shell 命令时,硬编码的工具列表是工程灾难。插件化是必经之路。


一、Agent 工具系统的工程挑战

一个真实的 AI Agent 可能需要几十个工具:查天气、搜文档、发邮件、操作数据库、执行代码……硬编码方式的问题:

问题 硬编码 插件化
新增工具 改代码 → 编译 → 部署 丢 jar 包到 plugins/ 目录
工具隔离 一个工具 OOM,Agent 崩溃 独立 ClassLoader,互不影响
权限控制 无,或全局一刀切 每个插件独立权限声明
版本管理 工具和 Agent 耦合 独立版本,独立升级
第三方贡献 提 PR,等合并 自行开发,热加载使用

本文用 Java 17+ 构建一个支持动态加载、声明式权限、JSON Schema 自动生成的 Agent 工具插件框架。


二、整体设计

┌──────────────────────────────────────────────┐
│              Agent Tool Registry             │
│                                               │
│  ┌──────────┐  ┌───────────┐  ┌───────────┐  │
│  │ Plugin   │  │ Permission│  │ Schema    │  │
│  │ Loader   │  │ Manager   │  │ Generator │  │
│  │          │  │           │  │           │  │
│  │ classpath│  │ allow/deny│  │ JSON      │  │
│  │ isolation│  │ by scope   │  │ Schema    │  │
│  └────┬─────┘  └─────┬─────┘  └─────┬─────┘  │
│       │              │              │         │
│  ┌────▼──────────────▼──────────────▼──────┐  │
│  │           ToolExecutor                   │  │
│  │  • timeout enforcement                   │  │
│  │  • result serialization                  │  │
│  │  • metrics collection                    │  │
│  └──────────────────────────────────────────┘  │
└──────────────────────────────────────────────┘

三、核心接口定义

3.1 Tool 注解——声明式工具定义

用 Java 注解声明工具的元数据,框架自动生成 LLM function calling 所需的 JSON Schema:

// api/src/main/java/com/agent/tool/api/Tool.java
package com.agent.tool.api;

import java.lang.annotation.*;

/**
 * 标记一个方法为 Agent 可调用的工具。
 * 框架会自动提取注解信息生成 function calling schema。
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Tool {

    /** 工具名称,LLM 通过此名称调用 */
    String name();

    /** 工具描述,会出现在 system prompt 的 tool description 中 */
    String description();

    /** 调用示例,帮助 LLM 理解如何使用 */
    String usage() default "";
}


// api/src/main/java/com/agent/tool/api/Param.java
package com.agent.tool.api;

import java.lang.annotation.*;

/**
 * 工具参数的元数据描述。
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Param {

    String description();

    boolean required() default true;

    /** 参数枚举值,用于生成 JSON Schema 的 enum 约束 */
    String[] enumValues() default {};

    /** 默认值 */
    String defaultValue() default "";
}

3.2 ToolDefinition——运行时描述

// api/src/main/java/com/agent/tool/api/ToolDefinition.java
package com.agent.tool.api;

import java.util.List;

/**
 * 工具运行时的元数据描述。
 */
public record ToolDefinition(
    String name,
    String description,
    String usage,
    List<ParameterDef> parameters,
    String pluginName,
    String pluginVersion,
    Class<?> toolClass
) {
    public record ParameterDef(
        String name,
        String description,
        Class<?> type,
        boolean required,
        List<String> enumValues,
        String defaultValue
    ) {}
}

四、插件热加载引擎

4.1 插件 ClassLoader——类路径隔离

每个插件 jar 使用独立的 URLClassLoader,避免依赖冲突,也支持卸载:

// core/src/main/java/com/agent/tool/loader/PluginClassLoader.java
package com.agent.tool.loader;

import java.net.URL;
import java.net.URLClassLoader;

/**
 * 插件专用的 ClassLoader。
 * 使用 child-first 加载策略:优先从插件自身加载类,避免与父 ClassLoader 冲突。
 */
public class PluginClassLoader extends URLClassLoader {

    private final String pluginName;

    public PluginClassLoader(String pluginName, URL[] urls, ClassLoader parent) {
        super(urls, parent);
        this.pluginName = pluginName;
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        // 先检查是否已加载
        Class<?> loaded = findLoadedClass(name);
        if (loaded != null) {
            return loaded;
        }

        // Child-first: 优先从插件 jar 加载
        // 排除 JDK 核心类和框架 API 类(让父 ClassLoader 加载,保持类型一致)
        if (name.startsWith("java.") || name.startsWith("javax.") ||
                name.startsWith("com.agent.tool.api.")) {
            return super.loadClass(name, resolve);
        }

        try {
            Class<?> clazz = findClass(name);
            if (resolve) {
                resolveClass(clazz);
            }
            return clazz;
        } catch (ClassNotFoundException e) {
            // 回退到父 ClassLoader
            return super.loadClass(name, resolve);
        }
    }

    public String getPluginName() {
        return pluginName;
    }
}

4.2 插件加载器——扫描、加载、缓存

// core/src/main/java/com/agent/tool/loader/PluginLoader.java
package com.agent.tool.loader;

import com.agent.tool.api.Tool;
import com.agent.tool.api.Param;
import com.agent.tool.api.ToolDefinition;

import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.net.URL;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class PluginLoader {

    private final Path pluginDir;
    private final Map<String, PluginClassLoader> loaders = new ConcurrentHashMap<>();
    private final Map<String, List<ToolDescriptor>> pluginTools = new ConcurrentHashMap<>();

    public record ToolDescriptor(
        String pluginName,
        Object instance,
        Method method,
        ToolDefinition definition
    ) {}

    public PluginLoader(Path pluginDir) {
        this.pluginDir = pluginDir;
        try {
            Files.createDirectories(pluginDir);
        } catch (IOException e) {
            throw new RuntimeException("Cannot create plugin dir: " + pluginDir, e);
        }
    }

    /**
     * 扫描并加载所有插件 jar。
     * 返回新加载的工具列表。
     */
    public List<ToolDescriptor> scanAndLoad() throws IOException {
        List<ToolDescriptor> newTools = new ArrayList<>();

        try (DirectoryStream<Path> jars = Files.newDirectoryStream(
                pluginDir, "*.jar")) {
            for (Path jarPath : jars) {
                String pluginName = jarPath.getFileName().toString()
                        .replace(".jar", "");
                // 跳过已加载的
                if (loaders.containsKey(pluginName)) {
                    continue;
                }
                List<ToolDescriptor> tools = loadPlugin(pluginName, jarPath);
                newTools.addAll(tools);
            }
        }
        return newTools;
    }

    private List<ToolDescriptor> loadPlugin(String pluginName, Path jarPath)
            throws IOException {
        List<ToolDescriptor> tools = new ArrayList<>();

        URL[] urls = { jarPath.toUri().toURL() };
        PluginClassLoader classLoader = new PluginClassLoader(
                pluginName, urls, getClass().getClassLoader());

        // 扫描 jar 中的所有类
        try (JarFile jar = new JarFile(jarPath.toFile())) {
            Enumeration<JarEntry> entries = jar.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                if (!entry.getName().endsWith(".class")) {
                    continue;
                }
                String className = entry.getName()
                        .replace('/', '.')
                        .replace(".class", "");

                try {
                    Class<?> clazz = classLoader.loadClass(className);
                    // 扫描带 @Tool 注解的方法
                    for (Method method : clazz.getDeclaredMethods()) {
                        Tool toolAnnotation = method.getAnnotation(Tool.class);
                        if (toolAnnotation == null) {
                            continue;
                        }
                        // 实例化工具类
                        Object instance = clazz.getDeclaredConstructor().newInstance();

                        ToolDefinition def = buildDefinition(
                                toolAnnotation, method, pluginName);

                        tools.add(new ToolDescriptor(pluginName, instance, method, def));
                    }
                } catch (NoClassDefFoundError | ClassNotFoundException e) {
                    // 依赖缺失,跳过
                    System.err.println("Skip class " + className + ": " + e.getMessage());
                } catch (Exception e) {
                    System.err.println("Failed to load " + className + ": " + e.getMessage());
                }
            }
        }

        loaders.put(pluginName, classLoader);
        pluginTools.put(pluginName, tools);
        System.out.println("Loaded plugin: " + pluginName + " (" + tools.size() + " tools)");
        return tools;
    }

    private ToolDefinition buildDefinition(Tool annotation, Method method,
                                           String pluginName) {
        List<ToolDefinition.ParameterDef> params = new ArrayList<>();
        for (Parameter param : method.getParameters()) {
            Param paramAnno = param.getAnnotation(Param.class);
            if (paramAnno == null) {
                throw new IllegalArgumentException(
                    "Parameter " + param.getName() + " in method " + method.getName()
                    + " is missing @Param annotation");
            }
            params.add(new ToolDefinition.ParameterDef(
                param.getName(), paramAnno.description(), param.getType(),
                paramAnno.required(),
                paramAnno.enumValues().length > 0
                    ? List.of(paramAnno.enumValues()) : List.of(),
                paramAnno.defaultValue().isEmpty() ? null : paramAnno.defaultValue()
            ));
        }
        return new ToolDefinition(
            annotation.name(), annotation.description(), annotation.usage(),
            params, pluginName, "1.0.0", method.getDeclaringClass()
        );
    }

    /**
     * 卸载插件,释放 ClassLoader。
     */
    public void unloadPlugin(String pluginName) {
        PluginClassLoader loader = loaders.remove(pluginName);
        pluginTools.remove(pluginName);
        if (loader != null) {
            try {
                loader.close();
            } catch (IOException e) {
                System.err.println("Error closing classloader: " + e.getMessage());
            }
        }
        System.gc(); // 建议 GC 回收 ClassLoader 持有的资源
    }

    public Map<String, List<ToolDescriptor>> getPluginTools() {
        return Collections.unmodifiableMap(pluginTools);
    }
}

五、JSON Schema 自动生成

LLM function calling 需要 JSON Schema 格式的工具描述,从注解自动生成:

// core/src/main/java/com/agent/tool/schema/SchemaGenerator.java
package com.agent.tool.schema;

import com.agent.tool.api.ToolDefinition;
import com.agent.tool.api.ToolDefinition.ParameterDef;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

import java.util.Map;

/**
 * 从 @Tool / @Param 注解自动生成 OpenAI function calling 格式的 JSON Schema。
 */
public class SchemaGenerator {

    private static final Map<Class<?>, String> TYPE_MAP = Map.of(
        String.class, "string",
        Integer.class, "integer", int.class, "integer",
        Long.class, "integer", long.class, "integer",
        Double.class, "number", double.class, "number",
        Float.class, "number", float.class, "number",
        Boolean.class, "boolean", boolean.class, "boolean"
    );

    /**
     * 生成单个工具的 function calling schema。
     *
     * 输出格式:
     * {
     *   "type": "function",
     *   "function": {
     *     "name": "search_docs",
     *     "description": "搜索文档",
     *     "parameters": {
     *       "type": "object",
     *       "properties": { ... },
     *       "required": ["query"]
     *     }
     *   }
     * }
     */
    public static JsonObject generate(ToolDefinition tool) {
        JsonObject func = new JsonObject();
        func.addProperty("name", tool.name());
        func.addProperty("description",
                tool.description() + (tool.usage().isEmpty() ? "" : "\n用法: " + tool.usage()));

        // parameters
        JsonObject params = new JsonObject();
        params.addProperty("type", "object");

        JsonObject properties = new JsonObject();
        JsonArray required = new JsonArray();

        for (ParameterDef param : tool.parameters()) {
            JsonObject prop = new JsonObject();
            String jsonType = TYPE_MAP.getOrDefault(param.type(), "string");
            prop.addProperty("type", jsonType);
            prop.addProperty("description", param.description());

            if (!param.enumValues().isEmpty()) {
                JsonArray enumArr = new JsonArray();
                param.enumValues().forEach(enumArr::add);
                prop.add("enum", enumArr);
            }
            if (param.defaultValue() != null) {
                prop.addProperty("default", param.defaultValue());
            }

            properties.add(param.name(), prop);

            if (param.required()) {
                required.add(param.name());
            }
        }

        params.add("properties", properties);
        params.add("required", required);
        func.add("parameters", params);

        JsonObject wrapper = new JsonObject();
        wrapper.addProperty("type", "function");
        wrapper.add("function", func);

        return wrapper;
    }

    /**
     * 生成所有工具列表的 schema 数组。
     */
    public static JsonArray generateAll(Iterable<ToolDefinition> tools) {
        JsonArray array = new JsonArray();
        for (ToolDefinition tool : tools) {
            array.add(generate(tool));
        }
        return array;
    }
}

六、安全沙箱与权限管理

工具调用需要权限控制——不能让 LLM 随意执行 rm -rf /

// core/src/main/java/com/agent/tool/security/PermissionManager.java
package com.agent.tool.security;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 声明式权限管理器。
 * 每个工具声明自己需要的权限,运行时检查当前会话是否授予了这些权限。
 */
public class PermissionManager {

    public enum Permission {
        READ_FILE,      // 读文件
        WRITE_FILE,     // 写文件
        EXEC_SHELL,     // 执行 shell 命令
        HTTP_GET,       // HTTP GET
        HTTP_POST,      // HTTP POST/PUT/DELETE
        DATABASE_READ,  // 数据库读
        DATABASE_WRITE, // 数据库写
        SEND_EMAIL,     // 发送邮件
        NETWORK_ANY,    // 任意网络访问
    }

    /** 工具 → 所需权限的映射 */
    private final Map<String, Set<Permission>> toolPermissions = new ConcurrentHashMap<>();

    /** 会话 → 已授予权限的映射 */
    private final Map<String, Set<Permission>> sessionPermissions = new ConcurrentHashMap<>();

    /**
     * 注册工具所需的权限。
     */
    public void registerToolPermissions(String toolName, Permission... permissions) {
        toolPermissions.put(toolName, Set.of(permissions));
    }

    /**
     * 为会话授予权限。
     */
    public void grantPermissions(String sessionId, Permission... permissions) {
        sessionPermissions.computeIfAbsent(sessionId, k -> EnumSet.noneOf(Permission.class))
                .addAll(List.of(permissions));
    }

    /**
     * 检查会话是否有权限调用此工具。
     *
     * @throws SecurityException 权限不足
     */
    public void checkPermission(String sessionId, String toolName)
            throws SecurityException {
        Set<Permission> required = toolPermissions.get(toolName);
        if (required == null || required.isEmpty()) {
            return; // 无权限要求,放行
        }

        Set<Permission> granted = sessionPermissions.get(sessionId);
        if (granted == null || !granted.containsAll(required)) {
            Set<Permission> missing = new HashSet<>(required);
            if (granted != null) {
                missing.removeAll(granted);
            }
            throw new SecurityException(
                "Tool '" + toolName + "' requires permissions: " + required
                + ". Missing: " + missing
                + ". Grant with: POST /sessions/" + sessionId + "/permissions"
            );
        }
    }

    /**
     * 撤销会话的所有权限。
     */
    public void revokeSession(String sessionId) {
        sessionPermissions.remove(sessionId);
    }
}

七、工具执行器

把加载、校验、执行、超时控制串起来:

// core/src/main/java/com/agent/tool/executor/ToolExecutor.java
package com.agent.tool.executor;

import com.agent.tool.api.ToolDefinition;
import com.agent.tool.loader.PluginLoader.ToolDescriptor;
import com.agent.tool.security.PermissionManager;
import com.agent.tool.schema.SchemaGenerator;
import com.google.gson.*;

import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.*;

/**
 * 统一的工具执行入口。
 * 负责:参数解析 → 权限检查 → 调用工具方法 → 超时控制 → 结果序列化。
 */
public class ToolExecutor {

    private final Map<String, ToolDescriptor> toolsByName = new ConcurrentHashMap<>();
    private final PermissionManager permissionManager;
    private final ExecutorService executor;
    private final long defaultTimeoutSeconds;

    public ToolExecutor(PermissionManager permissionManager,
                        int threadPoolSize, long defaultTimeoutSeconds) {
        this.permissionManager = permissionManager;
        this.executor = Executors.newFixedThreadPool(threadPoolSize);
        this.defaultTimeoutSeconds = defaultTimeoutSeconds;
    }

    /**
     * 注册工具到执行器。
     */
    public void registerTool(ToolDescriptor descriptor) {
        toolsByName.put(descriptor.definition().name(), descriptor);
    }

    /**
     * 执行工具调用。
     *
     * @param toolName    工具名称
     * @param arguments   JSON 格式的参数字符串,如 {"query": "hello"}
     * @param sessionId   会话 ID,用于权限检查
     * @param timeoutSec  超时秒数
     * @return 工具返回结果(JSON 字符串)
     */
    public ToolResult execute(String toolName, String arguments,
                              String sessionId, long timeoutSec) {
        // 1. 查找工具
        ToolDescriptor descriptor = toolsByName.get(toolName);
        if (descriptor == null) {
            return ToolResult.error("Unknown tool: " + toolName
                    + ". Available: " + toolsByName.keySet());
        }

        // 2. 权限检查
        try {
            permissionManager.checkPermission(sessionId, toolName);
        } catch (SecurityException e) {
            return ToolResult.error(e.getMessage());
        }

        // 3. 解析参数
        Object[] args;
        try {
            args = parseArguments(descriptor, arguments);
        } catch (Exception e) {
            return ToolResult.error("Argument parse error for '" + toolName
                    + "': " + e.getMessage());
        }

        // 4. 超时执行
        long timeout = timeoutSec > 0 ? timeoutSec : defaultTimeoutSeconds;
        Future<Object> future = executor.submit(() -> {
            try {
                Method method = descriptor.method();
                method.setAccessible(true);
                return method.invoke(descriptor.instance(), args);
            } catch (Exception e) {
                Throwable cause = e.getCause() != null ? e.getCause() : e;
                throw new ExecutionException(cause);
            }
        });

        try {
            Object result = future.get(timeout, TimeUnit.SECONDS);
            return ToolResult.success(result);
        } catch (TimeoutException e) {
            future.cancel(true);
            return ToolResult.error("Tool '" + toolName + "' timed out after "
                    + timeout + "s");
        } catch (ExecutionException e) {
            return ToolResult.error("Tool execution error: "
                    + e.getCause().getMessage());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return ToolResult.error("Tool execution interrupted");
        }
    }

    /**
     * 获取所有工具的 function calling schema(给 LLM 用)。
     */
    public String getToolsSchema() {
        List<ToolDefinition> definitions = toolsByName.values().stream()
                .map(ToolDescriptor::definition)
                .toList();
        return SchemaGenerator.generateAll(definitions).toString();
    }

    private Object[] parseArguments(ToolDescriptor descriptor, String arguments)
            throws Exception {
        JsonObject json;
        try {
            json = JsonParser.parseString(arguments).getAsJsonObject();
        } catch (JsonSyntaxException e) {
            throw new IllegalArgumentException("Invalid JSON arguments: " + arguments);
        }

        List<ToolDefinition.ParameterDef> params = descriptor.definition().parameters();
        Object[] args = new Object[params.size()];

        for (int i = 0; i < params.size(); i++) {
            ToolDefinition.ParameterDef param = params.get(i);
            String name = param.name();
            Class<?> type = param.type();

            if (json.has(name)) {
                args[i] = new Gson().fromJson(json.get(name), type);
            } else if (param.defaultValue() != null) {
                args[i] = new Gson().fromJson(param.defaultValue(), type);
            } else if (!param.required()) {
                args[i] = null;
            } else {
                throw new IllegalArgumentException(
                    "Required parameter '" + name + "' is missing");
            }
        }
        return args;
    }

    /** 工具执行结果 */
    public record ToolResult(boolean success, String content, String error) {
        public static ToolResult success(Object data) {
            return new ToolResult(true,
                data instanceof String ? (String) data : new Gson().toJson(data), null);
        }
        public static ToolResult error(String error) {
            return new ToolResult(false, null, error);
        }
    }
}

八、编写一个插件示例

// plugins/weather-plugin/src/main/java/com/example/WeatherTool.java
package com.example;

import com.agent.tool.api.Tool;
import com.agent.tool.api.Param;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class WeatherTool {

    private final HttpClient httpClient = HttpClient.newHttpClient();

    @Tool(
        name = "get_weather",
        description = "获取指定城市的实时天气信息",
        usage = "get_weather(city=\"北京\")"
    )
    public String getWeather(
        @Param(description = "城市名称,中文", required = true,
               enumValues = {"北京", "上海", "深圳", "杭州", "成都"})
        String city,

        @Param(description = "温度单位", required = false,
               enumValues = {"celsius", "fahrenheit"}, defaultValue = "celsius")
        String unit
    ) {
        // 模拟 API 调用
        try {
            HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.weather.example/v1/current?q=" + city))
                .timeout(java.time.Duration.ofSeconds(10))
                .GET()
                .build();
            HttpResponse<String> response = httpClient.send(
                request, HttpResponse.BodyHandlers.ofString());
            return response.body();
        } catch (Exception e) {
            return "{\"error\": \"" + e.getMessage() + "\"}";
        }
    }
}

九、集成到 Agent 主循环

// 启动时初始化
var pluginDir = Path.of("./plugins");
var pluginLoader = new PluginLoader(pluginDir);
var tools = pluginLoader.scanAndLoad();

var permissionManager = new PermissionManager();
permissionManager.registerToolPermissions("get_weather",
    PermissionManager.Permission.HTTP_GET);

var toolExecutor = new ToolExecutor(permissionManager, 4, 30);

for (var tool : tools) {
    toolExecutor.registerTool(tool);
}

// 获取 LLM 可用的工具 schema
String toolsSchema = toolExecutor.getToolsSchema();
// 注入到 system prompt 或 function calling 参数中

// 当 LLM 返回 function call 时
var result = toolExecutor.execute(
    "get_weather",
    "{\"city\": \"北京\"}",
    sessionId,
    30
);
if (result.success()) {
    // 将 result.content() 作为 tool 返回结果传给 LLM
}

十、总结

能力 实现 关键点
热加载 URLClassLoader + 目录扫描 child-first 策略,避免类型冲突
类型隔离 独立 PluginClassLoader close() 可释放,支持卸载
Schema 生成 注解 → JSON Schema 零配置,LLM 直接理解工具签名
权限控制 声明式许可 + 会话粒度 最小权限原则,防止 prompt injection 滥用
超时保护 Future.get(timeout) 防止卡死工具阻塞 Agent 主循环
参数校验 反射解析 + 类型转换 自动将 JSON 参数转为强类型 Java 对象

插件系统的核心价值不是 “能加新功能”,而是加新功能只需要丢一个 jar 包——不需要重新编译 Agent,不需要停服,不需要改配置文件。


构建:./gradlew jarmvn package,把生成的 jar 丢进 plugins/ 目录即可生效。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐