最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
Function Calling 从入门到落地指南
时间:2026-09-13 15:46:02 编辑:袖梨 来源:一聚教程网
在 Java 服务中接入大模型工具调用时,真正需要处理的远不只是让模型返回一个函数名。参数是否可信、用户身份如何安全传递、工具应在什么范围内暴露,以及调用失败后怎样维持对话,都需要由服务端明确控制。下面将以 Spring AI Alibaba 为基础,逐步拆解 Function Calling 的配置与实现主线。
适配版本:Spring Boot 3.5.9 · Spring AI 1.1.2 · Spring AI Alibaba 1.1.2.2(版本不通用,请勿跨版本照抄)
本文带你从零掌握 Function‑Calling(工具调用 / Tool Calling):先 10 分钟跑通第一个工具,再逐个吃透「定义工具、挂载工具、传上下文、处理异常」这条主线,最后附一张生产上线检查清单。
0. 版本基线 & 先建立一个关键认知
| 依赖 | 版本 | 说明 |
|---|---|---|
| Spring Boot | 3.5.9 | 官方 parent,已默认开启 -parameters(见第 6 节) |
| Spring AI | 1.1.2 | 统一抽象层:@Tool、ToolCallback、ChatClient 等 |
| Spring AI Alibaba | 1.1.2.2 | 对接通义千问(DashScope),其内部传递依赖 Spring AI 1.1.2 |
| JDK | 17+ | 最低要求;JDK 21(LTS)亦推荐 |
| 模型 | 通义千问 qwen-plus | 可换 qwen-max / qwen-turbo 等 |
一句话本质:
大模型只会「说出」它想调哪个工具、传什么参数(一个 JSON 意图);真正执行工具、校验参数、鉴权、返回结果,全部由 Java 服务端完成。模型只是「点菜」,厨房炒菜的是你的 Java 代码。
这个认知决定了后面所有规范:模型传进来的参数是不可信的,必须校验;模型不该看到的私密信息(userId、token 等),绝不能放进提示词,只能走 ToolContext。
1. 快速开始:10 分钟跑通第一个工具
目标:问一句「杭州明天会不会下雨?」,程序自动调用一个 getWeather 工具查询,再让模型把结果组织成自然语言回答你。
1.1 新建工程与 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 使用 Spring Boot 官方 parent:
1) 锁定所有插件版本;
2) 已默认开启 maven-compiler-plugin 的 -parameters 编译参数(见第 6 节),
因此工具方法的参数名能正确反射出来,无需手动加配置 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.9</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>function-calling-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>function-calling-demo</name>
<properties>
<java.version>17</java.version>
<!-- 统一收敛版本,后续升级只改这里 -->
<spring-ai.version>1.1.2</spring-ai.version>
<spring-ai-alibaba.version>1.1.2.2</spring-ai-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring AI BOM:统一管理 spring-ai-* 各模块版本 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring AI Alibaba BOM:统一管理 spring-ai-alibaba-* 各模块版本。
说明:Alibaba 1.1.2.2 本身传递依赖 Spring AI 1.1.2,这里显式引入
spring-ai-bom 是为了让版本关系透明、可控,避免被动升级。 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>${spring-ai-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Web 容器,提供 REST 接口用于测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI Alibaba DashScope starter:
内置通义千问 ChatModel、自动装配工具调用,开箱即用 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
版本说明:Spring AI Alibaba 1.1.2.2 的 starter 对
spring-boot-starter的依赖声明是 3.5.10,但本工程用spring-boot-starter-parent3.5.9,其 dependencyManagement 会把 Spring Boot 相关依赖统一锁定为 3.5.9(补丁级差异,兼容)。这也是推荐使用官方 parent 的原因之一。
1.2 配置 application.yml
spring:
ai:
dashscope:
# 通义千问 API Key(生产建议用环境变量注入,不要写死在配置里)
api-key: ${AI_DASHSCOPE_API_KEY:请替换成你的-api-key}
ch@t:
options:
# 模型名:qwen-plus 性价比均衡,按需替换为 qwen-max / qwen-turbo 等
model: qwen-plus
1.3 编写工具类 WeatherTool
package com.example.tool;
import [email protected]; // 服务端私有上下文
import org.springframework.ai.tool.annotation.Tool; // 把方法标记为「可被模型调用的工具」
import org.springframework.ai.tool.annotation.ToolParam; // 描述工具入参,给模型看
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
public class WeatherTool {
/**
* 查询城市天气预报。
*
* @Tool 的 description 是写给大模型看的「使用说明书」,
* 写清楚「什么时候该调、什么时候不该调」,能显著减少误调用。
*/
@Tool(description = "查询指定城市的天气预报。当用户询问天气、气温、是否下雨时调用;用户未给出具体城市时不要调用。")
public String getWeather(
// @ToolParam 描述入参含义,帮助模型正确填参
@ToolParam(description = "城市中文名称,例如:杭州、北京、上海") String city,
// ToolContext 必须放在最后一个形参,框架会自动注入;模型看不到、也无法篡改它
ToolContext toolContext
) {
// 从服务端私有上下文读取用户标识(由业务在下游 .toolContext() 注入)
String userId = (String) toolContext.getContext().get("userId");
// ① 参数校验:绝不信任模型传入的参数
if (!StringUtils.hasText(city)) {
// 返回友好提示,引导模型向用户追问,而不是直接抛异常中断对话
return "缺少城市名称,请先询问用户要查询哪个城市的天气。";
}
// ② 调用真实业务(生产环境替换为 HTTP / Feign / RPC 调用)
String weather = "晴,气温 24℃,无降雨";
// ③ 返回给模型的最终结果,模型会把它整理成自然语言回答用户
return String.format("[用户 %s] %s 明天%s", userId, city, weather);
}
}
1.4 编写接口 WeatherController
package com.example.controller;
import com.example.tool.WeatherTool;
import java.util.Map;
import [email protected];
import [email protected];
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WeatherController {
private final ChatClient ch@tClient;
private final WeatherTool weatherTool;
public WeatherController(ChatModel ch@tModel, WeatherTool weatherTool) {
// 这里不挂任何 defaultTools,工具改为「请求级」按需挂载(见第 4 节)
this.ch@tClient = ChatClient.builder(ch@tModel).build();
this.weatherTool = weatherTool;
}
@GetMapping("/weather")
public String weather(@RequestParam String question) {
// 一次调用自动完成「模型决策 → Java 执行工具 → 结果回传 → 生成最终回答」全流程
return [email protected]()
.user(question) // 用户提问
.tools(weatherTool) // 本次请求可用的工具
.toolContext(Map.of("userId", "10001")) // 服务端私有上下文(模型不可见)
.call()
.content();
}
}
1.5 启动并验证
启动类(@SpringBootApplication)就绪后,访问:
GET http://localhost:8080/weather?question=杭州明天会不会下雨?
返回类似:「根据查询结果,杭州明天是晴天,气温 24℃,无降雨。」——到这里,你已经跑通了第一个 Function‑Calling。
2. 一次工具调用的完整流程
以「杭州明天会不会下雨?」为例,框架在幕后做的事情:
用户提问 "杭州明天会不会下雨?"
│
▼
ChatClient 把「用户消息 + 工具说明书(ToolDefinition)」一起发给大模型
│
▼
模型判断需要查天气 → 返回一个 ToolCall(工具名 getWeather + 参数 {"city":"杭州"})
│
▼
框架(ToolCallingAdvisor,自动装配)拦截 ToolCall,在 Java 端执行 getWeather 方法
│
▼
工具返回结果 → 作为 ToolResponseMessage 回传给模型
│
▼
模型结合工具结果,生成最终自然语言回答
理解这套流程的关键组件:
| 组件 | 作用 | 面向谁 |
|---|---|---|
| ToolDefinition | 工具的「说明书」:名称、描述、参数 JSON Schema | 下发给模型 |
| ToolCallback | Java 侧工具执行器,封装真实业务方法 | 服务端执行 |
| ToolCall | 模型返回的调用意图:工具名 + 参数 JSON + 调用 ID | 模型 → 服务端 |
| ToolCallingAdvisor | 自动循环驱动器,串联「调用→执行→回传→追问」 | 框架自动装配 |
| ToolContext | 服务端私有上下文(userId / tenantId / token 等) | 仅服务端,模型不可见 |
核心差异:ChatModel.call() 只发一次请求、拿到的是「工具调用意图」而非最终答案;而 ChatClient + .tools() 内部由 ToolCallingAdvisor 自动完成多轮循环,生产直接用后者。绝大多数情况下你甚至感知不到这个 Advisor 的存在——只要调用了 .tools() 就自动生效。
3. 定义工具的三种方式
3.1 方式一:@Tool 注解式(推荐,覆盖 90% 场景)
把 @Tool 加在 Spring Bean 的 public 方法上,框架自动扫描并生成工具回调。第 1.3 节的 WeatherTool 就是完整示例,这里补充几条规范:
- 工具方法必须为
public。 - 每个业务入参都应加
@ToolParam(description = "...");默认required = true,可选参数用@ToolParam(required = false)。 - 需要服务端上下文时,把
ToolContext作为最后一个形参(可选)。 - 入参支持基本类型、
String、POJO、List、Map、枚举等常见类型,框架会自动生成参数 Schema。 - 返回值类型可为
String、POJO、Map等任意可序列化类型,String最常用。
注意:加了
@Tool的方法不会自动全局生效,必须显式通过.tools(...)(请求级)或.defaultTools(...)(全局)挂载后才会对模型可见。
@Tool(description = "更新客户资料")
public void updateCustomer(
@ToolParam(description = "客户 ID") Long id,
@ToolParam(description = "客户姓名") String name,
@ToolParam(description = "邮箱", required = false) String email, // 可选参数
ToolContext toolContext
) {
// 业务实现...
}
当入参较多时,推荐用 POJO(record)作为唯一入参:字段描述用 Jackson 的 @JsonPropertyDescription(或 Swagger 的 @Schema),@ToolParam 只描述整个对象。
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
// 入参对象:字段上的描述会进入参数 Schema,帮助模型正确填值
public record WeatherRequest(
@JsonPropertyDescription("城市中文名称,例如:杭州、北京") String city,
@JsonPropertyDescription("查询天数,取值 1-7") Integer days
) {}
@Tool(description = "查询指定城市未来几天的天气预报")
public String getWeather(
@ToolParam(description = "天气查询请求") WeatherRequest request,
ToolContext toolContext
) {
// 框架会把模型生成的参数 JSON 自动反序列化成 WeatherRequest 传入
return String.format("%s 未来 %d 天天气晴朗", request.city(), request.days());
}
3.2 方式二:FunctionToolCallback 函数式(无注解 / 临时工具)
不用注解,直接用一个普通函数(下面以匿名内部类实现 BiFunction)包装,适合存量代码、临时工具、需要拿到 ToolContext 的函数式写法。
import [email protected];
import org.springframework.ai.model.function.FunctionToolCallback;
import org.springframework.ai.tool.ToolCallback;
import java.util.Map;
import java.util.function.BiFunction;
// ① 用 BiFunction<入参, ToolContext, 返回> 定义逻辑,第二个参数即服务端上下文
// 这里用匿名内部类实现,避免 Lambda 写法(若偏好 Lambda 也可写成 (city, ctx) -> { ... })
BiFunction<String, ToolContext, String> weatherFunc = new BiFunction<String, ToolContext, String>() {
@Override
public String apply(String city, ToolContext ctx) {
String userId = (String) ctx.getContext().get("userId");
return String.format("[用户 %s] %s 天气晴朗", userId, city);
}
};
// ② 手动构建工具回调:工具名 + 函数 + 描述 + 入参类型
ToolCallback weatherCallback = FunctionToolCallback
.builder("get_weather", weatherFunc) // 工具名(模型看到的名字)+ 函数
.description("查询指定城市的天气预报")
.inputType(String.class) // 入参类型,框架据此生成参数 Schema
.build();
// ③ 请求级挂载使用
String answer = [email protected]()
.user("北京天气怎么样?")
.tools(weatherCallback) // 传入 ToolCallback
.toolContext(Map.of("userId", "10002"))
.call()
.content();
FunctionToolCallback支持Function、BiFunction、Supplier、Consumer,也能直接传一个 POJO(框架识别其调用方法)。inputType除Void外必填,用于生成参数 Schema。
3.3 方式三:MethodToolCallback 反射式(包装「不可注解」的存量方法)
当方法属于第三方代码、或你不便加 @Tool 注解时,用反射把方法手动包装成工具。注意:它比前两种啰嗦,需要手写工具说明书(含参数 Schema)。
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.tool.method.MethodToolCallback;
import java.lang.reflect.Method;
// 假设 WeatherService 是一个你不想改动、无法加 @Tool 注解的存量类
WeatherService weatherService = new WeatherService();
// ① 通过反射拿到目标方法
Method method = WeatherService.class.getDeclaredMethod("queryWeather", String.class);
// ② 手动构建工具回调:名称、描述、参数 Schema 都要写在 ToolDefinition 里
ToolCallback callback = MethodToolCallback.builder()
.toolDefinition(ToolDefinition.builder()
.name("get_weather") // 工具名写在这里
.description("查询指定城市的天气预报") // 描述也写在这里
.inputSchema(""" // 参数 JSON Schema 需手写
{
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市中文名称"}
},
"required": ["city"]
}
""")
.build())
.toolMethod(method) // 目标方法
.toolObject(weatherService) // 非 static 方法必须提供目标实例
.build();
String answer = [email protected]()
.user("上海天气怎么样?")
.tools(callback)
.call()
.content();
⚠️ 踩坑提示:
MethodToolCallback.builder()没有.toolName()/.description()这两个方法,名称和描述统一放进.toolDefinition(...)里。
三种方式怎么选:
| 方式 | 适用场景 | 是否推荐 |
|---|---|---|
@Tool 注解 | 新写的业务工具(绝大多数情况) | ✅ 首选 |
FunctionToolCallback | 无注解的存量代码、临时 Lambda | ✅ 灵活 |
MethodToolCallback | 包装第三方/不可注解方法 | ⚠️ 仅在必要时 |
4. 工具挂载:全局 vs 请求级(推荐请求级)
Spring AI 提供两种挂载维度:
// ① 全局默认挂载:所有请求默认可见
ChatClient globalClient = ChatClient.builder(ch@tModel)
.defaultTools(weatherTool) // 或 .defaultToolCallbacks(callback)
.build();
// ② 请求级挂载:仅本次请求生效(推荐)
[email protected]()
.user("查询天气")
.tools(weatherTool) // 或 .toolCallbacks(callback)
.call();
// ③ 多个工具一次挂载
[email protected]()
.user("帮我查天气并下单")
.tools(weatherTool, orderTool) // 支持一次传入多个工具
.call();
生产建议:
- 通用公共工具(如「查当前时间」)可以用
defaultTools全局挂载。 - 涉及权限、租户差异的工具必须请求级挂载——否则等于对所有请求暴露该工具,存在越权调用风险。
- 注意:请求级
.tools()与全局defaultTools同时存在时,请求级会覆盖(而非追加)全局默认工具。若需要两者叠加,请自行在请求级把全局工具一并传入。
5. ToolContext:模型碰不到的服务端私密上下文
ToolContext 用于向工具注入 userId、tenantId、token 等模型不可见的私密信息。它只存在于服务端,不会被序列化发给模型,因此模型无法读取、更无法篡改。
注入方式:
[email protected]()
.toolContext(Map.of("userId", "10001", "tenantId", "t-001"))
.tools(weatherTool)
.call();
读取方式(三种工具写法对应):
| 工具写法 | 如何读取 ToolContext |
|---|---|
@Tool 方法 | 把 ToolContext 作为最后一个形参,用 toolContext.getContext().get("key") 读取 |
FunctionToolCallback | 用 BiFunction<I, ToolContext, O>,第二个参数即上下文 |
MethodToolCallback | 方法签名中同样声明 ToolContext 形参 |
补充说明:
ToolContext还提供getSessionId()、getConversationId()等便捷方法。- 若同时设置了默认与运行时
toolContext,两者会合并,运行时值优先。
为什么这很关键:userId/tenantId 若由模型传参,就等同于把鉴权依据交给模型,存在被 Prompt 注入越权的风险。统一走 ToolContext 注入是铁律。
6. 编译参数 -parameters:一个「不卡坑」的真相
网上流传「不加 -parameters 会报错」的说法,其实不准确。真相是:
@ToolParam只有description、required两个属性,没有name。- 因此工具参数 Schema 里的参数名来自反射(
Method.getParameters()的name)。 - Java 默认编译会丢弃源码参数名,反射拿到的就是
arg0、arg1。 - 结果:工具照样能跑(参数绑定靠位置/类型),但模型看到的参数名是
arg0,说明书可读性差、难调试、易埋隐性映射问题。
最省心的真相:只要用 spring-boot-starter-parent 3.5.9(本文第 1.1 节),它已经默认开启 maven-compiler-plugin 的 <parameters>true</parameters>——你什么都不用做。
只有当你不用 Spring Boot parent(独立构建、Gradle 等)时,才需要手动开启:
<!-- 仅当你未使用 spring-boot-starter-parent 时才需要这段 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
// Gradle 等价配置
tasks.withType(JavaCompile).configureEach {
options.compilerArgs += '-parameters'
}
一句话:用官方 parent 就对了;没用 parent 才补
-parameters。IDEA 中请确保委托 Maven/Gradle 构建,否则仅改配置不生效。
7. 异常处理:让模型优雅转述,而非直接报错
7.1 全局开关
spring:
ai:
tools:
throw-exception-on-error: false # 默认 false
false(默认,推荐):工具抛出的RuntimeException会被转成消息回传给模型,由模型友好转述给用户。true:直接抛异常中断对话,用户看到的是技术报错。- 注意:受检异常(Checked Exception)和
Error始终直接抛出,不受此开关影响。
该开关由自动装配的 DefaultToolExecutionExceptionProcessor 处理;如需定制,可自行定义 ToolExecutionExceptionProcessor Bean。
7.2 业务规范(务必遵守)
@Tool(description = "...")
public String doSomething(@ToolParam(description = "...") String param, ToolContext ctx) {
// ① 绝不信任模型入参:非空、合法性、业务权限都要校验
if (!StringUtils.hasText(param)) {
// ② 参数缺失/非法:返回友好提示,引导模型反问用户,而不是抛业务异常
return "缺少必要参数,请先询问用户补充。";
}
// ③ userId / tenantId / token 一律从 ToolContext 取,禁止由模型传参,杜绝越权
String userId = (String) ctx.getContext().get("userId");
// ... 业务逻辑
}
若你自定义
ToolCallback实现,工具执行出错时应抛ToolExecutionException,框架才能捕获并按上述策略处理。
附:关键 API 速查表
| 类 / 注解 | 包路径 | 用途 |
|---|---|---|
@Tool | org.springframework.ai.tool.annotation | 标记工具方法 |
@ToolParam | org.springframework.ai.tool.annotation | 描述工具入参(description / required) |
ToolCallback | org.springframework.ai.tool | 工具执行器接口 |
ToolDefinition | org.springframework.ai.tool.definition | 工具说明书(名称/描述/入参 Schema) |
MethodToolCallback | org.springframework.ai.tool.method | 反射包装存量方法为工具 |
FunctionToolCallback | org.springframework.ai.model.function | 函数式包装为工具 |
ToolContext | [email protected] | 服务端私有上下文 |
ChatClient | [email protected] | 高层客户端(.tools() / .toolContext() / .call()) |
相关文章
- Codex 的 reasoning.effort 如何权衡推理质量、Token 成本与响应延迟? 09-13
- Codex 的 Reasoning Effort 应如何在 low、medium、high 与 xhigh 之间选择? 09-13
- Pi Agent 能否替代 Codex CLI 作为 Codex 的 Agent Harness? 09-13
- Claude Agent 的使用方式需要遵守哪些账号政策? 09-13
- 让 Agent 记住上下文:用 token 预算、轮次裁剪和滚动摘要管理多轮历史 09-13
- Linux入门学习之通过vmware虚拟机安装ubuntu系统的方法 09-13