首页
看点啥
插画图片
首页 科技看点 Function Calling 从入门到落地指南

Function Calling 从入门到落地指南

2026-10-16 0

在 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统一抽象层:@Tool、ToolCallback、ChatClient 等
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



    4.0.0

    
    
        org.springframework.boot
        spring-boot-starter-parent
        3.5.9
        
    

    com.example
    function-calling-demo
    0.0.1-SNAPSHOT
    function-calling-demo

    
        17
        
        1.1.2
        1.1.2.2
    

    
        
            
            
                org.springframework.ai
                spring-ai-bom
                ${spring-ai.version}
                pom
                import
            
            
            
                com.alibaba.cloud.ai
                spring-ai-alibaba-bom
                ${spring-ai-alibaba.version}
                pom
                import
            
        
    

    
        
        
            org.springframework.boot
            spring-boot-starter-web
        

        
        
            com.alibaba.cloud.ai
            spring-ai-alibaba-starter-dashscope
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

版本说明: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 就是完整示例,这里补充几条规范:

注意:加了 @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 weatherFunc = new BiFunction() {
    @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();

生产建议:


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,第二个参数即上下文
MethodToolCallback方法签名中同样声明 ToolContext 形参

补充说明:

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


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

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

最省心的真相:只要用 spring-boot-starter-parent 3.5.9(本文第 1.1 节),它已经默认开启 maven-compiler-plugin 的 true——你什么都不用做。

只有当你不用 Spring Boot parent(独立构建、Gradle 等)时,才需要手动开启:



    org.apache.maven.plugins
    maven-compiler-plugin
    
        true
    

// 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

该开关由自动装配的 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())
喜欢(0)

上一篇

少调模型多走代码:解析 resolve-harness 的确定性 Fast Path 运行时

少调模型多走代码:解析 resolve-harness 的确定性 Fast Path 运行时

下一篇

Pi Agent 应该如何通过 Agent SDK 正确连接 Claude 或 Codex?

Pi Agent 应该如何通过 Agent SDK 正确连接 Claude 或 Codex?
猜你喜欢