本章内容
- 用“工具”增强生成能力
- 声明式定义工具
- 将方法与函数暴露为工具
- 提供工具上下文
上次你去看医生是什么时候?当时你有没有想过,医生在评估你的健康状况时掌握的是什么类型的信息?
医生要经过大量训练才能获得行医执照。训练中他们学习医学与生理学的基础。但新的研究不断出现,患者也会呈现各种不常见的状况,医生需要查阅最新研究并与其他医护人员沟通,才能给出最佳治疗方案。即便如此,再多的训练和研究也无法告诉医生你此刻的感受或你的生命体征。要了解你的具体情况,医生必须用体温计、血压计、听诊器等工具实时测量。
从这个角度看,和 LLM 的集成也很相似。LLM 通过海量数据训练,能回答很多问题。若要让 LLM 掌握其训练语料之外的信息,我们可以像第 4 章那样使用 RAG。但有些东西 LLM 无法仅靠 RAG 获得:比如当前天气、股价、比赛比分、主题公园项目的实时排队时间等。这时就需要工具(Tools) 。
本章将展示如何使用工具为 LLM 提供实时信息,并在需要时让 LLM“采取行动”。
为理解如何应用工具,我们先用 OpenAI 的 GPT-4o 做一个很简单的示例,演示 Spring AI 如何支持工具,以及工具如何弥补 LLM 的部分局限。随后把这些做法迁移到 Board Game Buddy 中,让 LLM 在问答时按需获取实时数据。
6.1 开始使用 AI 工具
和 RAG 很像,工具也是让 LLM 回答其训练之外数据的方式。不同的是:
- RAG 通过文档把信息提供给 LLM;
- 工具通过应用逻辑把数据提供给 LLM,甚至还能执行动作(如更新数据、调用外部 API)。这等于让 LLM 不只“回答”,还能“做事”。
并非所有 LLM 都支持工具,但 Spring AI 让在支持工具的模型上启用工具调用变得很容易。许多受支持的提供方/模型都支持工具调用,包括:
- Amazon Bedrock Converse
- Anthropic(Claude)
- Azure OpenAI
- Google Gemini
- Groq
- Mistral
- MiniMax
- Moonshot
- NVIDIA(OpenAI-Proxy)
- Ollama(多种模型)
- OpenAI
- 智谱(ZhiPu)
尽管各家 API 对“工具”的实现细节不同,Spring AI 在提交提示词时提供了一致的工具用法接口。下面我们来实现一个基于工具的 Spring AI 应用。
6.1.1 开发一个启用工具的应用
我们来创建一个全新的 Spring AI 应用,回答全球任意城市的当前时间。先用 Spring Initializr(网页或 IDE)创建项目,包含 spring-boot-starter-web 和 Spring AI OpenAI 启动器,就像第 1 章搭建 Board Game Buddy 时那样。项目名随意,以下示例假设为 simple-tools。
创建好后,在 src/main/resources/application.yml 中配置 OpenAI API Key:
spring.ai.openai.api-key: ${OPENAI_API_KEY}
然后创建一个很简单的控制器,向 LLM 询问某城市的当前时间:
代码清单 6.1 向 LLM 询问给定城市的当前时间
package com.example.simpletools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GetTimeController {
private static final String CURRENT_TIME_TEMPLATE =
"What is the current time in {city}?"; // #1
private final ChatClient chatClient;
public GetTimeController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
@GetMapping(path="/time", params = "city")
public String getTime(@RequestParam("city") String city) {
return chatClient.prompt()
.user(userSpec -> {
userSpec
.text(CURRENT_TIME_TEMPLATE)
.param("city", city); // #2
})
.call()
.content();
}
}
如果你完成了前面章节,这段控制器代码应该很熟悉:/time 接收 city 参数,把它填入用户消息模板,然后返回模型响应的文本内容。
启动应用后试试,比如请求美国新墨西哥州的小镇 Jal:
$ http :8080/time?city=Jal+New+Mexico -b
I'm unable to provide real-time information, including the current time, ...
LLM 理解了问题,也给出了一些建议,但无法提供实时信息——这是 LLM 的天然限制。正是工具能解决的问题:我们定义一个工具来返回当前时间,并在提交问题时把这个工具提供给 LLM。为此,新建一个类,编写获取指定时区当前时间的方法,并让 ChatClient 允许 LLM 调用它:
代码清单 6.2 用 Java 方法定义一个工具
package com.example.simpletools;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneId;
@Component
public class TimeTools {
private static final Logger LOGGER =
LoggerFactory.getLogger(TimeTools.class);
@Tool(name = "getCurrentTime",
description = "Get the current time in the specified time zone.") // #1
public String getCurrentTime(String timeZone) {
LOGGER.info("Getting the current time in {}", timeZone);
var now = LocalDateTime.now(ZoneId.of(timeZone));
return now.toString();
}
}
getCurrentTime() 用 LocalDateTime.now(ZoneId.of(...)) 取指定时区当前时间并返回字符串。关键在于方法上的 @Tool 注解:这会把该方法暴露为可被 LLM 调用的工具,并通过 name 与 description 向 LLM 描述用途。一个类里可以有多个 @Tool 方法,但名称必须唯一(若未指定 name,默认用方法名)。
接下来,把该工具注入控制器,并在构建 ChatClient 时通过 defaultTools() 提供给它:
代码清单 6.3 发送提示词时启用工具
package com.example.simpletools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GetTimeController {
private static final String CURRENT_TIME_TEMPLATE =
"What is the current time in {city}?";
private final ChatClient chatClient;
public GetTimeController(ChatClient.Builder chatClientBuilder,
TimeTools timeTools) {
this.chatClient = chatClientBuilder
.defaultTools(timeTools)
.build();
}
@GetMapping(path="/time", params = "city")
public String getTime(@RequestParam("city") String city) {
return chatClient.prompt()
.user(userSpec -> {
userSpec
.text(CURRENT_TIME_TEMPLATE)
.param("city", city);
})
.call()
.content();
}
}
再次请求:
$ http :8080/time?city=Jal+New+Mexico -b
The current time in Jal, New Mexico is 9:19 PM on February 19, 2025.
这次模型给出了精确的当前时间。成功!
6.1.2 更深入地理解(Digging deeper)
也许你会好奇:Spring AI 是否把你定义的工具暴露成了一个供 OpenAI 调用的接口?看起来好像 LLM 能直接调用你的工具,但事实并非如此。
当你提出一个涉及“工具”的问题时,你的应用与模型之间会进行一段多轮对话。图 6.1 展示了这段对话中的每一步。
图 6.1 工具调用是应用与 LLM 之间的多段式对话
对话从用户消息(初始问题)开始,就像发送任何提示词一样。下面是发送给 OpenAI 的 GPT-4o 模型的请求 JSON 示例:
{
"messages": [
{
"content": "What is the current time in Jal New Mexico?",
"role": "user"
}
],
"model": "gpt-4o-mini",
"stream": false,
"temperature": 0.7,
"tools": [
{
"type": "function",
"function": {
"description": "Get the current time in the specified time zone.",
"name": "getCurrentTime",
"parameters": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"type": "object",
"properties": {
"timeZone": {
"type": "string"
}
},
"required": [
"timeZone"
]
}
}
}
]
}
这个请求的结构适用于 OpenAI 的 API;若你使用其他提供商,结构可能不同。但与“工具”交互的步骤在不同模型之间基本一致。
在这第一次请求中,只有一条携带问题的用户消息;同时,tools 属性列出了可供 LLM 使用的工具数组。此处仅有一个工具 getCurrentTime:description 描述工具能力,parameters 用 JSON Schema 说明输入参数格式。
模型对该请求的响应大致如下:
{
"id": "chatcmpl-B2sJlOCkIK1PDnJolKLdri3TtVAzo",
"object": "chat.completion",
"created": 1740025153,
"model": "gpt-4o-mini-2024-07-18",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_n64P6pcemoLpheNqMaXRNve8",
"type": "function",
"function": {
"name": "getCurrentTime",
"arguments": "{"timeZone":"America/Denver"}"
}
}
],
"refusal": null
},
"logprobs": null,
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 59,
"completion_tokens": 19,
"total_tokens": 78,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_13eed4fce1"
}
首先注意:此响应并未直接给出答案。LLM 需要帮助,于是返回一条来自“assistant”的消息,请求调用 getCurrentTime 工具。虽然 LLM 不能直接得知 Jal(新墨西哥州)当前时间,但它知道 getCurrentTime 可以做到,且需要一个时区参数。它也知道 Jal 处于 America/Denver 时区,于是请求以该参数调用工具。
注意 finish_reason 为 tool_calls。通常该字段为 stop,表示模型已完成回答;而 tool_calls 则表示对话尚未结束,需要应用代表它调用工具。
好消息是:虽然 LLM 请求应用调用工具,但你无需手写这段调用逻辑。Spring AI 会在幕后完成:
- 调用
getCurrentTime; - 把工具结果装入新请求;
- 将该新请求再次发送给模型进行生成。
这个“第二次”请求大致如下:
{
"messages": [
{
"content": "What is the current time in Jal New Mexico?",
"role": "user"
},
{
"role": "assistant",
"tool_calls": [
{
"id": "call_n64P6pcemoLpheNqMaXRNve8",
"type": "function",
"function": {
"name": "getCurrentTime",
"arguments": "{"timeZone":"America/Denver"}"
}
}
]
},
{
"content": ""2025-02-19T21:19:13.963458"",
"role": "tool",
"name": "getCurrentTime",
"tool_call_id": "call_n64P6pcemoLpheNqMaXRNve8"
}
],
"model": "gpt-4o-mini",
"stream": false,
"temperature": 0.7,
"tools": [
{
"type": "function",
"function": {
"description": "Get the current time in the specified time zone.",
"name": "getCurrentTime",
"parameters": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"type": "object",
"properties": {
"timeZone": {
"type": "string"
}
},
"required": [
"timeZone"
]
}
}
}
]
}
你会看到,它与初始请求类似,仍包含用户问题和工具定义。但现在还多了两条消息:
- 一条来自 assistant,表示工具调用的意图;
- 一条
role: "tool"的消息,携带工具的返回时间。
这三条消息构成了当前对话历史,其中最新的工具消息为 LLM 提供了生成答案所需的关键信息。
接下来,模型基于这个上下文返回最终答案:
{
"id": "chatcmpl-B2sJmwWTsSANYtQfdhitIc1P8L2Rv",
"object": "chat.completion",
"created": 1740025154,
"model": "gpt-4o-mini-2024-07-18",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "The current time in Jal, New Mexico is 9:19 PM
on February 19, 2025.",
"refusal": null
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 103,
"completion_tokens": 25,
"total_tokens": 128,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_13eed4fce1"
}
就是这样!最终响应中的 assistant 消息给出了答案。此时 finish_reason 为 stop,表示对话已经完成。
以上就是在 Spring AI 中使用工具的核心要点。接下来,我们会把这些做法应用到 Board Game Buddy,并探索在 Spring AI 中使用工具的更多方式。
6.2 实现工具(Implementing tools)
在 Board Game Buddy 应用里,除了回答各种游戏规则相关的问题,我们还希望 API 允许用户反馈某个游戏的复杂度。复杂度用 1–5 的等级来表示,大致分级如下:
1—简单
2—较易
3—中等
4—较难
5—困难
按设想,每个游戏的复杂度评分由 Board Game Buddy 的用户提供,但最终存储在数据库中。因此,这正适合作为工具提供给 LLM 使用的数据。
有了“游戏复杂度”这个设定,接下来我们把它融入 Board Game Buddy 应用,并最终支持回答“某游戏有多难”之类的问题。但在定义供生成式 AI 使用的工具之前,你需要先构建从数据库读取复杂度信息的基础代码。
6.2.1 编写工具的基础设施
游戏数据(例如复杂度)会保存在关系型数据库中。在 Spring 里处理关系数据,Spring Data 是最省心的做法。更具体地,可以使用 Spring Data JDBC,在构建文件中加入依赖:
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
你还需要准备一个数据库并在构建中加入数据库驱动。几乎任何关系型数据库都可以;这里先用 H2 内存数据库:
runtimeOnly 'com.h2database:h2'
这个依赖不仅会触发 Spring Boot 自动配置创建 Spring Data JDBC 所需的 DataSource,还会直接创建 H2 数据库。你只需要在 src/main/resources 下定义 schema.sql 来建表:
create table Game (
id identity,
title varchar(255) not null,
slug varchar(255) not null,
complexity float not null
);
Game 表当前比较简单,专注于存放复杂度所需的核心数据,字段包括:
- id——数据库主键;
- title——游戏标题;
- slug——标题的规范化形式(全小写、下划线分隔),与第 4 章中为文档块关联游戏所用的 slug 一致;
- complexity——游戏复杂度,1 到 5 的实数值。
按设想,这张表会由应用中其他组件在用户提交复杂度评价时写入。但为了演示,我们先用初始化 SQL 插入几条测试数据。在 src/main/resources 目录下创建 data.sql:
insert into Game (title, slug, complexity)
values ('Burger Battle', 'burger_battle', 1.33);
insert into Game (title, slug, complexity)
values ('Azul', 'azul', 1.76);
insert into Game (title, slug, complexity)
values ('Carcassonne', 'carcassonne', 1.89);
insert into Game (title, slug, complexity)
values ('Catan', 'catan', 2.29);
insert into Game (title, slug, complexity)
values ('Scythe', 'scythe', 3.44);
insert into Game (title, slug, complexity)
values ('Puerto Rico', 'puerto_rico', 3.27);
insert into Game (title, slug, complexity)
values ('7 Wonders', '7_wonders', 2.32);
接下来编写真正供工具使用的 Java 代码,首先需要一个承载游戏数据的 Java 类型。如下这个 Game 记录类型就可以:
package com.example.boardgamebuddy.gamedata;
import org.springframework.data.annotation.Id;
public record Game(
@Id Long id,
String slug,
String title,
float complexity) {
public GameComplexity complexityEnum() {
int rounded = Math.round(complexity);
return GameComplexity.values()[rounded];
}
}
除了与 Game 表一一对应的属性外,Game 记录还提供了 complexityEnum() 方法,用来把数值型复杂度转换为 GameComplexity 枚举值。GameComplexity 枚举如下:
package com.example.boardgamebuddy.gamedata;
public enum GameComplexity {
UNKNOWN(0),
EASY(1),
MODERATELY_EASY(2),
MODERATE(3),
MODERATELY_DIFFICULT(4),
DIFFICULT(5);
private final int value;
GameComplexity(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
然后需要一个仓库接口来从数据库读取游戏数据。下面这个 GameRepository 指定了通过 slug 查找游戏数据的 findBySlug() 方法:
package com.example.boardgamebuddy.gamedata;
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;
public interface GameRepository extends CrudRepository<Game, Long> {
Optional<Game> findBySlug(String slug);
}
GameRepository 还继承了 Spring Data 的 CrudRepository,从而自动获得多种读写方法。更重要的是,继承 CrudRepository 会触发 Spring Data 在运行时为仓库接口自动生成实现,你无需自己编写实现类。到这里,读取游戏复杂度数据所需的底层设施已经齐备,接下来就可以创建工具,让 AskController 能回答游戏复杂度相关的问题了。
6.2.2 定义工具(Defining the tool)
在清单 6.2 里,你在一个组件类中用 @Tool 注解了 getCurrentTime() 方法来定义工具。下面的清单展示了如何用同样的技巧为“游戏复杂度”创建一个新的 GameTools 组件。
清单 6.4 声明一个“游戏复杂度”工具
@Bean
package com.example.boardgamebuddy;
import com.example.boardgamebuddy.gamedata.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class GameTools {
private final GameRepository gameRepository;
public GameTools(GameRepository gameRepository) {
this.gameRepository = gameRepository;
}
private static final Logger LOGGER =
LoggerFactory.getLogger(GameTools.class);
@Tool(name = "getGameComplexity",
description = "Returns a game's complexity/difficulty " +
"given the game's title/name.")
public GameComplexityResponse getGameComplexity(
@ToolParam(description="The title of the game")
String gameTitle) {
var gameSlug = gameTitle
.toLowerCase()
.replace(" ", "_");
LOGGER.info("Getting complexity for {} ({})",
gameTitle, gameSlug);
var gameOpt = gameRepository.findBySlug(gameSlug);
var game = gameOpt.orElseGet(() -> {
LOGGER.warn("Game not found: {}", gameSlug);
return new Game(
null,
gameSlug,
gameTitle,
GameComplexity.UNKNOWN.getValue());
});
return new GameComplexityResponse(
game.title(), game.complexityEnum());
}
}
这里,getGameComplexity() 方法与之前的 getCurrentTime() 类似,用 @Tool 进行标注。description 属性给出说明,便于 LLM 判断何时可用该工具来回答问题。方法内部通过注入的 GameRepository 查询游戏数据;若未找到,创建一个占位的 Game 实例以表示复杂度未知。
该工具方法以 String 接收输入(游戏标题)。注意参数 gameTitle 用了 @ToolParam 注解,提供参数说明,帮助 LLM 理解该参数的用途,从而在请求调用工具时正确传参。
查询到复杂度后,工具返回 GameComplexityResponse(携带游戏标题和 GameComplexity 枚举):
package com.example.boardgamebuddy.gamedata;
public record GameComplexityResponse(
String title, GameComplexity complexity) {
}
剩下的工作,就是把这个工具纳入发送给 LLM 的提示中。下面就来完成它。
6.2.3 让工具投入使用(Putting the tool to work)
向 ChatClient 提供工具有两种方式:
- 在构建
ChatClient时统一提供:将GameTools注入到AiConfig的chatClient()方法里,并在创建ChatClient时传给defaultTools():
@Bean
ChatClient chatClient(ChatClient.Builder chatClientBuilder,
VectorStore vectorStore,
GameTools gameTools) {
return chatClientBuilder
.defaultAdvisors(
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().build()).build(),
MessageChatMemoryAdvisor.builder(
MessageWindowChatMemory.builder().build()).build())
.defaultTools(gameTools)
.build();
}
这样做能保证:来自该 ChatClient 的所有请求,都会自动携带 GameTools 中所有被 @Tool 注解的方法。
- 在构建单次提示时按需提供:把
GameTools注入到AskController(或服务类)中,在askQuestion()里通过tools()传入:
@Service
public class SpringAiBoardGameService implements BoardGameService {
private final ChatClient chatClient;
private final GameTools gameTools;
public SpringAiBoardGameService(ChatClient chatClient,
GameTools gameTools) {
this.chatClient = chatClient;
this.gameTools = gameTools;
}
// ...
@Override
public Answer askQuestion(Question question, String conversationId) {
// ...
return chatClient.prompt()
.user(question.question())
.tools(gameTools)
// ...
.call()
.entity(Answer.class);
}
}
无论选择在构建时全局注册还是在提示时按需注册,现在都可以启动应用进行体验了。针对样例数据,尝试询问几个游戏的复杂度,可能会得到如下结果:
$ http :8080/ask question="What is the complexity?" \
gameTitle="Puerto Rico" -b
{
"answer": "The complexity of the game is moderate.",
"game": "Puerto Rico"
}
$ http :8080/ask question="How complex is the game?" \
gameTitle="Burger Battle" -b
{
"answer": "The complexity of the game is easy.",
"game": "Burger Battle"
}
$ http :8080/ask question="What is the difficulty?" gameTitle="Azul" -b
{
"answer": "The complexity of the game is moderately easy.",
"game": "Azul"
}
另外,由于在工具里加了 LOGGER.info(),你可以通过日志确认工具确实被调用了,而不是 LLM“编的”。
到这里,Board Game Buddy 既能回答游戏规则问题,也能回答“这个游戏有多难”。在结束本章前,我们再看一种在提示上下文中提供工具的方式。
6.3 将函数用作工具(Enables functions as tools)
Spring AI 在早期里程碑版本中把“tools(工具)”称为“functions(函数)”,后来为了与多数 AI API 的术语保持一致,改用了“工具”。尽管名称改变了,但用“函数”来理解工具依然很贴切:就像 Java 的 Function 接口一样,工具通常接收输入→执行业务→返回结果;当没有输入或输出时,也可以分别映射到 Supplier 与 Consumer。
除了用带 @Tool 注解的 Bean 方法来声明工具,Spring AI 还支持把工具实现为 Function / Supplier / Consumer。下面演示如何把 GameTools 及其 getGameComplexity() 重写为实现 Java Function 的形式。
清单 6.5 以 Java Function 方式定义工具
package com.example.boardgamebuddy.gamedata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Description;
import org.springframework.stereotype.Component;
import java.util.Optional;
import java.util.function.Function;
@Component
@Description("Fetches the complexity of a game.")
public class GameTools
implements Function<GameComplexityRequest, GameComplexityResponse> {
public static final Logger LOGGER =
LoggerFactory.getLogger(GameTools.class);
private final GameRepository gameRepository;
public GameTools(GameRepository gameRepository) {
this.gameRepository = gameRepository;
}
@Override
public GameComplexityResponse apply(
GameComplexityRequest gameDataRequest) {
String gameSlug = gameDataRequest.title()
.toLowerCase()
.replace(" ", "_");
LOGGER.info("Getting complexity for {} ({})",
gameDataRequest.title(), gameSlug);
Optional<Game> gameOpt = gameRepository.findBySlug(gameSlug);
Game game = gameOpt.orElseGet(() -> {
LOGGER.warn("Game not found: {}", gameSlug);
return new Game(
null,
gameSlug,
gameDataRequest.title(),
GameComplexity.UNKNOWN.getValue());
});
return new GameComplexityResponse(
game.title(), game.complexityEnum());
}
}
这个版本的大体逻辑与清单 6.4 相同,但 GameTools 现在实现了 Function,核心逻辑写在 apply()(Function 唯一必须实现的方法)中。类上用 @Description 说明工具用途,并用 @Component 让 Spring 创建 Bean。
与先前的 getGameComplexity() 一样,apply() 返回 GameComplexityResponse。不同的是:Function 形式的工具不能直接接收 String 或其他 Java 原生类型,因此需要用一个自定义类型来包装请求参数——这里是 GameComplexityRequest:
package com.example.boardgamebuddy.gamedata;
import org.springframework.context.annotation.Description;
@Description("Request data about a game, given the game title.")
public record GameComplexityRequest(String title) {
}
这里对 GameComplexityRequest 使用了 @Description,与清单 6.4 中 @ToolParam.description 的作用相同:为模型提供参数语义说明。
剩下要做的就是把该工具注册到 ChatClient。但与传入 defaultTools() / tools() 的实例不同,Function 形式的工具要通过Bean 名称注册到 defaultToolNames() 或 toolNames()。由于 GameTools 被 @Component 标注,默认 Bean 名称是类名首字母小写:gameTools。因此可以在创建 ChatClient 时这样注册:
@Bean
ChatClient chatClient(ChatClient.Builder chatClientBuilder,
VectorStore vectorStore) {
return chatClientBuilder
.defaultAdvisors(
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().build()).build(),
MessageChatMemoryAdvisor.builder(
MessageWindowChatMemory.builder().build()).build())
.defaultToolNames("gameTools")
.build();
}
如果希望在构建单次提示时再指定工具,可以用 toolNames():
@Override
public Answer askQuestion(Question question, String conversationId) {
return chatClient.prompt()
...
.toolNames("gameTools")
.call()
.entity(Answer.class);
}
无论采用哪种方式,现在都可以启动应用,使用这个基于 Function 的工具来询问游戏复杂度。
小结
- 通过“工具”,LLM 可以基于实时提供的数据来回答问题。
- Spring AI 会在发送提示时附带工具的描述与参数模式。
- LLM 不会“直接调用”工具;它会请求应用去调用,然后应用将工具结果作为后续提示的上下文再发回给 LLM。
- Spring AI 为不同模型与 API 提供了一致的工具调用编程范式。
- 工具既可以是带
@Tool注解的方法,也可以是Function/Supplier/Consumer的实现。