【大模型专题】Spring AI Alibaba × Skill 整合实战:让 AI 真正“会干活

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 接口(最灵活,适合复杂场景))
    • [五、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 从"能聊天"变成"能干活"。


参考资料

相关推荐
米小虾1 小时前
AI Agent 记忆系统:从对话记录到认知架构
人工智能·agent
-山中问答-1 小时前
【智能体工具使用实战08】实战项目:代码仓库健康度分析Agent
人工智能·智能体·工具调用·工程实战
林间码客1 小时前
05 逻辑斯蒂回归(Logistic Regression)
人工智能·数据挖掘·回归
大黄说说1 小时前
深入理解 Go 协程 Goroutine:并发编程的核心精髓
java·数据库·python
米小虾1 小时前
AI Agent 上下文管理:从窗口到世界的桥梁
人工智能·agent
Gavynlee1 小时前
ubuntu22.04配置hermes(API以硅基流动为例)
人工智能
渡众机器人1 小时前
第八届全球校园人工智能算法精英大赛-算法应用赛-渡众机器人智能体对抗挑战赛规则
人工智能·算法·机器人·自动驾驶·自主导航·对抗赛
Dick5071 小时前
ROS2 视觉感知、目标检测与 TF 控制闭环复盘:从 /camera/image_raw 到 /cmd_vel 的机器人目标跟随实现
人工智能·计算机视觉·目标跟踪
于先生吖1 小时前
覆盖多行业的AI解决方案:AI知识库智能体落地全解析
大数据·人工智能