Java AI 框架技能系统设计:配置驱动的动态工具编排
·
一、技能系统的核心挑战
大多数 AI Agent 框架把"工具"定义为代码中的函数,需要修改代码、重新编译才能增减工具。这在生产环境中不现实:
| 场景 | 硬编码工具 | 配置驱动技能 |
|---|---|---|
| 新增一个搜索工具 | 改代码 → PR → CI → 部署 | 加一条 YAML 配置 |
| 临时禁用某个工具 | 注释代码 → 部署 | 改配置 → 热加载 |
| A/B 测试两套工具集 | 两套代码分支 | 两套配置文件 |
| 多租户不同工具集 | 需要框架层支持 | 按租户加载配置 |
技能(Skill)系统的本质是:把工具的组织、加载、编排从代码中剥离到配置层。
二、技能系统的架构设计
┌──────────────────────────────────────────────┐
│ SkillEngine │
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ Config │ │ Registry │ │ Executor │ │
│ │ Loader │──│ (工具注册)│──│ (技能执行器) │ │
│ └──────────┘ └──────────┘ └────────────┘ │
│ │ │ │
│ skills.yaml ┌───────┐ │
│ YAML/JSON 配置 │ Tool │ │
│ │ Pool │ │
│ └───────┘ │
└──────────────────────────────────────────────┘
核心概念
- Skill(技能):一组工具的集合,带元数据(名称、描述、标签、启用状态)
- Tool Definition:工具的定义(名称、参数 schema、实现类引用)
- Skill Config:YAML 配置文件,声明一个技能的完整信息
- Skill Engine:加载配置 → 实例化工具 → 注册到运行时 → 提供调用接口
三、配置契约:技能 YAML 文件
# skills/search-skill.yaml
skill:
name: web-search
version: "1.2.0"
description: "网络搜索技能,支持 Google 和 Bing"
enabled: true
tags:
- search
- web
tools:
- name: google_search
description: "使用 Google 搜索网页"
class: com.example.skills.search.GoogleSearchTool
timeout: 15s # 工具级别超时
retry:
maxAttempts: 3
backoff: exponential # exponential | fixed
initialDelay: 1s
inputSchema:
type: object
properties:
query:
type: string
description: "搜索关键词"
numResults:
type: integer
description: "返回结果数量"
default: 10
required:
- query
- name: bing_search
description: "使用 Bing 搜索网页"
class: com.example.skills.search.BingSearchTool
timeout: 10s
inputSchema:
type: object
properties:
query:
type: string
description: "搜索关键词"
required:
- query
# skills/data-skill.yaml
skill:
name: data-analysis
version: "1.0.0"
description: "数据分析技能"
enabled: false # 可临时禁用
tags:
- data
- analytics
depends_on: # 声明依赖的其他技能
- web-search
tools:
- name: sql_query
description: "执行 SQL 查询"
class: com.example.skills.data.SqlQueryTool
timeout: 30s
retry:
maxAttempts: 1 # 危险操作不重试
backoff: fixed
initialDelay: 0s
inputSchema:
type: object
properties:
sql:
type: string
description: "SQL 查询语句(只读)"
required:
- sql
四、核心实现
4.1 配置模型
// com/example/skillengine/config/SkillConfig.java
package com.example.skillengine.config;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public class SkillConfig {
@JsonProperty("skill")
private SkillDefinition skill;
// getters & setters...
public static SkillConfig load(File yamlFile) throws IOException {
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
return mapper.readValue(yamlFile, SkillConfig.class);
}
public static class SkillDefinition {
private String name;
private String version;
private String description;
private boolean enabled = true;
private List<String> tags;
private List<String> dependsOn;
private List<ToolDefinition> tools;
// getters & setters...
}
public static class ToolDefinition {
private String name;
private String description;
@JsonProperty("class")
private String className;
private String timeout; // 如 "15s"
private RetryConfig retry;
private InputSchema inputSchema;
// getters & setters...
}
public static class RetryConfig {
private int maxAttempts = 1;
private String backoff = "fixed"; // fixed | exponential
private String initialDelay = "1s";
// getters & setters...
}
public static class InputSchema {
private String type;
private Map<String, PropertyDef> properties;
private List<String> required;
// getters & setters...
}
public static class PropertyDef {
private String type;
private String description;
private Object defaultValue;
// getters & setters...
}
}
4.2 工具接口与注册表
// com/example/skillengine/tool/Tool.java
package com.example.skillengine.tool;
import com.fasterxml.jackson.databind.JsonNode;
/**
* 所有工具必须实现此接口。
* 工具实例由 SkillEngine 通过反射创建。
*/
public interface Tool {
/** 工具名称,必须与配置文件中的 name 一致 */
String getName();
/** 执行工具调用 */
ToolResult execute(JsonNode arguments) throws Exception;
/** 工具初始化(可选的资源分配,如建立连接池) */
default void init() {}
/** 工具销毁(关闭连接池等) */
default void destroy() {}
}
// com/example/skillengine/tool/ToolResult.java
package com.example.skillengine.tool;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class ToolResult {
private final String text;
private final boolean isError;
private final ObjectNode metadata; // 可附加结构化数据
public ToolResult(String text, boolean isError) {
this.text = text;
this.isError = isError;
this.metadata = null;
}
public static ToolResult success(String text) {
return new ToolResult(text, false);
}
public static ToolResult error(String message) {
return new ToolResult(message, true);
}
// getters...
}
// com/example/skillengine/registry/ToolRegistry.java
package com.example.skillengine.registry;
import com.example.skillengine.tool.Tool;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 线程安全的工具注册表。
* 支持运行时动态注册/卸载。
*/
public class ToolRegistry {
private final ConcurrentHashMap<String, ToolEntry> tools = new ConcurrentHashMap<>();
private static class ToolEntry {
final Tool tool;
final String skillName;
final int timeoutSeconds;
final int maxRetries;
ToolEntry(Tool tool, String skillName, int timeoutSeconds, int maxRetries) {
this.tool = tool;
this.skillName = skillName;
this.timeoutSeconds = timeoutSeconds;
this.maxRetries = maxRetries;
}
}
/** 注册工具 */
public void register(String toolName, Tool tool, String skillName,
int timeoutSeconds, int maxRetries) {
tools.put(toolName, new ToolEntry(tool, skillName, timeoutSeconds, maxRetries));
}
/** 卸载某个技能下的所有工具 */
public void unregisterBySkill(String skillName) {
tools.entrySet().removeIf(e -> skillName.equals(e.getValue().skillName));
}
/** 获取所有已注册的工具名 */
public Set<String> getToolNames() {
return Collections.unmodifiableSet(tools.keySet());
}
/** 获取工具 */
public ToolEntry get(String toolName) {
return tools.get(toolName);
}
/** 获取某个技能下的所有工具 */
public List<String> getToolsBySkill(String skillName) {
List<String> result = new ArrayList<>();
tools.forEach((name, entry) -> {
if (skillName.equals(entry.skillName)) result.add(name);
});
return result;
}
}
4.3 技能引擎:配置加载 + 反射实例化
// com/example/skillengine/SkillEngine.java
package com.example.skillengine;
import com.example.skillengine.config.SkillConfig;
import com.example.skillengine.config.SkillConfig.ToolDefinition;
import com.example.skillengine.registry.ToolRegistry;
import com.example.skillengine.tool.Tool;
import com.example.skillengine.tool.ToolResult;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 技能引擎:加载技能配置、实例化工具、提供调用入口。
* 支持 WatchService 热加载。
*/
public class SkillEngine implements AutoCloseable {
private final ToolRegistry registry = new ToolRegistry();
private final ExecutorService executor = Executors.newCachedThreadPool();
private final ConcurrentHashMap<String, SkillConfig> loadedSkills = new ConcurrentHashMap<>();
// 热加载相关
private WatchService watchService;
private final Path skillsDir;
private static final Pattern DURATION = Pattern.compile("(\\d+)(s|m)");
public SkillEngine(Path skillsDir) throws IOException {
this.skillsDir = skillsDir;
}
/** 加载 skillsDir 下所有 .yaml 技能配置 */
public void loadAll() throws IOException {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(skillsDir, "*.yaml")) {
for (Path path : stream) {
loadSkill(path.toFile());
}
}
}
/** 加载单个技能 */
public void loadSkill(File yamlFile) throws IOException {
SkillConfig config = SkillConfig.load(yamlFile);
String skillName = config.getSkill().getName();
if (!config.getSkill().isEnabled()) {
System.err.println("[SkillEngine] Skill disabled, skipping: " + skillName);
return;
}
// 检查依赖
for (String dep : config.getSkill().getDependsOn()) {
if (!loadedSkills.containsKey(dep)) {
throw new IllegalStateException(
"Skill '" + skillName + "' depends on '" + dep + "' which is not loaded");
}
}
// 反射实例化工具
for (ToolDefinition toolDef : config.getSkill().getTools()) {
try {
Class<?> clazz = Class.forName(toolDef.getClassName());
Tool tool = (Tool) clazz.getDeclaredConstructor().newInstance();
tool.init();
int timeoutSec = parseDuration(toolDef.getTimeout());
int maxRetries = toolDef.getRetry() != null ? toolDef.getRetry().getMaxAttempts() : 1;
registry.register(toolDef.getName(), tool, skillName, timeoutSec, maxRetries);
System.err.println("[SkillEngine] Registered tool: " + toolDef.getName()
+ " (skill: " + skillName + ", timeout: " + timeoutSec + "s)");
} catch (Exception e) {
System.err.println("[SkillEngine] Failed to instantiate tool: "
+ toolDef.getName() + " — " + e.getMessage());
throw new IOException("Tool instantiation failed: " + toolDef.getClassName(), e);
}
}
loadedSkills.put(skillName, config);
System.err.println("[SkillEngine] Skill loaded: " + skillName);
}
/** 卸载技能 */
public void unloadSkill(String skillName) {
registry.unregisterBySkill(skillName);
loadedSkills.remove(skillName);
System.err.println("[SkillEngine] Skill unloaded: " + skillName);
}
/** 重新加载技能(先卸载再加载) */
public void reloadSkill(File yamlFile) throws IOException {
String name = SkillConfig.load(yamlFile).getSkill().getName();
unloadSkill(name);
loadSkill(yamlFile);
}
/** 执行工具调用(带超时 + 重试) */
public ToolResult execute(String toolName, JsonNode arguments)
throws TimeoutException, InterruptedException {
ToolRegistry.ToolEntry entry = registry.get(toolName);
if (entry == null) {
return ToolResult.error("Unknown tool: " + toolName);
}
Exception lastError = null;
int maxAttempts = entry.maxRetries;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
Future<ToolResult> future = executor.submit(() -> {
return entry.tool.execute(arguments);
});
try {
return future.get(entry.timeoutSeconds, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
lastError = e;
System.err.println("[SkillEngine] Tool '" + toolName
+ "' timeout after " + entry.timeoutSeconds + "s (attempt " + attempt + ")");
} catch (ExecutionException e) {
lastError = (Exception) e.getCause();
System.err.println("[SkillEngine] Tool '" + toolName
+ "' failed: " + lastError.getMessage() + " (attempt " + attempt + ")");
}
if (attempt < maxAttempts) {
long delay = computeBackoff(attempt); // 指数退避
Thread.sleep(delay);
}
}
return ToolResult.error("Tool '" + toolName + "' failed after "
+ maxAttempts + " attempts: " + lastError.getMessage());
}
private long computeBackoff(int attempt) {
// 指数退避: 1s, 2s, 4s, 8s...
return (long) Math.pow(2, attempt - 1) * 1000;
}
/** 启用 WatchService 热加载 —— 监控 skillsDir 下文件变化 */
public void enableHotReload() throws IOException {
watchService = FileSystems.getDefault().newWatchService();
skillsDir.register(watchService,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_CREATE);
Thread watcher = new Thread(() -> {
System.err.println("[SkillEngine] Hot reload watcher started on " + skillsDir);
while (!Thread.currentThread().isInterrupted()) {
try {
WatchKey key = watchService.poll(5, TimeUnit.SECONDS);
if (key == null) continue;
for (WatchEvent<?> event : key.pollEvents()) {
Path changed = (Path) event.context();
if (changed.toString().endsWith(".yaml")) {
Path fullPath = skillsDir.resolve(changed);
System.err.println("[SkillEngine] Detected change: " + changed);
try {
reloadSkill(fullPath.toFile());
} catch (IOException e) {
System.err.println("[SkillEngine] Hot reload failed: " + e.getMessage());
}
}
}
key.reset();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}, "skill-hot-reload");
watcher.setDaemon(true);
watcher.start();
}
/** 获取所有已加载工具信息(供 MCP tools/list 使用) */
public List<Map<String, Object>> listTools() {
List<Map<String, Object>> result = new ArrayList<>();
loadedSkills.forEach((skillName, config) -> {
for (ToolDefinition td : config.getSkill().getTools()) {
Map<String, Object> info = new LinkedHashMap<>();
info.put("name", td.getName());
info.put("description", td.getDescription());
info.put("skill", skillName);
info.put("inputSchema", td.getInputSchema());
result.add(info);
}
});
return result;
}
private int parseDuration(String s) {
if (s == null) return 30; // 默认 30 秒
Matcher m = DURATION.matcher(s);
if (m.find()) {
int val = Integer.parseInt(m.group(1));
return "m".equals(m.group(2)) ? val * 60 : val;
}
return 30;
}
@Override
public void close() {
loadedSkills.keySet().forEach(this::unloadSkill);
executor.shutdown();
try { watchService.close(); } catch (Exception ignored) {}
}
}
4.4 示例工具实现
// com/example/skills/search/GoogleSearchTool.java
package com.example.skills.search;
import com.example.skillengine.tool.Tool;
import com.example.skillengine.tool.ToolResult;
import com.fasterxml.jackson.databind.JsonNode;
public class GoogleSearchTool implements Tool {
@Override
public String getName() { return "google_search"; }
@Override
public ToolResult execute(JsonNode arguments) {
String query = arguments.path("query").asText();
int numResults = arguments.path("numResults").asInt(10);
// 生产环境:调用 Google Custom Search API
// String apiKey = System.getenv("GOOGLE_API_KEY");
// HttpRequest req = HttpRequest.newBuilder()
// .uri(URI.create("https://customsearch.googleapis.com/..."))
// .build();
// 演示返回
return ToolResult.success(
"Google 搜索结果(" + numResults + "条),关键词:" + query + "\n" +
"1. 结果一: https://example.com/1\n" +
"2. 结果二: https://example.com/2\n" +
"...(演示数据)"
);
}
}
五、使用示例
// App.java
public class App {
public static void main(String[] args) throws Exception {
Path skillsDir = Path.of("./skills");
SkillEngine engine = new SkillEngine(skillsDir);
// 1. 加载所有技能
engine.loadAll();
// 2. 启用热加载(修改 YAML 自动生效)
engine.enableHotReload();
// 3. 查看所有工具
System.out.println("Loaded tools: " + engine.listTools());
// 4. 执行工具调用
ObjectMapper mapper = new ObjectMapper();
JsonNode args = mapper.createObjectNode().put("query", "AI Agent 框架");
ToolResult result = engine.execute("google_search", args);
System.out.println("Result: " + result.getText());
// 5. 动态注册新工具(无需改 YAML)
// engine.getRegistry().register("custom_tool", new MyTool(), "custom", 10, 2);
}
}
六、工程化要点
| 能力 | 实现 | 价值 |
|---|---|---|
| 配置驱动 | YAML 声明工具,反射实例化 | 新增工具不改代码 |
| 热加载 | WatchService 监控文件变化 | 运行时增减技能,零停机 |
| 超时+重试 | Future.get(timeout) + 指数退避 | 单工具故障不阻塞 Agent |
| 依赖管理 | depends_on 声明显式依赖 | 避免加载顺序问题 |
| 并发安全 | ConcurrentHashMap + 线程池 | 支持多 Agent 并发调用 |
| 技能隔离 | 按 skillName 分组管理 | 卸载/启用粒度到技能 |
更多推荐


所有评论(0)