如何构建从0到1的ClaudeAgent执行工具?

摘要:这是 Agent 进化的关键一步:从“只会说话”变成了“真正干活”。 Java 实现代码 public class AgentWithTools {配置 private static final Path WORKDIR = Pat
这是 Agent 进化的关键一步:从“只会说话”变成了“真正干活”。 Java 实现代码 public class AgentWithTools { // 配置 private static final Path WORKDIR = Paths.get(System.getProperty("user.dir")); // --- 核心:工具定义与分发 --- // 1. 定义工具枚举 public enum ToolType { BASH("bash", "Run a shell command."), READ_FILE("read_file", "Read file contents."), WRITE_FILE("write_file", "Write content to file."), EDIT_FILE("edit_file", "Replace exact text in file."); // ... 省略构造器 } // 2. 工具执行接口 @FunctionalInterface interface ToolExecutor { String execute(Map<String, Object> args) throws Exception; } // 3. 注册工具处理逻辑 private static final Map<String, ToolExecutor> TOOL_HANDLERS = new HashMap<>(); static { TOOL_HANDLERS.put(ToolType.BASH.name, args -> { String command = (String) args.get("command"); return runBash(command); }); TOOL_HANDLERS.put(ToolType.READ_FILE.name, args -> { String path = (String) args.get("path"); Integer limit = (Integer) args.get("limit"); return runRead(path, limit); }); TOOL_HANDLERS.put(ToolType.WRITE_FILE.name, args -> { String path = (String) args.get("path"); String content = (String) args.get("content"); return runWrite(path, content); }); TOOL_HANDLERS.put(ToolType.EDIT_FILE.name, args -> { String path = (String) args.get("path"); String oldText = (String) args.get("old_text"); String newText = (String) args.get("new_text"); return runEdit(path, oldText, newText); }); } // --- 核心循环 --- public static void agentLoop(List<Map<String, Object>> messages) { while (true) { // ... 省略相同的 LLM 调用、消息追加逻辑 // 4. 执行工具 List<Map<String, Object>> toolResults = new ArrayList<>(); List<Map<String, Object>> content = (List<Map<String, Object>>) response.get("content"); for (Map<String, Object> block : content) { if ("tool_use".equals(block.get("type"))) { String toolName = (String) block.get("name"); // 关键新增 String toolId = (String) block.get("id"); Map<String, Object> inputArgs = (Map<String, Object>) block.get("input"); // 路由分发 ToolExecutor handler = TOOL_HANDLERS.get(toolName); String output; try { if (handler != null) { output = handler.execute(inputArgs); } else { output = "Error: Unknown tool " + toolName; } } catch (Exception e) { output = "Error: " + e.getMessage(); } System.out.println("> " + toolName + ": " + output.substring(0, Math.min(output.length(), 100))); // ... 省略相同的工具结果构造逻辑 } } // ... 省略相同的回传逻辑 } } // --- 工具具体实现 --- private static Path safePath(String p) throws IOException { Path path = WORKDIR.resolve(p).normalize(); if (!path.startsWith(WORKDIR)) { throw new IOException("Path escapes workspace: " + p); } return path; } // ... 省略与之前相同的 runBash 实现 private static String runRead(String pathStr, Integer limit) throws IOException { Path path = safePath(pathStr); String content = Files.readString(path); if (limit != null && limit < content.length()) { return content.substring(0, limit) + "... (truncated)"; } return content; } private static String runWrite(String pathStr, String content) throws IOException { Path path = safePath(pathStr); Files.createDirectories(path.getParent()); Files.writeString(path, content); return "Wrote " + content.length() + " bytes to " + pathStr; } private static String runEdit(String pathStr, String oldText, String newText) throws IOException { Path path = safePath(pathStr); String content = Files.readString(path); if (!content.contains(oldText)) { return "Error: Text not found in " + pathStr; } String newContent = content.replace(oldText, newText); Files.writeString(path, newContent); return "Edited " + pathStr; } } 这段代码相比 s01,最大的进步在于能力的扩展和安全边界。说白了就是,你可以像搭积木一样给 Agent 塞入各种工具函数,让它的能力边界随插件无限延伸。 这段代码应该已经很清晰,我这里就不多解释了 工具抽象框架(策略模式) 核心思想:从"硬编码工具"升级为"可插拔架构",实现工具与主循环的解耦。 // 工具枚举 - 集中定义所有可用工具 public enum ToolType { BASH("bash", "Run a shell command."), READ_FILE("read_file", "Read file contents."), WRITE_FILE("write_file", "Write content to file."); // 枚举定义:工具名 + 描述 // 为LLM提供工具列表时使用 } // 工具执行接口 - 统一调用契约 @FunctionalInterface interface ToolExecutor { String execute(Map<String, Object> args) throws Exception; // 统一接口:所有工具都实现此方法 // 参数和返回值标准化 } // 工具注册表 - 动态路由 private static final Map<String, ToolExecutor> TOOL_HANDLERS = new HashMap<>(); static { TOOL_HANDLERS.put("bash", args -> { // 工具实现1 }); TOOL_HANDLERS.put("read_file", args -> { // 工具实现2 }); // 注册中心:工具名 -> 实现函数 // 新增工具只需在这里注册 } 开闭原则:不修改主循环就能添加新工具 统一管理:所有工具注册、调用逻辑一致 类型安全:通过枚举定义工具,避免硬编码字符串 文件操作工具集 核心思想:为Agent提供文件系统读写能力,使其能像人类开发者一样操作文件。 private static Path safePath(String p) throws IOException { Path path = WORKDIR.resolve(p).normalize(); if (!path.startsWith(WORKDIR)) { throw new IOException("Path escapes workspace: " + p); } return path; // 安全沙箱:确保工具只能操作工作目录内的文件 // 防止路径逃逸攻击 } private static String runRead(String pathStr, Integer limit) throws IOException { Path path = safePath(pathStr); String content = Files.readString(path); if (limit != null && limit < content.length()) { return content.substring(0, limit) + "... (truncated)"; } return content; // 带限制的读取:防止大文件内存溢出 // 自动截断,返回友好提示 } private static String runWrite(String pathStr, String content) throws IOException { Path path = safePath(pathStr); Files.createDirectories(path.getParent()); // 自动创建父目录 Files.writeString(path, content); return "Wrote " + content.length() + " bytes to " + pathStr; // 自动创建目录:用户体验优化 // 明确的结果反馈 } private static String runEdit(String pathStr, String oldText, String newText) throws IOException { Path path = safePath(pathStr); String content = Files.readString(path); if (!content.contains(oldText)) { return "Error: Text not found in " + pathStr; // 错误处理 } String newContent = content.replace(oldText, newText); Files.writeString(path, newContent); return "Edited " + pathStr; // 简单的文件编辑:文本查找替换 // 先验证后操作,避免损坏文件 } 沙箱安全:所有文件操作都经过safePath检查 渐进式反馈:读操作支持截断,避免响应过大 容错处理:编辑前检查文本是否存在 自动化:写文件时自动创建父目录 动态工具路由 // 在agentLoop中 String toolName = (String) block.get("name"); // 从LLM响应中提取工具名 Map<String, Object> inputArgs = (Map<String, Object>) block.get("input"); // 根据工具名查找处理器 ToolExecutor handler = TOOL_HANDLERS.get(toolName); String output; try { if (handler != null) { output = handler.execute(inputArgs); // 动态调用 } else { output = "Error: Unknown tool " + toolName; } } catch (Exception e) { output = "Error: " + e.getMessage(); // 统一错误处理 } 动态分派:根据LLM选择的工具名调用对应实现 统一错误处理:未知工具、执行异常都有统一格式的返回 解耦:主循环不需要知道具体工具的实现细节 架构对比与价值 从AgentLoop到AgentWithTools的演进: 维度 AgentLoop AgentWithTools 工具数量 1个(Bash) 4+个(可扩展) 架构设计 硬编码 策略模式 添加新工具 修改主代码 注册表添加 文件操作 无 读写编辑 安全性 命令检查 沙箱路径 代码复用 低 高 核心价值: 可扩展性:添加新工具只需在注册表中添加一行 维护性:工具实现与主循环分离 安全性:统一的路径和权限控制 专业性:为开发任务优化的专用工具集