Function Calling 讲解

一、单个Function Calling

大模型本质上是一个"文字接龙"引擎。它知道很多,但有三个致命缺陷:

  1. 知识有截止日期:不知道今天天气、最新股价、你的数据库里有什么。

  2. 无法执行动作:不能帮你发邮件、下单、修改数据库记录。

  3. 无法访问私有数据:不知道你公司内部的订单系统里有哪些数据。

Function Calling 就是解决这三个问题的。

核心思路是:模型只负责"决定调用哪个函数、传什么参数",真正的执行由你的 Java 代码来完成。

这里有一个非常重要的安全边界------模型永远无法直接访问你的 API

它只能输出一个结构化的"调用请求",Spring AI 拦截这个请求,执行你注册的 Java 方法,然后把结果返回给模型,模型再基于结果生成自然语言回答。

整个流程大致是这样的:

复制代码
用户提问
  → Spring AI 把提示 + 工具定义(名称、描述、参数 schema)发给模型
  → 模型判断需要调用工具,返回调用请求 {name: "getWeather", arguments: {city: "北京"}}
  → Spring AI 拦截请求,执行你写的 Java 方法
  → 把执行结果作为上下文注入,再次发给模型
  → 模型基于真实数据生成最终回答

两种实现方式

Spring AI 提供了两种定义工具的路径,新手建议从 @Tool 注解方式入手,它最直观、代码量最少。

方式一:@Tool 注解(推荐)

直接在方法上加注解。

Spring AI 会自动扫描方法签名,生成对应的 JSON Schema 给模型。

第一步:定义一个工具类
java 复制代码
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;

public class WeatherTools {

    @Tool(description = "查询指定城市的当前天气情况")
    public String getWeather(
            @ToolParam(description = "城市名称,例如:北京、上海") String city) {
        // 这里写真实的业务逻辑,比如调用第三方天气 API
        if ("北京".equals(city)) {
            return "北京当前天气:晴,25°C,湿度 40%";
        }
        return city + "当前天气:多云,22°C";
    }

    @Tool(description = "根据城市和日期预订航班")
    public String bookFlight(
            String origin,
            String destination,
            @ToolParam(description = "日期,格式 YYYY-MM-DD") String date) {
        return "已为您预订 " + date + " 从" + origin + "到" + destination + "的航班";
    }
}

关键点:

  • description 必须写清楚。模型靠它来判断"什么时候该调用这个工具"。描述越准确,模型的决策越靠谱。

  • @ToolParam 用来给每个参数加描述,帮助模型理解参数含义。

  • 方法参数和返回值类型没有限制,Spring AI 会自动处理序列化。

第二步:在 ChatClient 中注册并使用
java 复制代码
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                .tools(new WeatherTools())   // 注册工具实例
                .call()
                .content();
    }
}

测试一下,发一个请求 GET /chat?message=北京今天天气怎么样?

Spring AI 会自动完成上面流程图中的所有步骤。模型看到你注册了一个叫 getWeather 的工具,描述是"查询指定城市的当前天气情况",于是决定调用它,参数 city="北京",你的 Java 方法被执行,返回结果,模型最终生成一句自然语言回答。

方式二:@Bean 定义 Function(传统方式)

如果你需要把一个已有的 java.util.function.Function 注册为工具,可以用 @Bean 的方式。

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;

import java.util.function.Function;

@Configuration
public class FunctionConfig {

    public record WeatherRequest(String city) {}
    public record WeatherResponse(String condition, int tempCelsius) {}

    @Bean
    @Description("根据城市名称查询当前天气")
    public Function<WeatherRequest, WeatherResponse> weatherFunction() {
        return request -> {
            // 真实逻辑:调用天气 API
            return new WeatherResponse("晴", 25);
        };
    }
}

然后在调用时通过 bean 名称注册:

java 复制代码
chatClient.prompt()
    .user(message)
    .functions("weatherFunction")   // 注意是 functions(),不是 tools()
    .call()
    .content();

⚠️ 注意FunctionCallbackfunctions() API 在 Spring AI 新版本中已标记为废弃(Deprecated),官方推荐迁移到 ToolCallback@Tool 注解方式。新项目直接用 @Tool 即可。

补充一个关键细节:工具循环

你可能注意到,模型调用工具后,结果被送回模型,模型可能再次决定调用另一个工具,然后再一次执行、再一次送回。

Spring AI 内部通过 ToolCallingAdvisor 管理这个循环,直到模型返回一个不包含工具调用的响应为止。

这意味着你可以定义多个工具,模型会根据用户的问题自动组合调用。

比如用户说"北京天气怎么样?如果晴就帮我订一张明天去上海的机票",模型可能先调用 getWeather,拿到结果后再调用 bookFlight

小结一下

要点 说明
核心思想 模型只决策,执行在你的 Java 代码
推荐方式 @Tool 注解 + .tools() 注册
关键注解 @Tool(description=...) 告诉模型什么时候调用
参数描述 @ToolParam 帮助模型正确生成参数
底层机制 Spring AI 自动生成 JSON Schema,处理调用循环
安全边界 模型无法直接访问你的 API,只能发出调用请求

二、spring AI 一般的项目结构

java 复制代码
src/main/java/com/example/
├── controller/
│   └── ChatController.java
├── service/
│   ├── ChatService.java
│   └── WeatherService.java
├── ai/
│   ├── config/
│   │   └── ChatClientConfig.java
│   └── tool/
│       ├── WeatherTools.java
│       └── OrderTools.java
├── domain/
│   └── ...
└── infrastructure/
    └── ...

三、多个Function Calling

模型是"决策者",不是"执行者"。

多个 function 的调用顺序,要么由模型自主决定,要么由你的代码强制编排。

下面分三种模式讲清楚。


模式一:模型自主编排(最常见)

你注册多个工具,模型根据用户问题,自己决定调不调、调哪个、调几个、什么顺序

机制:ToolCallingAdvisor 循环

Spring AI 内部有一个循环,我画给你看:

java 复制代码
用户问题
  → 发给模型(附带所有工具定义)
  → 模型返回:我要调 getWeather(city="北京")
  → Spring AI 执行 getWeather,拿到结果
  → 把结果连同对话历史再次发给模型
  → 模型返回:我要调 bookFlight(origin="北京", dest="上海", date="2026-09-15")
  → Spring AI 执行 bookFlight,拿到结果
  → 再次发给模型
  → 模型返回:最终自然语言回答(不再要求调用工具)
  → 循环结束

关键点:这个循环会一直持续到模型不再要求调用工具为止。 模型可以连续调用 1 个、2 个、甚至 10 个工具。

代码示例

java 复制代码
@Service
@RequiredArgsConstructor
public class ChatService {

    private final ChatClient chatClient;
    private final WeatherTools weatherTools;
    private final FlightTools flightTools;

    public String chat(String message) {
        return chatClient.prompt()
                .user(message)
                .tools(weatherTools, flightTools)   // 注册多个工具
                .call()
                .content();
    }
}

工具类:

java 复制代码
@Component
@RequiredArgsConstructor
public class WeatherTools {

    private final WeatherService weatherService;

    @Tool(description = "查询指定城市的当前天气情况")
    public String getWeather(
            @ToolParam(description = "城市名称,例如:北京、上海") String city) {
        return weatherService.getCurrentWeather(city);
    }
}

@Component
@RequiredArgsConstructor
public class FlightTools {

    private final FlightService flightService;

    @Tool(description = "根据出发地、目的地和日期预订航班")
    public String bookFlight(
            @ToolParam(description = "出发城市") String origin,
            @ToolParam(description = "目的城市") String destination,
            @ToolParam(description = "日期,格式 YYYY-MM-DD") String date) {
        return flightService.book(origin, destination, date);
    }
}

测试

复制代码
用户:北京天气怎么样?如果晴就帮我订一张明天去上海的机票

模型可能的行为:

  1. 先调 getWeather("北京") → 得到"晴,25°C"

  2. 判断"晴",再调 bookFlight("北京", "上海", "2026-09-15")

  3. 生成最终回答:"北京今天晴,25°C,已为您预订明天北京到上海的航班。"

注意:这个顺序不是我写的代码,是模型自己推理出来的。 这就是 Function Calling 最强大的地方。


模式二:代码强制编排(业务流程固定时)

有时候业务逻辑是固定的,不允许模型自由发挥。比如"先查订单,再根据订单状态决定是否退款"。这时候有两种做法。

做法 A:把编排藏在工具内部

模型只看到一个工具,但工具内部调用了多个 Service:

java 复制代码
@Component
@RequiredArgsConstructor
public class OrderTools {

    private final OrderService orderService;
    private final RefundService refundService;

    @Tool(description = "处理订单退款:查询订单状态,如果符合条件则发起退款")
    public String processRefund(
            @ToolParam(description = "订单号") String orderId) {
        // 代码强制顺序:先查,再判断,再退款
        Order order = orderService.getOrder(orderId);
        if (order == null) {
            return "订单不存在";
        }
        if (!"PAID".equals(order.getStatus())) {
            return "订单状态为 " + order.getStatus() + ",不可退款";
        }
        refundService.refund(orderId);
        return "退款成功,订单号:" + orderId;
    }
}

优点 :模型只负责"决定要不要退款",具体流程由 Java 代码保证。

适用:流程固定、有严格业务规则、不允许模型自由发挥的场景。

做法 B:不用 Function Calling,直接代码编排

如果流程完全固定,模型根本不需要"决策",那就不该用 Function Calling。直接:

java 复制代码
public String processRefund(String orderId) {
    // 纯 Java 代码编排,模型只负责最后的文案生成
    Order order = orderService.getOrder(orderId);
    if (order == null) return "订单不存在";
    if (!"PAID".equals(order.getStatus())) return "不可退款";
    refundService.refund(orderId);
    return "退款成功";
}

什么时候用这个? 当"调用哪个函数"根本不需要智能决策时,Function Calling 就是过度设计。


模式三:多工具选择(模型选一个)

这是最简单的场景:注册多个工具,模型根据用户问题选一个

java 复制代码
chatClient.prompt()
    .user(message)
    .tools(weatherTools, orderTools, emailTools)
    .call()
    .content();
  • 用户问"北京天气" → 调 getWeather
  • 用户问"订单 12345 状态" → 调 getOrder
  • 用户问"给张三发邮件" → 调 sendEmail

模型根据每个工具的 description 来判断该用哪个。

所以 description 写得好不好,直接决定了模型选得对不对。


工程实践:怎么让模型选对、调对?

1. 工具描述要"互斥"

❌ 反例:

java 复制代码
@Tool(description = "查询信息")
public String getWeather(String city) { ... }

@Tool(description = "查询数据")
public String getOrder(String orderId) { ... }

两个描述都太模糊,模型会懵。

✅ 正例:

java 复制代码
@Tool(description = "查询指定城市的当前天气情况,包括温度、湿度、天气状况")
public String getWeather(String city) { ... }

@Tool(description = "根据订单号查询订单的详细信息和当前状态")
public String getOrder(String orderId) { ... }

2. 单次注册的工具不要太多

一般建议 5~10 个以内。工具太多,模型选择成本高,容易选错。

如果工具超过 15 个,建议按场景拆分多个 ChatClient

java 复制代码
@Configuration
public class ChatClientConfig {

    @Bean
    public ChatClient weatherChatClient(ChatClient.Builder builder, WeatherTools tools) {
        return builder.defaultTools(tools).build();
    }

    @Bean
    public ChatClient orderChatClient(ChatClient.Builder builder, OrderTools tools) {
        return builder.defaultTools(tools).build();
    }
}

不同业务场景注入不同的 ChatClient。

3. 用系统提示词引导顺序

如果你希望模型优先调用某个工具,可以在 system prompt 里写:

java 复制代码
chatClient.prompt()
    .system("""
        你是订票助手。当用户询问订票时,必须先调用 getWeather 查询目的地天气,
        如果天气恶劣(暴雨、大雪),先提醒用户再询问是否继续。
        """)
    .user(message)
    .tools(weatherTools, flightTools)
    .call()
    .content();

4. 参数校验放在工具方法内部

模型生成的参数不一定可靠,永远不要相信模型的参数。工具方法内部必须做校验:

java 复制代码
@Tool(description = "根据订单号查询订单")
public String getOrder(@ToolParam(description = "订单号,纯数字") String orderId) {
    if (orderId == null || !orderId.matches("\\d+")) {
        return "订单号格式不正确";
    }
    // ...
}

总结成一张表

模式 谁决定顺序 适用场景 实现方式
模型自主编排 模型 复杂推理、多步骤任务 注册多个工具,让模型自由调用
代码强制编排 你的 Java 代码 流程固定、有严格规则 工具内部调用多个 Service
直接代码编排 你的 Java 代码 无需智能决策 不用 Function Calling
多工具选择 模型 多功能入口 注册多个工具,模型选一个

最后一句忠告

Function Calling 不是"让模型帮你写业务逻辑",而是"让模型帮你决定调用哪个业务逻辑"。

业务规则、顺序约束、参数校验,永远应该由你的 Java 代码保证。模型只负责"理解用户意图"和"决定调用哪个工具"。

相关推荐
步行cgn3 小时前
Spring 注入 Properties 详解
java·python·spring
星空3 小时前
Map<String, String>`Map`是接口,不能直接 new
java·前端·算法
liangbo73 小时前
12-JVM 调优方法论与参数速查
java·jvm
RobinDevNotes3 小时前
TensorRT 与 ONNX Runtime 推理全解析
搜索引擎·ai·大模型·推理引擎
干到60岁退休的码农3 小时前
16.过滤器中处理异常响应返回
java·spring boot·mybatis
LuTshoes3 小时前
AI Agent 相关介绍
人工智能·ai
Java_2017_csdn4 小时前
StringUtils.hasText() 和 StringUtils.isNotBlank() 方法对比
java
Joy T4 小时前
Spring AI 2.0 进阶入门:RAG、Structured Output 与 Agent 信息闭环
java·人工智能·spring·rag·springai·agent入门
xiaoqiMikko4 小时前
jackson-databind 又出 4 条,Dependabot 一条都不报:2.21.5 还差 3 条
java·安全