AI Agent 的工具箱:深入理解 Tool Use 与 Spring AI Function Calling 实战

前言

我们之前聊过 AI Agent 的 ReAct 模式(”想一步做一步”)和 AI Agent 的记忆系统(”短期记忆 + 长期记忆”)。今天补齐 Agent 核心能力的最后一块拼图——Tool Use(工具使用)

你可以把 AI Agent 想象成一个刚入职的新员工:

  • ReAct 是他的思维方式——遇到问题先想(Reasoning),再动手(Acting)
  • Memory 是他的笔记本——记着之前干过什么、客户说了什么
  • Tool Use 是他桌上的各种工具——计算器、搜索引擎、数据库客户端

没有工具的 Agent,就像一个脑子很好但手被绑住的人,只能”纸上谈兵”。有了 Tool Use,Agent 才能真正和外部世界交互,完成实际任务。

什么是 Tool Use?

一句话解释:Tool Use 就是让大语言模型(LLM)决定”什么时候该调用哪个外部函数”,并把正确的参数传给它。

传统调用方式:

1
用户输入 → 程序硬编码逻辑 → 调用函数 → 返回结果

Tool Use 调用方式:

1
用户输入 → LLM 分析意图 → 决定调用哪个工具 + 传什么参数 → 程序执行函数 → 结果回传给 LLM → 生成最终回答

关键区别:传统方式是程序员提前写死所有分支,Tool Use 是让 LLM 在运行时动态决策。

一个生活化的例子

你问 Agent:”帮我查一下北京明天的天气,如果会下雨,就在我的待办里加一条’带伞’。”

没有 Tool Use 的 LLM 会回答:”抱歉,我无法访问天气数据……”

有了 Tool Use,Agent 的思考过程是这样的:

  1. (Reasoning):用户需要查天气,我应该调用 getWeather 工具
  2. (Acting):调用 getWeather(city="北京"),获得结果
  3. :明天会下雨,用户要加待办,调用 addTodo
  4. :调用 addTodo(content="带伞")
  5. 回复:北京明天有雨,已经帮你加了待办”带伞”

注意这个过程中,是 LLM 自己决定调什么工具、传什么参数,而不是你写 if-else 去判断。

Tool Use 的核心机制

1. 工具定义(Tool Definition)

首先,你需要告诉 LLM 有哪些工具可用。每个工具需要定义:

  • 名称(name):工具叫什么
  • 描述(description):工具是干什么的
  • 参数(parameters):需要传什么参数、类型是什么
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "getWeather",
"description": "查询指定城市的天气预报",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如'北京'"
},
"date": {
"type": "string",
"description": "日期,格式 yyyy-MM-dd,默认明天"
}
},
"required": ["city"]
}
}

重点:描述写得好不好,直接影响 LLM 能不能正确选择工具。这就像给新员工写操作手册,越清晰越好。

2. 工具选择(Tool Selection)

LLM 收到用户消息后,结合工具描述,决定是否需要调用工具。这里有三种情况:

  • 不需要工具:用户问”1+1等于几”,LLM 直接回答
  • 需要一个工具:用户问”北京天气怎样”,调用 getWeather
  • 需要多个工具:用户问”北京和上海明天天气分别怎样”,调用两次 getWeather

3. 工具执行与结果回传

LLM 只负责”决定调用”,实际执行是你的代码在做。执行完后,结果会回传给 LLM,由 LLM 生成最终的自然语言回答。

这个设计很关键——LLM 永远不会直接执行代码,它只是一个”调度员”。

Spring AI 实战:用 Java 实现 Function Calling

Spring AI 是 Spring 生态对 AI 的拥抱,它让 Java 开发者可以用最熟悉的方式接入 LLM。下面用一个完整例子演示如何实现 Tool Use。

环境准备

使用 Spring Boot 3.x + Spring AI:

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
</dependencies>

配置文件 application.yml

1
2
3
4
5
6
7
8
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: https://api.openai.com
chat:
options:
model: gpt-4o

第一步:定义工具

在 Spring AI 中,定义工具非常简单——写一个普通的 Java 方法,加上 @Tool 注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

@Component
public class WeatherTools {

@Tool(description = "查询指定城市的天气预报,返回天气状况和温度")
public String getWeather(
@ToolParam(description = "城市名称") String city,
@ToolParam(description = "日期,格式 yyyy-MM-dd") String date) {
// 实际项目中这里调用天气 API
// 这里用模拟数据演示
return String.format("%s %s:晴,温度 22-30°C,适合出行", city, date);
}

@Tool(description = "创建一条待办事项")
public String addTodo(
@ToolParam(description = "待办内容") String content,
@ToolParam(description = "优先级:高/中/低") String priority) {
// 实际项目中保存到数据库
System.out.println("创建待办:" + content + ",优先级:" + priority);
return "待办已创建:" + content;
}
}

注意 @Tool 注解的 description 非常重要——它就是告诉 LLM “这个工具是干嘛的”。写得越准确,LLM 调用得越精准。

第二步:注册工具并调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.stereotype.Service;

@Service
public class AgentService {

private final ChatClient chatClient;

public AgentService(ChatModel chatModel, WeatherTools weatherTools) {
this.chatClient = ChatClient.builder(chatModel)
.defaultTools(weatherTools) // 注册工具
.defaultSystem("你是一个贴心的生活助手,可以帮助用户查询天气、管理待办。")
.build();
}

public String chat(String userMessage) {
return this.chatClient.prompt()
.user(userMessage)
.call()
.content();
}
}

就这么多代码。Spring AI 会自动:

  1. @Tool 方法转成 OpenAI 的 Function Calling 格式
  2. 发送给 LLM 时带上工具描述
  3. 收到 LLM 的工具调用请求后,反射调用对应方法
  4. 把结果回传给 LLM,拿到最终回答

第三步:测试运行

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class AgentController {

@Autowired
private AgentService agentService;

@GetMapping("/chat")
public String chat(@RequestParam String message) {
return agentService.chat(message);
}
}

调用 GET /chat?message=帮我查北京明天天气,如果下雨就加个带伞的待办,你会看到 Agent 自动完成多步调用:

1
2
3
4
5
用户:帮我查北京明天天气,如果下雨就加个带伞的待办

→ Agent 调用 getWeather("北京", "2026-06-15")
→ Agent 调用 addTodo("带伞", "高")
→ Agent 回复:北京明天有雨,已帮你创建待办"带伞",优先级高。

进阶:用 ToolCallback 实现更灵活的工具注册

除了 @Tool 注解,Spring AI 还支持用 ToolCallback 方式动态注册工具,适合工具列表需要从配置或数据库加载的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.DefaultToolDefinition;
import org.springframework.ai.tool.metadata.ToolMetadata;

public class DynamicToolExample {

public ToolCallback createSearchTool() {
return ToolCallback.builder()
.toolDefinition(DefaultToolDefinition.builder()
.name("webSearch")
.description("搜索互联网获取最新信息")
.inputSchema("""
{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词"
}
},
"required": ["query"]
}
""")
.build())
.toolMethod((input) -> {
String query = input.get("query").toString();
return "搜索结果: " + query + " 的相关内容...";
})
.metadata(ToolMetadata.builder().returnDirect(false).build())
.build();
}
}

这种方式适合需要在运行时动态加载工具的场景,比如插件系统。

Tool Use 的几个关键设计原则

1. 工具描述要像写 API 文档

LLM 靠描述来理解工具。糟糕的描述会导致调用错误。

反面教材

1
@Tool(description = "查天气")

正面教材

1
@Tool(description = "查询指定城市的天气预报,需要提供城市名称和日期,返回天气状况、温度范围和出行建议")

2. 参数名要有语义

反面教材:参数名 cd——LLM 和人都看不懂

正面教材:参数名 citydate——一目了然

3. 工具数量要适度

理论上你可以注册几十个工具,但实际使用中,工具越多,LLM 选错的概率越大。建议单次对话不超过 10 个工具。如果工具很多,考虑用”路由”模式——先让 LLM 选类别,再选具体工具。

4. 工具执行要处理异常

工具执行可能失败(网络超时、参数错误等),记得 catch 异常并返回有意义的错误信息,而不是让整个调用链崩溃:

1
2
3
4
5
6
7
8
9
@Tool(description = "查询股票价格")
public String getStockPrice(@ToolParam(description = "股票代码") String code) {
try {
double price = stockService.fetchPrice(code);
return String.format("当前价格:%.2f 元", price);
} catch (Exception e) {
return "查询失败:" + e.getMessage() + ",请检查股票代码是否正确";
}
}

工具编排:从单工具到多步调用

现实中,一个复杂任务往往需要调用多个工具,甚至需要根据前一个工具的结果决定下一步调用什么。这就是 工具编排(Tool Orchestration)

Spring AI 默认支持多轮工具调用(multi-turn),你不需要手动编排——LLM 会根据上下文自动决定下一步该调什么。

但你需要注意两个问题:

1. 循环调用:LLM 有时会陷入循环,反复调用同一个工具。Spring AI 默认限制了最大调用轮数(默认 10 轮),你可以调整:

1
2
3
4
5
6
spring:
ai:
openai:
chat:
options:
tool-choice: auto # auto / required / none

2. Token 消耗:每次工具调用的结果都会占用上下文窗口。工具返回的数据要尽量精简,别把整个数据库表都丢给 LLM。

与之前知识的联系

现在把三篇 Agent 核心文章串起来看:

核心能力 解决的问题 对应文章
ReAct Agent 如何思考和行动 ReAct 模式
Memory Agent 如何记住上下文 记忆系统
Tool Use Agent 如何使用外部工具 本文

三者缺一不可:

  • 没有 ReAct,Agent 不知道该先做什么后做什么
  • 没有 Memory,Agent 每次都像失忆一样从头开始
  • 没有 Tool Use,Agent 只能”嘴上说说”,不能真正行动

小结

Tool Use 是让 AI Agent 从”聊天机器人”进化为”智能助手”的关键一步。Spring AI 让 Java 开发者可以用最低的成本实现这个能力——写个 @Tool 注解的方法,注册到 ChatClient,剩下的交给框架。

记住三个要点:

  1. 工具描述是灵魂——写清楚,LLM 才能用对
  2. LLM 是调度员——它只决定”调什么”,你的代码负责”怎么做”
  3. 异常处理不能忘——工具调用会失败,优雅地处理它

下一篇我们会聊聊 Agent 的另一个重要话题——Planning(规划),看看 Agent 如何把复杂任务拆解成可执行的步骤。敬请期待!