函数调用
大约 6 分钟
第十章:函数调用与工具集成
10.1 函数调用机制
函数调用(也常被称为 Tool Calling)解决的是一个非常实际的问题:
- 语言模型擅长“推理与表达”,但不擅长访问实时数据(天气、订单、库存)与执行动作(创建工单、下单、发送消息)
- 我们希望模型在需要外部信息或动作时,能够“提出调用工具的建议”,由后端去执行,再把结果交还给模型组织成最终答案
在 Spring AI 中,函数调用通常由三部分组成:
- 工具/函数的“能力描述”(名称、说明、参数 JSON Schema)
- 工具执行逻辑(你的 Java 方法)
- 模型返回的工具调用请求(包含函数名与参数),以及应用将执行结果回传给模型
10.2 外部工具集成:用 Schema 让模型“按规矩”传参
函数调用能否稳定,关键在于参数约束:
- 参数要尽量“结构化”:不要让模型把多个字段塞进一句自然语言
- 参数要有“范围”:字符串长度、枚举值、是否必填
- 工具要有“可解释的失败”:参数校验失败应该给出清晰的错误,便于模型纠正参数并重试
10.3 自定义工具开发:订单查询 + 天气建议(可复制运行)
这一节给出一个可直接跑起来的最小示例:
/tools/ask:用户输入自然语言问题(例如“帮我查一下订单 A1001 的状态,以及北京今天适合跑步吗?”)- 模型自动选择调用:
getOrderStatus(orderId)getWeatherAdvice(city)
- 你的后端实现这两个工具方法并返回结构化结果
10.3.1 pom.xml(Maven)
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>spring-ai-tools-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<spring-boot.version>3.2.0</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-qwen-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-deepseek-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>10.3.2 application.yml(三家模型任选其一,也支持 profile 切换)
server:
port: 8080
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY:}
chat:
options:
model: ${OPENAI_MODEL:gpt-4o-mini}
qwen:
api-key: ${QWEN_API_KEY:}
chat:
options:
model: ${QWEN_MODEL:qwen-plus}
deepseek:
api-key: ${DEEPSEEK_API_KEY:}
chat:
options:
model: ${DEEPSEEK_MODEL:deepseek-chat}10.3.3 启动类
package com.example.toolscalling;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ToolsDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ToolsDemoApplication.class, args);
}
}10.3.4 统一返回对象(Controller 不直接返回实体)
package com.example.toolscalling.api;
public record ApiResponse<T>(boolean success, T data, String error) {
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(true, data, null);
}
public static <T> ApiResponse<T> fail(String error) {
return new ApiResponse<>(false, null, error);
}
}10.3.5 工具:订单查询(模拟)
package com.example.toolscalling.domain;
public enum OrderStatus {
CREATED,
PAID,
SHIPPED,
DELIVERED,
CANCELED
}package com.example.toolscalling.service;
import com.example.toolscalling.domain.OrderStatus;
import java.util.Map;
import java.util.Optional;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private static final Map<String, OrderStatus> ORDER_STORE = Map.of(
"A1001", OrderStatus.SHIPPED,
"A1002", OrderStatus.DELIVERED,
"A1003", OrderStatus.CANCELED
);
public Optional<OrderStatus> findStatus(String orderId) {
return Optional.ofNullable(ORDER_STORE.get(orderId));
}
}10.3.6 工具:天气建议(模拟)
package com.example.toolscalling.service;
import java.util.Locale;
import org.springframework.stereotype.Service;
@Service
public class WeatherService {
public String adviceForCity(String city) {
String normalized = city == null ? "" : city.trim().toLowerCase(Locale.ROOT);
if (normalized.isBlank()) {
return "缺少城市信息,无法给出建议。";
}
if (normalized.contains("北京") || normalized.contains("beijing")) {
return "北京:建议傍晚跑步,注意补水与防风。";
}
return city + ":建议根据当地气温与空气质量选择室外/室内运动。";
}
}10.3.7 工具编排:把工具“声明”给模型,并实现参数校验 + 审计
package com.example.toolscalling.service;
import com.example.toolscalling.domain.OrderStatus;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.springframework.stereotype.Service;
@Service
public class ToolOrchestrationService {
private final OrderService orderService;
private final WeatherService weatherService;
private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
public ToolOrchestrationService(OrderService orderService, WeatherService weatherService) {
this.orderService = orderService;
this.weatherService = weatherService;
}
public Map<String, Object> getOrderStatus(Map<String, Object> arguments) {
GetOrderStatusArgs args = new GetOrderStatusArgs((String) arguments.get("orderId"));
validate(args);
Optional<OrderStatus> status = orderService.findStatus(args.orderId());
return Map.of(
"orderId", args.orderId(),
"found", status.isPresent(),
"status", status.map(Enum::name).orElse("UNKNOWN")
);
}
public Map<String, Object> getWeatherAdvice(Map<String, Object> arguments) {
GetWeatherAdviceArgs args = new GetWeatherAdviceArgs((String) arguments.get("city"));
validate(args);
String advice = weatherService.adviceForCity(args.city());
return Map.of(
"city", args.city(),
"advice", advice
);
}
private void validate(Object args) {
Set<ConstraintViolation<Object>> violations = validator.validate(args);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
public record GetOrderStatusArgs(
@NotBlank
@Pattern(regexp = "^[A-Z]\\d{4}$", message = "orderId 必须类似 A1001")
String orderId
) {}
public record GetWeatherAdviceArgs(
@NotBlank
String city
) {}
}10.3.8 Controller:把工具注册给 ChatClient(示例使用 FunctionCallback)
下面示例延续前面章节的写法,使用 FunctionCallback + Schema 的方式声明工具参数结构。
package com.example.toolscalling.api;
import com.example.toolscalling.service.ToolOrchestrationService;
import java.util.Map;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.Schema;
import org.springframework.ai.model.function.SchemaProperty;
import org.springframework.ai.model.function.SchemaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ToolCallingController {
private final ChatClient chatClient;
private final ToolOrchestrationService tools;
public ToolCallingController(ChatClient chatClient, ToolOrchestrationService tools) {
this.chatClient = chatClient;
this.tools = tools;
}
@GetMapping("/tools/ask")
public ApiResponse<String> ask(@RequestParam String question) {
FunctionCallback getOrderStatus = new FunctionCallback(
"getOrderStatus",
"查询订单状态。参数 orderId 形如 A1001。",
new Schema(SchemaType.OBJECT)
.addProperty("orderId", SchemaProperty.of(SchemaType.STRING, "订单号,例如 A1001"))
.addRequired("orderId"),
tools::getOrderStatus
);
FunctionCallback getWeatherAdvice = new FunctionCallback(
"getWeatherAdvice",
"获取某城市的运动建议(模拟天气)。参数 city 为中文城市名。",
new Schema(SchemaType.OBJECT)
.addProperty("city", SchemaProperty.of(SchemaType.STRING, "城市,例如 北京"))
.addRequired("city"),
tools::getWeatherAdvice
);
String answer = chatClient.prompt()
.system("你是严谨的助理。遇到需要外部信息时,优先调用已提供的函数。")
.user(question)
.functions(getOrderStatus, getWeatherAdvice)
.call()
.content();
return ApiResponse.ok(answer);
}
}10.4 安全性考虑(必须读)
函数调用最大的风险不是“模型胡说”,而是“模型让你做了不该做的事”。最常见的坑:
- Prompt 注入:用户试图让模型绕开规则,调用高危工具
- 越权:模型在未授权的上下文里调用“查询他人订单”“退款”等能力
- 参数投毒:模型传入超长参数、SQL 注入片段、路径穿越片段,触发后端漏洞
建议的最低安全基线:
- 工具白名单:只暴露你允许模型使用的函数,不要动态拼函数名
- 参数校验:所有工具参数都要做校验(长度、格式、枚举、范围),不通过直接失败
- 业务鉴权:工具执行前,按当前用户身份做 RBAC/ABAC 校验
- 审计日志:记录“谁在何时通过哪个模型调用了哪个工具(参数脱敏)”
- 超时与限流:工具执行必须有超时,防止卡死;模型调用也要限流防止 429 雪崩
10.5 运行方式
- 设置任意一家模型的 API Key(任选其一即可):
export OPENAI_API_KEY="xxx"
# 或
export QWEN_API_KEY="xxx"
# 或
export DEEPSEEK_API_KEY="xxx"- 启动 Spring Boot:
mvn spring-boot:run- 访问:
http://localhost:8080/tools/ask?question=帮我查一下订单A1001的状态,并给出北京今天是否适合跑步的建议如果你看到模型能同时给出订单状态与跑步建议,说明函数调用链路已跑通。 点击这里👇🏻获取:100万QPS短链系统、复杂的商城微服务系统、智能翻译助手AI Agent、SaaS点餐系统、刷题吧小程序、商城系统、秒杀系统、AI项目、代码生成神器、苏三demo项目、智能天气播报AI Agent、智能代码审查AI Agent等 10 个项目的:项目源代码、开发教程和技术答疑
