跳转至

Step 9: 工具与技能系统 —— 从单工具到能力组合

目标:实现工具注册表和 Skill 封装,支持工具组合与复用

难度:⭐⭐⭐ | 代码量:约 750 行 | 预计学习时间:3-4 小时


一、为什么需要技能系统?

1.1 Step 8 的问题

Step 8 的安全沙箱提供了防护,但工具管理还是单工具粒度

class Agent {
public:
    Agent() {
        // 硬编码注册单个工具
        tool_manager_.register_tool(std::make_shared<WeatherTool>());
        tool_manager_.register_tool(std::make_shared<CalcTool>());
        tool_manager_.register_tool(std::make_shared<TimeTool>());
        // 每加一个工具都要改代码、重新编译!
    }

    // 问题:工具之间没有关联,无法组合完成复杂任务
};

// 用户:"帮我规划去东京的旅行"
// Agent 只能一个个调用工具,无法形成连贯的"旅行规划"能力

三个核心问题: 1. 粒度太细 - 单个工具无法表达复杂能力 2. 缺乏组合 - 工具之间无法协作 3. 难以复用 - 同样的工具组合要重复配置

1.2 Skill 的概念

Skill(技能) 是完成特定任务的工具组合 + 工作流

单工具 vs Skill:

单工具:                              Travel Skill:
┌─────────────┐                       ┌─────────────────────────────┐
│ WeatherTool │                       │      旅行规划技能            │
└─────────────┘                       │  ┌─────────┐ ┌─────────┐  │
                                      │  │ Weather │ │ Flights │  │
┌─────────────┐                       │  └────┬────┘ └────┬────┘  │
│ FlightTool  │                       │       └─────┬─────┘       │
└─────────────┘                       │             ▼             │
                                      │      ┌──────────┐         │
┌─────────────┘                       │      │ 行程整合  │         │
│ HotelTool   │                       │      └──────────┘         │
└─────────────┘                       └─────────────────────────────┘

问题:工具零散                           优势:封装完整能力
      用户需自行组合                          一次配置,多次复用

Skill 的价值: - 封装复杂能力 - 一次配置,多次复用 - 工具组合 - 多个工具协作完成特定任务 - 领域抽象 - 面向业务场景而非单个功能


二、技能系统设计

2.1 核心概念

// Skill:封装完成特定任务的工具集合 + 工作流
struct Skill {
    std::string name;                    // 技能名称
    std::string description;             // 技能描述
    std::vector<std::string> tools;      // 包含的工具名
    json::object workflow;               // 执行工作流(工具调用顺序)
};

// 工具工厂:负责创建工具实例
using ToolFactory = std::function<std::shared_ptr<ITool>()>;

// 注册表:统一管理工具和 Skill
class ToolRegistry {
public:
    // 注册单个工具
    void register_tool(const std::string& name, ToolFactory factory);

    // 注册 Skill(工具组合)⭐ 新增
    void register_skill(const Skill& skill);

    // 创建工具实例
    std::shared_ptr<ITool> create_tool(const std::string& name);

    // 获取 Skill 包含的所有工具 ⭐ 新增
    std::vector<std::shared_ptr<ITool>> create_skill_tools(const std::string& skill_name);

    // 获取所有已注册的工具和 Skill
    std::vector<std::string> list_tools() const;
    std::vector<std::string> list_skills() const;  // ⭐ 新增

private:
    std::map<std::string, ToolFactory> factories_;
    std::map<std::string, Skill> skills_;  // ⭐ 新增
};

2.2 Skill 配置示例

{
    "skills": [
        {
            "name": "travel_planning",
            "description": "规划旅行行程,包括天气查询、航班搜索和酒店推荐",
            "tools": ["weather", "flight_search", "hotel_search"],
            "workflow": {
                "steps": [
                    {"tool": "weather", "input": "{destination}"},
                    {"tool": "flight_search", "input": "{origin}_to_{destination}"},
                    {"tool": "hotel_search", "input": "{destination}"}
                ]
            }
        },
        {
            "name": "data_analysis",
            "description": "数据分析技能:查询数据库并生成图表",
            "tools": ["sql_query", "chart_generator"],
            "workflow": {
                "steps": [
                    {"tool": "sql_query", "input": "{query}"},
                    {"tool": "chart_generator", "input": "{query_result}"}
                ]
            }
        }
    ]
}

三、注册表实现

3.1 基础注册表(支持 Skill)

class ToolRegistry {
public:
    // 注册工具工厂
    template<typename T, typename... Args>
    void register_tool(const std::string& name, Args... args) {
        static_assert(std::is_base_of_v<ITool, T>, 
                      "T must inherit from ITool");

        factories_[name] = [...args]() -> std::shared_ptr<ITool> {
            return std::make_shared<T>(args...);
        };

        std::cout << "[Registry] Registered tool: " << name << "\n";
    }

    // ⭐ 注册 Skill
    void register_skill(const Skill& skill) {
        // 验证所有工具都已注册
        for (const auto& tool_name : skill.tools) {
            if (!has_tool(tool_name)) {
                throw std::runtime_error(
                    "Skill '" + skill.name + "' requires unknown tool: " + tool_name
                );
            }
        }

        skills_[skill.name] = skill;
        std::cout << "[Registry] Registered skill: " << skill.name 
                  << " (" << skill.tools.size() << " tools)\n";
    }

    // 创建工具实例
    std::shared_ptr<ITool> create_tool(const std::string& name) const {
        auto it = factories_.find(name);
        if (it == factories_.end()) {
            throw std::runtime_error("Tool not found: " + name);
        }
        return it->second();
    }

    // ⭐ 获取 Skill 的所有工具实例
    std::vector<std::shared_ptr<ITool>> create_skill_tools(const std::string& skill_name) {
        auto it = skills_.find(skill_name);
        if (it == skills_.end()) {
            throw std::runtime_error("Skill not found: " + skill_name);
        }

        std::vector<std::shared_ptr<ITool>> tools;
        for (const auto& tool_name : it->second.tools) {
            tools.push_back(create_tool(tool_name));
        }
        return tools;
    }

    // 检查工具是否存在
    bool has_tool(const std::string& name) const {
        return factories_.count(name) > 0;
    }

    // ⭐ 检查 Skill 是否存在
    bool has_skill(const std::string& name) const {
        return skills_.count(name) > 0;
    }

    // 获取所有工具名
    std::vector<std::string> list_tools() const {
        std::vector<std::string> names;
        for (const auto& [name, _] : factories_) {
            names.push_back(name);
        }
        return names;
    }

    // ⭐ 获取所有 Skill 名
    std::vector<std::string> list_skills() const {
        std::vector<std::string> names;
        for (const auto& [name, _] : skills_) {
            names.push_back(name);
        }
        return names;
    }

    // ⭐ 获取 Skill 信息
    std::optional<Skill> get_skill(const std::string& name) const {
        auto it = skills_.find(name);
        if (it != skills_.end()) {
            return it->second;
        }
        return std::nullopt;
    }

private:
    std::map<std::string, ToolFactory> factories_;
    std::map<std::string, Skill> skills_;
};

3.2 配置文件驱动

class ToolLoader {
public:
    // 从 JSON 配置文件加载工具和 Skill
    void load_from_config(const std::string& config_path,
                          ToolRegistry& registry);

private:
    // 内置工具类型映射
    std::map<std::string, std::function<ToolFactory(const json&)>> 
        builtin_tools_ = {
        {"weather", [](const json& cfg) -> ToolFactory {
            std::string api_key = cfg.value("api_key", "");
            return [api_key]() -> std::shared_ptr<ITool> {
                return std::make_shared<WeatherTool>(api_key);
            };
        }},
        {"calculator", [](const json& cfg) -> ToolFactory {
            int precision = cfg.value("precision", 2);
            return [precision]() -> std::shared_ptr<ITool> {
                return std::make_shared<CalculatorTool>(precision);
            };
        }},
        {"time", [](const json&) -> ToolFactory {
            return []() -> std::shared_ptr<ITool> {
                return std::make_shared<TimeTool>();
            };
        }},
        {"flight_search", [](const json& cfg) -> ToolFactory {
            std::string api_key = cfg.value("api_key", "");
            return [api_key]() -> std::shared_ptr<ITool> {
                return std::make_shared<FlightSearchTool>(api_key);
            };
        }},
        {"hotel_search", [](const json& cfg) -> ToolFactory {
            std::string api_key = cfg.value("api_key", "");
            return [api_key]() -> std::shared_ptr<ITool> {
                return std::make_shared<HotelSearchTool>(api_key);
            };
        }}
    };
};

void ToolLoader::load_from_config(const std::string& config_path,
                                  ToolRegistry& registry) {
    // 读取配置文件
    std::ifstream file(config_path);
    json config;
    file >> config;

    // 1. 加载工具
    if (config.contains("tools")) {
        for (const auto& tool_config : config["tools"]) {
            std::string name = tool_config["name"];
            std::string type = tool_config["type"];

            if (!builtin_tools_.count(type)) {
                std::cerr << "Unknown tool type: " << type << "\n";
                continue;
            }

            ToolFactory factory = builtin_tools_[type](tool_config);
            registry.register_factory(name, factory);
        }
    }

    // 2. ⭐ 加载 Skill
    if (config.contains("skills")) {
        for (const auto& skill_config : config["skills"]) {
            Skill skill;
            skill.name = skill_config["name"];
            skill.description = skill_config.value("description", "");

            for (const auto& tool_name : skill_config["tools"]) {
                skill.tools.push_back(tool_name);
            }

            if (skill_config.contains("workflow")) {
                skill.workflow = skill_config["workflow"];
            }

            registry.register_skill(skill);
        }
    }
}

四、在 Agent 中使用 Skill

4.1 Agent 集成

class Agent {
public:
    Agent(const std::string& config_path) {
        // 1. 创建注册表
        ToolRegistry registry;

        // 2. 从配置文件加载工具和 Skill
        ToolLoader loader;
        loader.load_from_config(config_path, registry);

        // 3. 初始化安全组件
        security_checker_ = std::make_unique<SecurityChecker>(config_path);
        audit_logger_ = std::make_unique<AuditLogger>("./logs");

        registry_ = std::move(registry);
    }

    // 处理用户消息
    std::string process_message(const std::string& user_input) {
        // 检查是否调用 Skill
        if (registry_.has_skill(user_input)) {
            return execute_skill(user_input);
        }

        // 否则走常规 LLM 处理
        return process_with_llm(user_input);
    }

    // ⭐ 执行 Skill(顺序调用工具组合)
    std::string execute_skill(const std::string& skill_name) {
        auto skill_opt = registry_.get_skill(skill_name);
        if (!skill_opt) {
            return "Skill not found: " + skill_name;
        }

        const auto& skill = *skill_opt;
        json::object results;

        std::cout << "[Agent] Executing skill: " << skill.name << "\n";

        // 按 workflow 顺序调用工具
        for (const auto& step : skill.workflow.at("steps").as_array()) {
            std::string tool_name = std::string(step.at("tool").as_string());

            auto tool = registry_.create_tool(tool_name);

            // 安全检查
            if (!security_checker_->check(tool_name, {})) {
                audit_logger_->log(tool_name, false, "Security check failed");
                continue;
            }

            // 执行工具
            ToolResult result = tool->execute({}, {});
            results[tool_name] = result.data;

            audit_logger_->log(tool_name, result.success, "");
        }

        // 整合结果返回
        return format_skill_result(skill_name, results);
    }

    // 显示可用能力
    void show_capabilities() {
        std::cout << "\n📦 Available Tools:\n";
        for (const auto& name : registry_.list_tools()) {
            std::cout << "  • " << name << "\n";
        }

        std::cout << "\n🎯 Available Skills:\n";
        for (const auto& name : registry_.list_skills()) {
            auto skill = registry_.get_skill(name);
            std::cout << "  • " << name << " - " << skill->description << "\n";
            std::cout << "    (" << skill->tools.size() << " tools)\n";
        }
    }

private:
    ToolRegistry registry_;
    std::unique_ptr<SecurityChecker> security_checker_;
    std::unique_ptr<AuditLogger> audit_logger_;

    std::string format_skill_result(const std::string& skill_name, 
                                    const json::object& results);
    std::string process_with_llm(const std::string& input);
};

五、配置示例

5.1 完整配置文件

{
    "tools": [
        {
            "name": "get_weather",
            "type": "weather",
            "api_key": "${WEATHER_API_KEY}",
            "timeout_ms": 5000
        },
        {
            "name": "search_flights",
            "type": "flight_search",
            "api_key": "${FLIGHT_API_KEY}"
        },
        {
            "name": "search_hotels",
            "type": "hotel_search",
            "api_key": "${HOTEL_API_KEY}"
        },
        {
            "name": "calculate",
            "type": "calculator",
            "precision": 2
        },
        {
            "name": "get_time",
            "type": "time"
        }
    ],
    "skills": [
        {
            "name": "travel_planning",
            "description": "规划完整旅行行程",
            "tools": ["get_weather", "search_flights", "search_hotels"],
            "workflow": {
                "steps": [
                    {"tool": "get_weather", "input": "{destination}"},
                    {"tool": "search_flights", "input": "{origin}_to_{destination}"},
                    {"tool": "search_hotels", "input": "{destination}"}
                ]
            }
        },
        {
            "name": "trip_budget",
            "description": "计算旅行预算",
            "tools": ["calculate", "get_time"],
            "workflow": {
                "steps": [
                    {"tool": "get_time", "input": "now"},
                    {"tool": "calculate", "input": "{flight_price} + {hotel_price}"}
                ]
            }
        }
    ],
    "security": {
        "allowed_domains": ["api.weather.com", "api.flights.com"],
        "max_file_size": 10485760,
        "timeout_ms": 30000
    }
}

六、本章小结

6.1 演进对比

Step 8: 单个工具 + 安全沙箱
    ↓ 问题:工具粒度太细,无法组合
Step 9: 工具与技能系统 ⭐ 本章
    • ToolRegistry - 统一管理工具
    • Skill - 封装工具组合
    • 配置驱动 - 外部化配置

6.2 新增能力

特性 说明
Skill 封装 将多个工具组合成可复用的能力单元
工作流 定义工具调用顺序和数据传递
统一注册表 工具和 Skill 统一管理
配置驱动 JSON 配置文件定义能力

6.3 仍存在什么问题?

当前 Skill 的工具有限,都是我们自己实现的。如何接入社区生态中已有的工具?


七、课后思考

点击查看下一章要解决的问题 💡 **问题 1:工具生态局限** 当前所有工具都需要自己实现。如何使用社区现成的工具(如访问 GitHub、查询数据库、操作文件系统)? **问题 2:协议标准化** 不同厂商的工具接口各异。如何让 NuClaw 支持标准化的工具协议? **问题 3:动态发现** 目前工具和 Skill 都是配置文件写死的。如何实现运行时动态发现和加载? **Step 12 预告:MCP 协议接入** 我们将实现 **Model Context Protocol (MCP)**: - **MCP Client** - 连接任意 MCP Server - **工具自动发现** - 动态获取可用工具 - **标准化接入** - 一次实现,连接 100+ 社区工具 接入 MCP 后,NuClaw 可以无缝使用: - 文件系统操作 - 数据库查询 - GitHub 管理 - Slack 消息 - ...以及不断增长的生态

参考

  • Skill 概念参考:OpenAI Functions、LangChain Tools
  • 下一章 MCP:Anthropic Model Context Protocol

文件变更清单

文件 变更类型 说明
include/nuclaw/tool_registry.hpp 修改 新增 Skill 支持
src/tool_registry.cpp 修改 实现 Skill 注册和创建
include/nuclaw/skill.hpp 新增 Skill 定义结构
config/tools.json 修改 添加 skill 配置段
src/main.cpp 修改 演示 Skill 执行

复用文件: - 复用 Step 8 的安全检查组件 - 复用 Step 6 的工具基类