让 AI 查订单、查库存、发消息:Spring AI 自定义工具(Tool)实战与工具描述工程

让 AI 查订单、查库存、发消息:Spring AI 自定义工具(Tool)实战与工具描述工程

系列:Spring AI 深入浅出(第 14 篇)

版本:Spring AI 2.0.1 / Spring Boot 4.1.x / JDK 21

标签:#SpringAI #自定义工具 #Tool #函数调用

前置阅读:第 13 篇《Tool Calling 原理》

预计字数:6500 字|可复制源码 + 描述工程对照表 + 踩坑 FAQ

本文将解决 :把现有 Spring Bean 的方法变成模型能调的工具,关键不在代码,在"描述工程"------描述写不好,模型就调错。本文给出一套实战方法论。

你将学到:

  • 把 Spring Bean 方法暴露为工具
  • 描述工程:命名 + @Tool(description) 怎么写才调得对
  • 参数校验与类型
  • 工具错误处理与人工兜底
  • 海量工具场景的处理

一、版本标注

本文基于 Spring AI 2.0.1 / Boot 4.1 / JDK 21。@Tool/@ToolParam 与工具注册方式承接第 13 篇,精确签名以 2.0.1 官方文档为准。


二、把 Bean 方法暴露为工具

现有业务 Bean 加注解即可变成 AI 工具:

java 复制代码
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

@Component
public class OrderTool {

    @Tool(description = "根据订单号查询订单详情")
    public Order getOrder(@ToolParam(description = "订单号,如 A001") String orderId) {
        return orderRepository.findById(orderId);
    }

    @Tool(description = "查询某用户的订单列表")
    public List<Order> listOrders(@ToolParam(description = "用户ID") String userId) {
        return orderRepository.findByUserId(userId);
    }
}

注册到 ChatClient:

java 复制代码
@Bean
public ChatClient chatClient(ChatClient.Builder builder, OrderTool orderTool) {
    return builder
            .defaultTools(orderTool)   // 把整个 Bean 的方法都注册为工具
            .build();
}

三、描述工程(重点):怎么写模型才调得对

模型是"读描述决定调哪个工具、传什么参"的。描述质量直接决定调用正确率。

3.1 好坏描述对照表

维度 ❌ 差 ✅ 好
工具名 doSomething getOrderById(动词+名词,见名知义)
工具描述 "处理订单" "根据订单号查询订单详情"
参数描述 "id" "订单号,形如 A001 的字符串"
边界 不写 "订单号不存在时返回 null"

3.2 三条描述原则

  1. 见名知义 :工具名用"动词+名词",如 getOrder、sendMessage、checkStock。
  2. 描述具体到能区分 :两个相似工具(如 getOrder vs listOrders)要在描述里说清区别。
  3. 参数写清格式与范围 :类型、格式(如日期 yyyy-MM-dd)、枚举取值、是否必填。

四、参数校验与类型

工具参数要有类型和校验,减少"传错参":

java 复制代码
@Tool(description = "给指定手机号发送短信")
public void sendSms(
        @ToolParam(description = "手机号,11位数字") String phone,
        @ToolParam(description = "短信内容") String content) {

    if (!phone.matches("1[3-9]\\d{9}")) {
        throw new IllegalArgumentException("手机号格式错误");
    }
    // 发送逻辑
}
  • 参数类型优先用 String/int/boolean/枚举等模型容易理解的类型
  • 复杂对象参数要拆成简单字段,避免模型拼不出正确 JSON

五、工具错误处理与人工兜底

5.1 错误要"返回给模型",不要"抛出去"

工具内部出错时,把错误信息作为结果返回给模型,让模型知道失败并调整:

java 复制代码
@Tool(description = "查询库存")
public String checkStock(@ToolParam(description = "商品SKU") String sku) {
    try {
        int stock = stockService.query(sku);
        return "库存充足:" + stock;
    } catch (Exception e) {
        return "查询失败:" + e.getMessage();   // 返回给模型,而非抛异常
    }
}

这样模型会基于"查询失败"这个信息,生成"抱歉,暂时查不到库存"之类的回复,而不是整个请求 500。

5.2 高风险操作要人工兜底

扣款、删除、发消息这类不可逆操作,不要全自动,要加"人工确认"环节:

  • 工具先返回"将执行 X,请确认",等用户确认后再真正执行
  • 或给工具加权限/限额(呼应第 21 篇安全)

六、海量工具场景

工具少时全量注册没问题;工具几十上百时,用 ToolSearchToolCallingAdvisor 做检索式暴露(第 13 篇提过):

  • 把工具描述也向量化,用户提问时只检索出最相关的几个工具给模型
  • 避免把上百个工具定义全塞进上下文

适用:企业中台把整个服务的几十上百个方法暴露给 AI 的场景。


七、原理图解:Bean 方法 → 工具 → 模型调用

#mermaid-svg-qZVw5BWqyochF3Wq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qZVw5BWqyochF3Wq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qZVw5BWqyochF3Wq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qZVw5BWqyochF3Wq .error-icon{fill:#552222;}#mermaid-svg-qZVw5BWqyochF3Wq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qZVw5BWqyochF3Wq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qZVw5BWqyochF3Wq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qZVw5BWqyochF3Wq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qZVw5BWqyochF3Wq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qZVw5BWqyochF3Wq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qZVw5BWqyochF3Wq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qZVw5BWqyochF3Wq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qZVw5BWqyochF3Wq .marker.cross{stroke:#333333;}#mermaid-svg-qZVw5BWqyochF3Wq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qZVw5BWqyochF3Wq p{margin:0;}#mermaid-svg-qZVw5BWqyochF3Wq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qZVw5BWqyochF3Wq .cluster-label text{fill:#333;}#mermaid-svg-qZVw5BWqyochF3Wq .cluster-label span{color:#333;}#mermaid-svg-qZVw5BWqyochF3Wq .cluster-label span p{background-color:transparent;}#mermaid-svg-qZVw5BWqyochF3Wq .label text,#mermaid-svg-qZVw5BWqyochF3Wq span{fill:#333;color:#333;}#mermaid-svg-qZVw5BWqyochF3Wq .node rect,#mermaid-svg-qZVw5BWqyochF3Wq .node circle,#mermaid-svg-qZVw5BWqyochF3Wq .node ellipse,#mermaid-svg-qZVw5BWqyochF3Wq .node polygon,#mermaid-svg-qZVw5BWqyochF3Wq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qZVw5BWqyochF3Wq .rough-node .label text,#mermaid-svg-qZVw5BWqyochF3Wq .node .label text,#mermaid-svg-qZVw5BWqyochF3Wq .image-shape .label,#mermaid-svg-qZVw5BWqyochF3Wq .icon-shape .label{text-anchor:middle;}#mermaid-svg-qZVw5BWqyochF3Wq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qZVw5BWqyochF3Wq .rough-node .label,#mermaid-svg-qZVw5BWqyochF3Wq .node .label,#mermaid-svg-qZVw5BWqyochF3Wq .image-shape .label,#mermaid-svg-qZVw5BWqyochF3Wq .icon-shape .label{text-align:center;}#mermaid-svg-qZVw5BWqyochF3Wq .node.clickable{cursor:pointer;}#mermaid-svg-qZVw5BWqyochF3Wq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qZVw5BWqyochF3Wq .arrowheadPath{fill:#333333;}#mermaid-svg-qZVw5BWqyochF3Wq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qZVw5BWqyochF3Wq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qZVw5BWqyochF3Wq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qZVw5BWqyochF3Wq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qZVw5BWqyochF3Wq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qZVw5BWqyochF3Wq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qZVw5BWqyochF3Wq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qZVw5BWqyochF3Wq .cluster text{fill:#333;}#mermaid-svg-qZVw5BWqyochF3Wq .cluster span{color:#333;}#mermaid-svg-qZVw5BWqyochF3Wq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qZVw5BWqyochF3Wq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qZVw5BWqyochF3Wq rect.text{fill:none;stroke-width:0;}#mermaid-svg-qZVw5BWqyochF3Wq .icon-shape,#mermaid-svg-qZVw5BWqyochF3Wq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qZVw5BWqyochF3Wq .icon-shape p,#mermaid-svg-qZVw5BWqyochF3Wq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qZVw5BWqyochF3Wq .icon-shape rect,#mermaid-svg-qZVw5BWqyochF3Wq .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qZVw5BWqyochF3Wq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qZVw5BWqyochF3Wq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qZVw5BWqyochF3Wq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Spring Bean 方法

@Tool 注解
框架生成工具 Schema
工具注册到 ChatClient
模型读到工具描述
决定调用并生成参数
框架执行真实方法
结果回喂模型


八、踩坑 FAQ

坑 1:方法重载/泛型导致 Schema 解析错

现象 :工具注册失败或 Schema 生成异常。

原因 :方法重载、复杂泛型让框架难以生成 Schema。

解决:工具方法避免重载,参数用简单类型,复杂返回对象用明确的 POJO。

坑 2:描述太简略,模型调错

现象 :模型该调 A 工具却调了 B,或传错参数。

原因 :描述含糊,模型无法区分。

解决:按第 3 节的"描述工程"原则,把工具名、描述、参数格式写具体。

坑 3:工具抛异常,整个请求 500

现象 :工具内部报错,请求直接失败。

原因 :异常没捕获,直接抛给框架。

解决:工具方法 try-catch,把错误信息作为结果返回给模型。

坑 4:工具名与保留字冲突

现象 :注册报错或工具不生效。

原因 :工具名和框架保留字/关键字冲突。

解决:用清晰的业务命名(动词+名词),避开保留字。

坑 5:敏感操作被模型"自动"执行了

现象 :模型直接调用了扣款/删除等敏感工具。

原因 :没做人工兜底和权限控制。

解决:敏感操作加确认环节 + 工具级权限校验(第 21 篇展开)。


九、小结与合集导航

本篇核心:把 Bean 方法变成 AI 工具,代码简单,难在描述工程与错误处理。

  • @Tool + @ToolParam 暴露 Bean 方法
  • 描述工程三原则(见名知义、描述具体、参数写清格式)
  • 错误返回给模型而非抛出
  • 敏感操作人工兜底

下一篇:《MCP 2.0 双向集成》------跨出本进程,消费远程 MCP 工具 + 把 Spring 服务暴露成 MCP Server。


十、互动钩子

投票:你最想把什么业务暴露给 AI?

  • 订单/交易查询
  • 库存/商品
  • 消息推送
  • 内部系统操作

评论区聊聊你的工具描述是怎么写的;源码见置顶评论。

相关推荐
Yoyo Chen1802 小时前
GEO 落地拆解:让 AI 读懂你的品牌,需要哪三层结构
人工智能·aigc·ip·内容运营
EnCi Zheng2 小时前
脉冲神经网络是什么:从 LIF 神经元到第三代神经网络的能耗优势
人工智能·深度学习·神经网络·脉冲神经网络
芯片人0072 小时前
GEK100系列芯片进入上市公司上海移远通信供应链
人工智能·单片机·嵌入式硬件·物联网·芯片
Rain的Java大神之路2 小时前
我,13年Java老兵,建了个免费网站,公开叫板“Java已死”:没有广告,没有付费墙,151个页面全是踩坑干货
java·后端·程序员
萧瑟余晖2 小时前
Spring Integration企业集成概览详解
java·后端·spring
l1t2 小时前
DeepSeek总结的pgrust以光速编程:构建 Gravity 查询引擎
数据库·人工智能·postgresql
重生之我在番茄自学网安拯救世界2 小时前
AI开发者日报 第2期 | Anthropic零 Rust 团队靠Claude重写核心 API,CPU 降 40–50%
人工智能·网络安全·大模型·ai编程·claude
zwc32104320552 小时前
Day 8:Canny边缘检测
人工智能·计算机视觉
kaixin_啊啊2 小时前
【零基础学AI】第 21 章——神经网络怎样工作
人工智能·深度学习·神经网络