AI Agent 的插件化工具系统:动态注册、热加载与安全沙箱
·
当你的 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 jar 或 mvn package,把生成的 jar 丢进 plugins/ 目录即可生效。
更多推荐

所有评论(0)