字数 3832,阅读大约需 16 分钟
Spring AI 工具调用:让 AI 模型具备行动能力
本文是 Spring AI Agent 开发学习系列的第 4 篇
开篇
前几篇我们让 AI 能"说话",能"结构化输出",但仔细想想——AI 只能生成文本,它没法获取实时数据、没法调用外部 API、没法执行计算。
这就是工具调用(Tool Calling / Function Calling)的价值所在。
读完本文,你将收获:
• ✅ 理解工具调用的核心原理(模型→工具→模型) • ✅ 使用 @Tool注解定义工具方法• ✅ 使用 @ToolParam描述工具参数• ✅ 对比"无工具"与"有工具"的效果差异 • ✅ 配套代码验证通过的所有接口
一、什么是工具调用?
工具调用是 AI 模型与外部系统交互的桥梁。
工作流程
用户提问:"北京天气怎么样?" ↓模型分析:需要天气数据 → 决定调用工具 get_weather("北京") ↓Spring AI 执行 ToolService.getWeather("北京") ↓返回结果:"晴,22°C,空气质量:良好" ↓模型将工具结果组织成自然语言回答用户没有工具 vs 有工具的对比
这就是本质区别:模型从"知识回忆"变成了"能力调用"。
二、@Tool 注解详解
Spring AI 2.0.0-M8 的 @Tool 注解定义在 spring-ai-model 中:
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Tool { String name(); // 工具名称 String description(); // 工具描述(很重要!) boolean returnDirect() default false; // 是否直接返回工具结果 Class<? extends ToolCallResultConverter> resultConverter() default ...;}参数描述使用 @ToolParam:
@Target(ElementType.PARAMETER)@Retention(RetentionPolicy.RUNTIME)public @interface ToolParam { boolean required() default true; String description(); // 参数描述(模型根据描述来填充值)}核心设计思想
1. 描述就是一切: @Tool的description和@ToolParam的description是模型判断何时调用、如何调用的关键2. 模型自主决策:不是你调用工具,是模型决定是否调用 3. name 要见名知意:例如 get_weather比tool1更容易被模型理解
三、实战:构建工具服务
3.1 定义工具类
@Componentpublic class ToolService { @Tool(name = "get_current_time", description = "获取当前的日期和时间") public String getCurrentTime() { return "当前日期:" + LocalDate.now() + ",时间:" + LocalTime.now(); } @Tool(name = "get_weather", description = "查询指定城市的实时天气") public String getWeather( @ToolParam(description = "城市名称,如:北京、上海") String city) { // 从数据源查询天气... return city + "天气:晴,22°C"; } @Tool(name = "calculate", description = "执行数学表达式计算,支持加减乘除和括号") public String calculate( @ToolParam(description = "数学表达式") String expression) { // 执行表达式计算... return expression + " = " + result; } @Tool(name = "search_info", description = "搜索相关信息(模拟搜索引擎)") public String searchInfo( @ToolParam(description = "搜索关键词") String keyword) { // 模拟搜索... return "搜索结果..."; }}3.2 注册工具到 ChatClient
Spring AI 2.0.0-M8 提供了灵活的注册方式:
// 方式一:直接传工具实例(最简单)chatClient.prompt() .user(message) .tools(toolService) // ← 自动扫描 @Tool 注解 .call() .content();// 方式二:使用 ToolSpec 细粒度控制chatClient.prompt() .user(message) .tools(t -> t.instances(toolService)) .call() .content();// 方式三:只启用特定的工具chatClient.prompt() .user(message) .toolNames("get_weather", "get_current_time") .tools(toolService) .call() .content();.tools(Object...) 接受任意带有 @Tool 方法的 Spring Bean,自动扫描并注册。
3.3 完整接口一览
本项目部署在 8083 端口,设计了 6 个 REST 端点:
对比实验
GET /api/tool/chat | |||
GET /api/tool/chat-with-tools |
工具演示
GET /api/tool/time | |||
GET /api/tool/weather?city=北京 | |||
GET /api/tool/calculate?expression=(15+3)*2 | |||
GET /api/tool/search?keyword=Spring AI |
快速启动
export AI_API_KEY=your-api-key-herecd module-03-tools/01-basic-toolsmvn spring-boot:run# 对比效果curl "http://localhost:8083/api/tool/chat?message=现在几点了?" # ❌ 无法回答curl "http://localhost:8083/api/tool/time" # ✅ 精确时间# 其他工具curl "http://localhost:8083/api/tool/weather?city=北京"curl "http://localhost:8083/api/tool/calculate?expression=(15%2B3)*2"curl "http://localhost:8083/api/tool/search?keyword=Spring+AI"四、工具调用的完整流程
请求链路
HTTP 请求 → Controller → ChatClient.tools(toolService).call() ↓ ChatClient 构建 Prompt(含工具定义) ↓ AI 模型收到请求 + 工具描述(JSON Schema) ↓ 模型决定调用哪个工具 → 返回工具调用请求 ↓ Spring AI 执行 MethodToolCallback ↓ ToolService.方法() 被执行 ↓ 工具结果返回给模型 ↓ 模型用工具结果生成最终回答 ↓ HTTP 响应Spring AI 内部机制
当你调用 .tools(toolService):
1. MethodToolCallback.Builder扫描toolService的所有方法2. 找到带有 @Tool注解的方法3. 提取 name、description、参数类型和@ToolParam描述4. 生成 JSON Schema 描述工具签名 5. 将工具定义作为 Prompt 的一部分发送给模型 6. 模型返回调用请求 → 执行方法 → 返回结果
五、模型是怎样"看到"工具的?
理解前面的流程后,你可能会好奇:Spring AI 是如何把 Java 方法"翻译"给 AI 模型听的?
答案是通过 JSON Schema。
5.1 @Tool 到 JSON Schema 的转换
当你调用 .tools(toolService),Spring AI 内部使用 JsonSchemaGenerator(基于 victools/jsonschema-generator 库)扫描每个 @Tool 方法,生成 JSON Schema 描述。
以 get_weather 方法为例:
@Tool(name = "get_weather", description = "查询指定城市的实时天气情况")public String getWeather( @ToolParam(description = "城市名称,如:北京、上海、广州") String city) {生成的 JSON Schema 如下:
{ "$schema": "https://json-schema.org/draft-07/schema#", "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,如:北京、上海、广州" } }, "required": ["city"], "additionalProperties":false}然后 Spring AI 按照 OpenAI 协议包装,最终发给模型的是这样的:
{ "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的实时天气情况", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,如:北京、上海、广州" } }, "required": ["city"] } }}5.2 模型收到了什么
本项目的 ToolService 定义了 4 个工具,所以实际上模型收到的是这样一组描述:
[ { "name": "get_current_time", "description": "获取当前的日期和时间", "parameters": { "type": "object", "properties": {} } }, { "name": "get_weather", "description": "查询指定城市的实时天气情况", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,如:北京、上海、广州" } }, "required": ["city"] } }, { "name": "calculate", "description": "执行数学表达式计算,支持加减乘除", "parameters": { "type": "object", "properties": { "expression": { "type": "string", "description": "数学表达式,例如:1 + 2 * 3" } }, "required": ["expression"] } }, { "name": "search_info", "description": "搜索相关信息(模拟搜索引擎)", "parameters": { "type": "object", "properties": { "keyword": { "type": "string", "description": "搜索关键词" } }, "required": ["keyword"] } }]模型收到用户消息 + 这组 JSON 描述后,会做三件事:
description | @Tool.description | |
@ToolParam.description |
这就是"描述即代码"的技术原理——
@Tool.description和@ToolParam.description不是写给人类看的注释,而是写给模型看的"使用说明书"。
5.3 无参数的工具
像 get_current_time 这样没有参数的方法,生成的 Schema 中 properties 为空对象:
{ "name": "get_current_time", "description": "获取当前的日期和时间", "parameters": { "type": "object", "properties": {} }}模型看到这个描述后,只要判断"用户想知道当前时间",就会直接触发调用,不需要提取任何参数。
六、最佳实践
6.1 描述要准确
// ❌ 差:模糊的描述@Tool(description = "查询天气")public String getWeather(String city)// ✅ 好:明确的描述@Tool(name = "get_weather", description = "查询指定城市的实时天气,包括温度、天气状况、空气质量")public String getWeather(@ToolParam(description = "城市名称,如:北京、上海、广州") String city)描述越准确,模型越容易在正确时机调用。
6.2 返回值要结构化
工具返回值可以是文本,也可以是 JSON。如果是 JSON,模型会自动解析并使用。
6.3 善用 returnDirect
当工具返回值本身就是最终答案时,使用 returnDirect = true 可以跳过模型重新处理结果的步骤:
@Tool(name = "get_weather", description = "...", returnDirect = true)6.4 工具粒度要合适
• 太粗:一个工具做太多事 → 模型不好判断 • 太细:每个小操作都创建一个工具 → 模型选择困难
建议:一个工具对应一个明确的业务能力。
📌 核心知识点总结
✅ 掌握内容
•@Tool 注解:name、description、returnDirect 属性 •@ToolParam 注解:为参数添加描述信息 •ChatClient.tools() 注册工具实例 •模型自动决策:判断何时、是否调用工具 •工具调用完整流程:模型→执行→结果→回答 •JSON Schema 转换原理:@Tool → 模型收到的 JSON 描述
🔑 关键要点
1. 模型说了算:不是你调用工具,是模型决定要不要调用 2. 描述即代码: @Tool.description和@ToolParam.description是模型决策的依据——它们被写入 JSON Schema 发给模型3. 模型"看到"的是 JSON:@Tool 被转换成 JSON Schema 格式的工具描述,和用户消息一起发给模型 4. 无参数工具也一样:没有参数的方法也会生成工具定义(properties 为空对象),模型同样可以触发调用 5. .tools(Object...):最便捷的注册方式,自动扫描 @Tool 注解
🎯 实战能力
读完本文,你应该能够:
• ✅ 使用 @Tool 定义自己的工具方法 • ✅ 使用 @ToolParam 描述参数,引导模型正确传参 • ✅ 将工具注册到 ChatClient 并处理多工具场景 • ✅ 理解工具调用流程,能排查工具调用失败的问题
下期预告
第5篇:工具进阶与生态——构建企业级工具框架
我们已经学会了定义工具,但生产环境还需要参数验证、动态注册、安全控制、多工具协调。下期我们深入这些进阶话题。
敬请期待!
快速验证工具调用效果:
cd module-03-tools/01-basic-toolsexport AI_API_KEY=your-keymvn spring-boot:run# 先试无工具:curl localhost:8083/api/tool/chat?message=现在几点了# 再试有工具:curl localhost:8083/api/tool/time# 对比差异,秒懂工具调用的价值关注本系列,系统掌握 Spring AI Agent 开发!🔔
夜雨聆风