Spring AI 函数调用实战——让大模型调用你的 Java 服务
Spring AI 函数调用实战——让大模型调用你的 Java 服务
适读人群:Java 后端工程师、Spring AI 入门者 | 阅读时长:约16分钟 | 核心价值:掌握 Function Calling 让 AI 真正有"手"能干活
上周做了一个智能客服系统的需求评审,产品经理的需求文档写得很美好:用户输入"我的订单到哪了",AI 直接回答"您的订单已发货,预计明天送达,快递单号是 SF123456789"。
我直接问他:你觉得大模型怎么知道用户的订单状态?
他愣了一下:"你们不是接了 AI 吗?"
对,接了 AI,但 AI 的训练数据里没有你们公司的订单数据。要让 AI 能查订单,需要 Function Calling。
Function Calling 是什么
Function Calling(也叫 Tool Calling)是大模型的一个能力:当用户的问题需要调用外部工具才能回答时,模型不会瞎编,而是告诉你"我需要调用 X 工具,参数是 Y",由你去实际执行,拿到结果后再返回给模型,模型再给出最终回答。
整个流程:
用户提问 → 模型判断需要调用工具 → 返回工具调用请求
→ 你的代码执行工具 → 把结果返回给模型 → 模型生成最终回答Spring AI 把这个流程完全封装好了,你只需要定义工具,剩下的框架处理。
定义工具:三种方式
方式一:@Bean + Function 接口(推荐)
// 定义输入输出的 record
public record OrderQueryRequest(
@JsonProperty(required = true, value = "orderId")
@JsonPropertyDescription("订单ID,格式如 ORD-20240101-001")
String orderId
) {}
public record OrderQueryResponse(
String orderId,
String status,
String trackingNumber,
String estimatedDelivery
) {}
// 实际的业务服务
@Service
public class OrderService {
public OrderQueryResponse queryOrder(String orderId) {
// 这里调用真实的订单数据库/接口
// 示例数据
return new OrderQueryResponse(
orderId,
"已发货",
"SF" + orderId.hashCode(),
"2025-01-15"
);
}
}
// 注册为 Spring Bean,Spring AI 自动识别
@Configuration
public class ToolConfig {
@Autowired
private OrderService orderService;
@Bean
@Description("查询订单状态和物流信息,当用户询问订单到哪了、订单状态时调用")
public Function<OrderQueryRequest, OrderQueryResponse> queryOrderStatus() {
return request -> orderService.queryOrder(request.orderId());
}
@Bean
@Description("查询商品库存数量,当用户询问某商品是否有货时调用")
public Function<StockQueryRequest, StockQueryResponse> queryStock() {
return request -> {
// 查库存逻辑
int stock = 100; // 示例
return new StockQueryResponse(request.productId(), stock, stock > 0);
};
}
}方式二:@Tool 注解(Spring AI 1.0 新特性)
@Component
public class WeatherTools {
@Tool(description = "获取指定城市的实时天气信息")
public WeatherInfo getWeather(
@ToolParam(description = "城市名称,如北京、上海") String city) {
// 调用天气 API
return weatherApiClient.getWeather(city);
}
@Tool(description = "获取未来7天天气预报")
public List<WeatherForecast> getWeatherForecast(
@ToolParam(description = "城市名称") String city,
@ToolParam(description = "预报天数,1-7") int days) {
return weatherApiClient.getForecast(city, days);
}
}方式三:动态工具(运行时注册)
// 适合工具定义从数据库/配置中心动态加载的场景
ToolCallback dynamicTool = FunctionToolCallback.builder(
"customTool",
(String input) -> "处理结果: " + input
)
.description("动态注册的工具")
.inputType(String.class)
.build();
chatClient.prompt()
.user("...")
.tools(dynamicTool)
.call()
.content();在 ChatClient 中使用工具
@RestController
@RequestMapping("/customer-service")
public class CustomerServiceController {
private final ChatClient chatClient;
private final WeatherTools weatherTools;
public CustomerServiceController(
ChatClient.Builder builder,
WeatherTools weatherTools) {
this.weatherTools = weatherTools;
this.chatClient = builder
.defaultSystem("""
你是一个智能客服助手。
当用户询问订单状态时,使用 queryOrderStatus 工具查询。
当用户询问天气时,使用天气工具查询。
回答要简洁友好,用中文回复。
""")
// 注册 Bean 方式定义的工具
.defaultTools("queryOrderStatus", "queryStock")
.build();
}
@PostMapping("/chat")
public String chat(
@RequestParam String message,
@RequestParam String sessionId) {
return chatClient.prompt()
.user(message)
// 也可以在这里动态添加工具
.tools(weatherTools)
.call()
.content();
}
}测试:
POST /customer-service/chat
message=我的订单ORD-20240101-001到哪了&sessionId=user123
响应(实测耗时 2.1s,包含一次工具调用):
您的订单 ORD-20240101-001 目前状态为「已发货」,快递单号 SF-1234567,预计明天(1月15日)送达,请注意查收。完整的客服系统示例
@Service
@Slf4j
public class IntelligentCustomerService {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
public IntelligentCustomerService(
ChatClient.Builder builder,
ChatMemory chatMemory,
OrderService orderService,
ProductService productService) {
this.chatMemory = chatMemory;
this.chatClient = builder
.defaultSystem("""
你是「优购商城」的智能客服小优。
你能做的事:
1. 查询用户订单状态(需要订单号)
2. 查询商品库存
3. 回答常见问题(退换货政策、配送时效等)
注意事项:
- 如果用户没提供订单号,先礼貌询问
- 无法解决的问题,告知用户可以联系人工客服:400-xxx-xxxx
- 始终保持友好耐心的语气
""")
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory),
new SimpleLoggerAdvisor()
)
.defaultTools("queryOrderStatus", "queryStock", "getReturnPolicy")
.build();
}
public Flux<String> streamChat(String userId, String message) {
log.info("用户 {} 发送消息: {}", userId, message);
return chatClient.prompt()
.user(message)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, userId))
.stream()
.content()
.doOnComplete(() -> log.info("用户 {} 对话完成", userId));
}
}踩坑实录
坑一:工具调用死循环
现象:某些 Prompt 下,模型在工具调用之间反复跳转,消耗了大量 token,最后抛超时异常。
原因:System Prompt 描述模糊,模型不确定调哪个工具,就两个轮流调。具体是这样:查订单工具和查物流工具两个独立的 Bean,模型调完订单查询,觉得还不够,又调物流查询,物流查询结果又让它想再调订单……
解法:合并功能相近的工具,或在工具描述里明确互斥关系:
@Bean
@Description("查询订单完整信息,包含订单状态和物流跟踪,订单和物流信息都在这里,不需要调用其他工具")
public Function<OrderQueryRequest, OrderDetailResponse> queryOrderDetail() {
// 合并订单+物流查询
}坑二:工具参数解析失败
现象:模型调工具时传了 order_id(下划线),但我定义的是 orderId(驼峰),导致参数为 null。
原因:JSON 序列化时字段名大小写问题。模型生成的 JSON Key 遵循其理解的命名规范,不一定跟你的 Java 字段名一致。
解法:用 @JsonProperty 显式指定字段名,同时开启 Jackson 的宽容模式:
public record OrderQueryRequest(
@JsonProperty(value = "order_id", required = true)
@JsonAlias({"orderId", "order_id", "id"}) // 兼容多种命名
@JsonPropertyDescription("订单ID")
String orderId
) {}坑三:工具调用结果 token 超限
现象:查询接口返回了一个 500 行的 JSON,导致整个对话 context 超限(4096 tokens),模型返回错误。
原因:工具返回的数据没有过滤,全量传给了模型。
解法:工具返回值要精简,只返回模型需要的信息:
@Bean
@Description("查询商品列表,支持分页,每次最多返回10条")
public Function<ProductSearchRequest, ProductSearchResponse> searchProducts() {
return request -> {
List<Product> products = productService.search(request);
// 只返回关键字段,不返回图片URL、详细描述等大字段
return new ProductSearchResponse(
products.stream()
.map(p -> new ProductSummary(p.getId(), p.getName(), p.getPrice()))
.collect(toList()),
products.size()
);
};
}工具安全性
生产环境必须考虑:
@Bean
@Description("查询订单状态")
public Function<OrderQueryRequest, OrderQueryResponse> queryOrderStatus(
HttpServletRequest httpRequest) {
return request -> {
// 1. 鉴权:从当前用户上下文获取 userId
String currentUserId = SecurityContextHolder.getContext()
.getAuthentication().getName();
// 2. 权限校验:用户只能查自己的订单
Order order = orderService.findById(request.orderId());
if (!order.getUserId().equals(currentUserId)) {
throw new AccessDeniedException("无权限查询此订单");
}
// 3. 敏感信息脱敏:手机号、地址部分隐藏
return OrderQueryResponse.builder()
.orderId(order.getId())
.status(order.getStatus())
.phone(maskPhone(order.getReceiverPhone())) // 138****1234
.build();
};
}总结
Function Calling 是让 AI 真正实用的关键能力,有了它,AI 才能:
- 查实时数据(订单、库存、天气)
- 执行操作(发邮件、创建工单、发送通知)
- 调用内部服务(你公司的任何接口都能暴露给 AI)
Spring AI 把 Function Calling 的实现复杂度大幅降低,你只需要定义好 Bean 和描述,框架处理一切。
关键是工具描述写好,这直接决定模型是否能正确选择工具和传参。描述要明确说清:什么时候调、需要什么参数、返回什么。
下一篇讲 Spring AI + RAG,让 AI 能搜索你的内部文档,实现真正的企业知识库。
延伸:Function Calling 的进阶场景
掌握了基础的工具定义之后,有几个进阶场景值得单独说说。
场景一:工具返回结构化数据
不一定非得返回字符串,工具也可以返回 JSON 结构,让模型处理更丰富的信息:
@Bean
@Description("获取用户的最近订单列表,返回最多5条最新订单")
public Function<UserOrderRequest, List<OrderSummary>> getUserRecentOrders() {
return request -> {
List<Order> orders = orderService.findByUserId(request.userId(), 5);
return orders.stream().map(order -> new OrderSummary(
order.getId(),
order.getStatus(),
order.getAmount(),
order.getCreatedAt().toString()
)).collect(toList());
};
}模型收到这个列表后,会自动把多条订单信息整理成自然语言回复给用户,不需要你手动拼接文字。
场景二:工具调用结果触发进一步操作
工具不只是查询,也可以执行操作。比如一个"确认退货申请"的工具:
@Bean
@Description("创建退货申请。调用此工具之前,必须先向用户确认退货原因和退货商品,获得用户明确同意后再调用")
public Function<ReturnApplicationRequest, ReturnApplicationResponse> createReturnApplication() {
return request -> {
// 验证订单状态
Order order = orderService.findById(request.orderId());
if (!order.isEligibleForReturn()) {
throw new ToolExecutionException("订单 " + request.orderId() + " 不满足退货条件:" + order.getNonReturnReason());
}
// 创建退货单
String returnId = returnService.create(
request.orderId(),
request.reason(),
request.items()
);
return new ReturnApplicationResponse(returnId, "退货申请已提交,预计 3 个工作日内处理");
};
}注意工具描述里写了"必须先向用户确认"——这个约束会被模型理解,它会在调用工具之前先跟用户核实,而不是直接执行操作。
场景三:并行工具调用
GPT-4o 和 Claude 3.5 支持并行调用多个工具(Parallel Tool Calling),在需要同时查询多个数据源时效率翻倍:
比如用户问"我最近买的三件衣服的退货政策分别是什么",模型会同时触发三个订单查询,而不是串行查询,总时间约等于一次查询的时间。
Spring AI 默认支持这个功能,不需要额外配置,但要注意你的工具方法要是线程安全的。
场景四:工具调用的审批流
对于敏感操作(比如退款超过 1000 元),可以加一个人工审批环节:
@Bean
@Description("为用户申请退款。退款金额超过1000元时,需要进入人工审批流程,不能自动执行")
public Function<RefundRequest, RefundResponse> applyRefund() {
return request -> {
if (request.amount().compareTo(BigDecimal.valueOf(1000)) > 0) {
// 创建审批工单,等待人工处理
String ticketId = approvalService.createTicket(
request.orderId(),
request.amount(),
request.reason()
);
return new RefundResponse(
"PENDING_APPROVAL",
"退款申请已提交人工审批,工单号:" + ticketId + ",预计1个工作日内处理"
);
}
// 小额退款自动处理
refundService.processRefund(request.orderId(), request.amount());
return new RefundResponse("SUCCESS", "退款成功,预计1-3个工作日到账");
};
}这样 AI 客服能处理大部分常规退款,超额的自动转人工,既提效又控风险。
工具调用的设计哲学
做了一段时间 Function Calling 相关的系统,我总结了几个设计原则:
原则一:工具要"原子化"
每个工具只做一件事,不要做多功能工具。"查询订单并发送通知"这种工具很不好,因为模型不知道在什么情况下该发通知、在什么情况下不该发。拆成两个工具:查询订单、发送通知,让模型自己判断。
原则二:工具描述是最重要的代码
工具的 Java 实现逻辑可以后改,但工具描述写得不好,模型就不知道什么时候调、怎么调。花在工具描述上的时间和花在业务逻辑上的时间,应该差不多。
原则三:工具失败要优雅降级
工具抛异常不是罪,但要确保异常信息能帮助模型做出下一步判断。"出错了"是没用的错误,"订单 X 不存在,请检查订单号是否正确"才有用。
原则四:敏感操作要二次确认
任何不可逆的操作(删除、退款、取消),都要在工具描述里要求模型先确认用户意图。"先询问用户是否确认"这种约束,模型基本都能遵守。
这四个原则看起来简单,但真正做系统的时候容易忽视,等出了问题再回来改,成本很高。
