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,它包含了"查询参会人时间 → 生成纪要模板 → 发送邮件"的标准流程,并在过程中调用queryAvailableTimeSlots和sendMeetingMinutes两个 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 |
六、常见问题排查清单
- Skill 没有被加载 :检查
skills.base-path路径是否正确,SKILL.md是否存在且 YAML 头格式无误。 - 工具调用了但参数错误 :在
@ToolParam中提供清晰的description,帮助模型正确填充参数。 - 模型不调用工具:在系统提示词或 Skill 指令中使用"必须调用"、"强制使用"等强语气词。
- 多轮对话记忆丢失 :使用
MemorySaver并确保每次请求带相同的threadId(可通过RunnableConfig设置)。
七、总结与扩展建议
通过本次实战,我们成功验证了 Spring AI Alibaba Skills 机制的完整工作流:
- Skills 使得复杂任务得以标准化、可复用。
- Function Calling 作为原子能力注入 Skill 步骤中。
- 渐进式披露 大幅降低 Token 开销,支持技能热加载。
后续可以扩展:
- 为 Skill 添加更多参考文档(
references/),让模型生成更专业的会议纪要。 - 集成真实邮件服务(SMTP)替换模拟工具。
- 接入数据库或日历 API 实现真实的日程查询。
- 利用
SkillsAgentHook的autoReload实现技能的热部署,无需重启应用。