📅 难度:⭐⭐⭐⭐☆ 高级 | 阅读约 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 工具调用框架:
- 理解本质:AI 不直接执行代码,只输出"要调用什么工具、传什么参数",执行权在你手里
- 注解驱动注册 :
@ClawTool+@ToolParam声明式定义工具,ToolRegistry自动扫描注册,无需手动维护工具列表 - 完整调用循环 :发送请求 → 检测
tool_use→ 执行工具 → 返回结果 → 再次请求 → 直到end_turn - 多工具组合 :一次响应可包含多个
tool_use块,框架统一执行后批量返回结果 - 优雅的错误处理:工具执行失败不崩溃,将错误信息返回给模型,由模型决定如何回复
- 三个真实工具:天气查询(外部 API)、订单查询(数据库)、计算器(本地计算)
- 流式 + 工具调用:工具执行期间向前端推送状态事件,完成后流式输出最终回复
📌 如果本文对你有帮助,欢迎点赞 👍 收藏 ⭐ 关注,你的支持是我持续创作的动力!