《Spring AI 实战系列 入门篇》第 2 篇

2 阅读8分钟

Spring AI 实战系列 | 第 2 篇

Tool Calling:让 AI 调用外部函数

系列说明:本文为《Spring AI 实战系列 入门篇》第 2 篇

前置知识:完成第 1 篇(基础概念与快速上手)

预计阅读时间:15 分钟


📖 目录

  1. 为什么需要 Tool Calling?
  2. 工作原理
  3. 声明式工具定义:@Tool 注解
  4. 编程式工具定义
  5. 实战案例
  6. 常见问题与最佳实践
  7. 系列预告与回顾

一、为什么需要 Tool Calling?

1.1 AI 模型的局限性

尽管大语言模型(LLM)很强大,但它们有天然的缺陷:

局限性示例
知识陈旧GPT-4 训练数据截至 2023 年,不知道今天的天气
无法访问实时信息无法查询实时股价、新闻、库存
无法执行外部操作不能发邮件、下订单、控制 IoT 设备
计算能力有限数学计算可能出错

1.2 Tool Calling 解决什么问题?

Tool Calling = 扩展 AI 的能力边界

┌─────────────────────────────────────────────────────────┐
│                    没有 Tool Calling                      │
├─────────────────────────────────────────────────────────┤
│  用户:"北京今天多少度?"                                   │
│  AI:"我没有实时天气数据,无法回答。"(❌)                   │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                    有 Tool Calling                       │
├─────────────────────────────────────────────────────────┤
│  用户:"北京今天多少度?"                                   │
│  AI:调用 getWeather("北京")                              │
│       ↓                                                  │
│  工具返回:25°C,晴                                        │
│       ↓                                                  │
│  AI:"北京今天天气晴朗,温度25°C。"(✅)                    │
└─────────────────────────────────────────────────────────┘

1.3 典型使用场景

场景工具示例
🌤️ 天气查询getWeather(city)
📅 日历管理createEvent(title, time)
📧 邮件处理sendEmail(to, subject, body)
📊 数据库查询query(sql)
🔍 网络搜索search(query)
💰 支付处理createPayment(amount)

二、工作原理

2.1 执行流程

┌────────────────────────────────────────────────────────────┐
│                    Tool Calling 完整流程                      │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  1️⃣ 注册工具                                               │
│  ┌─────────────────┐                                       │
│  │ 天气工具         │                                       │
│  │ - name: get_weather                                      │
│  │ - desc: 获取城市天气                                       │
│  │ - param: city    │                                       │
│  └────────┬─────────┘                                       │
│           ↓                                                 │
│  2️⃣ 用户提问                                                │
│  "北京今天多少度?"                                          │
│           ↓                                                 │
│  3️⃣ 模型决策                                                │
│  模型判断需要调用 get_weather,参数 city="北京"               │
│           ↓                                                 │
│  4️⃣ 执行工具                                                │
│  应用调用 getWeather("北京") → 返回 "25°C, 晴"              │
│           ↓                                                 │
│  5️⃣ 返回结果                                                │
│  工具结果发送给模型                                          │
│           ↓                                                 │
│  6️⃣ 生成回答                                                │
│  "北京今天天气晴朗,气温25°C。"                               │
│                                                            │
└────────────────────────────────────────────────────────────┘

2.2 Spring AI 中的核心组件

组件作用
@Tool声明式定义工具
ToolCallback工具回调接口
ToolCallingManager工具执行管理器
ChatClient支持 .tools() 方法

三、声明式工具定义:@Tool 注解

3.1 基础用法

最简单的一个工具:

package com.example.demo.tools;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;

@Component  // 需要注册为 Spring Bean
public class WeatherTools {

    @Tool(description = "获取指定城市的当前天气")
    public String getWeather(String city) {
        // 这里调用真实的天气 API
        return switch (city) {
            case "北京" -> "25°C,晴";
            case "上海" -> "28°C,多云";
            case "深圳" -> "30°C,雷阵雨";
            default -> "未知城市";
        };
    }
}

在 ChatClient 中使用:

@RestController
public class ToolController {
    
    private final ChatClient chatClient;
    private final WeatherTools weatherTools;
    
    public ToolController(ChatClient.Builder builder, WeatherTools weatherTools) {
        this.chatClient = builder.build();
        this.weatherTools = weatherTools;
    }
    
    @GetMapping("/weather")
    public String weather(@RequestParam String city) {
        return chatClient.prompt()
            .user("北京今天多少度?")
            .tools(weatherTools)  // 注入工具
            .call()
            .content();
    }
}

3.2 带参数描述

使用 @ToolParam 为参数添加描述,帮助模型理解:

public class AdvancedTools {

    @Tool(description = "设置闹钟")
    public void setAlarm(
        @ToolParam(description = "闹钟时间,格式:yyyy-MM-dd HH:mm") String time
    ) {
        // 设置闹钟逻辑
        System.out.println("闹钟已设置:" + time);
    }
    
    @Tool(description = "计算两个数的运算结果")
    public double calculate(
        @ToolParam(description = "第一个数字") double a,
        @ToolParam(description = "运算符:add/subtract/multiply/divide") String operator,
        @ToolParam(description = "第二个数字") double b
    ) {
        return switch (operator) {
            case "add" -> a + b;
            case "subtract" -> a - b;
            case "multiply" -> a * b;
            case "divide" -> b != 0 ? a / b : 0;
            default -> 0;
        };
    }
}

3.3 多工具组合

一个类可以定义多个工具:

@Component
public class DateTimeTools {

    @Tool(description = "获取当前日期时间")
    public String getCurrentDateTime() {
        return java.time.LocalDateTime.now().toString();
    }
    
    @Tool(description = "计算指定天数后的日期")
    public String addDays(
        @ToolParam(description = "起始日期,yyyy-MM-dd格式") String startDate,
        @ToolParam(description = "要增加的天数") int days
    ) {
        // 日期计算逻辑
        return "2026-04-01";  // 返回计算结果
    }
    
    @Tool(description = "获取用户所在时区")
    public String getTimeZone() {
        return java.time.ZoneId.systemDefault().toString();
    }
}

使用多个工具:

@GetMapping("/ask")
public String ask(@RequestParam String question) {
    DateTimeTools dateTimeTools = new DateTimeTools();
    
    return chatClient.prompt()
        .user(question)
        .tools(dateTimeTools)  // 一个实例包含多个工具
        .call()
        .content();
}

3.4 默认工具 vs 运行时工具

// 方式1:运行时工具(仅当前请求有效)
chatClient.prompt()
    .user("今天几号?")
    .tools(new DateTimeTools())  // 只在这个请求中使用
    .call();

// 方式2:默认工具(所有请求共享)
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultTools(new CommonTools())  // 所有请求都可用
    .build();

四、编程式工具定义

当 @Tool 注解不够用时,可以用 MethodToolCallback 手动创建:

import org.springframework.ai.tool.support.MethodToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;

@Service
public class CustomToolService {
    
    // 需要手动包装的方法
    public String searchProducts(String keyword, int limit) {
        // 搜索产品逻辑
        return "找到 3 个商品:商品A、商品B、商品C";
    }
    
    @Bean
    public ToolCallback searchToolCallback() {
        return MethodToolCallback.builder()
            .toolDefinition(ToolDefinition.builder()
                .name("search_products")
                .description("搜索商品列表")
                .inputSchema("""
                    {
                        "type": "object",
                        "properties": {
                            "keyword": {"type": "string", "description": "搜索关键词"},
                            "limit": {"type": "integer", "description": "返回数量限制"}
                        },
                        "required": ["keyword"]
                    }
                    """)
                .build())
            .toolMethod(getMethod("searchProducts", String.class, int.class))
            .toolObject(this)
            .build();
    }
    
    private Method getMethod(String name, Class<?>... paramTypes) {
        try {
            return getClass().getMethod(name, paramTypes);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }
}

五、实战案例

5.1 案例一:AI 日程助手

@Component
public class CalendarTools {

    private final List<Map<String, String>> events = new ArrayList<>();
    
    @Tool(description = "创建日程事件")
    public String createEvent(
        @ToolParam(description = "事件标题") String title,
        @ToolParam(description = "事件时间,ISO格式") String datetime,
        @ToolParam(description = "事件描述") String description
    ) {
        Map<String, String> event = Map.of(
            "title", title,
            "datetime", datetime,
            "description", description
        );
        events.add(event);
        return "日程已创建:" + title + ",时间:" + datetime;
    }
    
    @Tool(description = "查询指定日期的日程")
    public String getEvents(
        @ToolParam(description = "查询日期,yyyy-MM-dd格式") String date
    ) {
        List<String> dayEvents = events.stream()
            .filter(e -> e.get("datetime").startsWith(date))
            .map(e -> "- " + e.get("title") + " (" + e.get("datetime") + ")")
            .toList();
        
        return dayEvents.isEmpty() 
            ? date + " 没有日程安排" 
            : String.join("\n", dayEvents);
    }
    
    @Tool(description = "删除日程")
    public String deleteEvent(
        @ToolParam(description = "要删除的事件标题") String title
    ) {
        boolean removed = events.removeIf(e -> e.get("title").equals(title));
        return removed ? "已删除:" + title : "未找到日程:" + title;
    }
}

@RestController
@RequestMapping("/calendar")
public class CalendarController {
    
    private final ChatClient chatClient;
    private final CalendarTools calendarTools;
    
    public CalendarController(ChatClient.Builder builder, CalendarTools tools) {
        this.chatClient = builder.build();
        this.calendarTools = tools;
    }
    
    @GetMapping("/ask")
    public String ask(@RequestParam String question) {
        return chatClient.prompt()
            .system("你是一个智能日程助手,可以帮用户创建、查询和删除日程。")
            .user(question)
            .tools(calendarTools)
            .call()
            .content();
    }
}

测试:

# 创建日程
curl "http://localhost:8080/calendar/ask?question=帮我安排一个明天上午10点的会议"

# 查询日程
curl "http://localhost:8080/calendar/ask?question=明天有什么安排?"

# 删除日程
curl "http://localhost:8080/calendar/ask?question=取消明天的会议"

5.2 案例二:AI 计算助手

@Component
public class MathTools {

    @Tool(description = "进行数学运算")
    public double calculate(
        @ToolParam(description = "第一个数") double a,
        @ToolParam(description = "运算符:add/sub/mult/div/pow/sqrt") String op,
        @ToolParam(description = "第二个数") double b
    ) {
        return switch (op) {
            case "add"  -> a + b;
            case "sub"  -> a - b;
            case "mult" -> a * b;
            case "div"  -> b != 0 ? a / b : Double.NaN;
            case "pow"  -> Math.pow(a, b);
            case "sqrt" -> Math.sqrt(a);
            default     -> 0;
        };
    }
    
    @Tool(description = "获取圆周率")
    public double getPi() {
        return Math.PI;
    }
}

@RestController
@RequestMapping("/math")
public class MathController {
    
    private final ChatClient chatClient;
    
    public MathController(ChatClient.Builder builder, MathTools tools) {
        this.chatClient = builder.build();
    }
    
    @GetMapping("/solve")
    public String solve(@RequestParam String question) {
        return chatClient.prompt()
            .system("你是一个数学计算助手,用户提出数学问题时,使用 calculate 工具来精确计算。")
            .user(question)
            .tools(new MathTools())
            .call()
            .content();
    }
}

测试:

curl "http://localhost:8080/math/solve?question=15的平方根是多少?"
curl "http://localhost:8080/math/solve?question=100加200等于多少?"

六、常见问题与最佳实践

6.1 常见问题

Q1: 模型不调用工具?

检查以下几点:

  1. 工具描述是否清晰明确?
  2. 工具参数是否有 @ToolParam 描述?
  3. 提示词是否引导模型使用工具?
  4. 模型训练参数太少,我本地用的qwen2.5:7b,偶尔就会出现AI回复了,但不调用工具。换成有免费额度的大模型就好

Q2: 工具返回结果格式?

@Tool
public String getData() {
    return "返回的字符串";  // 返回 String,会自动转为 JSON
}

Q3: 工具执行失败怎么办?

@Tool
public String riskyOperation() {
    try {
        // 正常逻辑
        return "成功";
    } catch (Exception e) {
        // 返回错误信息,模型会解释给用户
        return "操作失败:" + e.getMessage();
    }
}

6.2 最佳实践

实践说明
详细描述@Tool(description = "获取...") 要写清楚
参数命名参数名要有意义,如 cityName 而非 arg0
错误处理工具内部做好 try-catch
单工具类相关工具放一个类,复杂应用多个类
Spring Bean工具类加 @Component 自动注入

6.3 @Tool 注解参数

参数说明默认值
name工具名称方法名
description工具描述(最重要方法名
returnDirect是否直接返回结果false
@Tool(
    name = "get_weather",
    description = "获取指定城市的当前天气,包括温度和天气状况",
    returnDirect = false
)
public String getWeather(String city) { ... }

七、系列预告与回顾

7.1 本篇小结

  • ✅ 理解了 Tool Calling 的价值与原理
  • ✅ 掌握了 @Tool 声明式定义
  • ✅ 学会了多工具组合使用
  • ✅ 完成了两个实战案例

7.2 系列回顾

篇目内容状态
第 1 篇核心概念 + 快速上手✅ 已完成
第 2 篇Tool Calling + 工具调用✅ 本文
第 3 篇VectorStore + RAG🔜 下一篇
第 4 篇结构化输出🔜 待编写
第 5 篇Advisors 中间件🔜 待编写
第 6 篇国产模型集成🔜 待编写

7.3 下篇预告

第 3 篇:VectorStore + RAG:构建私有知识库

  • 向量数据库选型与配置
  • 文档 ETL 管道
  • Embedding 模型使用
  • RAG 完整实现

📚 参考资料

  1. Spring AI Tool Calling 官方文档

  2. MethodToolCallback 文档


📌 引用说明:本文核心概念与技术描述参考自 Spring AI 官方文档(docs.spring.io/spring-ai/r…


关注公众号「AI日撰」,点击菜单「获取源码」获取完整代码(Gitee 仓库)。


系列:《Spring AI 实战系列 入门篇》第 2 篇(共 6 篇)