Spring AI Alibaba Skills 完整实战:从零构建智能会议助手

Spring AI Alibaba Skills 完整实战:从零构建智能会议助手

本文基于实际项目经验,详细讲解如何使用 Spring AI Alibaba 1.1.2.0 框架构建具有 Skills(技能)能力的 AI Agent,并通过一个智能会议助手案例,演示从环境搭建、Skill 定义、Function Calling 集成到最终测试的全过程,同时记录了踩坑经历与解决方案。

一、什么是 AI Agent Skills?

在 AI Agent 领域,Skills 是一种将复杂任务拆解为标准工作流程的结构化能力包。它本质上是一个包含 元数据 (名称、描述)、执行指令 (步骤 SOP)、参考模板可执行脚本 的文件夹,让 AI 模型能够像执行"SOP"一样稳定、可重复地完成特定业务操作。

1.1 Skills 的核心价值

  • 标准化:将不同业务场景的专家经验固化为可复用的"技能"。
  • 可组合:Agent 可根据用户意图动态加载一个或多个 Skill,串联完成复杂任务。
  • 渐进式披露:模型初始只读取 Skill 的简短描述(~100 tokens),当判断需要时才加载完整指令(<5k tokens),大幅降低 Token 消耗。

1.2 Skills 与 Function Calling 的关系

  • Function Calling :底层机制,让模型能够请求执行一个外部函数(如"发送邮件"、"查询天气"),但模型不关心函数内部逻辑。
  • Tool:具体被调用的原子能力单元。
  • Skill工作流程层 ,它教会模型何时、如何、按什么顺序使用多个 Tool 来完成一个完整业务目标。

举例:sendEmail 是一个 Tool;meeting-notes-creator 是一个 Skill,它包含了"查询参会人时间 → 生成纪要模板 → 发送邮件"的标准流程,并在过程中调用 queryAvailableTimeSlotssendMeetingMinutes 两个 Tool。

二、技术栈与选型

组件 版本 说明
Spring Boot 3.2.5 基础框架
Spring AI Alibaba 1.1.2.0 提供 Agent Framework 与 DashScope 集成
DashScope (阿里云百炼) - 模型服务(使用 deepseek-v4-flash / qwen-max)
Jackson 2.16.2 解决版本冲突

选择 DashScope 原生端点(非兼容模式),避免了 OpenAI Starter 与 Agent Framework 的版本冲突问题。

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

三、项目搭建全流程

3.1 环境准备

  • 安装 JDK 17、Maven 3.6+。
  • 注册阿里云百炼账号,获取 API Key,开通 deepseek-v4-flash 模型服务(免费额度可用)。
  • 设置环境变量 DASHSCOPE_API_KEY

3.2 最终可用的 pom.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
    </parent>

    <groupId>com.badao.ai</groupId>
    <artifactId>spring-ai-alibaba-bailian-skill</artifactId>
    <version>1.0</version>

    <properties>
        <java.version>17</java.version>
        <spring-ai-alibaba.version>1.1.2.0</spring-ai-alibaba.version>
        <jackson.version>2.16.2</jackson.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Agent Framework 核心 -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-agent-framework</artifactId>
            <version>${spring-ai-alibaba.version}</version>
        </dependency>

        <!-- DashScope 模型接入 -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
            <version>${spring-ai-alibaba.version}</version>
        </dependency>

        <!-- 锁定 Jackson 版本避免 NoSuchMethodError -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.fasterxml.jackson</groupId>
                <artifactId>jackson-bom</artifactId>
                <version>${jackson.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>
</project>

3.3 application.yml 配置

yaml 复制代码
server:
  port: 885

spring:
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY}   # 从环境变量读取
      chat:
        options:
          model: deepseek-v4-flash     # 百炼平台模型名

skills:
  base-path: ./skills      # 技能文件夹路径
  auto-reload: true        # 开发时热加载

关键点 :不要配置 base-url,DashScope Starter 会使用官方原生端点,该端点完全支持 deepseek-v4-flash

3.4 技能目录结构(渐进式披露的实现)

复制代码
skills/
└── meeting-notes-creator/
    ├── SKILL.md                    # 核心指令文件
    ├── references/                 # 模板等引用文件
    │   └── meeting-template.md
    └── examples/                   # 示例输出
SKILL.md 示例
markdown 复制代码
---
name: meeting-notes-creator
description: 根据参会人和会议信息,自动生成标准格式的会议纪要,并通过邮件发送给所有参会人
---

# 会议纪要生成技能

## 功能说明
你是会议纪要生成助手。当用户需要生成会议纪要或整理会议记录时,严格按照以下步骤执行。

## 使用场景
- 用户说"帮我生成会议纪要"
- 用户说"整理一下今天的周会记录"
- 用户说"把会议结论整理出来并发送给所有参会人"

## 标准执行流程

### 第一步:获取会议基本信息
如果用户未提供完整信息,需要**主动询问**:
1. 会议主题是什么?
2. 会议时间是什么时候?
3. 参会人有哪些?(姓名或邮箱地址)

### 第二步:查询参会人可用时段 (调用工具)
使用 `ScheduleQueryTool` 中的 `queryAvailableTimeSlots` 工具:
- 传入参会人ID列表
- 传入会议时间范围

### 第三步:查询会议室可用性 (调用工具)
使用 `ScheduleQueryTool` 中的 `checkMeetingRoomAvailability` 工具:
- 根据参会人数推荐合适的会议室
- 确认该会议室在目标时间可用

### 第四步:生成会议纪要正文
按照 `references/meeting-template.md` 的格式模板生成会议纪要,必须包含:
- 会议基本信息 (主题、时间、地点)
- 参会人列表
- 会议议程
- 讨论要点
- 决议事项
- 待办事项 (负责人 + 截止时间)

### 第五步:发送会议纪要 (必须调用工具)
**强制要求**:收到用户"确认发送"指令后,**必须调用** `sendMeetingMinutes` 工具,参数:
- toEmails: 所有参会人邮箱
- subject: [会议纪要] {会议主题} - {日期}
- content: 上一步生成的完整 Markdown 纪要
不允许以文本形式替代工具调用。

### 第六步:确认完成
返回执行结果摘要,包括:
- 已发送会议纪要给哪些人
- 邮件发送状态
- 下次操作建议(如有)

## 可用资源
- **references/meeting-template.md**: 会议纪要标准模板
- **examples/sample-meeting.md**: 完整的会议纪要示例

## 输出格式要求
最终回复应包含以下信息:
1. 会议纪要生成状态
2. 邮件发送结果
3. 简要的会议总结(不超过3个要点)
sample-meeting.md 示例
复制代码
# 示例:前端技术周会会议纪要

## 基本信息
- 会议主题:前端技术组第23次周会
- 会议时间:2026年6月10日 14:00 - 15:30
- 会议地点:线上会议(腾讯会议)

## 讨论要点
1. **项目A进度**:已完成70%,预计6月20日提测
2. **性能优化方案**:初步确定采用代码分割 + 懒加载策略
3. **新人入职安排**:6月15日两名前端新人报到,安排导师对接

## 待办事项
- 项目A提测准备 → @张三 → 6月19日
- 性能优化方案文档 → @李四 → 6月16日
- 新人入职手册更新 → @王五 → 6月14日
meeting-template.md 示例
复制代码
# 会议纪要

## 基本信息

| 项目 | 内容 |
| :--- | :--- |
| 会议主题 | ${MEETING_TITLE} |
| 会议时间 | ${MEETING_TIME} |
| 会议地点 | ${MEETING_ROOM} |
| 主持人 | ${HOST_NAME} |
| 记录人 | AI智能助理 |

## 参会人员
- ${ATTENDEES_LIST}

## 会议议程
${AGENDA_ITEMS}

## 讨论要点
${DISCUSSION_POINTS}

## 决议事项
${DECISIONS}

## 待办事项

| 待办内容 | 负责人 | 截止日期 | 状态 |
| :--- | :--- | :--- | :--- |
| ${ACTION_ITEM_1} | ${OWNER_1} | ${DEADLINE_1} | 待开始 |
| ${ACTION_ITEM_2} | ${OWNER_2} | ${DEADLINE_2} | 待开始 |

## 下次会议
- 时间:${NEXT_MEETING_TIME}
- 地点:${NEXT_MEETING_LOCATION}

四、核心代码实现

4.1 工具类:Function Calling 的基础

java 复制代码
package com.badao.ai.tools;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.util.*;

/**
 * 日程查询工具 - 演示Function Calling
 * 根据参会人ID和时间范围查询可用时段
 */
@Component
public class ScheduleQueryTool {

    /**
     * 查询参会人的可用时段
     * @Tool注解:description必填,帮助AI理解何时调用此工具
     */
    @Tool(description = "根据参会人ID列表和时间范围,查询每个参会人的空闲时间段")
    public String queryAvailableTimeSlots(
            @ToolParam(description = "参会人ID列表,如: [\"zhangsan\", \"lisi\", \"wangwu\"]") List<String> attendeeIds,
            @ToolParam(description = "开始时间,格式: yyyy-MM-dd HH:mm") String startTime,
            @ToolParam(description = "结束时间,格式: yyyy-MM-dd HH:mm") String endTime
    ) {

        // 模拟从数据库/日历API查询可用时段
        Map<String, List<String>> availabilityMap = new HashMap<>();

        // 模拟数据:张三李四有空,王五在周三下午有会议冲突
        availabilityMap.put("zhangsan", Arrays.asList("2026-06-10 14:00-15:00", "2026-06-10 15:30-16:30"));
        availabilityMap.put("lisi", Arrays.asList("2026-06-10 14:00-15:00", "2026-06-10 16:00-17:00"));
        availabilityMap.put("wangwu", Arrays.asList("2026-06-10 13:00-14:00"));  // 只有1小时空档

        StringBuilder result = new StringBuilder();
        result.append("参会人可用时段查询结果:\n");

        for (String attendeeId : attendeeIds) {
            List<String> slots = availabilityMap.getOrDefault(attendeeId, Collections.emptyList());
            result.append("- ").append(attendeeId).append(": ");
            if (slots.isEmpty()) {
                result.append("该时间段内无空闲");
            } else {
                result.append(String.join("; ", slots));
            }
            result.append("\n");
        }

        return result.toString();
    }


    /**
     * 查询指定会议室的预定情况
     */
    @Tool(description = "查询指定会议室在特定时间段的预定状态")
    public String checkMeetingRoomAvailability(
            @ToolParam(description = "会议室名称") String roomName,
            @ToolParam(description = "查询日期,格式: yyyy-MM-dd") String date
    ) {
        // 模拟查询会议室预定状态
        return String.format("会议室[%s]在%s全天可预定,建议选择14:00-16:00时段", roomName, date);
    }
}

4.2 模拟邮件工具(解决 ToolCallback 找不到问题)

java 复制代码
package com.badao.ai.tools;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * 邮件发送工具 - 模拟版本(不实际发送邮件)
 */
@Component
public class EmailTool {

    private static final Logger logger = LoggerFactory.getLogger(EmailTool.class);

    /**
     * 模拟发送会议纪要邮件
     * 只打印日志并返回模拟成功消息,不实际调用邮件服务器
     */
   @Tool(description = "【必须调用】发送会议纪要邮件给指定收件人。当用户确认发送会议纪要时,必须调用此工具。参数:toEmails=收件人邮箱列表, subject=邮件主题, content=邮件正文(支持HTML)。返回发送结果。")
    public String sendMeetingMinutes(
            @ToolParam(description = "收件人邮箱地址列表,如: [\"zhangsan@example.com\"]") List<String> toEmails,
            @ToolParam(description = "邮件主题") String subject,
            @ToolParam(description = "邮件正文内容(支持HTML)") String content
    ) {
        // 模拟发送:记录日志
        logger.info("========== 模拟发送邮件 ==========");
        logger.info("收件人: {}", String.join(", ", toEmails));
        logger.info("主题: {}", subject);
        logger.info("正文内容预览: {}", content.length() > 100 ? content.substring(0, 100) + "..." : content);
        logger.info("=================================");

        // 返回模拟成功消息
        return String.format("[模拟] 邮件发送成功!收件人: %s (实际未发送)", String.join(", ", toEmails));
    }
}

4.3 Agent 配置(关键:显式传递工具)

java 复制代码
package com.badao.ai.config;

import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.agent.hook.skills.SkillsAgentHook;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;
import com.alibaba.cloud.ai.graph.skills.registry.SkillRegistry;
import com.alibaba.cloud.ai.graph.skills.registry.filesystem.FileSystemSkillRegistry;
import com.badao.ai.tools.EmailTool;
import com.badao.ai.tools.ScheduleQueryTool;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;

import java.util.List;

@Configuration
public class AgentConfig {

    @Value("${skills.base-path:./skills}")
    private String skillsBasePath;

    @Value("${skills.auto-reload:false}")
    private boolean autoReload;

    @Bean
    public SkillRegistry skillRegistry() {
        return FileSystemSkillRegistry.builder()
                .projectSkillsDirectory(skillsBasePath)
                .build();
    }

    @Bean
    public SkillsAgentHook skillsAgentHook(SkillRegistry skillRegistry) {
        return SkillsAgentHook.builder()
                .skillRegistry(skillRegistry)
                .autoReload(autoReload)
                .build();
    }

    @Bean
    @Description("查询参会人日程的工具")
    public ScheduleQueryTool scheduleQueryTool() {
        return new ScheduleQueryTool();
    }

    @Bean
    @Description("模拟发送邮件的工具")
    public EmailTool emailTool() {
        return new EmailTool();
    }

    @Bean
    public ReactAgent meetingAgent(ChatModel chatModel,
                                   SkillsAgentHook skillsAgentHook,
                                   ScheduleQueryTool scheduleQueryTool,
                                   EmailTool emailTool) {
        // 显式构建工具回调(关键修改)
        ToolCallback scheduleCallback = MethodToolCallbackProvider.builder()
                .toolObjects(scheduleQueryTool)
                .build()
                .getToolCallbacks()[0];
        ToolCallback emailCallback = MethodToolCallbackProvider.builder()
                .toolObjects(emailTool)
                .build()
                .getToolCallbacks()[0];

        return ReactAgent.builder()
                .name("MeetingAssistantAgent")
                .model(chatModel)
                .hooks(List.of(skillsAgentHook))
                .tools(List.of(scheduleCallback, emailCallback))  // 显式注入工具
                .systemPrompt("""
                        你是智能会议助手Agent,专注于帮助用户完成会议相关的任务。
                        
                        ## 可用能力
                        - 可调用查询日程和会议室的工具来获取信息
                        - 可按需加载 meeting-notes-creator 技能来生成会议纪要
                        - 可调用邮件发送工具分发会议纪要
                        
                        ## 工作原则
                        1. 优先尝试使用已有的工具完成请求
                        2. 遇到复杂流程任务时,加载对应的 Skill 并按其中步骤执行
                        3. 任何涉及发邮件的操作都需要用户确认收件人
                        
                        ## 重要规则
                        - 当用户明确说"确认发送"时,你**必须**调用 `sendMeetingMinutes` 工具,而不是只输出邮件内容。
                        - 邮件正文必须使用上一步生成的会议纪要内容。
                        - 如果缺少收件人邮箱,主动询问。
                        """)
                .saver(new MemorySaver())
                .build();
    }
}

4.4 Service 与 Controller(常规)

java 复制代码
@Service
public class MeetingAgentService {
    public String processUserMessage(String message) {
        AssistantMessage result = meetingAgent.call(message);
        return result.getText();
    }
}

@RestController
public class AgentController {
    @PostMapping("/chat")
    public Map<String, Object> chat(@RequestBody Map<String, String> req) {
        String resp = meetingAgentService.processUserMessage(req.get("message"));
        return Map.of("success", true, "response", resp);
    }
}

五、测试过程与多轮对话验证

5.1 第一轮:触发 Skill

json 复制代码
{"message": "帮我生成一个会议纪要,主题是项目周会,参会人有张三和李四"}

返回 :Agent 识别到需要 meeting-notes-creator Skill,并按步骤询问会议时间、地点、邮箱。

5.2 第二轮:提供缺失信息

json 复制代码
{"message": "时间是2026-06-10 14:00-15:00,地点A201,张三邮箱a@x.com,李四邮箱b@x.com"}

返回:Agent 调用工具查询会议室和日程,生成纪要草稿。

5.3 第三轮:确认发送

json 复制代码
{"message": "确认发送"}

预期 :调用 sendMeetingMinutes 模拟发送邮件。

5.4 一次发送完整信息

复制代码
{
  "message": "帮我生成会议纪要:主题项目周会,时间2026-06-10 14:00-15:00,地点A201,参会人张三(zhangsan@example.com)、李四(lisi@example.com)。议程:进度汇报、风险讨论、下阶段计划。讨论:前端正常后端延期。决议:后端加人。待办:张三协调后端资源(下周三),李四风险报告(本周五)。"
}

测试结果:

发送确认信息:

复制代码
{"message": "确认发送"}

效果:

5.4 遇到的典型错误及解决

错误现象 根本原因 解决方案
NoSuchMethodError: ObjectMapper.treeToValue Jackson 版本不足(需要 2.16+) 在 pom 中强制锁定 jackson.version=2.16.2
HTTP 404 调用 DashScope 错误使用了 base-url 兼容模式 去掉 base-url,使用原生端点
No ToolCallback found for toolName Agent 未显式注册 ToolCallback ReactAgent.builder() 中添加 .tools(List.of(toolCallbacks))
FunctionCallingOptions 接口不匹配 Spring AI 版本不统一 全部使用 Spring AI Alibaba 1.1.2.0 内置的传递依赖,不加外部 OpenAI Starter

六、常见问题排查清单

  1. Skill 没有被加载 :检查 skills.base-path 路径是否正确,SKILL.md 是否存在且 YAML 头格式无误。
  2. 工具调用了但参数错误 :在 @ToolParam 中提供清晰的 description,帮助模型正确填充参数。
  3. 模型不调用工具:在系统提示词或 Skill 指令中使用"必须调用"、"强制使用"等强语气词。
  4. 多轮对话记忆丢失 :使用 MemorySaver 并确保每次请求带相同的 threadId(可通过 RunnableConfig 设置)。

七、总结与扩展建议

通过本次实战,我们成功验证了 Spring AI Alibaba Skills 机制的完整工作流:

  • Skills 使得复杂任务得以标准化、可复用。
  • Function Calling 作为原子能力注入 Skill 步骤中。
  • 渐进式披露 大幅降低 Token 开销,支持技能热加载。

后续可以扩展:

  • 为 Skill 添加更多参考文档(references/),让模型生成更专业的会议纪要。
  • 集成真实邮件服务(SMTP)替换模拟工具。
  • 接入数据库或日历 API 实现真实的日程查询。
  • 利用 SkillsAgentHookautoReload 实现技能的热部署,无需重启应用。
相关推荐
眠りたいです1 小时前
LangChainv1:agent快速上手与中间件认识
人工智能·python·中间件·langchain·langgraph
JJJennie7771 小时前
从苹果 2026 落地场景,看系统级 Agent 时代的隐私边界与 MAI Gateway 的企业Token治理
人工智能·gateway·apple
极客先躯1 小时前
高级java每日一道面试题-2026年02月04日-实战篇[Docker]-如何在容器之间共享数据?
java·运维·网络·docker·容器·自动化·高级面试题
真实的菜1 小时前
微服务架构痛点
java·微服务·架构
小楊不秃头1 小时前
Spring:Bean的存储
java·spring·bean
甲维斯1 小时前
我超!Claude Fable真来了,比Mythos还强?!
人工智能
西凉的悲伤1 小时前
多线程彻底掌握 CompletableFuture:从入门到项目实战
java·多线程·future·completable·异步
用户298698530141 小时前
Java 中的 HTML 解析:从文件读取、URL 抓取到数据提取
java·后端
plainGeekDev1 小时前
ContentProvider → Room + Repository
android·java·kotlin