第6篇:《智能调度中心:当我同时塞给AI查订单、查库存、查物流三个工具》

承上:上一篇我们给AI装了一只手------它会查天气了。但一个工具哪够?真实业务系统里,订单、库存、物流、用户、优惠券......几十个服务等着被调用。今天,我们要让AI学会面对多个工具时,自己决策"该用哪个"。

1. 问题场景:AI的"选择困难症"

先回顾上一篇的架构:

复制代码
用户问天气 → AI判断需要调工具 → 调用getWeather → 返回结果

这是单工具场景,决策很简单:要么调,要么不调。

但真实业务是这样的:

复制代码
用户说:"我上周买的那个手机怎么还没到?帮我催一下。"

这句话里隐含了三个需求:

  1. 查订单:找到上周买的手机订单
  2. 查物流:看看快递到哪了
  3. 催单:如果物流卡住了,发起催单流程

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协作完成

相关推荐
月白白4 小时前
AI时代下,历史项目如何共享上下文?
ai编程
武子康4 小时前
Token 单价更低,Agent 任务为什么反而更贵:4 层成本口径 + 最小事件账本 + 3 个决策问题
人工智能·agent·ai编程
大模型搬砖师5 小时前
金融、制造、互联网:三个行业的 AI 网关落地实录
aigc·ai编程·gpu算力
舒灿5 小时前
喵ing Tab 1.0.0 正式发布
前端·ai编程
小徐_23336 小时前
Open Wot 1.0.5 发布:让 AI 接入 wot-ui,只需要两条命令
前端·uni-app·ai编程
程序员老刘7 小时前
Flutter版本选择指南:3.44密集修BUG,9月大限将至 | 2026年7月
flutter·ai编程·客户端
东小西7 小时前
第5篇:《AI学会调接口了:一个@Tool注解让大模型查了今天的天气》
openai·ai编程
格尔曼Noah7 小时前
插入USB设备时,vmware不提示连接到主机还是虚拟机
ai编程
春风野草7 小时前
AI Agent 工程底座实战:模型路由、限流、错误处理和成本意识
aigc·ai编程