Function Calling 与工具调用:让 AI 真正干活【OpenClAW + Spring Boot 系列 第5篇】

📅 难度:⭐⭐⭐⭐☆ 高级 | 阅读约 25 分钟 | 适用:Spring Boot 3.x | Java 17+


目录

[一、什么是 Function Calling?](#一、什么是 Function Calling?)

[1.1 没有 Function Calling 时](#1.1 没有 Function Calling 时)

[1.2 Function Calling 的本质](#1.2 Function Calling 的本质)

[1.3 典型应用场景](#1.3 典型应用场景)

二、整体架构设计

[三、工具定义:告诉 AI 有哪些工具](#三、工具定义:告诉 AI 有哪些工具)

[3.1 原始 JSON 工具定义格式](#3.1 原始 JSON 工具定义格式)

[3.2 Java 工具定义封装](#3.2 Java 工具定义封装)

四、注解驱动:声明式工具注册

[4.1 定义注解](#4.1 定义注解)

[4.2 工具接口](#4.2 工具接口)

[4.3 工具注册中心](#4.3 工具注册中心)

五、实现具体工具

[5.1 工具一:实时天气查询](#5.1 工具一:实时天气查询)

[5.2 工具二:订单查询(数据库操作)](#5.2 工具二:订单查询(数据库操作))

[5.3 工具三:计算器(本地计算,无外部依赖)](#5.3 工具三:计算器(本地计算,无外部依赖))

六、核心:工具调用服务实现

[6.1 工具调用响应解析](#6.1 工具调用响应解析)

[6.2 ToolChatService 接口](#6.2 ToolChatService 接口)

[6.3 ToolChatServiceImpl:完整实现](#6.3 ToolChatServiceImpl:完整实现)

[七、Controller 层](#七、Controller 层)

八、完整调用流程演示

[8.1 场景一:单工具调用(天气查询)](#8.1 场景一:单工具调用(天气查询))

[8.2 场景二:多工具组合调用](#8.2 场景二:多工具组合调用)

[8.3 场景三:工具调用失败的处理](#8.3 场景三:工具调用失败的处理)

九、进阶:工具调用的最佳实践

[9.1 工具描述的写作指南](#9.1 工具描述的写作指南)

[9.2 工具调用的安全控制](#9.2 工具调用的安全控制)

[9.3 强制/禁止工具调用](#9.3 强制/禁止工具调用)

[9.4 工具调用与流式输出结合](#9.4 工具调用与流式输出结合)

十、完整项目结构

十一、常见问题排查

十二、本篇总结



一、什么是 Function Calling?

一句话定义:Function Calling(工具调用)让大模型从"只会说话"变成"能干活"

1.1 没有 Function Calling 时

bash 复制代码
用户:北京今天天气怎么样?
AI:我是语言模型,没有联网能力,无法获取实时天气... 😕

有 Function Calling 之后:

bash 复制代码
用户:北京今天天气怎么样?
AI(内部思考):我需要调用天气查询工具
  → 模型输出:{tool: "get_weather", args: {city: "北京"}}
  → 你的代码执行天气 API
  → 返回结果:{"temp": 22, "weather": "晴"}
AI(基于结果回答):北京今天天气晴,气温 22°C,适合出行 ☀️

1.2 Function Calling 的本质

很多人以为 AI 在"自动调用"函数,其实不然。完整流程是:

bash 复制代码
┌─────────────────────────────────────────────────────┐
│  第 1 步:你告诉模型"它有哪些工具可以用"(工具定义)   │
└─────────────────────────┬───────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────┐
│  第 2 步:模型决定"要用哪个工具、传什么参数"           │
│  输出:tool_use 块(不是文字,是结构化 JSON)          │
└─────────────────────────┬───────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────┐
│  第 3 步:你的代码执行工具(调用真实 API/数据库/服务)  │
└─────────────────────────┬───────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────┐
│  第 4 步:把工具结果还给模型                          │
└─────────────────────────┬───────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────┐
│  第 5 步:模型基于工具结果生成最终自然语言回复         │
└─────────────────────────────────────────────────────┘

核心认知:AI 不直接执行代码,它只是"告诉你要调用什么",实际执行权始终在你手里。这保证了安全性和可控性。

1.3 典型应用场景

场景 工具示例
查询实时数据 天气 API、股价接口、汇率查询
操作数据库 查询订单、更新用户信息
调用内部服务 发送邮件、创建工单、触发审批流
文件处理 读取文档、生成报表
复杂计算 财务计算、数据统计
外部平台 发送消息到钉钉/飞书、调用地图 API

二、整体架构设计

本篇将构建一个完整的工具调用框架,支持:

  • 声明式工具注册 (用注解 @ClawTool 定义工具)
  • 自动工具路由(根据 AI 的 tool_use 响应自动分发执行)
  • 多工具组合(一次对话中按需调用多个工具)
  • 真实业务示例(天气查询 + 订单查询 + 数据库操作)
bash 复制代码
┌──────────────────────────────────────────────────────────┐
│                   ChatController                          │
└─────────────────────────┬────────────────────────────────┘
                          │
┌─────────────────────────▼────────────────────────────────┐
│               ToolChatService                             │
│  ① 携带工具定义发送第一次请求                              │
│  ② 检测 tool_use 响应                                     │
│  ③ 分发到 ToolExecutor 执行                               │
│  ④ 携带工具结果发送第二次请求                              │
│  ⑤ 返回最终自然语言回复                                    │
└───────────┬──────────────────────────┬───────────────────┘
            │                          │
┌───────────▼──────────┐  ┌────────────▼─────────────────┐
│   ToolRegistry        │  │      ToolExecutor             │
│  管理所有已注册的工具  │  │  根据工具名路由到具体实现      │
└───────────────────────┘  └──────────────────────────────┘
            │                          │
   ┌────────┴────────┐        ┌────────┴─────────────────┐
   │  @ClawTool 注解  │        │  WeatherTool / OrderTool  │
   │  自动扫描注册    │        │  具体工具实现              │
   └─────────────────┘        └──────────────────────────┘

三、工具定义:告诉 AI 有哪些工具

Claude API 使用 JSON Schema 格式定义工具。先来理解原始格式,再看如何在 Java 中优雅封装。

3.1 原始 JSON 工具定义格式

bash 复制代码
{
  "name": "get_weather",
  "description": "查询指定城市的实时天气信息。当用户询问天气相关问题时使用此工具。",
  "input_schema": {
    "type": "object",
    "properties": {
      "city": {
        "type": "string",
        "description": "城市名称,如「北京」「上海」「广州」"
      },
      "unit": {
        "type": "string",
        "enum": ["celsius", "fahrenheit"],
        "description": "温度单位,默认 celsius(摄氏度)"
      }
    },
    "required": ["city"]
  }
}

💡 工具描述(description)至关重要:模型靠描述判断何时调用哪个工具,描述越清晰准确,模型的决策越正确。这是 Function Calling 效果好坏的核心因素。

3.2 Java 工具定义封装

java 复制代码
package com.example.openclaw_demo.tool.model;

import lombok.Builder;
import lombok.Data;

import java.util.List;
import java.util.Map;

/**
 * 工具定义,对应 Claude API 的 tool 结构
 */
@Data
@Builder
public class ToolDefinition {

    /** 工具名称(全局唯一,只允许字母、数字、下划线) */
    private String name;

    /** 工具描述(告诉模型何时使用此工具,越详细越好) */
    private String description;

    /** 参数 Schema(JSON Schema 格式) */
    private InputSchema inputSchema;

    @Data
    @Builder
    public static class InputSchema {
        private String type;                         // 固定为 "object"
        private Map<String, PropertyDef> properties; // 参数定义
        private List<String> required;               // 必填参数列表
    }

    @Data
    @Builder
    public static class PropertyDef {
        private String       type;        // string / number / boolean / array / object
        private String       description; // 参数说明
        private List<String> enumValues;  // 枚举值(可选)
        private String       format;      // 格式约束(可选,如 date-time)
    }
}

四、注解驱动:声明式工具注册

手动构建 ToolDefinition 太繁琐,我们用注解实现声明式注册,像写普通 Spring Bean 一样定义工具。

4.1 定义注解

java 复制代码
package com.example.openclaw_demo.tool.annotation;

import java.lang.annotation.*;

/**
 * 标记一个类为 AI 可调用工具
 * 被标注的类需同时是 Spring Bean(@Component/@Service)
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ClawTool {

    /** 工具名称,默认使用类名转下划线 */
    String name() default "";

    /** 工具描述(必填,模型靠此判断何时调用) */
    String description();
}


/**
 * 标记工具的参数字段
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ToolParam {
    String description();                     // 参数说明
    boolean required()    default true;       // 是否必填
    String  type()        default "string";   // 参数类型
    String[] enumValues() default {};         // 枚举值
}

4.2 工具接口

java 复制代码
package com.example.openclaw_demo.tool;

import java.util.Map;

/**
 * 所有 AI 工具必须实现此接口
 */
public interface ClawToolHandler {

    /**
     * 执行工具
     *
     * @param args 模型传来的参数(key-value,已由框架从 JSON 解析)
     * @return 工具执行结果(会原样返回给模型)
     */
    Object execute(Map<String, Object> args);
}

4.3 工具注册中心

java 复制代码
package com.example.openclaw_demo.tool;

import com.example.openclaw_demo.tool.annotation.ClawTool;
import com.example.openclaw_demo.tool.annotation.ToolParam;
import com.example.openclaw_demo.tool.model.ToolDefinition;
import com.example.openclaw_demo.tool.model.ToolDefinition.InputSchema;
import com.example.openclaw_demo.tool.model.ToolDefinition.PropertyDef;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.*;

/**
 * 工具注册中心
 * 启动时自动扫描所有 @ClawTool 注解的 Bean,注册为可调用工具
 */
@Slf4j
@Component
public class ToolRegistry implements InitializingBean {

    private final ApplicationContext context;

    // 工具名 → 处理器实例
    private final Map<String, ClawToolHandler> handlerMap = new LinkedHashMap<>();

    // 工具名 → 工具定义(发送给 Claude API)
    private final Map<String, ToolDefinition> definitionMap = new LinkedHashMap<>();

    public ToolRegistry(ApplicationContext context) {
        this.context = context;
    }

    // Spring 容器初始化完成后自动扫描
    @Override
    public void afterPropertiesSet() {
        Map<String, Object> beans = context.getBeansWithAnnotation(ClawTool.class);

        beans.forEach((beanName, bean) -> {
            if (!(bean instanceof ClawToolHandler handler)) {
                log.warn("[ToolRegistry] Bean '{}' 标注了 @ClawTool 但未实现 ClawToolHandler,已跳过", beanName);
                return;
            }

            ClawTool annotation = bean.getClass().getAnnotation(ClawTool.class);

            // 工具名:注解指定 > 类名转下划线
            String toolName = annotation.name().isEmpty()
                    ? toSnakeCase(bean.getClass().getSimpleName())
                    : annotation.name();

            // 构建工具定义
            ToolDefinition definition = buildDefinition(toolName, annotation.description(), bean.getClass());

            handlerMap.put(toolName, handler);
            definitionMap.put(toolName, definition);

            log.info("[ToolRegistry] 注册工具: {} → {}", toolName, bean.getClass().getSimpleName());
        });

        log.info("[ToolRegistry] 共注册 {} 个工具: {}", handlerMap.size(), handlerMap.keySet());
    }

    /**
     * 获取所有工具定义(发送给 Claude API)
     */
    public List<ToolDefinition> getAllDefinitions() {
        return new ArrayList<>(definitionMap.values());
    }

    /**
     * 获取指定工具定义
     */
    public Optional<ToolDefinition> getDefinition(String toolName) {
        return Optional.ofNullable(definitionMap.get(toolName));
    }

    /**
     * 执行指定工具
     */
    public Object execute(String toolName, Map<String, Object> args) {
        ClawToolHandler handler = handlerMap.get(toolName);
        if (handler == null) {
            throw new IllegalArgumentException("未找到工具: " + toolName);
        }
        log.info("[ToolRegistry] 执行工具: {}, 参数: {}", toolName, args);
        return handler.execute(args);
    }

    /**
     * 通过类字段上的 @ToolParam 注解构建 InputSchema
     */
    private ToolDefinition buildDefinition(String name, String description, Class<?> clazz) {
        Map<String, PropertyDef> properties = new LinkedHashMap<>();
        List<String> required = new ArrayList<>();

        // 递归扫描父类字段
        Class<?> current = clazz;
        while (current != null && current != Object.class) {
            for (Field field : current.getDeclaredFields()) {
                ToolParam param = field.getAnnotation(ToolParam.class);
                if (param == null) continue;

                PropertyDef.PropertyDefBuilder propBuilder = PropertyDef.builder()
                        .type(param.type())
                        .description(param.description());

                if (param.enumValues().length > 0) {
                    propBuilder.enumValues(Arrays.asList(param.enumValues()));
                }

                properties.put(field.getName(), propBuilder.build());
                if (param.required()) {
                    required.add(field.getName());
                }
            }
            current = current.getSuperclass();
        }

        return ToolDefinition.builder()
                .name(name)
                .description(description)
                .inputSchema(InputSchema.builder()
                        .type("object")
                        .properties(properties)
                        .required(required)
                        .build())
                .build();
    }

    /** 驼峰转下划线:WeatherTool → weather_tool */
    private String toSnakeCase(String name) {
        return name.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
    }
}

五、实现具体工具

现在来实现三个真实场景的工具,展示不同类型的工具调用。

5.1 工具一:实时天气查询

java 复制代码
package com.example.openclaw_demo.tool.impl;

import com.example.openclaw_demo.tool.ClawToolHandler;
import com.example.openclaw_demo.tool.annotation.ClawTool;
import com.example.openclaw_demo.tool.annotation.ToolParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;

/**
 * 天气查询工具
 * 实际项目中替换为真实天气 API(如和风天气、高德天气等)
 */
@Slf4j
@Component
@ClawTool(
    name = "get_weather",
    description = """
        查询指定城市的实时天气信息,包括温度、天气状况、湿度、风速等。
        适用场景:用户询问某地天气、需要根据天气做决策(如是否带伞、适合户外活动等)。
        注意:只能查询中国大陆城市,不支持港澳台及海外城市。
        """
)
public class WeatherTool implements ClawToolHandler {

    // 参数定义:字段名 = 参数名,@ToolParam 描述参数
    @ToolParam(description = "城市名称,如「北京」「上海」「杭州」,不需要带「市」字")
    private String city;

    @ToolParam(
        description = "温度单位",
        required = false,
        enumValues = {"celsius", "fahrenheit"}
    )
    private String unit;

    @Override
    public Object execute(Map<String, Object> args) {
        String city = (String) args.getOrDefault("city", "北京");
        String unit = (String) args.getOrDefault("unit", "celsius");

        log.info("[WeatherTool] 查询天气: city={}, unit={}", city, unit);

        // ====== 真实项目替换区域 ======
        // 示例:调用高德天气 API
        // String apiKey = "your_api_key";
        // String url = "https://restapi.amap.com/v3/weather/weatherInfo"
        //            + "?key=" + apiKey + "&city=" + city + "&extensions=all";
        // Map result = new RestTemplate().getForObject(url, Map.class);
        // ==============================

        // 此处返回模拟数据,格式与真实 API 一致
        return buildMockWeather(city, unit);
    }

    private Map<String, Object> buildMockWeather(String city, String unit) {
        boolean isCelsius = "celsius".equals(unit);
        String updateTime = LocalDateTime.now()
                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));

        // 模拟不同城市天气
        Map<String, Object> weatherData = switch (city) {
            case "北京"  -> Map.of("temp", isCelsius ? 22 : 72,  "weather", "晴", "humidity", 35, "windSpeed", "3级");
            case "上海"  -> Map.of("temp", isCelsius ? 28 : 82,  "weather", "多云", "humidity", 72, "windSpeed", "2级");
            case "广州"  -> Map.of("temp", isCelsius ? 33 : 91,  "weather", "阵雨", "humidity", 88, "windSpeed", "1级");
            case "成都"  -> Map.of("temp", isCelsius ? 26 : 79,  "weather", "阴", "humidity", 65, "windSpeed", "1级");
            default      -> Map.of("temp", isCelsius ? 25 : 77,  "weather", "晴", "humidity", 50, "windSpeed", "2级");
        };

        return Map.of(
            "city",       city,
            "unit",       isCelsius ? "°C" : "°F",
            "temp",       weatherData.get("temp"),
            "weather",    weatherData.get("weather"),
            "humidity",   weatherData.get("humidity") + "%",
            "windSpeed",  weatherData.get("windSpeed"),
            "updateTime", updateTime,
            "suggestion", buildSuggestion((String) weatherData.get("weather"), (int) weatherData.get("temp"))
        );
    }

    private String buildSuggestion(String weather, int temp) {
        if (weather.contains("雨")) return "建议携带雨伞,注意防滑";
        if (temp > 35)              return "天气炎热,注意防暑,减少户外活动";
        if (temp < 10)              return "天气较冷,注意保暖,添加衣物";
        return "天气适宜,适合户外活动";
    }
}

5.2 工具二:订单查询(数据库操作)

java 复制代码
package com.example.openclaw_demo.tool.impl;

import com.example.openclaw_demo.tool.ClawToolHandler;
import com.example.openclaw_demo.tool.annotation.ClawTool;
import com.example.openclaw_demo.tool.annotation.ToolParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 订单查询工具
 * 演示如何在工具中注入 Spring Bean(Repository/Service)
 */
@Slf4j
@Component
@RequiredArgsConstructor
@ClawTool(
    name = "query_order",
    description = """
        查询用户的订单信息。
        可按订单号精确查询,也可按用户ID查询近期订单列表。
        返回订单状态、金额、商品信息、物流状态等详情。
        用户询问"我的订单"、"快递到哪了"、"什么时候发货"等问题时使用。
        """
)
public class OrderQueryTool implements ClawToolHandler {

    @ToolParam(description = "订单号(如 ORD-20240101-001),与 userId 二选一")
    private String orderId;

    @ToolParam(description = "用户ID,查询该用户最近 5 条订单", required = false)
    private String userId;

    @ToolParam(
        description = "查询类型",
        required = false,
        enumValues = {"by_order_id", "by_user_id"}
    )
    private String queryType;

    // 注入真实的业务 Service(演示可注入能力)
    // private final OrderService orderService;

    @Override
    public Object execute(Map<String, Object> args) {
        String orderId   = (String) args.get("orderId");
        String userId    = (String) args.get("userId");
        String queryType = (String) args.getOrDefault("queryType",
                orderId != null ? "by_order_id" : "by_user_id");

        log.info("[OrderQueryTool] 查询订单: type={}, orderId={}, userId={}", queryType, orderId, userId);

        return switch (queryType) {
            case "by_order_id" -> queryByOrderId(orderId);
            case "by_user_id"  -> queryByUserId(userId);
            default            -> Map.of("error", "不支持的查询类型: " + queryType);
        };
    }

    private Object queryByOrderId(String orderId) {
        if (orderId == null || orderId.isBlank()) {
            return Map.of("error", "订单号不能为空");
        }

        // 实际项目:return orderService.findByOrderId(orderId);
        // 模拟数据
        return Map.of(
            "orderId",      orderId,
            "status",       "已发货",
            "statusCode",   "SHIPPED",
            "totalAmount",  "¥299.00",
            "payTime",      "2025-04-18 14:32:00",
            "shipTime",     "2025-04-19 09:15:00",
            "expressNo",    "SF1234567890",
            "expressName",  "顺丰速运",
            "currentLocation", "上海转运中心",
            "estimatedDelivery", "2025-04-21",
            "items", java.util.List.of(
                Map.of("name", "Spring Boot 实战图书", "qty", 1, "price", "¥199.00"),
                Map.of("name", "技术贴纸套装", "qty", 2, "price", "¥50.00")
            )
        );
    }

    private Object queryByUserId(String userId) {
        if (userId == null || userId.isBlank()) {
            return Map.of("error", "用户ID不能为空");
        }

        // 实际项目:return orderService.findRecentOrders(userId, 5);
        return Map.of(
            "userId",      userId,
            "totalOrders", 3,
            "orders", java.util.List.of(
                Map.of("orderId", "ORD-20250418-001", "status", "已发货",  "amount", "¥299.00", "date", "2025-04-18"),
                Map.of("orderId", "ORD-20250410-002", "status", "已完成",  "amount", "¥158.00", "date", "2025-04-10"),
                Map.of("orderId", "ORD-20250401-003", "status", "已完成",  "amount", "¥520.00", "date", "2025-04-01")
            )
        );
    }
}

5.3 工具三:计算器(本地计算,无外部依赖)

java 复制代码
package com.example.openclaw_demo.tool.impl;

import com.example.openclaw_demo.tool.ClawToolHandler;
import com.example.openclaw_demo.tool.annotation.ClawTool;
import com.example.openclaw_demo.tool.annotation.ToolParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Map;

/**
 * 精确计算工具
 * 使用 BigDecimal 避免浮点精度问题,适合金融计算
 */
@Slf4j
@Component
@ClawTool(
    name = "calculator",
    description = """
        执行精确的数学计算,支持加减乘除和百分比运算。
        当用户需要计算具体数值时使用,如:价格计算、折扣计算、数量统计等。
        优先使用此工具而非自行估算,以确保数值准确。
        """
)
public class CalculatorTool implements ClawToolHandler {

    @ToolParam(description = "第一个操作数(数字字符串,如 \"199.9\")")
    private String a;

    @ToolParam(description = "第二个操作数(数字字符串)")
    private String b;

    @ToolParam(
        description = "运算符",
        enumValues = {"add", "subtract", "multiply", "divide", "percent"}
    )
    private String operator;

    @Override
    public Object execute(Map<String, Object> args) {
        try {
            BigDecimal numA     = new BigDecimal(args.get("a").toString());
            BigDecimal numB     = new BigDecimal(args.get("b").toString());
            String     operator = args.get("operator").toString();

            BigDecimal result = switch (operator) {
                case "add"      -> numA.add(numB);
                case "subtract" -> numA.subtract(numB);
                case "multiply" -> numA.multiply(numB);
                case "divide"   -> {
                    if (numB.compareTo(BigDecimal.ZERO) == 0) {
                        throw new ArithmeticException("除数不能为零");
                    }
                    yield numA.divide(numB, 10, RoundingMode.HALF_UP)
                              .stripTrailingZeros();
                }
                case "percent"  -> numA.multiply(numB)
                                       .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
                default -> throw new IllegalArgumentException("不支持的运算符: " + operator);
            };

            log.info("[Calculator] {} {} {} = {}", numA, operator, numB, result);

            return Map.of(
                "expression", numA.toPlainString() + " " + operator + " " + numB.toPlainString(),
                "result",     result.toPlainString(),
                "resultNum",  result
            );

        } catch (NumberFormatException e) {
            return Map.of("error", "参数格式错误,请传入数字字符串");
        } catch (ArithmeticException e) {
            return Map.of("error", e.getMessage());
        }
    }
}

六、核心:工具调用服务实现

这是整篇文章的核心------实现工具调用的完整循环。

6.1 工具调用响应解析

先定义响应类型模型:

java 复制代码
package com.example.openclaw_demo.tool.model;

import lombok.Data;
import java.util.List;
import java.util.Map;

/**
 * 封装 Claude API 可能返回的内容块类型
 * Claude 的响应 content 是一个数组,每个元素可能是以下类型之一
 */
public class ContentBlock {

    /** 纯文本块 */
    @Data
    public static class TextBlock {
        private String type;    // "text"
        private String text;
    }

    /** 工具调用块 */
    @Data
    public static class ToolUseBlock {
        private String              type;   // "tool_use"
        private String              id;     // 工具调用 ID(返回结果时需要带上)
        private String              name;   // 工具名称
        private Map<String, Object> input;  // 工具参数
    }

    /** 工具结果块(发回给模型时用) */
    @Data
    public static class ToolResultBlock {
        private String type;      // "tool_result"
        private String toolUseId; // 对应 ToolUseBlock 的 id
        private Object content;   // 工具执行结果
        private boolean isError;  // 是否是错误结果
    }
}

6.2 ToolChatService 接口

java 复制代码
package com.example.openclaw_demo.service;

import com.example.openclaw_demo.dto.ChatRequestDTO;

/**
 * 带工具调用能力的对话服务
 */
public interface ToolChatService {

    /**
     * 执行带工具调用的对话(自动完成工具路由和多轮调用)
     *
     * @param request 用户请求
     * @return AI 最终的自然语言回复
     */
    String chatWithTools(ChatRequestDTO request);

    /**
     * 指定工具集合的对话(只启用部分工具)
     *
     * @param request   用户请求
     * @param toolNames 工具名称列表(为 null 时使用全部工具)
     * @return AI 最终回复
     */
    String chatWithTools(ChatRequestDTO request, java.util.List<String> toolNames);
}

6.3 ToolChatServiceImpl:完整实现

java 复制代码
package com.example.openclaw_demo.service.impl;

import com.example.openclaw_demo.config.ClawProperties;
import com.example.openclaw_demo.dto.ChatRequestDTO;
import com.example.openclaw_demo.service.ToolChatService;
import com.example.openclaw_demo.tool.ToolRegistry;
import com.example.openclaw_demo.tool.model.ContentBlock;
import com.example.openclaw_demo.tool.model.ToolDefinition;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.openclaw.client.ClaudeService;
import io.openclaw.model.request.ChatRequest;
import io.openclaw.model.request.Message;
import io.openclaw.model.response.ChatResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class ToolChatServiceImpl implements ToolChatService {

    private final ClaudeService  claudeService;
    private final ClawProperties clawProperties;
    private final ToolRegistry   toolRegistry;
    private final ObjectMapper   objectMapper;

    // 最大工具调用轮数(防止无限循环)
    private static final int MAX_TOOL_ROUNDS = 5;

    @Override
    public String chatWithTools(ChatRequestDTO request) {
        return chatWithTools(request, null);
    }

    @Override
    public String chatWithTools(ChatRequestDTO request, List<String> toolNames) {
        // 获取工具定义列表
        List<ToolDefinition> tools = getTools(toolNames);
        if (tools.isEmpty()) {
            log.warn("[ToolChat] 没有可用工具,退化为普通对话");
            return fallbackNormalChat(request);
        }

        // 初始化对话消息列表
        List<Message> messages = new ArrayList<>();
        messages.add(Message.user(request.getMessage()));

        String systemPrompt = resolveSystemPrompt(request);

        // ============================================================
        // 工具调用核心循环
        // ============================================================
        for (int round = 0; round < MAX_TOOL_ROUNDS; round++) {
            log.info("[ToolChat] 第 {} 轮请求,消息数={}", round + 1, messages.size());

            // 1. 发送请求给 Claude(携带工具定义)
            ChatRequest clawReq = ChatRequest.builder()
                    .model(resolveModel(request))
                    .maxTokens(resolveMaxTokens(request))
                    .systemPrompt(systemPrompt)
                    .messages(messages)
                    .tools(tools)           // 工具定义列表
                    .toolChoice("auto")     // auto: 模型自己决定是否调用工具
                    .build();

            ChatResponse response = claudeService.chat(clawReq);

            log.info("[ToolChat] 第 {} 轮响应: stopReason={}, contentBlocks={}",
                    round + 1, response.getStopReason(), response.getContent().size());

            // 2. 检查停止原因
            if ("end_turn".equals(response.getStopReason())) {
                // 模型直接给出最终回复,不需要工具了
                String finalText = extractTextContent(response);
                log.info("[ToolChat] 对话完成,最终回复长度={}", finalText.length());
                return finalText;
            }

            if (!"tool_use".equals(response.getStopReason())) {
                // 其他停止原因(max_tokens 等)
                log.warn("[ToolChat] 意外的停止原因: {}", response.getStopReason());
                return extractTextContent(response);
            }

            // 3. stopReason == "tool_use":需要执行工具
            // 把模型的回复加入消息历史(包含 tool_use 块)
            messages.add(Message.assistant(response.getContent()));

            // 4. 执行所有工具调用(一次可能有多个)
            List<ContentBlock.ToolResultBlock> toolResults = executeTools(response);

            // 5. 把工具结果加入消息历史
            messages.add(Message.toolResult(toolResults));

            log.info("[ToolChat] 第 {} 轮工具执行完毕,执行了 {} 个工具", round + 1, toolResults.size());
        }

        // 超出最大轮数
        log.error("[ToolChat] 超出最大工具调用轮数 {}", MAX_TOOL_ROUNDS);
        throw new RuntimeException("工具调用轮数超出限制,请简化问题");
    }

    // ----------------------------------------------------------------
    // 执行本轮所有工具调用
    // ----------------------------------------------------------------
    private List<ContentBlock.ToolResultBlock> executeTools(ChatResponse response) {
        List<ContentBlock.ToolResultBlock> results = new ArrayList<>();

        for (Object contentBlock : response.getContent()) {
            if (!isToolUseBlock(contentBlock)) continue;

            ContentBlock.ToolUseBlock toolUse = parseToolUseBlock(contentBlock);

            log.info("[ToolChat] 执行工具: name={}, id={}, args={}",
                    toolUse.getName(), toolUse.getId(), toolUse.getInput());

            ContentBlock.ToolResultBlock result = new ContentBlock.ToolResultBlock();
            result.setToolUseId(toolUse.getId());
            result.setType("tool_result");

            try {
                // 调用工具注册中心执行工具
                Object toolResult = toolRegistry.execute(toolUse.getName(), toolUse.getInput());
                result.setContent(toJsonString(toolResult));
                result.setError(false);

                log.info("[ToolChat] 工具 {} 执行成功", toolUse.getName());

            } catch (Exception e) {
                // 工具执行失败:不抛出,将错误信息返回给模型,让模型做判断
                String errMsg = "工具执行失败: " + e.getMessage();
                result.setContent(errMsg);
                result.setError(true);

                log.error("[ToolChat] 工具 {} 执行失败: {}", toolUse.getName(), e.getMessage());
            }

            results.add(result);
        }

        return results;
    }

    // ----------------------------------------------------------------
    // 辅助方法
    // ----------------------------------------------------------------
    private List<ToolDefinition> getTools(List<String> toolNames) {
        if (toolNames == null) {
            return toolRegistry.getAllDefinitions();
        }
        return toolNames.stream()
                .map(name -> toolRegistry.getDefinition(name))
                .filter(Optional::isPresent)
                .map(Optional::get)
                .collect(Collectors.toList());
    }

    private String extractTextContent(ChatResponse response) {
        return response.getContent().stream()
                .filter(block -> isTextBlock(block))
                .map(block -> getTextFromBlock(block))
                .collect(Collectors.joining("\n"))
                .trim();
    }

    private String resolveSystemPrompt(ChatRequestDTO dto) {
        return StringUtils.hasText(dto.getSystemPrompt())
                ? dto.getSystemPrompt()
                : clawProperties.getGlobalSystemPrompt();
    }

    private String resolveModel(ChatRequestDTO dto) {
        return dto.getModel() != null ? dto.getModel() : clawProperties.getDefaultModel();
    }

    private int resolveMaxTokens(ChatRequestDTO dto) {
        return dto.getMaxTokens() != null ? dto.getMaxTokens() : clawProperties.getDefaultMaxTokens();
    }

    private String fallbackNormalChat(ChatRequestDTO request) {
        // 退化到普通对话(调用第2篇的 ClawChatService)
        return "(无可用工具)" + request.getMessage();
    }

    private String toJsonString(Object obj) {
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            return obj.toString();
        }
    }

    // 以下方法依赖 OpenClAW 具体 API,实际使用时按 SDK 文档调整
    private boolean isToolUseBlock(Object block) { /* ... */ return false; }
    private boolean isTextBlock(Object block)    { /* ... */ return false; }
    private String  getTextFromBlock(Object block){ /* ... */ return ""; }
    private ContentBlock.ToolUseBlock parseToolUseBlock(Object block) { /* ... */ return null; }
}

七、Controller 层

java 复制代码
package com.example.openclaw_demo.controller;

import com.example.openclaw_demo.common.ApiResult;
import com.example.openclaw_demo.dto.ChatRequestDTO;
import com.example.openclaw_demo.service.ToolChatService;
import com.example.openclaw_demo.tool.ToolRegistry;
import com.example.openclaw_demo.tool.model.ToolDefinition;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/v1/tool-chat")
@RequiredArgsConstructor
public class ToolChatController {

    private final ToolChatService toolChatService;
    private final ToolRegistry    toolRegistry;

    /**
     * POST /api/v1/tool-chat
     * AI 对话(自动按需调用工具)
     */
    @PostMapping
    public ApiResult<String> chat(@RequestBody @Validated ChatRequestDTO request) {
        String result = toolChatService.chatWithTools(request);
        return ApiResult.ok(result);
    }

    /**
     * POST /api/v1/tool-chat/with-tools
     * 指定工具集合
     * Body: { "request": {...}, "tools": ["get_weather", "calculator"] }
     */
    @PostMapping("/with-tools")
    public ApiResult<String> chatWithSpecificTools(
            @RequestBody @Validated ChatWithToolsRequest req) {
        String result = toolChatService.chatWithTools(req.getRequest(), req.getTools());
        return ApiResult.ok(result);
    }

    /**
     * GET /api/v1/tool-chat/tools
     * 查看当前注册的所有工具
     */
    @GetMapping("/tools")
    public ApiResult<List<ToolDefinition>> listTools() {
        return ApiResult.ok(toolRegistry.getAllDefinitions());
    }

    // 请求体定义
    public record ChatWithToolsRequest(
            @Validated ChatRequestDTO request,
            List<String> tools
    ) {}
}

八、完整调用流程演示

8.1 场景一:单工具调用(天气查询)

请求:

bash 复制代码
curl -X POST http://localhost:8080/api/v1/tool-chat \
  -H "Content-Type: application/json" \
  -d '{"message": "北京今天天气怎么样,适合去爬山吗?"}'

内部执行过程(日志):

bash 复制代码
[ToolChat] 第 1 轮请求,消息数=1
[ToolChat] 第 1 轮响应: stopReason=tool_use, contentBlocks=1
[ToolChat] 执行工具: name=get_weather, id=toolu_01ABC, args={city=北京}
[WeatherTool] 查询天气: city=北京, unit=celsius
[ToolChat] 工具 get_weather 执行成功
[ToolChat] 第 2 轮请求,消息数=3
[ToolChat] 第 2 轮响应: stopReason=end_turn, contentBlocks=1
[ToolChat] 对话完成,最终回复长度=127

最终响应:

bash 复制代码
{
  "success": true,
  "code": 200,
  "data": "北京今天天气晴,气温22°C,湿度35%,风力3级,非常适合户外活动!爬山的话建议上午出发,做好防晒,携带足够的水。今日生活指数良好,享受好天气吧 ☀️"
}

8.2 场景二:多工具组合调用

请求:

bash 复制代码
curl -X POST http://localhost:8080/api/v1/tool-chat \
  -H "Content-Type: application/json" \
  -d '{"message": "我想知道北京和上海今天的天气,另外帮我算一下,如果机票北京飞上海要1280元,打8折是多少钱?"}'

内部执行过程:

bash 复制代码
[ToolChat] 第 1 轮请求
[ToolChat] 第 1 轮响应: stopReason=tool_use, contentBlocks=3
  → 执行工具: get_weather (北京)
  → 执行工具: get_weather (上海)
  → 执行工具: calculator  (1280 * 80 percent)
[ToolChat] 第 2 轮请求(携带 3 个工具结果)
[ToolChat] 第 2 轮响应: stopReason=end_turn

💡 Claude 会在一次响应中输出多个 tool_use 块,你的代码需要并发或顺序执行所有工具,再一次性返回所有结果。上面的实现已支持此场景。

最终响应:

bash 复制代码
北京今天晴,22°C;上海多云,28°C,湿度较高。

关于机票折扣:¥1280 × 80% = **¥1024**,打8折后节省了256元。

天气来看,今天两地都适合出行,上海较热较湿,建议清爽穿搭~

8.3 场景三:工具调用失败的处理

bash 复制代码
用户: 帮我查一下订单号 ORD-INVALID-999
  ↓
执行 query_order 工具 → 返回: {"error": "订单不存在"}
  ↓
Claude(收到错误): 抱歉,未找到订单号 ORD-INVALID-999 的相关记录。
请确认订单号是否正确,或者您可以提供注册手机号,我帮您查询名下的订单。

工具返回错误信息时,模型会智能处理,给出引导性回复,而不是崩溃。


九、进阶:工具调用的最佳实践

9.1 工具描述的写作指南

工具描述是 Function Calling 效果的核心,遵循以下原则:

java 复制代码
// ❌ 糟糕的描述:太简短,模型不知道何时调用
@ClawTool(description = "查天气")

// ❌ 糟糕的描述:没有说明使用场景
@ClawTool(description = "Get weather information for a city")

// ✅ 好的描述:明确场景、能力边界、输入格式
@ClawTool(
    description = """
        查询指定中国大陆城市的实时天气信息,包括温度、湿度、风速、天气状况和生活建议。
        
        【使用场景】:用户询问某地天气、出行建议、穿衣建议、是否适合户外活动时调用。
        
        【能力边界】:
        - 支持:中国大陆各城市(直接写城市名,不需要带"市"字)
        - 不支持:港澳台、海外城市;历史天气;天气预报(只有实时天气)
        
        【输入格式】:city 传城市名,如「北京」「成都」,不要传「北京市」
        """
)

9.2 工具调用的安全控制

java 复制代码
/**
 * 在执行工具前增加权限检查
 */
@Override
public Object execute(Map<String, Object> args) {

    // 1. 参数白名单校验
    String city = (String) args.get("city");
    if (city != null && city.length() > 50) {
        return Map.of("error", "城市名称过长");
    }

    // 2. 频率限制(配合 Redis 实现)
    // rateLimiter.checkLimit("weather_tool:" + city);

    // 3. 敏感参数过滤(如数据库工具,禁止 DROP/DELETE 等)
    // sqlValidator.validate(args.get("sql").toString());

    return doExecute(args);
}

9.3 强制/禁止工具调用

java 复制代码
// 强制模型使用某个工具(不让模型自由选择)
ChatRequest clawReq = ChatRequest.builder()
        .tools(tools)
        .toolChoice(Map.of(
            "type", "tool",
            "name", "get_weather"   // 强制调用 get_weather
        ))
        .build();

// 完全禁止工具调用(纯文本对话)
ChatRequest clawReq = ChatRequest.builder()
        .tools(tools)
        .toolChoice("none")         // 模型不会调用任何工具
        .build();

// 自动模式(推荐,让模型自己决定)
.toolChoice("auto")

9.4 工具调用与流式输出结合

流式 + 工具调用是生产中最常见的组合:工具执行前推送"正在查询...",执行后推送实际内容。

java 复制代码
public SseEmitter streamChatWithTools(ChatRequestDTO request) {
    SseEmitter emitter = new SseEmitter(3 * 60 * 1000L);

    streamExecutor.execute(() -> {
        try {
            List<Message> messages = new ArrayList<>();
            messages.add(Message.user(request.getMessage()));
            List<ToolDefinition> tools = toolRegistry.getAllDefinitions();

            for (int round = 0; round < MAX_TOOL_ROUNDS; round++) {

                ChatResponse response = claudeService.chat(
                    ChatRequest.builder()
                        .messages(messages)
                        .tools(tools)
                        .toolChoice("auto")
                        .build()
                );

                if ("end_turn".equals(response.getStopReason())) {
                    // 最终回复:流式发送
                    String finalText = extractTextContent(response);
                    for (String token : splitToTokens(finalText)) {
                        emitter.send(SseEmitter.event().name("token").data(token));
                        Thread.sleep(10); // 模拟打字机效果
                    }
                    emitter.send(SseEmitter.event().name("done").data("{}"));
                    emitter.complete();
                    return;
                }

                if ("tool_use".equals(response.getStopReason())) {
                    // 通知前端:正在调用工具
                    for (Object block : response.getContent()) {
                        if (isToolUseBlock(block)) {
                            String toolName = getToolName(block);
                            emitter.send(SseEmitter.event()
                                .name("tool_start")
                                .data("{\"tool\":\"" + toolName + "\"}"));
                        }
                    }

                    // 执行工具
                    messages.add(Message.assistant(response.getContent()));
                    List<ContentBlock.ToolResultBlock> results = executeTools(response);

                    // 通知前端:工具执行完毕
                    emitter.send(SseEmitter.event()
                        .name("tool_end")
                        .data("{\"count\":" + results.size() + "}"));

                    messages.add(Message.toolResult(results));
                }
            }
        } catch (Exception e) {
            try {
                emitter.send(SseEmitter.event().name("error").data(e.getMessage()));
                emitter.completeWithError(e);
            } catch (Exception ignored) {}
        }
    });

    return emitter;
}

前端效果:

bash 复制代码
用户:北京天气怎么样?

[工具调用中: get_weather] ⏳

北京今天天气晴,气温22°C... (打字机效果输出)

十、完整项目结构

bash 复制代码
openclaw-demo/
├── src/main/java/com/example/openclaw_demo/
│   │
│   ├── tool/                                    ★ 新增目录
│   │   ├── ClawToolHandler.java                 ★ 工具执行接口
│   │   ├── ToolRegistry.java                    ★ 工具注册中心
│   │   │
│   │   ├── annotation/
│   │   │   ├── ClawTool.java                    ★ 工具类注解
│   │   │   └── ToolParam.java                   ★ 参数注解
│   │   │
│   │   ├── model/
│   │   │   ├── ToolDefinition.java              ★ 工具定义 DTO
│   │   │   └── ContentBlock.java                ★ 内容块类型
│   │   │
│   │   └── impl/                               ★ 具体工具实现
│   │       ├── WeatherTool.java                 ★ 天气查询
│   │       ├── OrderQueryTool.java              ★ 订单查询
│   │       └── CalculatorTool.java              ★ 计算器
│   │
│   ├── service/
│   │   ├── ToolChatService.java                 ★ 工具对话接口
│   │   └── impl/
│   │       └── ToolChatServiceImpl.java         ★ 工具对话实现
│   │
│   └── controller/
│       └── ToolChatController.java              ★ 工具对话 Controller
│
└── ...(其他模块同前几篇)

十一、常见问题排查

问题现象 可能原因 解决方法
模型从不调用工具,总是直接回答 工具描述不够清晰 改进 description,明确说明使用场景
模型一直调用工具,死循环 MAX_TOOL_ROUNDS 不够大,或工具返回不完整 检查工具返回格式,确保返回有意义的结果
ToolRegistry 没有扫描到工具类 Bean 未被 Spring 管理 确认工具类有 @Component 且在扫描路径内
参数解析失败(ClassCastException) JSON 数字被解析为 Integer 而非 Long ((Number) args.get("x")).longValue()
工具执行超时 外部 API 响应慢 给工具调用设置独立超时,返回超时错误信息
多工具并发执行结果顺序混乱 未对齐 tool_use_id 确保 ToolResultBlock 的 toolUseId 与请求 id 一致
模型忽略了部分工具 工具数量太多(>10个) 精简工具数量,或按场景动态传入工具子集

十二、本篇总结

本篇完整实现了生产级的 Function Calling 工具调用框架:

  1. 理解本质:AI 不直接执行代码,只输出"要调用什么工具、传什么参数",执行权在你手里
  2. 注解驱动注册@ClawTool + @ToolParam 声明式定义工具,ToolRegistry 自动扫描注册,无需手动维护工具列表
  3. 完整调用循环 :发送请求 → 检测 tool_use → 执行工具 → 返回结果 → 再次请求 → 直到 end_turn
  4. 多工具组合 :一次响应可包含多个 tool_use 块,框架统一执行后批量返回结果
  5. 优雅的错误处理:工具执行失败不崩溃,将错误信息返回给模型,由模型决定如何回复
  6. 三个真实工具:天气查询(外部 API)、订单查询(数据库)、计算器(本地计算)
  7. 流式 + 工具调用:工具执行期间向前端推送状态事件,完成后流式输出最终回复

📌 如果本文对你有帮助,欢迎点赞 👍 收藏 ⭐ 关注,你的支持是我持续创作的动力!

相关推荐
rOuN STAT2 小时前
Spring Boot 2.7.x 至 2.7.18 及更旧的版本,漏洞说明
java·spring boot·后端
汀、人工智能2 小时前
必知必会:大模型训练通信开销计算详解与面试指南
人工智能
victory04312 小时前
桌面agent
人工智能
zjjsctcdl2 小时前
Spring Boot 整合 MyBatis 与 PostgreSQL 实战指南
spring boot·postgresql·mybatis
Lentou2 小时前
程序调用AI大模型方式(SDK\HTTP\SPRINGAI\LANFCHAIN4J)
人工智能·网络协议·http
yong99902 小时前
基于直方图优化的图像去雾技术MATLAB实现
人工智能·计算机视觉·matlab
熊猫钓鱼>_>2 小时前
GenUI:从“文本对话”到“可操作界面”的范式转移
开发语言·人工智能·agent·sdk·vibecoding·assistant·genui
我叫黑大帅2 小时前
Golang中的map的key可以是哪些类型?可以嵌套map吗?
后端·面试·go
其实防守也摸鱼2 小时前
部署本地AI大模型--ollma
人工智能·安全·ai·大模型·软件工程·本地大模型