承上:上一篇我们给AI装了一只手------它会查天气了。但一个工具哪够?真实业务系统里,订单、库存、物流、用户、优惠券......几十个服务等着被调用。今天,我们要让AI学会面对多个工具时,自己决策"该用哪个"。
1. 问题场景:AI的"选择困难症"
先回顾上一篇的架构:
用户问天气 → AI判断需要调工具 → 调用getWeather → 返回结果
这是单工具场景,决策很简单:要么调,要么不调。
但真实业务是这样的:
用户说:"我上周买的那个手机怎么还没到?帮我催一下。"
这句话里隐含了三个需求:
- 查订单:找到上周买的手机订单
- 查物流:看看快递到哪了
- 催单:如果物流卡住了,发起催单流程
AI需要自己判断:先调哪个?传什么参数?拿到第一个结果后,要不要继续调第二个?
这就是多工具协作的核心问题:AI如何根据用户意图,自主编排工具调用顺序。
2. 战前准备:三个Tool工具
java
package com.yunxi.ai.tools;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Component
public class BussinessTools {
@Tool(description = "根据关键词查询用户订单,关键词可以是商品名称或订单号")
public String queryOrders(
@ToolParam(description = "查询关键词,例如'手机'、'ORD001'") String keyword) {
return queryOrdersMock(keyword);
}
@Tool(description = "根据订单号查询订单详情")
public String getOrderById(
@ToolParam(description = "订单号,格式为ORD开头加3位数字,例如ORD001") String orderId) {
return getOrderByIdMock(orderId);
}
// ========== 库存相关 ==========
@Tool(description = "查询商品库存,用于判断商品是否有货")
public String checkStock(
@ToolParam(description = "商品名称,例如'iPhone 16'、'AirPods Pro'") String productName) {
return checkStockMock(productName);
}
// ========== 物流相关 ==========
@Tool(description = "查询订单的物流配送信息")
public String queryTracking(
@ToolParam(description = "订单号,例如ORD001") String orderId) {
return queryTrackingMock(orderId);
}
// 模拟订单数据库
private final List<Map<String, Object>> orders = List.of(
Map.of("id", "ORD001", "product", "iPhone 16", "amount", 6999.00,
"status", "已发货", "createTime", "2026-07-20"),
Map.of("id", "ORD002", "product", "AirPods Pro", "amount", 1499.00,
"status", "待发货", "createTime", "2026-07-22"),
Map.of("id", "ORD003", "product", "MacBook Pro 16寸", "amount", 19999.00,
"status", "已签收", "createTime", "2026-07-15")
);
public String queryOrdersMock(String keyword) {
List<Map<String, Object>> result = orders.stream()
.filter(o -> o.get("product").toString().contains(keyword)
|| o.get("id").toString().contains(keyword))
.collect(Collectors.toList());
if (result.isEmpty()) {
return "未找到包含「" + keyword + "」的订单";
}
return "查询结果:" + result.toString();
}
public String getOrderByIdMock(String orderId) {
return orders.stream()
.filter(o -> o.get("id").equals(orderId))
.findFirst()
.map(Object::toString)
.orElse("订单" + orderId + "不存在");
}
private final Map<String, String> tracking = Map.of(
"ORD001", "SF1234567890 - 已到达【北京朝阳分拨中心】,预计7月25日派送",
"ORD003", "YT9876543210 - 已于7月17日签收,签收人:本人"
);
public String queryTrackingMock(String orderId) {
String info = tracking.get(orderId);
if (info == null) {
return "订单" + orderId + "暂无物流信息,可能尚未发货";
}
return "订单" + orderId + "物流信息:" + info;
}
private final Map<String, Integer> inventory = Map.of(
"iPhone 16", 128,
"AirPods Pro", 0, // 缺货!
"MacBook Pro 16寸", 35,
"Apple Watch", 42
);
public String checkStockMock(String productName) {
Integer stock = inventory.getOrDefault(productName, -1);
if (stock == -1) {
return "未找到商品「" + productName + "」的库存信息";
}
if (stock == 0) {
return "商品「" + productName + "」当前缺货,预计下周补货";
}
return "商品「" + productName + "」当前库存:" + stock + "件";
}
}
java
package com.yunxi.ai.service;
import com.yunxi.ai.tools.BussinessTools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
public class BusinessService {
private final ChatClient chatClient;
private final BussinessTools bussinessTools;
public BusinessService(ChatClient.Builder builder, BussinessTools bussinessTools) {
this.chatClient = builder.build();
this.bussinessTools = bussinessTools;
}
public String businessChat(String message) {
return chatClient.prompt()
.system("你是一个专业的助手,可以回答用户的问题")
.user(message)
.tools(bussinessTools)
.call()
.content();
}
}
3. 实战测试:AI的智能调度能力
3.1. 场景1:单一意图,精准匹配
css
用户:帮我查一下AirPods Pro有没有货
AI调用链:[checkStock("AirPods Pro")]
AI回复:商品「AirPods Pro」当前缺货,预计下周补货。
建议您考虑其他型号或设置到货通知。
AI只调了 checkStock,没有多余动作。


3.2. 场景2:模糊查询,先搜再定位
scss
用户:我上周买的那个耳机发货了没?
AI调用链:
1. [queryOrders("耳机")] → 未找到包含「耳机」的订单
2. [queryOrders("AirPods")] → 找到ORD002(AirPods Pro)
3. [queryTracking("ORD002")] → 暂无物流信息
AI回复:您购买的AirPods Pro(订单号ORD002)目前还未发货,
预计我们会尽快处理。需要我帮您催单吗?
AI先模糊搜索订单,拿到订单号后再查物流。两步骤,自动编排。


3.3. 场景3:多意图,链式调用
css
用户:帮我查一下iPhone 16有没有货,有的话顺便看看我的订单到哪了
AI调用链:
1. [checkStock("iPhone 16")] → 库存128件
2. [queryOrders("iPhone 16")] → 找到ORD001
3. [queryTracking("ORD001")] → 已到达北京朝阳分拨中心
AI回复:好消息!iPhone 16目前有货(库存128件)。
您的订单ORD001已到达北京朝阳分拨中心,预计7月25日派送。
需要我帮您做什么吗?
AI自动拆解了用户的复合需求,调了三个不同工具,最后整合成一个回复。


3.4. 场景4:闲聊,不调用任何工具
markdown
用户:你好,你是谁?
AI调用链:无
AI回复:您好!我是您的AI业务助手,可以帮您查询订单、
库存和物流信息。请问有什么可以帮您的?
AI判断这不是一个需要调用工具的问题,直接自己回答。


4. 踩坑实战:多工具调用的三大陷阱
4.1. 坑1:AI反复调用同一个工具(死循环)
场景复现:
erlang
用户:查一下ORD999的物流
AI调用链:
1. [queryTracking("ORD999")] → "订单ORD999不存在"
2. [queryOrders("ORD999")] → "未找到订单"
3. [queryTracking("ORD999")] → "订单ORD999不存在" ← 又调了一次!
4. [queryOrders("ORD999")] → "未找到订单" ← 还在调!
...无限循环...
为什么? AI的提示词里说"要帮用户解决问题",它拿不到满意结果就一直尝试。
解决方案:设置最大工具调用次数。
yaml
spring:
ai:
openai:
chat:
options:
max-tool-calls: 5 # 最多调用5次工具,超过就停止
或者在代码里:
scss
return chatClient.prompt()
.user(question)
.tools(businessTools)
.options(options -> options.setMaxToolCalls(5))
.call()
.content();
4.2. 坑2:AI编造不存在的工具
场景复现:
erlang
用户:帮我给ORD001的收货人发个短信提醒
AI:好的,我来调用 sendSms 工具...
但你的 BusinessTools 里根本没有 sendSms 这个方法。
为什么? AI的"幻觉"------它根据语义猜测你可能有的工具。
解决方案:在SystemMessage里约束行为。
.system("你只能使用已注册的工具。如果某个功能没有对应的工具,请告知用户当前不支持该操作,不要编造或假设工具的存在。")
4.3. 坑3:AI传的参数"差不多对"
场景复现:
css
用户:查一下苹果手机的库存
AI调用:[checkStock("苹果手机")]
返回:未找到商品「苹果手机」的库存信息
AI传了"苹果手机",但数据库里的商品名是"iPhone 16"。
解决方案:
- 方法1 :在
@ToolParam里加更精确的描述 - 方法2 :业务端做模糊匹配(在
checkStock里加分词搜索) - 方法3 :先让AI调
queryOrders("苹果")找到具体商品名,再调checkStock
方法3最靠谱------让AI先用搜索工具定位,再用精确值查询。
5. 本篇小结
这一篇我们把AI从"单线程"升级到了"多线程调度":
| 能力 | 单工具 | 多工具 |
|---|---|---|
| 决策 | 调/不调 | 调哪个、按什么顺序调 |
| 流程 | 线性 | 链式、并行、条件分支 |
| 参数来源 | 用户直接提供 | 从上一轮工具结果中提取 |
| 异常处理 | 返回错误 | 降级换工具、重试 |
核心心法:多工具协作的本质不是技术问题,是描述问题。 你对工具的 description 写得越清楚,AI的调度就越准确。
但现在还有一个问题:这四个工具都是我们自己写代码接的。如果下个月老板说"换成DeepSeek模型",所有工具都得重新适配一遍。有没有一个标准,让工具写一次,到处能用?
下一篇,我们聊聊 MCP------工具的USB接口协议。
本文与DeepSeek协作完成