让 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 三条描述原则
- 见名知义 :工具名用"动词+名词",如
getOrder、sendMessage、checkStock。 - 描述具体到能区分 :两个相似工具(如
getOrdervslistOrders)要在描述里说清区别。 - 参数写清格式与范围 :类型、格式(如日期
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?
- 订单/交易查询
- 库存/商品
- 消息推送
- 内部系统操作
评论区聊聊你的工具描述是怎么写的;源码见置顶评论。