集成工具
工具调用也称为函数调用,是指在与模型交互时,模型可以调用外部函数或API来获取数据或执行操作。Spring AI提供了对工具调用的支持,使得模型可以在对话中动态地调用外部函数。
尽管我们通常将工具调用视为一种模型能力,但实际上提供工具调用逻辑的是客户端应用程序。模型只能请求工具调用并提供输入参数,而应用程序则负责根据输入参数执行工具调用并返回结果。模型永远无法访问作为工具提供的任何 API,这是一个关键的安全考量。Spring AI 提供了便捷的 API 来定义工具、解析来自模型的工具调用请求并执行工具调用.
工具用途
根据Spring AI 文档,工具调用的主要用于:
-
信息检索. 此类工具可用于从外部源(如数据库、网络服务、文件系统或网络搜索引擎)检索信息。其目标是增强模型的知识,使其能够回答原本无法回答的问题。因此,它们可用于检索增强生成(RAG)场景。例如,工具可用于检索给定地点的当前天气、获取最新新闻文章,或查询数据库以获取特定记录。
-
执行操作. 此类工具可用于在软件系统中执行操作,例如发送电子邮件、在数据库中创建新记录、提交表单或触发工作流。其目标是自动化那些原本需要人工干预或显式编程的任务。例如,工具可用于为与聊天机器人交互的客户预订航班、在网页上填写表单,或在代码生成场景中基于自动化测试(TDD)实现一个Java类。
将方法作为工具
在Spring AI中,我们可以将Java方法作为工具来使用。Spring AI会自动将方法转换为工具,并在模型请求工具调用时执行该方法。
我们可以通过在方法上添加@Tool注解来将其标记为工具。以下是一个示例:
在 src/main/java/com/example/canaan/tool目录下创建一个新的工具类 UserTools.java,并添加以下内容:
package com.chestnut.canaan.tool;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import java.util.Objects;
@Service
public class UserTools {
@Tool(
name = "getUserFrom",
description = "根据用户名获取用户来源"
)
public String getUserFrom(@ToolParam(description = "用户名称") String userName) {
if (userName == null || userName.isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
// 模拟获取用户来源的逻辑
if (Objects.equals(userName, "Canaan")) {
return "中国西北";
}
if (Objects.equals(userName, "Tifa")) {
return "中国东北";
}
return "未知来源";
}
}
在这个示例中,我们定义了一个名为 getUserFrom 的工具方法,它接受一个用户名作为参数,并返回该用户的来源。我们使用 @Tool 注解来标记这个方法为工具,并提供了工具的名称和描述。
接下来,我们需要修改一下 src/main/java/com/example/canaan/config/ai/ChatConfiguration.java,使其支持工具调用:
package com.chestnut.canaan.config.ai;
import com.chestnut.canaan.tool.UserTools;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatConfiguration {
@Resource
private ChatMemory chatMemory;
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultTools(new UserTools())
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.defaultSystem("you are a friendly assistant!").build();
}
}
在这个配置中,我们使用 defaultTools 方法将 UserTools 类注册为工具类。这样,当模型根据描述命中到 getUserFrom 工具时,Spring AI将能够调用该方法。
现在,我们可以通过访问 /ai/chat?input=用户输入 来与模型进行交互,并使用工具获取用户来源。例如,输入 Canaan此人来自哪里 将返回 中国西北。
将函数作为工具
我们可以通过 FunctionToolCallback 将函数式类型(Function、Supplier、Consumer 或 BiFunction)转换为工具,例如:
package com.chestnut.canaan.config.ai;
import com.chestnut.canaan.tool.UserTools;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.function.Function;
@Configuration
public class ChatConfiguration {
@Resource
private ChatMemory chatMemory;
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
ToolCallback weatherTool = FunctionToolCallback
.builder("currentWeather", (Function<WeatherRequest, WeatherResponse>) _ -> new WeatherResponse(25.0, Unit.C))
.description("Get the weather in location")
.inputType(WeatherRequest.class)
.build();
return builder
.defaultTools(new UserTools())
.defaultToolCallbacks(weatherTool)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.defaultSystem("you are a friendly assistant!").build();
}
}
enum Unit { C, F }
record WeatherRequest(@ToolParam(description = "位置名称,例如:上海") String location, Unit unit) {}
record WeatherResponse(double temp, Unit unit) {}
注意
如果使用函数作为工具时,必须确保函数的输入和输出可以是Void或POJO。输入和输出的POJO必须是可序列化的,因为结果将被序列化并发送回模型。以上述示例为例,WeatherRequest 和 WeatherResponse 都是可序列化的 POJO。简单来说,如果序列化为json,则必须是用{}包裹的对象。单个的字符串或数字等基本类型不能作为输入或输出。
// 可以
(Function<WeatherRequest, WeatherResponse>) _ -> new WeatherResponse(25.0, Unit.C)
// 不可以
(Function<String, String>) _ -> "25.0"
额外说明
在Spring AI官方文档中,DeepSeek的start包并不支持工具调用。
但是在上述尝试中可以发现,无论是使用方法还是使用函数作为工具时,DeepSeek模型仍然能够正确调用工具并返回结果。