Spring AI Alibaba × Skill 整合实战:让 AI 真正"会干活
-
- [一、为什么需要 Skill?](#一、为什么需要 Skill?)
- 二、整体架构
-
- [2.1 请求链路图](#2.1 请求链路图)
- [2.2 核心模块依赖](#2.2 核心模块依赖)
- [2.3 Skill 注册机制](#2.3 Skill 注册机制)
- 三、快速上手
-
- [3.1 引入依赖](#3.1 引入依赖)
- [3.2 配置 API Key](#3.2 配置 API Key)
- [四、三种 Skill 定义方式](#四、三种 Skill 定义方式)
-
- [方式一:`@Tool` 注解(推荐,最简洁)](#方式一:
@Tool注解(推荐,最简洁)) - [方式二:`FunctionCallback` 接口(灵活,适合动态注册)](#方式二:
FunctionCallback接口(灵活,适合动态注册)) - [方式三:`ToolCallback` 接口(最灵活,适合复杂场景)](#方式三:
ToolCallback接口(最灵活,适合复杂场景))
- [方式一:`@Tool` 注解(推荐,最简洁)](#方式一:
- [五、ChatClient 整合所有 Skill](#五、ChatClient 整合所有 Skill)
- [六、进阶:动态 Skill 路由](#六、进阶:动态 Skill 路由)
- [七、Skill 最佳实践](#七、Skill 最佳实践)
-
- [7.1 Description 写作规范](#7.1 Description 写作规范)
- [7.2 异常处理](#7.2 异常处理)
- [7.3 参数设计原则](#7.3 参数设计原则)
- 八、完整项目结构
- 九、验证效果
- 十、常见问题
- 总结
适用版本: Spring AI Alibaba 1.1.x · Spring Boot 3.3+ · JDK 17+
难度: ★★★☆☆ | 预计阅读时间: 15 分钟
一、为什么需要 Skill?
大模型本身是个"聪明的语言机器",但它不能直接查数据库、调 REST 接口、发邮件。
Tool Calling(工具调用) 是 LLM 时代的核心扩展机制------模型决策"该用哪个工具",Skill 负责"把活干了"。
Spring AI Alibaba 把 Tool Calling 做得很优雅:你只需要写一个普通的 Java 方法,加几行注解,大模型就能在合适的时机自动调用它。
用户提问 → LLM 理解意图 → 决定调用 Skill → Skill 执行并返回 → LLM 汇总回答
二、整体架构
2.1 请求链路图
Skill: 工单创建 Skill: 天气查询 阿里百炼 LLM Spring AI ChatClient ChatController 前端/调用方 Skill: 工单创建 Skill: 天气查询 阿里百炼 LLM Spring AI ChatClient ChatController 前端/调用方 #mermaid-svg-sMTyYPldINrcmOOf{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-sMTyYPldINrcmOOf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sMTyYPldINrcmOOf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sMTyYPldINrcmOOf .error-icon{fill:#552222;}#mermaid-svg-sMTyYPldINrcmOOf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sMTyYPldINrcmOOf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sMTyYPldINrcmOOf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sMTyYPldINrcmOOf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sMTyYPldINrcmOOf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sMTyYPldINrcmOOf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sMTyYPldINrcmOOf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sMTyYPldINrcmOOf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sMTyYPldINrcmOOf .marker.cross{stroke:#333333;}#mermaid-svg-sMTyYPldINrcmOOf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sMTyYPldINrcmOOf p{margin:0;}#mermaid-svg-sMTyYPldINrcmOOf .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sMTyYPldINrcmOOf text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-sMTyYPldINrcmOOf .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sMTyYPldINrcmOOf .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-sMTyYPldINrcmOOf .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-sMTyYPldINrcmOOf .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-sMTyYPldINrcmOOf #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-sMTyYPldINrcmOOf .sequenceNumber{fill:white;}#mermaid-svg-sMTyYPldINrcmOOf #sequencenumber{fill:#333;}#mermaid-svg-sMTyYPldINrcmOOf #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-sMTyYPldINrcmOOf .messageText{fill:#333;stroke:none;}#mermaid-svg-sMTyYPldINrcmOOf .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sMTyYPldINrcmOOf .labelText,#mermaid-svg-sMTyYPldINrcmOOf .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-sMTyYPldINrcmOOf .loopText,#mermaid-svg-sMTyYPldINrcmOOf .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-sMTyYPldINrcmOOf .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sMTyYPldINrcmOOf .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-sMTyYPldINrcmOOf .noteText,#mermaid-svg-sMTyYPldINrcmOOf .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-sMTyYPldINrcmOOf .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sMTyYPldINrcmOOf .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sMTyYPldINrcmOOf .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sMTyYPldINrcmOOf .actorPopupMenu{position:absolute;}#mermaid-svg-sMTyYPldINrcmOOf .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-sMTyYPldINrcmOOf .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sMTyYPldINrcmOOf .actor-man circle,#mermaid-svg-sMTyYPldINrcmOOf line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-sMTyYPldINrcmOOf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户 发送消息 "帮我查北京天气并创建提醒工单" POST /chat call(prompt + tools) 发送消息 + 可用工具列表 tool_calls: getWeather("北京"), createTicket(...) getWeather("北京") {"temp":"28°C","weather":"晴"} createTicket("北京今日28°C 晴,出行提醒") {"ticketId":"TK-2026001","status":"created"} 工具执行结果回传 "北京今天28°C,晴天,已为您创建提醒工单 TK-2026001" 最终回复 返回结果 用户
2.2 核心模块依赖
#mermaid-svg-S6DRfBxIwbuUYUlL{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-S6DRfBxIwbuUYUlL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-S6DRfBxIwbuUYUlL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-S6DRfBxIwbuUYUlL .error-icon{fill:#552222;}#mermaid-svg-S6DRfBxIwbuUYUlL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-S6DRfBxIwbuUYUlL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-S6DRfBxIwbuUYUlL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-S6DRfBxIwbuUYUlL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-S6DRfBxIwbuUYUlL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-S6DRfBxIwbuUYUlL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-S6DRfBxIwbuUYUlL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-S6DRfBxIwbuUYUlL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-S6DRfBxIwbuUYUlL .marker.cross{stroke:#333333;}#mermaid-svg-S6DRfBxIwbuUYUlL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-S6DRfBxIwbuUYUlL p{margin:0;}#mermaid-svg-S6DRfBxIwbuUYUlL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-S6DRfBxIwbuUYUlL .cluster-label text{fill:#333;}#mermaid-svg-S6DRfBxIwbuUYUlL .cluster-label span{color:#333;}#mermaid-svg-S6DRfBxIwbuUYUlL .cluster-label span p{background-color:transparent;}#mermaid-svg-S6DRfBxIwbuUYUlL .label text,#mermaid-svg-S6DRfBxIwbuUYUlL span{fill:#333;color:#333;}#mermaid-svg-S6DRfBxIwbuUYUlL .node rect,#mermaid-svg-S6DRfBxIwbuUYUlL .node circle,#mermaid-svg-S6DRfBxIwbuUYUlL .node ellipse,#mermaid-svg-S6DRfBxIwbuUYUlL .node polygon,#mermaid-svg-S6DRfBxIwbuUYUlL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-S6DRfBxIwbuUYUlL .rough-node .label text,#mermaid-svg-S6DRfBxIwbuUYUlL .node .label text,#mermaid-svg-S6DRfBxIwbuUYUlL .image-shape .label,#mermaid-svg-S6DRfBxIwbuUYUlL .icon-shape .label{text-anchor:middle;}#mermaid-svg-S6DRfBxIwbuUYUlL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-S6DRfBxIwbuUYUlL .rough-node .label,#mermaid-svg-S6DRfBxIwbuUYUlL .node .label,#mermaid-svg-S6DRfBxIwbuUYUlL .image-shape .label,#mermaid-svg-S6DRfBxIwbuUYUlL .icon-shape .label{text-align:center;}#mermaid-svg-S6DRfBxIwbuUYUlL .node.clickable{cursor:pointer;}#mermaid-svg-S6DRfBxIwbuUYUlL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-S6DRfBxIwbuUYUlL .arrowheadPath{fill:#333333;}#mermaid-svg-S6DRfBxIwbuUYUlL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-S6DRfBxIwbuUYUlL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-S6DRfBxIwbuUYUlL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-S6DRfBxIwbuUYUlL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-S6DRfBxIwbuUYUlL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-S6DRfBxIwbuUYUlL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-S6DRfBxIwbuUYUlL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-S6DRfBxIwbuUYUlL .cluster text{fill:#333;}#mermaid-svg-S6DRfBxIwbuUYUlL .cluster span{color:#333;}#mermaid-svg-S6DRfBxIwbuUYUlL 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-S6DRfBxIwbuUYUlL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-S6DRfBxIwbuUYUlL rect.text{fill:none;stroke-width:0;}#mermaid-svg-S6DRfBxIwbuUYUlL .icon-shape,#mermaid-svg-S6DRfBxIwbuUYUlL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-S6DRfBxIwbuUYUlL .icon-shape p,#mermaid-svg-S6DRfBxIwbuUYUlL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-S6DRfBxIwbuUYUlL .icon-shape .label rect,#mermaid-svg-S6DRfBxIwbuUYUlL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-S6DRfBxIwbuUYUlL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-S6DRfBxIwbuUYUlL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-S6DRfBxIwbuUYUlL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Spring Boot App
spring-ai-alibaba-starter
DashScope ChatModel
ToolCallbackProvider
Skill A: 天气
Skill B: 工单
Skill C: 数据库查询
阿里云百炼 API
2.3 Skill 注册机制
#mermaid-svg-ani9X3kMKXPaFrjN{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-ani9X3kMKXPaFrjN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ani9X3kMKXPaFrjN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ani9X3kMKXPaFrjN .error-icon{fill:#552222;}#mermaid-svg-ani9X3kMKXPaFrjN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ani9X3kMKXPaFrjN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ani9X3kMKXPaFrjN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ani9X3kMKXPaFrjN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ani9X3kMKXPaFrjN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ani9X3kMKXPaFrjN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ani9X3kMKXPaFrjN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ani9X3kMKXPaFrjN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ani9X3kMKXPaFrjN .marker.cross{stroke:#333333;}#mermaid-svg-ani9X3kMKXPaFrjN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ani9X3kMKXPaFrjN p{margin:0;}#mermaid-svg-ani9X3kMKXPaFrjN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ani9X3kMKXPaFrjN .cluster-label text{fill:#333;}#mermaid-svg-ani9X3kMKXPaFrjN .cluster-label span{color:#333;}#mermaid-svg-ani9X3kMKXPaFrjN .cluster-label span p{background-color:transparent;}#mermaid-svg-ani9X3kMKXPaFrjN .label text,#mermaid-svg-ani9X3kMKXPaFrjN span{fill:#333;color:#333;}#mermaid-svg-ani9X3kMKXPaFrjN .node rect,#mermaid-svg-ani9X3kMKXPaFrjN .node circle,#mermaid-svg-ani9X3kMKXPaFrjN .node ellipse,#mermaid-svg-ani9X3kMKXPaFrjN .node polygon,#mermaid-svg-ani9X3kMKXPaFrjN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ani9X3kMKXPaFrjN .rough-node .label text,#mermaid-svg-ani9X3kMKXPaFrjN .node .label text,#mermaid-svg-ani9X3kMKXPaFrjN .image-shape .label,#mermaid-svg-ani9X3kMKXPaFrjN .icon-shape .label{text-anchor:middle;}#mermaid-svg-ani9X3kMKXPaFrjN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ani9X3kMKXPaFrjN .rough-node .label,#mermaid-svg-ani9X3kMKXPaFrjN .node .label,#mermaid-svg-ani9X3kMKXPaFrjN .image-shape .label,#mermaid-svg-ani9X3kMKXPaFrjN .icon-shape .label{text-align:center;}#mermaid-svg-ani9X3kMKXPaFrjN .node.clickable{cursor:pointer;}#mermaid-svg-ani9X3kMKXPaFrjN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ani9X3kMKXPaFrjN .arrowheadPath{fill:#333333;}#mermaid-svg-ani9X3kMKXPaFrjN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ani9X3kMKXPaFrjN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ani9X3kMKXPaFrjN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ani9X3kMKXPaFrjN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ani9X3kMKXPaFrjN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ani9X3kMKXPaFrjN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ani9X3kMKXPaFrjN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ani9X3kMKXPaFrjN .cluster text{fill:#333;}#mermaid-svg-ani9X3kMKXPaFrjN .cluster span{color:#333;}#mermaid-svg-ani9X3kMKXPaFrjN 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-ani9X3kMKXPaFrjN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ani9X3kMKXPaFrjN rect.text{fill:none;stroke-width:0;}#mermaid-svg-ani9X3kMKXPaFrjN .icon-shape,#mermaid-svg-ani9X3kMKXPaFrjN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ani9X3kMKXPaFrjN .icon-shape p,#mermaid-svg-ani9X3kMKXPaFrjN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ani9X3kMKXPaFrjN .icon-shape .label rect,#mermaid-svg-ani9X3kMKXPaFrjN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ani9X3kMKXPaFrjN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ani9X3kMKXPaFrjN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ani9X3kMKXPaFrjN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 运行层
注册层
定义层
@Component
标注的普通Bean
@Tool 注解的方法
ToolCallback 接口实现
ToolCallbackProvider
ChatClient Builder
ChatClient
LLM Tool Choice
三、快速上手
3.1 引入依赖
xml
<!-- pom.xml -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>1.1.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring AI Alibaba DashScope Starter -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3.2 配置 API Key
yaml
# application.yml
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY} # 从阿里云百炼控制台获取
chat:
options:
model: qwen-max # 或 qwen-plus / qwen-turbo
获取 API Key: 登录 阿里云百炼控制台 → API-KEY → 创建 API Key
四、三种 Skill 定义方式
方式一:@Tool 注解(推荐,最简洁)
java
package com.example.skill;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
/**
* 天气查询 Skill
* 模型会根据 description 自动判断何时调用此方法
*/
@Component
public class WeatherSkill {
@Tool(description = "查询指定城市的实时天气,返回温度、天气状况、湿度")
public String getWeather(String city) {
// 实际场景接入天气 API,此处模拟返回
return String.format("""
{
"city": "%s",
"temperature": "28°C",
"weather": "晴",
"humidity": "45%%"
}
""", city);
}
@Tool(description = "查询指定城市未来3天的天气预报")
public String getWeatherForecast(String city) {
return String.format("[{'day':'明天','weather':'多云'},{'day':'后天','weather':'小雨'}]");
}
}
方式二:FunctionCallback 接口(灵活,适合动态注册)
java
package com.example.skill;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.function.Function;
@Configuration
public class SkillConfig {
/**
* 工单创建 Skill(函数式风格)
*/
@Bean
public FunctionCallback createTicketSkill() {
return FunctionCallback.builder()
.function("createTicket", (CreateTicketRequest req) -> {
// 对接内部工单系统
String ticketId = "TK-" + System.currentTimeMillis();
return new CreateTicketResponse(ticketId, "created", req.title());
})
.description("创建内部工单,用于记录待办事项或问题追踪")
.inputType(CreateTicketRequest.class)
.build();
}
record CreateTicketRequest(
@JsonProperty(required = true) String title,
@JsonProperty(defaultValue = "normal") String priority
) {}
record CreateTicketResponse(String ticketId, String status, String title) {}
}
方式三:ToolCallback 接口(最灵活,适合复杂场景)
java
package com.example.skill;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.stereotype.Component;
/**
* 数据库查询 Skill(自定义 JSON Schema 描述)
*/
@Component
public class DatabaseQuerySkill implements ToolCallback {
@Override
public ToolDefinition getToolDefinition() {
return ToolDefinition.builder()
.name("queryDatabase")
.description("执行SQL查询,返回业务数据,仅支持SELECT语句")
.inputSchema("""
{
"type": "object",
"properties": {
"sql": {
"type": "string",
"description": "要执行的SELECT SQL语句"
},
"limit": {
"type": "integer",
"description": "返回的最大行数,默认10",
"default": 10
}
},
"required": ["sql"]
}
""")
.build();
}
@Override
public String call(String toolInput) {
// 解析参数并查询数据库
// 实际场景注入 JdbcTemplate 或 MyBatis
return "[{\"id\":1,\"name\":\"示例数据\"}]";
}
}
五、ChatClient 整合所有 Skill
java
package com.example.controller;
import com.example.skill.DatabaseQuerySkill;
import com.example.skill.WeatherSkill;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/chat")
public class ChatController {
private final ChatClient chatClient;
public ChatController(
ChatClient.Builder builder,
WeatherSkill weatherSkill,
DatabaseQuerySkill databaseQuerySkill,
FunctionCallback createTicketSkill
) {
this.chatClient = builder
// 注册全局可用的 Skill
.defaultTools(weatherSkill, databaseQuerySkill)
.defaultTools(createTicketSkill)
.build();
}
/**
* 普通对话(携带 Skill 能力)
*/
@PostMapping
public String chat(@RequestBody ChatRequest request) {
return chatClient.prompt()
.user(request.message())
.call()
.content();
}
/**
* 流式对话
*/
@GetMapping(value = "/stream", produces = "text/event-stream")
public reactor.core.publisher.Flux<String> stream(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.stream()
.content();
}
record ChatRequest(String message) {}
}
六、进阶:动态 Skill 路由
实际项目中,不同角色的用户能使用的 Skill 往往不同。可以按需在请求级别动态注入:
java
@PostMapping("/v2")
public String chatWithDynamicSkills(
@RequestBody ChatRequest request,
@RequestHeader("X-User-Role") String role
) {
ChatClient.ChatClientRequestSpec spec = chatClient.prompt()
.user(request.message());
// 根据角色动态注入 Skill
if ("admin".equals(role)) {
spec = spec.tools(databaseQuerySkill);
}
if ("ops".equals(role)) {
spec = spec.tools(createTicketSkill);
}
return spec.call().content();
}
七、Skill 最佳实践
7.1 Description 写作规范
description 是模型决策的唯一依据,写得越精准,调用越准确:
| ❌ 差 | ✅ 好 |
|---|---|
查天气 |
查询指定城市的实时天气,包括温度、天气状况、风速,适用于回答"今天天气怎么样"类问题 |
创建工单 |
在内部工单系统创建任务,适用于"帮我记录一下"、"创建待办"等用户意图,需要提供标题 |
查数据库 |
执行只读SQL查询返回业务数据,仅限SELECT语句,禁止INSERT/UPDATE/DELETE |
7.2 异常处理
Skill 抛出的异常会被 Spring AI 捕获,并以文本形式返回给模型,模型会尝试重试或告知用户:
java
@Tool(description = "查询用户账户余额")
public String getBalance(String userId) {
try {
User user = userService.findById(userId);
if (user == null) {
// 返回可读的错误信息,让模型理解并告知用户
return String.format("{\"error\": \"用户 %s 不存在\", \"suggestion\": \"请检查用户ID是否正确\"}", userId);
}
return String.format("{\"userId\": \"%s\", \"balance\": %.2f}", userId, user.getBalance());
} catch (Exception e) {
return "{\"error\": \"系统繁忙,请稍后重试\"}";
}
}
7.3 参数设计原则
java
// ✅ 使用 record 定义参数,自动生成 JSON Schema
record WeatherRequest(
@JsonProperty(required = true, value = "city")
@JsonPropertyDescription("城市名称,如:北京、上海、广州")
String city,
@JsonProperty(defaultValue = "metric")
@JsonPropertyDescription("单位制:metric(摄氏度)或 imperial(华氏度)")
String unit
) {}
八、完整项目结构
spring-ai-skill-demo/
├── src/main/java/com/example/
│ ├── SkillDemoApplication.java
│ ├── config/
│ │ └── SkillConfig.java # FunctionCallback 注册
│ ├── controller/
│ │ └── ChatController.java # 对话接口
│ └── skill/
│ ├── WeatherSkill.java # @Tool 注解方式
│ ├── DatabaseQuerySkill.java # ToolCallback 接口方式
│ └── TicketSkill.java # FunctionCallback 方式
├── src/main/resources/
│ └── application.yml
└── pom.xml
九、验证效果
启动项目后,用 curl 测试:
bash
# 测试天气 + 工单联合调用
curl -X POST http://localhost:8080/chat \
-H "Content-Type: application/json" \
-d '{"message": "帮我查一下上海的天气,如果是晴天就给我创建一个出行提醒工单"}'
# 期望输出:
# 上海今天晴天,气温26°C,已为您创建出行提醒工单 TK-2026001,祝您出行愉快!
十、常见问题
Q: Skill 没有被调用,模型直接回答了?
A: 检查 description 是否清晰描述了使用场景,过于简短的描述会让模型误判不需要调用工具。
Q: 报错 No tool found with name: xxx?
A: 确认 Skill Bean 已被 Spring 扫描(加了 @Component),或在 ChatClient.Builder 中显式注册。
Q: 能否并发调用多个 Skill?
A: 支持。qwen-max 等高阶模型支持 Parallel Tool Calling,会在一次响应中返回多个工具调用请求,Spring AI 会并发执行。
Q: 如何限制模型只能调用指定工具?
A: 通过 ChatOptions 设置 toolChoice:
java
DashScopeChatOptions options = DashScopeChatOptions.builder()
.withToolChoice("auto") // auto / none / required
.build();
总结
| 方式 | 适合场景 | 代码量 |
|---|---|---|
@Tool 注解 |
快速接入、业务简单 | 最少 |
FunctionCallback |
需要动态配置、函数式风格 | 中等 |
ToolCallback 接口 |
复杂 Schema、自定义序列化 | 最多 |
Spring AI Alibaba 把 Tool Calling 做到了"写个 Java 方法就完事"的体验,真正让 AI 从"能聊天"变成"能干活"。
参考资料