以 Sprin‏g AI 框架为例,学习 A‏I 应用开发的核心特性 ——‏ 工具调用,大幅增强 AI ‏的能力,并实战主流工具的开发‌,熟悉工具的原理和高级特性。

重点理解

1.Tool Calling 的工作原理

    用户一次请求,大模型进行判断是否调用工具,后端进行一次或多次工具调用,将工具返回信息再次传送给大模型,模型整合信息进行输出。

2.Spring AI 实现工具调用的流程;

3.使用 @Tool和 @ToolParam注解标记类方法

定义工具

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";
    }
}

然后将工具类转换为‏ ToolCallback 工具定义‏类,之后就可以把这个类绑定给 Cha‏tClient,从而让 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();

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

在定义工具时,需要注‏意方法参数和返回值类型的选择。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 绑定工具(毕竟工具‏调用是 AI 大模型支持的能力),‌适合需要更精细控制的场景。

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

4. 动态解析:一般情况下,使用前面 3 种方式即可。对于更复杂的应用,Spring AI 还支持通过 ToolCallbackResolver 在运行时动态解析工具。这种方式特别适合工具需要根据上下文动态确定的场景,比如从数据库中根据工具名搜索要调用的工具。在本节的工具进阶知识中会讲到,先了解到有这种方式即可。

总结一下,在使用工具时,Spring AI 会自动处理工具调用的全过程:

  • 从 AI 模型决定调用工具 =>
  • 到后端执行工具方法 =>
  • 再到将结果返回给模型 =>
  • 最后模型基于工具结果生成最终回答。
  • 这整个过程对开发者来说是透明的,我们只需专注于 实现工具 的业务逻辑即可。

那么,怎么实现工具呢?

工具生态

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

虽然文档里只提到了屈指可数的插件数,但我们可以顺藤摸瓜,在 GitHub 社区找到官方提供的更多 工具源码,包含大量有用的工具!比如翻译工具、网页搜索工具、爬虫工具、地图工具等:

💡 这种搜‏集资源的能力,希望大家也‏能够掌握,尤其是学新技术‏的时候,即使官方文档写的‏不够清晰完善,我们也可以‌从开源社区中获取到一手信息。如果社区中没找到合‏适的工具,我们就要自主开发。需要注‏意的是,AI 自身能够实现的功能通‏常没必要定义为额外的工具,因为这会‏增加一次额外的交互,我们应该将工具‌用于 AI 无法直接完成的任务。

工具调用检测机制

1. 结构检测:AssistantMessage 与 ToolCall

当 AI 模型返回响应时,Spring AI 会将其封装在 AssistantMessage 对象中。这个类是检测工具调用的第一道关卡。

AssistantMessage.java:69-71

public boolean hasToolCalls() {
    return!CollectionUtils.isEmpty(this.toolCalls);
}

通过检查 toolCalls 列表是否为空,框架可以快速判断响应中是否包含了结构化的工具调用请求。每一个工具调用请求都被解析为一个 ToolCall 记录(Record):

AssistantMessage.java:103-105

public record ToolCall(String id, String type, String name, String arguments) {
}

这个记录清晰地定义了工具调用的所有要素:一个唯一的 id、类型(通常是 "function")、工具的 name 以及 JSON 格式的 arguments

2. 元数据检测:验证 finishReason

仅仅有 ToolCall 结构还不够。大语言模型在返回响应时,会附带一个 finishReason 元数据,用来说明生成停止的原因(例如,正常结束、达到长度限制,或是因为需要调用工具)。Spring AI 利用这个元数据作为第二重验证。

核心的检测逻辑位于 AbstractToolCallSupport 类中:

AbstractToolCallSupport.java:265-272

protected boolean isToolCall(Generation generation, Set<String> toolCallFinishReasons) {
    var finishReason = (generation.getMetadata().getFinishReason()!= null)
           ? generation.getMetadata().getFinishReason() : "";
    return generation.getOutput().hasToolCalls() && toolCallFinishReasons.stream()
           .map(s -> s.toLowerCase())
           .toList()
           .contains(finishReason.toLowerCase());
}

这段代码清晰地展示了双重验证:

  • generation.getOutput().hasToolCalls(): 进行结构检测,确认 AssistantMessage 中存在 ToolCall 对象。
  • toolCallFinishReasons.stream()...contains(finishReason.toLowerCase()): 进行元数据检测,确认模型的 finishReason 是 "tool_calls" 或 "function_call" 等预期的值。

只有当这两个条件同时满足时,Spring AI 才会确认这是一个有效的工具调用意图,后端才会真正调用工具。

3. 模型响应解析的核心:buildGeneration 方法

那么,AssistantMessage 中的 toolCalls 列表和 finishReason 是从何而来的呢?答案在于各个模型实现中的响应解析逻辑。以 OpenAI 为例,其核心转换逻辑如下:

private Generation buildGeneration(Choice choice, Map<String, Object> metadata, ChatCompletionRequest request) {
    List<AssistantMessage.ToolCall> toolCalls = choice.message().toolCalls() == null? List.of()
            : choice.message()
               .toolCalls()
               .stream()
               .map(toolCall -> new AssistantMessage.ToolCall(toolCall.id(), "function",
                        toolCall.function().name(), toolCall.function().arguments()))
               .toList();
 
    String finishReason = (choice.finishReason()!= null? choice.finishReason().name() : "");
    
    var generationMetadataBuilder = ChatGenerationMetadata.builder().finishReason(finishReason);
    
    //... 其他逻辑...
 
    var assistantMessage = new AssistantMessage(textContent, metadata, toolCalls, media);
    return new Generation(assistantMessage, generationMetadataBuilder.build());
}

这段代码是连接模型原始输出和 Spring AI 内部模型的桥梁。它明确地从模型的原始响应(Choice 对象)中提取 toolCalls 和 finishReason,并将它们构建成 Spring AI 的标准 Generation 和 AssistantMessage 对象。

toolCalls 和 finishReason 是实现 Agent 功能不可或缺的两个关键要素。任何主流的大模型如果想要融入 Spring AI 生态,就必须支持这两个特性,因为它们是模型从一个纯粹的文本生成器转变为一个能够执行动作的智能代理的基础。

工具执行机制

一旦检测到工具调用意图,执行的接力棒就交给了 DefaultToolCallingManager

DefaultToolCallingManager.java:195-233

for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
    String toolName = toolCall.name();
    String toolInputArguments = toolCall.arguments();
    
    // 查找匹配的工具回调
    FunctionCallback toolCallback = toolCallbacks.stream()
       .filter(tool -> toolName.equals(tool.getName()))
       .findFirst()
       .orElseGet(() -> toolCallbackResolver.resolve(toolName));
 
    // 执行工具并获取结果
    String toolResult = toolCallback.call(toolInputArguments, toolContext);
    
    toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolName, toolResult));
}

这个流程清晰地展示了从解析到执行的每一步:

  • 遍历工具调用:从 AssistantMessage 中获取所有 ToolCall 请求并逐一处理。
  • 解析参数:提取工具的名称和 JSON 格式的参数。
  • 解析与匹配:根据 toolName 在已注册的 FunctionCallback 列表中查找并解析出对应的工具实例。
  • 执行调用:调用 toolCallback.call() 方法,将参数传入,执行实际的 Java 函数。
  • 封装结果:将执行结果封装成 ToolResponseMessage,准备在下一轮对话中返回给模型

无缝集成:ChatClient 的角色

这一切复杂的内部机制,对于开发者而言,都被 DefaultChatClient 优雅地封装了起来。通过其内置的 advisor链,整个工具调用的检测、解析、执行和结果反馈流程被自动化处理。开发者只需简单地配置 ToolCallbackProvider,并在调用时使用 .tools() 或 .functions() 方法注册工具即可。

精心设计的责任分离

通过对源码的深入分析,我们可以看到 Spring AI 在工具调用功能上体现出的卓越设计思想:

  • 双重检测机制:结合结构化数据(toolCalls)和元数据(finishReason)进行双重验证,保证了意图识别的准确性和鲁棒性。
  • 责任分离:模型层(ChatModel)专注于将服务商的原始响应解析为统一的 ToolCall 对象,而 ChatClient和 ToolCallingManager 则负责工具的解析、执行和生命周期管理。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐