一、技能系统的核心挑战

大多数 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 分组管理 卸载/启用粒度到技能
Logo

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

更多推荐