一、前言

前4节课我们完成了Agent的“大脑”(Agent Loop)和“短期记忆”(上下文管理与压缩),但Agent要真正“做事”,还需要“手脚”——工具调用系统。Agent的自主执行能力,本质就是通过调用各类工具(文件操作、Shell命令、网络请求、代码编译等)与外部环境交互,将推理转化为实际行动。

Claude Code的工具调用系统采用“插件化架构”,支持动态注册、权限控制、异常重试、结果标准化,既能满足编码场景的各类需求,又具备极强的可扩展性。本节课我们拆解其核心设计、源码实现,教你搭建一个可复用、高可靠的工业级工具调用系统。

核心结论:工具调用系统的核心是“解耦与标准化”——将工具实现与Agent推理解耦,统一工具调用接口和结果格式,让Agent无需关注工具细节,只需专注于“何时调用、调用哪个工具”。

二、工具调用系统的核心架构(Claude Code实战)

Claude Code的工具调用系统分为4层,从下到上依次为:工具实现层、注册中心层、调用执行层、结果处理层,每层职责清晰、解耦彻底,架构如下:

1. 架构分层详解(从下到上)

(1)工具实现层(最底层):具体工具的实现逻辑,如文件读取、Shell执行、网络搜索等,所有工具遵循统一接口,可独立开发、测试、部署。

(2)工具注册中心层:管理所有可调用工具,提供工具注册、查询、注销功能,Agent通过注册中心获取工具信息,无需直接依赖工具实现类(解耦核心)。

(3)调用执行层:接收Agent的工具调用指令,负责权限校验、参数校验、工具调用、异常重试,是工具调用的“调度中心”。

(4)结果处理层:将工具执行结果标准化处理,转化为Agent可识别的格式,存入会话记忆,为后续推理提供统一格式的反馈。

2. 核心设计原则(避坑重点)

- 接口标准化:所有工具实现同一接口,确保Agent调用方式统一,新增工具无需修改Agent核心代码;

- 插件化可扩展:支持动态注册工具,无需重启Agent即可新增功能(如新增数据库操作工具、代码测试工具);

- 安全可控:所有工具调用前需经过权限校验,危险工具(如删除文件、执行rm命令)需额外用户确认;

- 异常可恢复:工具调用失败时,支持重试机制,可配置重试次数和间隔,避免因临时异常导致任务中断。

三、核心组件源码解析(Claude Code重点类)

Claude Code的工具调用系统核心包为com.claudecode.tool,包含4个核心类:Tool(工具接口)、ToolRegistry(注册中心)、ToolExecutor(调用执行器)、ToolResult(标准化结果),我们逐一拆解。

1. 工具接口:Tool(统一规范所有工具)

所有工具必须实现该接口,定义了工具的核心信息和执行方法,是接口标准化的核心。

package com.claudecode.tool;

import lombok.Data;

/**
 * 工具统一接口,所有工具必须实现此接口
 */
public interface Tool {
    // 工具唯一标识(如:file-read、shell-exec、network-search)
    String getToolId();

    // 工具名称(用于Agent识别和用户展示)
    String getToolName();

    // 工具描述(告诉Agent该工具的用途、参数说明)
    String getToolDescription();

    // 工具参数校验(校验输入参数是否合法)
    boolean validateParameters(ToolParameters parameters);

    // 工具执行逻辑(核心方法,实现工具的具体功能)
    ToolResult execute(ToolParameters parameters) throws Exception;

    // 是否为危险工具(如删除文件、修改系统配置)
    default boolean isDangerous() {
        return false; // 默认非危险工具
    }

    // 危险工具的确认提示(危险工具调用前需展示给用户)
    default String getDangerPrompt() {
        return "该操作存在风险,请确认是否继续?";
    }

    // 工具参数封装类(统一所有工具的参数格式)
    @Data
    class ToolParameters {
        // 参数名称-参数值映射
        private Map<String, Object> params;

        // 获取参数(支持默认值)
        public <T> T getParam(String key, T defaultValue) {
            Object value = params.get(key);
            return value == null ? defaultValue : (T) value;
        }
    }
}
    
2. 工具注册中心:ToolRegistry(管理所有工具)

采用单例模式,负责工具的注册、查询、注销,实现Agent与工具实现的解耦——Agent只需通过工具ID查询工具,无需关注工具具体实现类。

package com.claudecode.tool;

import java.util.HashMap;
import java.util.Map;

/**
 * 工具注册中心(单例),管理所有可调用工具
 */
public class ToolRegistry {
    // 单例实例
    private static final ToolRegistry INSTANCE = new ToolRegistry();

    // 工具缓存:key=工具ID,value=工具实例
    private final Map<String, Tool&gt; toolCache = new HashMap<>();

    // 私有构造,防止外部实例化
    private ToolRegistry() {}

    // 获取单例
    public static ToolRegistry getInstance() {
        return INSTANCE;
    }

    // 注册工具(支持批量注册)
    public void registerTool(Tool... tools) {
        for (Tool tool : tools) {
            String toolId = tool.getToolId();
            if (toolCache.containsKey(toolId)) {
                throw new RuntimeException("工具ID已存在:" + toolId);
            }
            toolCache.put(toolId, tool);
            System.out.println("工具注册成功:" + tool.getToolName() + "(ID:" + toolId + ")");
        }
    }

    // 根据工具ID查询工具
    public Tool getTool(String toolId) {
        Tool tool = toolCache.get(toolId);
        if (tool == null) {
            throw new RuntimeException("未找到工具:" + toolId);
        }
        return tool;
    }

    // 注销工具
    public void unregisterTool(String toolId) {
        toolCache.remove(toolId);
        System.out.println("工具注销成功:" + toolId);
    }

    // 判断工具是否存在
    public boolean existsTool(String toolId) {
        return toolCache.containsKey(toolId);
    }
}
    

3. 工具调用执行器:ToolExecutor(调度核心)

接收Agent的调用指令,完成权限校验、参数校验、工具调用、异常重试,是工具调用的“核心调度器”,也是安全控制的关键。

package com.claudecode.tool;

import com.claudecode.core.PermissionManager;

/**
 * 工具调用执行器,负责工具调用的全流程调度
 */
public class ToolExecutor {
    // 权限管理器(校验工具调用权限)
    private final PermissionManager permissionManager;
    // 工具注册中心
    private final ToolRegistry toolRegistry;
    // 重试次数(可配置)
    private static final int RETRY_COUNT = 2;
    // 重试间隔(毫秒)
    private static final long RETRY_INTERVAL = 1000;

    public ToolExecutor(PermissionManager permissionManager) {
        this.permissionManager = permissionManager;
        this.toolRegistry = ToolRegistry.getInstance();
    }

    /**
     * 执行工具调用
     * @param toolId 工具ID
     * @param parameters 工具参数
     * @param userId 用户ID(用于权限校验)
     * @return 标准化工具结果
     */
    public ToolResult executeTool(String toolId, Tool.ToolParameters parameters, String userId) {
        // 1. 校验工具是否存在
        if (!toolRegistry.existsTool(toolId)) {
            return ToolResult.fail("工具不存在:" + toolId);
        }

        Tool tool = toolRegistry.getTool(toolId);

        // 2. 权限校验(用户是否有权调用该工具)
        if (!permissionManager.hasPermission(userId, toolId)) {
            return ToolResult.fail("权限不足,用户" + userId + "无法调用工具:" + toolId);
        }

        // 3. 危险工具校验(需用户确认)
        if (tool.isDangerous() && !parameters.getParam("userConfirm", false)) {
            return ToolResult.needConfirm(tool.getDangerPrompt());
        }

        // 4. 参数校验
        if (!tool.validateParameters(parameters)) {
            return ToolResult.fail("参数校验失败,请检查参数格式:" + parameters);
        }

        // 5. 执行工具(支持重试)
        ToolResult result = null;
        for (int i = 0; i <= RETRY_COUNT; i++) {
            try {
                result = tool.execute(parameters);
                // 执行成功,直接返回
                return result;
            } catch (Exception e) {
                // 最后一次重试失败,返回异常信息
                if (i == RETRY_COUNT) {
                    return ToolResult.fail("工具调用失败(重试" + RETRY_COUNT + "次):" + e.getMessage());
                }
                // 重试间隔
                try {
                    Thread.sleep(RETRY_INTERVAL);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("工具调用失败,正在重试(第" + (i+1) + "次):" + e.getMessage());
            }
        }

        return ToolResult.fail("工具调用异常");
    }
}
   

4. 标准化结果:ToolResult(统一反馈格式)

所有工具的执行结果都统一封装为该类,确保Agent能快速解析结果,无需关注不同工具的结果格式差异,是结果处理层的核心。

package com.claudecode.tool;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * 工具执行结果标准化封装
 */
@Data
@AllArgsConstructor
public class ToolResult {
    // 执行状态:SUCCESS(成功)、FAIL(失败)、NEED_CONFIRM(需用户确认)
    private ResultStatus status;
    // 结果信息(成功返回结果,失败返回错误信息,需确认返回提示信息)
    private String message;
    // 具体结果数据(成功时返回,如文件内容、Shell输出等)
    private Object data;
    // 工具ID(标识哪个工具的执行结果)
    private String toolId;
    // 执行时间戳
    private long timestamp;

    // 静态工厂方法:成功
    public static ToolResult success(String toolId, String message, Object data) {
        return new ToolResult(ResultStatus.SUCCESS, message, data, toolId, System.currentTimeMillis());
    }

    // 静态工厂方法:失败
    public static ToolResult fail(String message) {
        return new ToolResult(ResultStatus.FAIL, message, null, null, System.currentTimeMillis());
    }

    // 静态工厂方法:需用户确认(危险工具)
    public static ToolResult needConfirm(String prompt) {
        return new ToolResult(ResultStatus.NEED_CONFIRM, prompt, null, null, System.currentTimeMillis());
    }

    // 执行状态枚举
    public enum ResultStatus {
        SUCCESS, FAIL, NEED_CONFIRM
    }
}
    

四、实操练习:开发一个文件操作工具(完整可运行)

结合本节课所学,开发一个“文件操作工具”(包含读取、写入功能),实现Tool接口,注册到工具中心,并通过ToolExecutor调用,完整模拟工业级工具的开发与调用流程。

1. 实现文件操作工具(FileOperationTool)
package com.claudecode.tool.impl;

import com.claudecode.tool.Tool;
import com.claudecode.tool.ToolResult;

import java.io.*;
import java.util.Map;

/**
 * 文件操作工具(实现Tool接口),支持读取文件、写入文件
 */
public class FileOperationTool implements Tool {
    // 工具ID(唯一)
    private static final String TOOL_ID = "file-operation";
    // 工具名称
    private static final String TOOL_NAME = "文件操作工具";
    // 工具描述
    private static final String TOOL_DESCRIPTION = "用于读取本地文件、写入本地文件,参数说明:" +
            "1. operation:操作类型(read=读取,write=写入);" +
            "2. filePath:文件路径(必填);" +
            "3. content:写入内容(operation=write时必填);" +
            "4. userConfirm:用户确认(删除、覆盖文件时必填)。";

    @Override
    public String getToolId() {
        return TOOL_ID;
    }

    @Override
    public String getToolName() {
        return TOOL_NAME;
    }

    @Override
    public String getToolDescription() {
        return TOOL_DESCRIPTION;
    }

    @Override
    public boolean validateParameters(ToolParameters parameters) {
        // 参数校验:必须包含operation和filePath
        Map<String, Object> params = parameters.getParams();
        if (!params.containsKey("operation") || !params.containsKey("filePath")) {
            return false;
        }
        // 写入操作必须包含content
        String operation = (String) params.get("operation");
        if ("write".equals(operation) && !params.containsKey("content")) {
            return false;
        }
        // 覆盖写入时必须包含userConfirm
        if ("write".equals(operation) && (boolean) params.getOrDefault("overwrite", false) 
                && !(boolean) params.getOrDefault("userConfirm", false)) {
            return false;
        }
        return true;
    }

    @Override
    public ToolResult execute(ToolParameters parameters) throws Exception {
        String operation = (String) parameters.getParam("operation", "");
        String filePath = (String) parameters.getParam("filePath", "");
        String content = (String) parameters.getParam("content", "");
        boolean overwrite = (boolean) parameters.getParam("overwrite", false);

        File file = new File(filePath);

        // 读取文件操作
        if ("read".equals(operation)) {
            if (!file.exists()) {
                return ToolResult.fail("文件不存在:" + filePath);
            }
            // 读取文件内容
            StringBuilder fileContent = new StringBuilder();
            try (BufferedReader br = new BufferedReader(new FileReader(file))) {
                String line;
                while ((line = br.readLine()) != null) {
                    fileContent.append(line).append("\n");
                }
            }
            return ToolResult.success(TOOL_ID, "文件读取成功", fileContent.toString().trim());
        }

        // 写入文件操作
        if ("write".equals(operation)) {
            // 若文件存在且不允许覆盖,返回失败
            if (file.exists() && !overwrite) {
                return ToolResult.fail("文件已存在,若需覆盖,请设置overwrite=true并确认");
            }
            // 写入文件
            try (BufferedWriter bw = new BufferedWriter(new FileWriter(file, false))) {
                bw.write(content);
            }
            return ToolResult.success(TOOL_ID, "文件写入成功", filePath);
        }

        return ToolResult.fail("不支持的操作类型:" + operation);
    }

    // 覆盖写入时视为危险操作
    @Override
    public boolean isDangerous() {
        return true;
    }

    @Override
    public String getDangerPrompt() {
        return "该操作将覆盖已有文件,可能导致数据丢失,请确认是否继续?";
    }
}
    

2. 工具注册与调用测试
package com.claudecode.test;

import com.claudecode.core.PermissionManager;
import com.claudecode.tool.Tool;
import com.claudecode.tool.ToolRegistry;
import com.claudecode.tool.ToolExecutor;
import com.claudecode.tool.ToolResult;
import com.claudecode.tool.impl.FileOperationTool;

import java.util.HashMap;
import java.util.Map;

/**
 * 工具调用测试:注册文件操作工具,并测试读取、写入功能
 */
public class ToolTest {
    public static void main(String[] args) {
        // 1. 初始化组件
        ToolRegistry toolRegistry = ToolRegistry.getInstance();
        PermissionManager permissionManager = new PermissionManager(); // 简化版,默认允许所有用户调用
        ToolExecutor toolExecutor = new ToolExecutor(permissionManager);

        // 2. 注册文件操作工具
        Tool fileTool = new FileOperationTool();
        toolRegistry.registerTool(fileTool);

        // 3. 测试:读取文件(模拟Agent调用)
        Map&lt;String, Object&gt; readParams = new HashMap<>();
        readParams.put("operation", "read");
        readParams.put("filePath", "test.txt");
        Tool.ToolParameters readToolParams = new Tool.ToolParameters();
        readToolParams.setParams(readParams);

        ToolResult readResult = toolExecutor.executeTool("file-operation", readToolParams, "test-user");
        System.out.println("读取文件结果:" + readResult);

        // 4. 测试:写入文件(覆盖写入,需用户确认)
        Map&lt;String, Object&gt; writeParams = new HashMap<>();
        writeParams.put("operation", "write");
        writeParams.put("filePath", "test.txt");
        writeParams.put("content", "测试文件写入内容(工业级Agent工具调用测试)");
        writeParams.put("overwrite", true);
        writeParams.put("userConfirm", true); // 模拟用户确认
        Tool.ToolParameters writeToolParams = new Tool.ToolParameters();
        writeToolParams.setParams(writeParams);

        ToolResult writeResult = toolExecutor.executeTool("file-operation", writeToolParams, "test-user");
        System.out.println("写入文件结果:" + writeResult);

        // 5. 再次读取文件,验证写入成功
        ToolResult reReadResult = toolExecutor.executeTool("file-operation", readToolParams, "test-user");
        System.out.println("再次读取文件结果:" + reReadResult.getData());
    }
}
    

3. 测试结果说明

运行测试类后,会依次执行“注册工具→读取文件→写入文件→再次读取文件”,控制台会输出工具注册信息、各步骤执行结果,验证工具调用的全流程正确性。若参数不合法、权限不足或工具调用失败,会返回对应的标准化失败信息,符合工业级开发的容错要求。

五、关键避坑点与实操建议

1. 避坑点

- 避免工具与Agent强耦合:不要在Agent核心代码中直接new工具实例,必须通过ToolRegistry获取,否则无法实现动态扩展;

- 必须做参数校验:工具调用前务必校验参数合法性,防止因参数错误导致工具执行异常(如文件路径为空、操作类型错误);

- 危险工具必须加确认:删除、覆盖、系统命令等危险操作,必须添加用户确认逻辑,避免Agent误操作导致数据丢失或系统异常;

- 结果必须标准化:无论工具执行成功还是失败,都要返回统一格式的ToolResult,否则Agent无法解析结果,导致闭环中断。

2. 实操建议

- 工具分类开发:将工具按功能分类(如文件操作类、Shell类、网络类、代码类),统一管理,便于维护;

- 配置化重试:将重试次数、重试间隔配置到配置文件中,无需修改代码即可调整;

- 日志记录:在工具执行过程中添加详细日志,便于排查工具调用失败的原因(如文件读取失败的具体异常信息);

- 批量注册工具:开发多个工具后,通过批量注册方法注册到ToolRegistry,提高开发效率。

六、本课重点总结

1. 工具调用系统是Agent的“手脚”,核心价值是将Agent的推理转化为实际行动,与外部环境交互;

2. 工业级工具调用系统的核心架构:工具实现层→注册中心层→调用执行层→结果处理层,核心原则是“接口标准化、插件化可扩展、安全可控”;

3. 核心组件作用:Tool接口定义规范、ToolRegistry管理工具、ToolExecutor调度执行、ToolResult标准化结果;

4. 实操关键:开发工具必须实现Tool接口,通过注册中心注册,调用时通过执行器完成全流程调度,确保解耦和容错。

下节课预告

第06课:Agent权限控制系统设计——如何实现细粒度权限管理,防止工具滥用、数据泄露,对应Claude Code的PermissionManager和UserManager源码解析。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐