摘要:本文以 Sprin؜؜؜g AI 框架为例,学习‏‏‏工具调用,大幅增强 AI ‌‌‌的能力,并实战主流工具的开发‏‏‏。

具体内容包括:工具调用介绍,Spring AI 工具开发,主流工具开发,工具调用进阶知识

一  需求分析

之前我们通过 RAG 技术让 AI 应用具备了根据苍穹外卖知识库获取信息并回答的能力,但是直到目前为止,AI 应用还只是个 "项目知识问答助手"本节我们可以利用工具调用特性,实现更多实战开发需求。

1)异常智能分析:比如定位项目启动报错、接口异常、Bug 根源,示例用户提问:

  • 帮我看下这个空指针异常是哪里引起的?
  • 为什么 Redis 连接失败,该怎么解决?

2)代码逻辑解释:比如讲解苍穹外卖业务代码、接口作用、流程逻辑,示例用户提问:

  • 帮我解释下订单提交接口的这段代码是干嘛的?
  • 这段 WebSocket 推送代码的业务流程是什么?

而且这些需求还可以进行组合,比如用户先让 AI 解释一段代码、再分析运行时报错、最后给出完整可运行的修复方案,形成一站式开发辅助。

如果 AI 能够完成上述需求,就不再只是一个有项目知识的 "大脑",而是有手有脚,会利用工具排查问题、分析代码的 "智能开发助手" 了。

下面我们就来学习下实现上述需求的关键 —— 工具调用 技术。

二  工具调用介绍

什么是工具调用?

工具调用(Tool Calling)可以理解为让 AI 大模型 借用外部工具 来完成它自己做不到的事情。

跟人类一样؜؜؜,如果只凭手脚⁠完⁠成⁠不了工作,‏那么‏就可‏以利用‌工具箱来‌完成‌。

工具可以是؜؜؜任何东西,如网⁠⁠页⁠搜索、对外部 ‏‏AP‏I 的调用、‌‌访问外‌部数据、或‏‏执行特定‏的代码等。

比如用户提؜؜؜问 “帮我查询上海最⁠⁠⁠新的天气”,AI 本‏‏‏身并没有这些知识,它‌‌‌就可以调用 “查询天‏‏‏气工具”,来完成任务。

目前工具调؜؜؜用技术发展的已经比较⁠⁠⁠成熟了,几乎所有主流‏‏‏的、新出的 AI 大‌‌‌模型和 AI 应用开‏‏‏发平台都支持工具调用。

工具调用的工作原理

其实,工具调用的工作原理非常简单,并不是 AI 服务器自己调用这些工具、也不是把工具的代码发送给 AI 服务器让它执行,它只能提出要求,表示 “我需要执行 XX 工具完成任务”。而真正执行工具的是我们自己的应用程序,执行后再把结果告诉 AI,让它继续工作。

举个例子,؜؜؜假如用户提问 ⁠"编⁠程导航网站‏有哪‏些热‏门文章‌?",‌就需要‌经‏历下列流‏程:

  1. 用户提出问题:"编程导航网站有哪些热门文章?"
  2. 程序将问题传递给大模型
  3. 大模型分析问题,判断需要使用工具(网页抓取工具)来获取信息
  4. 大模型输出工具名称和参数(网页抓取工具,URL参数为 codefather.cn)
  5. 程序接收工具调用请求,执行网页抓取操作
  6. 工具执行抓取并返回文章数据
  7. 程序将抓取结果传回给大模型
  8. 大模型分析网页内容,生成关于编程导航热门文章的回答
  9. 程序将大模型的回答返回给用户

虽然看起来是 AI 在调用工具,但实际上整个过程是 由我们的应用程序控制的。AI 只负责决定什么时候需要用工具,以及需要传递什么参数,真正执行工具的是我们的程序。

你可能会好؜؜؜奇,为啥要这么设计⁠⁠⁠呢?这样不是要让程‏‏‏序请求 AI 多次‌‌‌么?为啥不让 AI‏‏‏ 服务器直接调用工具程序?

有这个想法很正常,但如果让你自己设计一个 AI 大模型服务,你就能理解了。很关键的一点是 安全性,AI 模型永远无法直接接触你的 API 或系统资源,所有操作都必须通过你的程序来执行,这样你可以完全控制 AI 能做什么、不能做什么。

举个例子,你有一个爆破工具؜؜؜,用户像 AI 提了需求 ”我要拆这栋房子“,虽然⁠⁠⁠ AI 表示可以用爆破工具,但是需要经过你的同意,‏‏‏才能执行爆破。反之,如果把爆破工具植入给 AI,A‌‌‌I 觉得自己能炸了,就炸了,不需要再问你的意见。而‏‏‏且这样也给 AI 服务器本身增加了压力。

工具调用和功能调用

大家可能看到过 F؜؜؜unction Calling(功⁠⁠⁠能调用)这个概念,别担心,其实它和‏‏‏ Tool Calling(工具调‌‌‌用)完全是同一概念!只是不同平台或‏‏‏每个人习惯的叫法不同而已。

Spring AI 工具调用文档 的开头就说明了这一点:

个人更喜؜؜؜欢 "工具调用" 这个⁠⁠⁠说法,因为 Funct‏‏‏ion 这个词更像是计‌‌‌算机行业的术语,不如工‏‏‏具更形象易懂、更具普适性。

工具调用的技术选型

我们先来梳理一下工具调用的流程:

  1. 工具定义:程序告诉 AI "你可以使用这些工具",并描述每个工具的功能和所需参数
  2. 工具选择:AI 在对话中判断需要使用某个工具,并准备好相应的参数
  3. 返回意图:AI 返回 “我想用 XX 工具,参数是 XXX” 的信息
  4. 工具执行:我们的程序接收请求,执行相应的工具操作
  5. 结果返回:程序将工具执行的结果发回给 AI
  6. 继续对话:AI 根据工具返回的结果,生成最终回答给用户

通过上述流程,我们会发现,؜؜؜程序需要和 AI 多次进行交互、还要能够执行对应的⁠⁠⁠工具,怎么实现这些呢?我们当然可以自主开发,不过还‏‏‏是更推荐使用 Spring AI、LangChai‌‌‌n 等开发框架。此外,有些 AI 大模型服务商也提‏‏‏供了对应的 SDK,都能够简化代码编写。

本教程后续؜؜؜部分将以 Sp⁠r⁠i⁠ng AI‏ 为‏例,‏带大家‌实战工‌具调‌用开‏发。

💡 需要注意的是,不是所有大模型都支持工具调用。有些基础模型或早期版本可能不支持这个能力。可以在 Spring AI 官方文档 中查看各模型支持情况。

三  Spring AI 工具开发

首先我们通过 Spring AI 官方 提供的图片来理解 Spring AI 在实现工具调用时都帮我们做了哪些事情?

  1. 工具定义与注册:Spring AI 可以通过简洁的注解自动生成工具定义和 JSON Schema,让 Java 方法轻松转变为 AI 可调用的工具。
  2. 工具调用请求:Spring AI 自动处理与 AI 模型的通信并解析工具调用请求,并且支持多个工具链式调用。
  3. 工具执行:Spring AI 提供统一的工具管理接口,自动根据 AI 返回的工具调用请求找到对应的工具并解析参数进行调用,让开发者专注于业务逻辑实现。
  4. 处理工具结果:Spring AI 内置结果转换和异常处理机制,支持各种复杂 Java 对象作为返回值并优雅处理错误情况。
  5. 返回结果给模型:Spring AI 封装响应结果并管理上下文,确保工具执行结果正确传递给模型或直接返回给用户。
  6. 生成最终响应:Spring AI 自动整合工具调用结果到对话上下文,支持多轮复杂交互,确保 AI 回复的连贯性和准确性。

下面是一个较早版本的流程图,也能帮助我们理解这个过程:

定义工具
工具定义模式

在 Spr؜؜؜ing AI 中,定⁠⁠⁠义工具主要有两种模式‏‏‏:基于 Method‌‌‌s 方法或者 Fun‏‏‏ctions 函数式编程。

1)Methods 模式:通过 @Tool 注解定义工具,通过 tools 方法绑定工具

class WeatherTools {
    @Tool(description = "Get current weather for a location")
    public String getWeather(@ToolParam(description = "The city name") String city) {
        return "Current weather in " + city + ": Sunny, 25°C";
    }
}

// 使用方式
ChatClient.create(chatModel)
    .prompt("What's the weather in Beijing?")
    .tools(new WeatherTools())
    .call();

2)Functions 模式:通过 @Bean 注解定义工具,通过 functions 方法绑定工具

@Configuration
public class ToolConfig {
    @Bean
    @Description("Get current weather for a location")
    public Function<WeatherRequest, WeatherResponse> weatherFunction() {
        return request -> new WeatherResponse("Weather in " + request.getCity() + ": Sunny, 25°C");
    }
}

// 使用方式
ChatClient.create(chatModel)
    .prompt("What's the weather in Beijing?")
    .functions("weatherFunction")
    .call();

显然 Met؜؜؜hods 模式的开发量更⁠⁠⁠少,更推荐这种方‏‏‏式,所以下面重点讲解这种方式。

定义工具

Spring AI 提供了两种定义工具的方法 —— 注解式编程式

1)注解式:只需使用 @Tool 注解标记普通 Java 方法,就可以定义工具了,简单直观。

每个工具最好都添加详细清晰的描述,帮助 AI 理解何时应该调用这个工具。对于工具方法的参数,可以使用 @ToolParam 注解提供额外的描述信息和是否必填。

class WeatherTools {
    @Tool(description = "获取指定城市的当前天气情况")
    String getWeather(@ToolParam(description = "城市名称") String city) {
        // 获取天气的实现逻辑
        return "北京今天晴朗,气温25°C";
    }
}

2)编程式؜؜؜:如果想在运行⁠时⁠动⁠态创建工具‏,可‏以选‏择编程‌式来定‌义工具,‏‌更灵活。

先定义工具类:

class WeatherTools {
    String getWeather(String city) {
        // 获取天气的实现逻辑
        return "北京今天晴朗,气温25°C";
    }
}

然后将工具类؜؜؜转换为 ToolCall⁠⁠⁠back 工具定义类,之‏‏‏后就可以把这个类绑定给 ‌‌‌ChatClient,从‏‏‏而让 AI 使用工具了。

Method method = ReflectionUtils.findMethod(WeatherTools.class, "getWeather", String.class);
ToolCallback toolCallback = MethodToolCallback.builder()
    .toolDefinition(ToolDefinition.builder(method)
            .description("获取指定城市的当前天气情况")
            .build())
    .toolMethod(method)
    .toolObject(new WeatherTools())
    .build();
String result = chatClient.prompt()
    .user("北京今天天气怎么样?")  // 用户问题
    .tools(toolCallback)         // 绑定工具
    .call()
    .content();

其实你会发؜؜؜现,编程式就是⁠把⁠注⁠解式的那些‏参数‏,改‏成通过‌调用方‌法来设置‏‌了而已。

在定义工具时,需要注؜؜؜意方法参数和返回值类型的选择。Sprin⁠⁠⁠g AI 支持大多数常见的 Java 类‏‏‏型作为参数和返回值,包括基本类型、复杂对象‌、‌‌集合等。而且返回值需要是可序列化的,‏‏‏因为它将被发送给 AI 大模型。

以下类型目前不支持作为工具方法的参数或返回类型:

  • Optional
  • 异步类型(如 CompletableFuture, Future)
  • 响应式类型(如 Flow, Mono, Flux)
  • 函数式类型(如 Function, Supplier, Consumer)

使用工具

定义好工具后؜؜؜,Spring AI ⁠⁠⁠提供了多种灵活的方式将‏‏‏工具提供给 ChatC‌‌‌lient,让 AI ‏‏‏能够在需要时调用这些工具。

1)按需使用:这是最简单的方式,直接在构建 ChatClient 请求时通过 tools() 方法附加工具。这种方式适合只在特定对话中使用某些工具的场景。

String response = ChatClient.create(chatModel)
    .prompt("北京今天天气怎么样?")
    .tools(new WeatherTools())  // 在这次对话中提供天气工具
    .call()
    .content();

2)全局使用:如؜؜؜果某些工具需要在所有对话中都可用⁠⁠⁠,可以在构建 ChatClien‏‏‏t 时注册默认工具。这样,这些工‌‌‌具将对从同一个 ChatClie‏‏‏nt 发起的所有对话可用。

ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultTools(new WeatherTools(), new TimeTools())  // 注册默认工具
    .build();

3)更底层的使用方؜؜؜式:除给 ChatClient ⁠⁠⁠绑定工具外,也可以给更底层的 Ch‏‏‏atModel 绑定工具。

ToolCallback[] weatherTools = ToolCallbacks.from(new WeatherTools());
// 绑定工具到对话
ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(weatherTools)
    .build();
// 构造 Prompt 时指定对话选项
Prompt prompt = new Prompt("北京今天天气怎么样?", chatOptions);
chatModel.call(prompt);

总结一下,在使用工具时,Spring AI 会自动处理工具调用的全过程:从 AI 模型决定调用工具 => 到执行工具方法 => 再到将结果返回给模型 => 最后模型基于工具结果生成最终回答。这整个过程对开发者来说是透明的,我们只需专注于实现工具的业务逻辑即可。

那么,怎么؜实现工具呢?

工具生态

首先,工具的本质就是一种插件。能不自己写的插件,就尽量不要自己写。我们可以直接在网上找一些优秀的工具实现,比如 Spring AI Alibaba 官方文档 中提到了社区插件。

四  主流工具开发

如果社区中没找到合؜؜؜适的工具,我们就要自主开发。需要注⁠⁠⁠意的是,AI 自身能够实现的功能通‏‏‏常没必要定义为额外的工具,因为这会‌‌‌增加一次额外的交互,我们应该将工具‏‏‏用于 AI 无法直接完成的任务。

下面我们依次来实现需求分析中提到的 6 大工具,开发过程中我们要 格外注意工具描述的定义,因为它会影响 AI 决定是否使用工具。

先在项目根包下新建 tools 包,将所有工具类放在该包下;并且工具的返回值尽量使用 String 类型,让结果的含义更加明确。

文件操作

文件操作工具主要提供 2 大功能:保存文件、读取文件。

由于会影响系统资源,所以我们需要将文件统一存放到一个隔离的目录进行存储,在 constant 包下新建文件常量类,约定文件保存目录为项目根目录下的 /tmp 目录中。

public interface FileConstant {

    /**
     * 文件保存目录
     */
    String FILE_SAVE_DIR = System.getProperty("user.dir") + "/tmp";
}

建议同时将这个目录添加到 .gitignore 文件中,避免提交隐私信息。

编写文件操作工具类,通过注解式定义工具,代码如下:

public class FileOperationTool {

    private final String FILE_DIR = FileConstant.FILE_SAVE_DIR + "/file";

    @Tool(description = "Read content from a file")
    public String readFile(@ToolParam(description = "Name of the file to read") String fileName) {
        String filePath = FILE_DIR + "/" + fileName;
        try {
            return FileUtil.readUtf8String(filePath);
        } catch (Exception e) {
            return "Error reading file: " + e.getMessage();
        }
    }

    @Tool(description = "Write content to a file")
    public String writeFile(
        @ToolParam(description = "Name of the file to write") String fileName,
        @ToolParam(description = "Content to write to the file") String content) {
        String filePath = FILE_DIR + "/" + fileName;
        try {
            // 创建目录
            FileUtil.mkdir(FILE_DIR);
            FileUtil.writeUtf8String(content, filePath);
            return "File written successfully to: " + filePath;
        } catch (Exception e) {
            return "Error writing to file: " + e.getMessage();
        }
    }
}

网页抓取

网页抓取工具的作用是根据网址解析到网页的内容,可使؜؜؜用 jsoup⁠ ⁠库⁠实现网页内‏容抓‏取和‏解析

1)首先给‌项目添‌加‏依赖:

<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.19.1</version>
</dependency>

2)编写网页抓取工具类:

public class WebScrapingTool {

    @Tool(description = "Scrape the content of a web page")
    public String scrapeWebPage(@ToolParam(description = "URL of the web page to scrape") String url) {
        try {
            Document doc = Jsoup.connect(url).get();
            return doc.html();
        } catch (IOException e) {
            return "Error scraping web page: " + e.getMessage();
        }
    }
}

资源下载

资源下载工具的作用是通过链接下载文件到本地,使用 Hutool 的 HttpUtil.downloadFile 实现。

资源下载工具类的代码如下:

public class ResourceDownloadTool {

    @Tool(description = "Download a resource from a given URL")
    public String downloadResource(@ToolParam(description = "URL of the resource to download") String url, @ToolParam(description = "Name of the file to save the downloaded resource") String fileName) {
        String fileDir = FileConstant.FILE_SAVE_DIR + "/download";
        String filePath = fileDir + "/" + fileName;
        try {
            // 创建目录
            FileUtil.mkdir(fileDir);
            // 使用 Hutool 的 downloadFile 方法下载资源
            HttpUtil.downloadFile(url, new File(filePath));
            return "Resource downloaded successfully to: " + filePath;
        } catch (Exception e) {
            return "Error downloading resource: " + e.getMessage();
        }
    }
}

集中注册

开发好了这么多工具类后,结合我们自己的需求,可以给 AI 一次性提供所有工具,让它自己决定何时调用。所以我们可以创建 工具注册类,方便统一管理和绑定所有工具。

代码如下:

@Configuration
public class ToolRegistration {

    @Bean
    public ToolCallback[] allTools() {

        FileOperationTool fileOperationTool = new FileOperationTool();
        WebScrapingTool webScrapingTool = new WebScrapingTool();
        ResourceDownloadTool resourceDownloadTool = new ResourceDownloadTool();

        return ToolCallbacks.from(
            fileOperationTool,
            webScrapingTool,
            resourceDownloadTool
        );
    }
}

有了这个注؜؜؜册类,如果需要⁠添⁠加⁠或移除工具‏,只‏需修‏改这一‌个类即‌可,更利‏‌于维护。

使用工具

在 Lov؜؜؜eApp 类中⁠添⁠加⁠工具调用的‏代码‏,通‏过 t‌ool‌s 方‌法‏绑定所有‏已注册的‏工具:

@Resource
private ToolCallback[] allTools;

public String doChatWithTools(String message, String chatId) {
    ChatResponse response = chatClient
            .prompt()
            .user(message)
            .advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
                    .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
            // 开启日志,便于观察效果
            .advisors(new MyLoggerAdvisor())
            .tools(allTools)
            .call()
            .chatResponse();
    String content = response.getResult().getOutput().getText();
    log.info("content: {}", content);
    return content;
}

五  指定工具开发

异常分析

异常分析工具的作用是智能解析苍穹外卖项目的报错日志、异常堆栈,自动定位 Bug 原因并给出修复方案,通过大模型实现专业异常诊断。

异常分析工具类的代码如下:

@Component
public class ExceptionDiagnoseTool {

    private final ChatClient chatClient;

    public ExceptionDiagnoseTool(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @Tool(description = "智能分析苍穹外卖项目的报错日志、异常堆栈、Bug 定位与修复方案")
    public String analyzeException(@ToolParam(description = "完整的异常堆栈或报错信息") String errorLog) {
        String prompt = """
                你是苍穹外卖项目专属技术专家。
                请分析以下报错日志:
                1. 明确异常原因
                2. 指出在项目中可能出现的模块和位置
                3. 提供可直接落地的解决方案

                报错内容:
                %s
                """.formatted(errorLog);

        return chatClient.prompt().user(prompt).call().content();
    }
}

代码解释

代码解释工具的作用是智能解析苍穹外卖项目中的代码片段,讲解代码功能、业务作用、核心逻辑,帮助快速理解项目代码。

代码解释工具类的代码如下:

@Component
public class CodeExplainTool {

    private final ChatClient chatClient;

    public CodeExplainTool(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @Tool(description = "解释苍穹外卖项目中的代码逻辑、功能作用、业务流程")
    public String explainCode(@ToolParam(description = "需要解释的代码片段") String code) {
        String prompt = """
                你是苍穹外卖项目专家,请详细解释这段代码:
                1. 代码功能
                2. 业务作用
                3. 关键逻辑说明

                代码内容:
                %s
                """.formatted(code);

        return chatClient.prompt().user(prompt).call().content();
    }
}

集中注册

开发好了两个工具类后,结合我们自己的需求,可以给 AI 一次性提供所有工具,让它自己决定何时调用。所以我们可以创建工具注册类,方便统一管理和绑定所有工具。

代码如下:

@Configuration
public class ToolRegistration {

    @Bean
    public ToolCallback[] allTools(
            ExceptionDiagnoseTool exceptionDiagnoseTool,
            CodeExplainTool codeExplainTool
    ) {
        return ToolCallbacks.from(
                exceptionDiagnoseTool,
                codeExplainTool
        );
    }
}

使用工具

在业务类中添加工具调用的代码,通过 tools 方法绑定所有已注册的工具:

@Resource
private ToolCallback[] allTools;

public String doChatWithTools(String message, String chatId) {
    ChatResponse response = chatClient
            .prompt()
            .user(message)
            .advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
                    .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
            .tools(allTools)
            .call()
            .chatResponse();
            
    String content = response.getResult().getOutput().getText();
    return content;
}

六  工具调用进阶

工具上下文

在实际应用中,工具执行可能需要额外的上下文信息,比如登录用户信息、会话 ID 或者其他环境参数。Spring AI 通过 ToolContext 提供了这一能力。如图:

null

我们可以在؜؜؜调用 AI 大模⁠型⁠时,传⁠递上下文‏‏参数。比如传递用‏‌户名为‌ purse_wind:

// 从已登录用户中获取用户名称
String loginUserName = getLoginUserName();
​
String response = chatClient
        .prompt("帮我查询用户信息")
        .tools(new CustomerTools())
        .toolContext(Map.of("userName", "purse_wind"))
        .call()
        .content();
​
System.out.println(response);

在工具中使؜؜؜用上下文参⁠数。比⁠如⁠从数据库中‏查询‏ purse_wind 的信‌息:

class CustomerTools {
​
    @Tool(description = "Retrieve customer information")
    Customer getCustomerInfo(Long id, ToolContext toolContext) {
        return customerRepository.findById(id, toolContext.getContext().get("userName"));
    }
​
}

看源码我们会发现,ToolContext 本质上就是一个 Map:

它可以携带任何与当前请求相关的信息,但这些信息 不会传递给 AI 模型,只在应用程序内部使用。这样做既增强了工具的安全性,也很灵活。适用于下面的场景:

  • 用户认证信息:可以在上下文中传递用户 token,而不暴露给模型

  • 请求追踪:在上下文中添加请求 ID,便于日志追踪和调试

  • 自定义配置:根据不同场景传递特定配置参数

举个应用例子,假如做了؜؜؜一个用户自助退款功能,如果已登录用户跟 AI⁠⁠⁠ 说:"我要退款",AI 就不需要再问用户 ‏‏‏"你是谁?",让用户自己输入退款信息了;而是‌‌‌直接从系统中读取到 userId,在工具调用‏‏‏时根据 userId 操作退款即可。

立即返回

有时候,工具执行的结果不需要再经过 AI 模型处理,而是希望直接返回给用户(比如生成 PDF 文档)。Spring AI 通过 returnDirect 属性支持这一功能,流程如图:

null

立即返回模式改变了工具调用的基本流程:

  1. 定义工具时,将 returnDirect 属性设为 true

  2. 当模型请求调用这个工具时,应用程序执行工具并获取结果

  3. 结果直接返回给调用者,不再 发送回模型进行进一步处理

这种模式很适合؜؜؜需要返回二进制数据(比如图⁠⁠⁠片 / 文件)的工具、返回‏‏‏大量数据而不需要 AI 解‌‌‌释的工具,以及产生明确结果‏‏‏的操作(如数据库操作)。

启用立即返؜؜؜回的方法非常简⁠单⁠,⁠使用注解方‏式时‏指定‏ re‌tur‌nDi‌r‏ect ‏参数:

class CustomerTools {
    @Tool(description = "Retrieve customer information", returnDirect = true)
    Customer getCustomerInfo(Long id) {
        return customerRepository.findById(id);
    }
}

使用编程方式时,手动构造 ToolMetadata 对象:

// 设置元数据包含 returnDirect 属性
ToolMetadata toolMetadata = ToolMetadata.builder()
    .returnDirect(true)
    .build();
​
Method method = ReflectionUtils.findMethod(CustomerTools.class, "getCustomerInfo", Long.class);
ToolCallback toolCallback = MethodToolCallback.builder()
    .toolDefinition(ToolDefinition.builder(method)
            .description("Retrieve customer information")
            .build())
    .toolMethod(method)
    .toolObject(new CustomerTools())
    .toolMetadata(toolMetadata)
    .build();

工具底层执行原理

Spring AI 提供了两种工具执行模式:框架控制的工具执行和用户控制的工具执行。这两种模式都离不开一个核心组件 ToolCallingManager

ToolCallingManager

ToolCallingManager 接口可以说是 Spring AI 工具调用中最值得学习的类了。它是 管理 AI 工具调用全过程 的核心组件,负责根据 AI 模型的响应执行对应的工具并返回执行结果给大模型。此外,它还支持异常处理,可以统一处理工具执行过程中的错误情况。

接口定义如图:

其中的 2؜؜ 个核心方法: ⁠ ⁠       ‏  ‏      ‌   ‌     ‏    ‏   

  1. resolveToolDefinitions:发起对话请求前,筛选并组装出 AI 模型能够识别的 "可用工具列表"

  2. executeToolCalls:执行模型请求对应的工具调用


ToolCallingManager 并不会主动判断是否要调用工具,而是完全由 AI 模型的返回结果决定:

1. 从 响应中提取工具调用:遍历 ChatResponse 里的 Generation,检查是否存在非空toolCalls列表:

Optional<Generation> toolCallGeneration = chatResponse
    .getResults().stream()
    .filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls()))
    .findFirst();

如果没有找到任何 toolCalls,就直接抛出异常:No tool call requested by the chat model,表示模型没有要求调用工具。

2. 执行工具并构造对话历史:若存在工具调用,则从 Generation 中取出 AssistantMessage,构建 ToolContext,调用 executeToolCall 执行具体工具逻辑,最后把工具执行结果包装成新的消息,追加到对话历史中,作为后续 AI 回答的上下文。

框架控制的工具执行

这是默认且最简单؜؜؜的模式,由 Spring AI⁠⁠⁠ 框架自动管理整个工具调用‏‏‏流程。所以我们刚刚开发时,基本没写‌‌‌几行非业务逻辑的代码,大多数活‏‏‏儿都交给框架负重前行了。

在这种模式下:

  • 框架自动检测模型是否请求调用工具

  • 自动执行工具调用并获取结果

  • 自动将结果发送回模型

  • 管理整个对话流程直到得到最终答案

如图:

null

上图中,我们会发现 ToolCallingManager 起到了关键作用,由框架使用默认初始化的 DefaultToolCallingManager 来自动管理整个工具调用流程,适合大多数简单场景。

用户控制的工具执行

对于需要更精细控制的复؜؜؜杂场景,Spring AI 提供了用户控制模⁠⁠⁠式,可以通过设置 ToolCallingCh‏‏‏atOptions 的 internalTo‌‌‌olExecutionEnabled 属性为‏‏‏ false 来禁用内部工具执行。

// 配置不自动执行工具
ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(ToolCallbacks.from(new WeatherTools()))
    .internalToolExecutionEnabled(false)  // 禁用内部工具执行
    .build();

然后我们就؜؜؜可以自己从 A⁠I⁠ ⁠的响应结果‏中提‏取工‏具调用‌列表,‌再依次‌执‏行了:

ChatModel chatModel = ...
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();

ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(new CustomerTools())
    .internalToolExecutionEnabled(false)
    .build();
Prompt prompt = new Prompt("Tell me more about the customer with ID 42", chatOptions);

ChatResponse chatResponse = chatModel.call(prompt);

while (chatResponse.hasToolCalls()) {
    ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(prompt, chatResponse);

    prompt = new Prompt(toolExecutionResult.conversationHistory(), chatOptions);

    chatResponse = chatModel.call(prompt);
}

System.out.println(chatResponse.getResult().getOutput().getText());

恭喜你学习完毕!✿

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐