函数调用
大约 5 分钟
第五章:函数调用与工具集成
5.1 函数调用解决的是什么问题?
大模型擅长推理与表达,但不擅长两类事情:
- 获取实时数据:订单状态、库存、用户信息、配置中心数据
- 执行动作:创建工单、发短信、调用支付、写入数据库
函数调用(也称 Tool Calling)要做的就是:让模型在需要外部信息或动作时,提出“调用哪个工具 + 参数是什么”,由你的后端去执行,再把结果回传给模型生成最终答案。
5.2 关键原则:模型只负责“提议”,系统负责“执行”
生产系统里,工具调用稳定与安全的核心不是“模型多聪明”,而是你把边界定得够清楚:
- 工具必须白名单:只暴露你允许的工具
- 参数必须校验:长度、格式、枚举、范围
- 执行必须鉴权:按当前用户身份做权限判断
- 结果必须审计:记录工具调用(脱敏)与失败原因
5.3 可复制运行示例:订单查询 + 运费估算
这个示例提供一个接口 /tools/ask:
- 用户问:“帮我查订单 A1001 状态,并估算到上海的运费”
- 模型自动决定调用:
getOrderStatus(orderId)estimateShippingFee(city, weightKg)
5.3.1 pom.xml(Maven)
<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>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
</dependencies>5.3.2 application.yml
spring:
ai:
dashscope:
api-key: ${AI_DASHSCOPE_API_KEY:}
chat:
options:
model: ${DASHSCOPE_CHAT_MODEL:qwen-plus}
temperature: 0.15.3.3 统一返回对象
package com.example.saa.api.dto;
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);
}
}5.3.4 订单服务(模拟)
package com.example.saa.domain;
public enum OrderStatus {
CREATED,
PAID,
SHIPPED,
DELIVERED,
CANCELED
}package com.example.saa.service;
import com.example.saa.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));
}
}5.3.5 工具编排:参数校验 + 执行
package com.example.saa.service;
import com.example.saa.domain.OrderStatus;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import java.math.BigDecimal;
import java.math.RoundingMode;
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 Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
public ToolOrchestrationService(OrderService orderService) {
this.orderService = orderService;
}
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> estimateShippingFee(Map<String, Object> arguments) {
EstimateShippingFeeArgs args = new EstimateShippingFeeArgs(
(String) arguments.get("city"),
toInt(arguments.get("weightKg"))
);
validate(args);
BigDecimal base = BigDecimal.valueOf(8);
BigDecimal perKg = BigDecimal.valueOf(3);
BigDecimal fee = base.add(perKg.multiply(BigDecimal.valueOf(args.weightKg())));
if (args.city().contains("上海")) {
fee = fee.multiply(BigDecimal.valueOf(0.9));
}
return Map.of(
"city", args.city(),
"weightKg", args.weightKg(),
"fee", fee.setScale(2, RoundingMode.HALF_UP).toPlainString(),
"currency", "CNY"
);
}
private Integer toInt(Object raw) {
if (raw == null) {
return null;
}
if (raw instanceof Integer i) {
return i;
}
if (raw instanceof Number n) {
return n.intValue();
}
return Integer.valueOf(String.valueOf(raw));
}
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 EstimateShippingFeeArgs(
@NotBlank String city,
@Min(1) @Max(50) Integer weightKg
) {}
}5.3.6 Controller:把工具注册给 ChatClient(FunctionCallback + Schema)
package com.example.saa.api;
import com.example.saa.api.dto.ApiResponse;
import com.example.saa.service.ToolOrchestrationService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.model.function.FunctionCallback;
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 estimateShippingFee = new FunctionCallback(
"estimateShippingFee",
"估算运费。参数 city 为城市名,weightKg 为重量(kg)。",
new Schema(SchemaType.OBJECT)
.addProperty("city", SchemaProperty.of(SchemaType.STRING, "城市,例如 上海"))
.addProperty("weightKg", SchemaProperty.of(SchemaType.INTEGER, "重量(kg),例如 2"))
.addRequired("city")
.addRequired("weightKg"),
tools::estimateShippingFee
);
String answer = chatClient.prompt()
.system("你是严谨的电商助手。遇到需要实时信息时,优先调用已提供的函数。")
.user(question)
.functions(getOrderStatus, estimateShippingFee)
.call()
.content();
return ApiResponse.ok(answer);
}
}5.4 必须读:安全基线
函数调用最大的风险不是“模型胡说”,而是“模型让系统做了不该做的事”。建议最小基线:
- 工具白名单:只注册固定工具集合,不要根据用户输入动态拼工具
- 参数校验:所有工具输入都校验,拒绝超长、非法格式与边界值
- 业务鉴权:执行工具前按用户身份做 RBAC/ABAC
- 审计与追责:记录工具调用元信息(脱敏后参数、耗时、结果状态)
- 超时与限流:工具执行与模型调用都要有超时/并发限制
5.5 本章小结
你已经把模型从“只会聊天”升级为“能调用业务能力”,并且建立了最关键的工程化边界:模型只提议、系统负责执行与治理。
下一章我们会解决另一个生产痛点:让模型输出可稳定解析的数据结构(JSON),用于入库、风控、路由与后续自动化流程。 点击这里👇🏻获取:100万QPS短链系统、复杂的商城微服务系统、智能翻译助手AI Agent、SaaS点餐系统、刷题吧小程序、商城系统、秒杀系统、AI项目、代码生成神器、苏三demo项目、智能天气播报AI Agent、智能代码审查AI Agent等 10 个项目的:项目源代码、开发教程和技术答疑
