Function Calling 从入门到落地指南

作者:袖梨 2026-09-13

在 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 Boot3.5.9官方 parent,已默认开启 -parameters(见第 6 节)
Spring AI1.1.2统一抽象层:@ToolToolCallbackChatClient
Spring AI Alibaba1.1.2.2对接通义千问(DashScope),其内部传递依赖 Spring AI 1.1.2
JDK17+最低要求;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-parent 3.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下发给模型
ToolCallbackJava 侧工具执行器,封装真实业务方法服务端执行
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、ListMap、枚举等常见类型,框架会自动生成参数 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 支持 FunctionBiFunctionSupplierConsumer,也能直接传一个 POJO(框架识别其调用方法)。inputTypeVoid 外必填,用于生成参数 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") 读取
FunctionToolCallbackBiFunction<I, ToolContext, O>,第二个参数即上下文
MethodToolCallback方法签名中同样声明 ToolContext 形参

补充说明:

  • ToolContext 还提供 getSessionId()getConversationId() 等便捷方法。
  • 若同时设置了默认与运行时 toolContext,两者会合并,运行时值优先。

为什么这很关键:userId/tenantId 若由模型传参,就等同于把鉴权依据交给模型,存在被 Prompt 注入越权的风险。统一走 ToolContext 注入是铁律。


6. 编译参数 -parameters:一个「不卡坑」的真相

网上流传「不加 -parameters 会报错」的说法,其实不准确。真相是:

  • @ToolParam 只有 descriptionrequired 两个属性,没有 name
  • 因此工具参数 Schema 里的参数名来自反射Method.getParameters()name)。
  • Java 默认编译会丢弃源码参数名,反射拿到的就是 arg0arg1
  • 结果:工具照样能跑(参数绑定靠位置/类型),但模型看到的参数名是 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 速查表

类 / 注解包路径用途
@Toolorg.springframework.ai.tool.annotation标记工具方法
@ToolParamorg.springframework.ai.tool.annotation描述工具入参(description / required
ToolCallbackorg.springframework.ai.tool工具执行器接口
ToolDefinitionorg.springframework.ai.tool.definition工具说明书(名称/描述/入参 Schema)
MethodToolCallbackorg.springframework.ai.tool.method反射包装存量方法为工具
FunctionToolCallbackorg.springframework.ai.model.function函数式包装为工具
ToolContext[email protected]服务端私有上下文
ChatClient[email protected]高层客户端(.tools() / .toolContext() / .call()

相关文章

精彩推荐