第四章-SpringAI-函数调用&ToolCalling

Spring AI 的函数调用(Function Calling)让大语言模型能够请求执行应用中预先定义的函数,用于查询数据、调用接口或执行业务逻辑。模型负责理解问题和生成调用参数,Java 程序负责执行函数,模型再根据返回结果组织回答。

本文先介绍函数调用的执行流程,再通过两个案例说明实现方式:用加法与乘法理解基本调用,用订单、物流和库存查询演示多个方法的协作。

1. 函数调用的工作原理

1.1 什么是函数与工具

这里的"函数"就是应用提供的方法,例如计算两个数的和、查询某个订单。把方法的名称、用途和参数结构提供给模型后,模型便可以请求调用它。在 Spring AI 中,这类可调用能力也称为 Tool(工具),对应机制称为 Tool Calling。

模型不会直接运行 Java 代码,也不需要读取方法源码。实际执行发生在应用侧。

1.2 一次调用如何完成

  1. 定义函数:声明方法及其名称、描述和参数。
  2. 注册并请求模型:应用将函数定义和用户问题一起发送给模型。
  3. 执行函数:模型返回函数名与参数,Spring AI 调用对应方法。
  4. 回传结果:Spring AI 将执行结果发送给模型,模型生成回答,或继续请求其他函数。

一次用户提问可能触发多次函数调用,也可能无需调用函数。一次 .call() 因此不一定只对应一次模型请求。

1.3 System Prompt 的作用

System Prompt 用来说明角色与调用规则,例如"查询订单时必须依据函数返回的数据,缺少订单号时先询问"。函数则负责实际获取数据。

提示词和函数注册需要配合:提示词说明什么时候调用、如何使用结果;注册函数让模型知道有哪些能力可用。

2. 工程结构

示例使用 Java 17、Spring Boot 3.5.15 和 Spring AI 1.1.8,包名为 org.xingyue.ai。应用需已配置可用的 ChatClient Bean,底层模型支持工具调用。

组件 职责
ToolChatController 接收 HTTP 请求,检查输入
ToolChatService 设置提示词、注册工具、调用模型
CalculatorService 定义加法与乘法 Function Bean
StoreTools 暴露订单与库存查询方法
StoreService 组合订单、物流和库存查询
StoreRepository 提供数据访问

业务查询沿用 Controller、Service、Repository 分层;Tool 是供模型调用的业务入口。

3. 入门案例:调用加法与乘法函数

以"10 加 20,再乘以 4"为例,演示函数定义、注册和结果回传。本例将加法与乘法逻辑封装为 Function Bean,供模型请求调用。

3.1 定义函数

使用 @Bean 注册 Function 对象,以 record 定义输入参数,通过 @Description 描述函数用途。

CalculatorService.java
java 复制代码
package org.xingyue.ai.config;

import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;

@Configuration(proxyBeanMethods = false)
public class CalculatorService {
    private static final Logger log = LoggerFactory.getLogger(CalculatorService.class);

    public record AddOperation(int a, int b) {}
    public record MulOperation(int m, int n) {}

    @Bean
    @Description("计算两个整数的和,参数 a 和 b 为两个加数")
    public Function<AddOperation, Integer> addOperation() {
        return request -> {
            int result = Math.addExact(request.a(), request.b());
            log.info("执行加法函数:{} + {} = {}", request.a(), request.b(), result);
            return result;
        };
    }

    @Bean
    @Description("计算两个整数的乘积,参数 m 和 n 为两个乘数")
    public Function<MulOperation, Integer> mulOperation() {
        return request -> {
            int result = Math.multiplyExact(request.m(), request.n());
            log.info("执行乘法函数:{} × {} = {}", request.m(), request.n(), result);
            return result;
        };
    }
}

Function<AddOperation, Integer> 表示接收一个 AddOperation 参数对象,返回一个整数。addOperationmulOperation 默认也是两个 Bean 的名称。

创建 Bean 时,配置方法只是返回 Function 对象,并不执行计算。真正的计算发生在 Lambda 的函数体中:Spring AI 收到模型的调用请求后,将参数转换为对应的 record,再执行 Function 的 apply(request)

例如,模型请求 addOperation 并传入 {"a":10,"b":20},实际执行相当于:

java 复制代码
addFunction.apply(new CalculatorService.AddOperation(10, 20));

这里的 addFunction 指 Spring 容器中的加法 Function Bean。以上仅说明执行过程,业务调用代码无需手动调用 apply()

Math.addExactMath.multiplyExact 在整数溢出时抛出异常;正常范围内与 +* 的结果相同。日志记录实际计算过程。

3.2 注册函数并发起调用

在 Service 中,通过 .toolNames(...) 指定本次请求可以使用的 Function Bean 名称。核心调用如下,完整 Service 和 Controller 见第 5 节。

java 复制代码
return chatClient.prompt()
        .system("你是计算助手。加法、乘法请调用对应函数,根据返回结果用中文回答。")
        .user(message)
        .toolNames("addOperation", "mulOperation")
        .call()
        .content();

Bean 名称必须与定义一致。仅定义 @Bean 不会自动向所有对话开放函数,也不需要在 Service 中注入 CalculatorService

本文使用 Spring AI 1.1.8 的 .toolNames(...) 按名称解析 Function Bean。实现方式可参考 Spring AI 函数注册文档

3.3 测试函数调用

完成第 5 节的接口配置并启动应用后,发送以下请求,设置 Content-Type: application/json。示例端口为 9999。

接口:POST http://localhost:9999/ai/tools/calculate

json 复制代码
{"message": "请使用工具计算:10 加 20,再乘以 4。"}

期望结果为 120。正常执行两次工具时,可观察到类似日志:

text 复制代码
执行加法函数:10 + 20 = 30
执行乘法函数:30 × 4 = 120

单看答案 120 不能证明函数执行过,需要检查上述日志或在 Lambda 内设置断点。

4. 进阶案例:组合查询订单、物流与库存

加法与乘法展示了函数调用过程。实际开发中,更常见的需求是查询数据库或调用业务接口,例如订单状态、物流和库存。这些信息不能仅凭模型已有知识判断。这里使用固定的内存数据,便于运行和核对;更换数据源时,只需调整 Repository。

用户可以一次提出:"ORD1001 发货了吗?这个订单里的商品还有多少库存?"应用需要先查询订单和物流,再用订单返回的商品编号查询库存。

4.1 数据访问

订单 ORD1001 对应商品 SKU1001SKU1001 库存为 17,SKU1002 库存为 0。

StoreRepository.java
java 复制代码
package org.xingyue.ai.repository;

import java.util.Map;
import org.springframework.stereotype.Repository;

@Repository
public class StoreRepository {
    public record Order(String orderNo, String sku, String status) {}
    public record Stock(String sku, int available) {}

    private final Map<String, Order> orders = Map.of(
            "ORD1001", new Order("ORD1001", "SKU1001", "已发货"));
    private final Map<String, String> logistics = Map.of(
            "ORD1001", "包裹已到达杭州转运中心");
    private final Map<String, Stock> stocks = Map.of(
            "SKU1001", new Stock("SKU1001", 17),
            "SKU1002", new Stock("SKU1002", 0));

    public Order findOrder(String orderNo) {
        return orders.get(orderNo);
    }

    public String findLogistics(String orderNo) {
        return logistics.getOrDefault(orderNo, "暂无物流信息");
    }

    public Stock findStock(String sku) {
        return stocks.get(sku);
    }
}

4.2 业务逻辑

订单查询内部依次查询订单与物流;找不到订单时直接返回。库存为零与商品不存在分别处理。

StoreService.java
java 复制代码
package org.xingyue.ai.service;

import org.springframework.stereotype.Service;
import org.xingyue.ai.repository.StoreRepository;

@Service
public class StoreService {
    private final StoreRepository repository;

    public StoreService(StoreRepository repository) {
        this.repository = repository;
    }

    public String queryOrder(String orderNo) {
        var order = repository.findOrder(orderNo);
        if (order == null) {
            return "未找到订单:" + orderNo;
        }
        String logistics = repository.findLogistics(orderNo);
        return "订单号:" + order.orderNo() + ",商品:" + order.sku()
                + ",状态:" + order.status() + ",物流:" + logistics;
    }

    public String queryStock(String sku) {
        var stock = repository.findStock(sku);
        return stock == null ? "未找到商品:" + sku
                : "商品:" + stock.sku() + ",可售库存:" + stock.available();
    }
}

4.3 暴露工具

本例使用另一种定义方式:用 @Tool 标记普通方法,用 @ToolParam 描述参数,再通过 .tools(storeTools) 注册。这与 Function Bean 写法都属于函数调用,区别在于定义与注册方式。

工具层校验参数后委托 Service,并记录执行结果。业务方法无需了解模型或提示词。

StoreTools.java
java 复制代码
package org.xingyue.ai.tool;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import org.xingyue.ai.service.StoreService;

@Component
public class StoreTools {
    private static final Logger log = LoggerFactory.getLogger(StoreTools.class);
    private final StoreService storeService;

    public StoreTools(StoreService storeService) {
        this.storeService = storeService;
    }

    @Tool(description = "根据用户提供的订单号查询订单状态、商品编号和物流")
    public String queryOrder(@ToolParam(description = "订单号,例如 ORD1001") String orderNo) {
        if (orderNo == null || !orderNo.matches("ORD[0-9]{4}")) {
            return "订单号格式错误,请提供 ORD 加四位数字的订单号";
        }
        String result = storeService.queryOrder(orderNo);
        log.info("tool=queryOrder orderNo={} result={}", orderNo, result);
        return result;
    }

    @Tool(description = "根据商品编号查询当前可售库存")
    public String queryStock(@ToolParam(description = "商品编号,例如 SKU1001") String sku) {
        if (sku == null || !sku.matches("SKU[0-9]{4}")) {
            return "商品编号格式错误,请提供 SKU 加四位数字的编号";
        }
        String result = storeService.queryStock(sku);
        log.info("tool=queryStock sku={} result={}", sku, result);
        return result;
    }
}

这里有两种不同的调用关系:

  • 内部方法调用queryOrder 中查询订单、查询物流,是一次工具执行中的普通 Java 调用。
  • 多次工具调用 :模型先请求 queryOrder,再根据返回的商品编号请求 queryStock,是两个独立的工具调用。

4.4 注册业务函数

业务请求使用 .tools(storeTools),使模型可以调用 queryOrderqueryStock。系统提示词要求数据来自查询结果,并明确"先查订单,再根据商品编号查库存"的顺序。完整实现见第 5 节的 askStore 方法。

4.5 联合查询测试

接口:POST http://localhost:9999/ai/tools/store

json 复制代码
{"message": "帮我查一下 ORD1001 发货了吗?这个订单里的商品还有多少库存?"}

预期调用顺序:

text 复制代码
queryOrder("ORD1001")
    → 返回订单状态、物流、商品编号 SKU1001
queryStock("SKU1001")
    → 返回可售库存 17

正常调用时的日志示例:

text 复制代码
tool=queryOrder orderNo=ORD1001 result=订单号:ORD1001,商品:SKU1001,状态:已发货,物流:包裹已到达杭州转运中心
tool=queryStock sku=SKU1001 result=商品:SKU1001,可售库存:17

回答应包含"已发货""杭州转运中心""可售库存 17",具体措辞由模型决定。

5. 完整的 Service 与 HTTP 接口

计算接口通过 .toolNames(...) 启用 Function Bean;业务查询接口通过 .tools(...) 注册带有 @Tool 注解的方法。两者都只为相应请求提供调用能力。

ToolChatService.java

java 复制代码
package org.xingyue.ai.service;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import org.xingyue.ai.tool.StoreTools;

@Service
public class ToolChatService {
    private final ChatClient chatClient;
    private final StoreTools storeTools;

    public ToolChatService(ChatClient chatClient, StoreTools storeTools) {
        this.chatClient = chatClient;
        this.storeTools = storeTools;
    }

    public String calculate(String message) {
        return chatClient.prompt()
                .system("""
                        你是计算助手,支持整数加法和乘法。
                        用户需要计算时,请调用对应的函数。
                        缺少操作数或运算类型时,先向用户询问。
                        多步计算时,使用上一步的结果继续计算。
                        不支持的运算请明确说明。
                        根据函数返回的结果,用中文回答。
                        """)
                .user(message)
                .toolNames("addOperation", "mulOperation")
                .call()
                .content();
    }

    public String askStore(String message) {
        return chatClient.prompt()
                .system("""
                        你是订单与库存查询助手,使用中文回答。
                        订单、物流和库存信息必须依据工具查询结果,不得猜测。
                        缺少订单号或商品编号时,先向用户询问。
                        查询订单商品的库存时,先查订单,再使用返回的商品编号查库存。
                        未查到记录时如实说明;库存为零表示缺货,不表示商品不存在。
                        只支持查询,不执行下单、退款或取消订单。
                        """)
                .user(message)
                .tools(storeTools)
                .call()
                .content();
    }
}

.system(...) 为本次请求指定系统提示词,覆盖构建 ChatClient 时的默认系统文本。订单助手的规则有三个重点:数据必须来自工具、缺少编号先询问、查询库存时使用订单返回的商品编号。

在默认工具执行流程下,Spring AI 负责执行工具并回传结果。因此,一次 .call() 可能包含多轮模型请求。提示词可以引导调用,但不能代替程序校验或强制执行策略。

ToolChatController.java

java 复制代码
package org.xingyue.ai.controller;

import org.springframework.http.MediaType;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import org.xingyue.ai.service.ToolChatService;

@RestController
@RequestMapping("/ai/tools")
public class ToolChatController {
    private final ToolChatService service;

    public ToolChatController(ToolChatService service) {
        this.service = service;
    }

    public record AskRequest(String message) {}

    @PostMapping(value = "/calculate", produces = MediaType.TEXT_PLAIN_VALUE)
    public String calculate(@RequestBody AskRequest request) {
        return service.calculate(requireMessage(request));
    }

    @PostMapping(value = "/store", produces = MediaType.TEXT_PLAIN_VALUE)
    public String store(@RequestBody AskRequest request) {
        return service.askStore(requireMessage(request));
    }

    private String requireMessage(AskRequest request) {
        if (request == null || request.message() == null || request.message().isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "message 不能为空");
        }
        return request.message().trim();
    }
}

两个 HTTP 入口分别处理计算和业务咨询;业务入口可根据问题选择订单查询、库存查询,或组合使用。

6. 边界测试与调用核验

6.1 业务边界

以下问题均发送到 /ai/tools/store

message 验证点
查询 SKU1001 的库存 调用 queryStock,返回 17
SKU1002 还有货吗? 库存为 0,回答缺货
查一下 ORD9999 返回未找到订单,不编造物流
SKU9999 还有多少库存? 返回未找到商品
帮我查订单 先询问订单号,不猜测编号
帮我取消 ORD1001 说明不支持取消,不执行写操作

6.2 确认内部方法确实执行

先检查工具日志或在工具方法中设置断点,再核对参数与返回值。进一步验证时,可以将 SKU1001 的库存从 17 改为 23,重启应用后重新查询:工具日志与回答应一起变化。

工具本身的边界测试可以直接使用 JUnit,无需连接模型。例如:

java 复制代码
var tools = new StoreTools(new StoreService(new StoreRepository()));

assertEquals("商品:SKU1001,可售库存:17", tools.queryStock("SKU1001"));
assertEquals("商品:SKU1002,可售库存:0", tools.queryStock("SKU1002"));
assertEquals("未找到订单:ORD9999", tools.queryOrder("ORD9999"));
assertTrue(tools.queryStock(null).contains("格式错误"));

直接调用方法验证的是业务逻辑;通过 HTTP 请求并观察日志,验证的是模型到工具的完整链路。

7. 开发时注意什么

  • 工具描述写清用途与参数。 名称、描述和参数语义共同影响模型的选择。
  • 缺少业务数据时不要补写答案。 未找到订单、商品不存在、库存为零,应有明确区分。
  • 权限检查放在应用侧。 真实订单接口需从登录上下文校验订单归属,不能只靠提示词限制访问。
  • 写操作单独设计。 退款、取消订单等操作需要授权、确认和幂等控制,不应直接照搬只读查询。

System Prompt 规定如何回答,Tool Calling 提供可执行能力。将工具委托给业务 Service 后,订单、库存等现有接口就能成为 AI 助手的数据来源,而调用日志让结果有据可查。

相关推荐
minhuan11 分钟前
AI Infra全栈拆解:大模型应用背后的基础设施,算力集群、网络、存储与服务治理体系26.0
人工智能·架构·ai infra·ai基础设施·ai任务调度
江畔柳前堤12 分钟前
具身智能全景深度指南(2026年9月版):从“会聊天的AI“到“能干活的机器“
大数据·javascript·图像处理·人工智能·分布式·智慧城市·原型模式
麻花地21 分钟前
RealTalk_AI:基于 Qwen3.5 LiveTranslate 的实时双向语音翻译工具
人工智能
张继雁23 分钟前
不同类型磨削液使用安全注意事项
人工智能·深度学习·安全·业界资讯·远程工作·空间计算
Mr数据杨24 分钟前
用会计信息做GDP增速预测的时序回归实战解析
人工智能·数据分析·kaggle竞赛
神仙别闹25 分钟前
基于C++ MFC 实现的智慧公交系统
开发语言·c++·mfc
ClouGence26 分钟前
一句话直出可运行程序!GLM‑5.3 系列多任务实测
人工智能·agent·ai编程
秋田君30 分钟前
Qt_webSocket协议编程实战
开发语言·qt·websocket
Mr数据杨31 分钟前
企业年报文本变化预测实战 从文本回归到信息披露分析
人工智能·数据分析·kaggle竞赛