Agent Skill 踩坑记录 | SpringBoot 打包后 Skill 加载失败问题排查与解决
在之前的开发中，我使用 ClasspathSkillRegistry 作为 Agent Skill 的查找策略，本地开发环境下调试、运行都一切正常，没有出现任何异常。感兴趣的小伙伴可以回顾上一篇相关内容：https://www.lucaju.cn/index.php/archives/168/一、问题出现：上线后频繁报「Skill not found」本以为本地测试无误后，上线就能顺利运行，结果程序部署到服务器后，日志中频繁出现 Skill not found: 错误，导致相关功能完全无法使用。二、问题排查：排除常见误区遇到问题后，首先排查了最容易出现问题的两个点，均排除异常：确认 Skill 已成功打包：检查部署的 jar 包，解压后确认 skill 相关文件已正常包含在内，不存在打包遗漏的情况；确认 Skill Name 无误：核对代码中调用 Skill 的名称与配置文件中的名称，完全一致，排除拼写错误、大小写错误等问题。三、问题根源：找到官方 Issue排除上述常见问题后，推测可能是框架本身的兼容性问题，于是去 GitHub 上搜索相关问题，果然发现已有其他开发者遇到过相同情况，并且提交了 Issue：https://github.com/alibaba/spring-ai-alibaba/issues/4426从 Issue 中得知，问题根源在于 SpringBoot 项目打包成 jar 包后，路径机制发生变化，导致 ClasspathSkillRegistry 无法正常加载 resources 下的 Skill 目录，这是目前 spring-ai-alibaba 框架的一个已知 bug。四、临时解决方案：改用 FileSystemSkillRegistry由于官方尚未修复该 bug，为了不影响线上功能正常使用，我采用了临时解决方案：将 Skill 目录存放在服务器的文件系统中，通过指定真实路径获取 Skill，对应的查找策略改用 FileSystemSkillRegistry。修改后的核心代码如下：/** * 获取技能智能体钩子。 */ private static SkillsAgentHook getSkillsAgentHook() { // 注释掉原有的ClasspathSkillRegistry方式（本地可用，打包后失效） // SkillRegistry registry = ClasspathSkillRegistry.builder() // .classpathPath("skills") // .build(); /* 临时解决方案说明： spring ai alibaba 目前使用 ClasspathSkillRegistry 打包后，无法获取 Resource 下的目录 因此修改为使用 FileSystemSkillRegistry，从文件系统真实路径获取 Skill 相关 Issue：https://github.com/alibaba/spring-ai-alibaba/issues/4426 */ String skillPath = SpringUtils.getProperty("doc.skills-path"); log.info("skillPath: {}",skillPath); FileSystemSkillRegistry registry = FileSystemSkillRegistry.builder() .projectSkillsDirectory(skillPath) .build(); return SkillsAgentHook.builder() .skillRegistry(registry) .build(); }五、后续需要说明的是，这只是一个临时解决方案——使用该方案后，需要手动将 Skill 配置文件同步到服务器的指定路径（即配置项 doc.skills-path 对应的路径），增加了少量部署成本。后续会持续关注官方 Issue 的修复进度，待 bug 修复后，再切换回 ClasspathSkillRegistry 方式，减少部署环节的手动操作。也希望遇到相同问题的小伙伴，能通过这篇踩坑记录少走弯路～
Agent Skills | Spring Ai Alibaba从零构建可扩展 AI 智能体
在当今AI应用开发领域，让AI智能体具备灵活的工具调用能力和可扩展的技能体系，是落地复杂业务场景的核心挑战。Spring AI Alibaba 原生提供了完善的 Skill 技能支持，能让智能体实现「技能发现→按需加载→工具执行」的全流程自动化。本文将从核心概念、组件解析、实战场景、最佳实践四个维度，手把手教你掌握 Spring AI Alibaba Skill 的使用方法。一、核心概念1.1 渐进式披露（核心设计思想）Spring AI Alibaba Skill 采用渐进式披露机制，最大化提升模型效率：系统初始仅注入技能元数据（名称、描述、路径）；模型判断需要使用某技能时，调用 read_skill(skill_name) 加载完整的 SKILL.md 文档；最后按需访问技能资源、执行绑定工具。1.2 Skill 标准目录结构每个技能独立为一个子目录，SKILL.md 为强制必需文件，目录结构如下：skills/ # 技能根目录 └── pdf-extractor/ # 单个技能目录（自定义命名） ├── SKILL.md # ✅ 必需：技能核心定义文档 ├── references/ # 可选：技能参考资料 ├── examples/ # 可选：使用示例 └── scripts/ # 可选：执行脚本1.3 SKILL.md 格式规范SKILL.md 采用元数据+正文结构，元数据通过 --- 包裹，是模型识别技能的关键：--- name: pdf-extractor # 必需：小写字母、数字、连字符，最长64字符 description: 用于从PDF文档中提取文本、表格和表单数据 # 必需：简洁描述，超长会被截断 --- # PDF 提取技能 ## 功能说明 You are a PDF extraction specialist. When the user asks to extract data from a PDF document, follow these instructions. ## 使用方法 1. **Validate Input** - Confirm the PDF file path is provided. - The default path for the pdf file is the current working directory. - Use the `shell` or `read_file` tool to check if the file exists - Verify it's a valid PDF format 2. **Extract Content** - Execute the extraction script using the `shell` tool: ```bash python scripts/extract_pdf.py <pdf_file_path> ``` - The script will output JSON format with extracted data 3. **Process Results** - Parse the JSON output from the script - Structure the data in a readable format - Handle any encoding issues (UTF-8, special characters) 4. **Present Output** - Summarize what was extracted - Present data in the requested format (JSON, Markdown, plain text) - Highlight any issues or limitations ## Script Location The extraction script is located at: `scripts/extract_pdf.py` ## Output Format The script returns JSON: ```json { "success": true, "filename": "report.pdf", "text": "Full text content...", "page_count": 10, "tables": [ { "page": 1, "data": [["Header1", "Header2"], ["Value1", "Value2"]] } ], "metadata": { "title": "Document Title", "author": "Author Name", "created": "2024-01-01" } } ```scripts/extract_pdf.py简介，因为目前测试脚本的执行，所以mock返回了一个json格式的解析内容。二、核心组件解析Spring AI Alibaba Skill 体系由四大核心组件构成，各司其职、协同工作：2.1 对话模型（ChatModel）对接阿里通义千问大模型，是智能体的「大脑」，负责推理决策：private static ChatModel getChatModel() { // 构建通义千问API客户端 DashScopeApi dashScopeApi = DashScopeApi.builder() .apiKey(System.getenv("AliQwen_API")) // 环境变量配置API Key .build(); // 创建对话模型 return DashScopeChatModel.builder() .dashScopeApi(dashScopeApi) .build(); }2.2 技能注册中心（SkillRegistry）负责加载、管理所有技能，Spring AI Alibaba 提供两种常用实现：ClasspathSkillRegistry：从项目类路径（resources）加载技能FileSystemSkillRegistry：从本地文件系统加载技能代码示例：SkillRegistry registry = ClasspathSkillRegistry.builder() .classpathPath("skills") // 指向resources/skills目录 .build();2.3 技能钩子（SkillsAgentHook）连接智能体与技能系统的桥梁，负责注入技能上下文、提供工具调用能力：SkillsAgentHook hook = SkillsAgentHook.builder() .skillRegistry(registry) // 绑定技能注册中心 .build();2.4 ReAct 智能体（ReactAgent）基于 ReAct（推理+执行） 模式的智能体，自主完成「思考→选技能→执行任务」的全流程：ReactAgent agent = ReactAgent.builder() .name("skills-agent") // 智能体名称 .model(chatModel) // 绑定大模型 .saver(new MemorySaver()) // 内存记忆（保存对话上下文） .hooks(List.of(hook)) // 绑定技能钩子 .build();三、实战场景场景一：技能发现（智能体自我认知）让AI智能体主动披露自身具备的所有技能，验证技能加载是否成功。核心代码private static void findSkills() throws GraphRunnerException { // 1. 初始化核心组件 ChatModel chatModel = getChatModel(); SkillsAgentHook hook = getSkillsAgentHook(); // 2. 构建技能智能体 ReactAgent agent = ReactAgent.builder() .name("skills-agent") .model(chatModel) .saver(new MemorySaver()) .hooks(List.of(hook)) .build(); // 3. 执行对话：查询技能 AssistantMessage resp = agent.call("请介绍你有哪些技能"); System.out.println(resp.getText()); }执行结果11:29:36.067 [main] INFO com.alibaba.cloud.ai.graph.skills.registry.classpath.ClasspathSkillRegistry -- Loaded 1 skills from classpath: skills 我目前拥有的技能包括： - **pdf-extractor**: 用于从 PDF 文档中提取文本、表格和表单数据，适用于分析和处理 PDF 文件。当用户需要提取、解析或分析 PDF 文件时可使用此技能。 如需了解某项技能的详细说明（例如具体操作步骤、支持的文件类型、使用示例等），我可以为您读取其完整的技能文档（`SKILL.md`）。您也可以告诉我您的具体需求（例如：“请帮我从一份PDF中提取所有表格”），我会判断是否适用某项技能并执行相应操作。 是否需要我为您详细展开某一项技能？场景二：技能使用（PDF 信息提取）结合技能系统 + Python 执行工具 + Shell 工具，实现多工具协作，完成 PDF 文本/表格提取。步骤1：自定义 Python 执行工具（GraalVM 支持）依赖如下 org.graalvm.polyglot polyglot 24.2.1 org.graalvm.polyglot python-community 24.2.1 pom 用于执行 Python 脚本完成 PDF 解析，工具代码如下：package com.jcq.springaialibabaagent.tools; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Engine; import org.graalvm.polyglot.PolyglotException; import org.graalvm.polyglot.Value; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.model.ToolContext; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.tool.function.FunctionToolCallback; import java.util.function.BiFunction; /** * GraalVM 沙箱环境执行 Python 代码的工具 * 安全沙箱：禁用文件IO、进程创建、本地访问 */ public class PythonTool implements BiFunction<PythonTool.PythonRequest, ToolContext, String> { public static final String DESCRIPTION = """ Executes Python code and returns the result. - 代码必须为合法Python语法 - 沙箱执行，安全无风险 - 支持返回数字、字符串、数组、执行结果 """; private static final Logger log = LoggerFactory.getLogger(PythonTool.class); private final Engine engine; public PythonTool() { this.engine = Engine.newBuilder() .option("engine.WarnInterpreterOnly", "false") .build(); } /** 构建Spring AI 标准工具回调 */ public static ToolCallback createPythonToolCallback(String description) { return FunctionToolCallback.builder("python_tool", new PythonTool()) .description(description) .inputType(PythonRequest.class) .build(); } @Override public String apply(PythonRequest request, ToolContext toolContext) { if (request.code == null || request.code.trim().isEmpty()) { return "Error：Python代码不能为空"; } // 沙箱环境执行Python try (Context context = Context.newBuilder("python") .engine(engine) .allowAllAccess(false) .allowIO(false) .allowNativeAccess(false) .allowCreateProcess(false) .allowHostAccess(true) .build()) { log.debug("执行Python代码：{}", request.code); Value result = context.eval("python", request.code); // 结果类型转换 if (result.isNull()) return "执行完成，无返回值"; if (result.isString()) return result.asString(); if (result.isNumber() || result.isBoolean()) return result.toString(); if (result.hasArrayElements()) { StringBuilder sb = new StringBuilder("["); long size = result.getArraySize(); for (long i = 0; i < size; i++) { if (i > 0) sb.append(", "); sb.append(result.getArrayElement(i)); } return sb.append("]").toString(); } return result.toString(); } catch (PolyglotException e) { log.error("Python执行异常", e); return "执行错误：" + e.getMessage(); } } /** 工具请求参数 */ public static class PythonRequest { @JsonProperty(required = true) @JsonPropertyDescription("需要执行的Python代码") public String code; public PythonRequest() {} public PythonRequest(String code) { this.code = code; } } }步骤2：构建多技能协作智能体private static void useSkills() throws Exception { // 1. 初始化大模型 ChatModel chatModel = getChatModel(); // 2. 绑定技能钩子 + Shell工具钩子 SkillsAgentHook skillsHook = getSkillsAgentHook(); ShellToolAgentHook shellHook = ShellToolAgentHook.builder() .shellTool2(ShellTool2.builder(System.getProperty("user.dir")).build()) .build(); // 3. 构建智能体（绑定技能+工具+日志） ReactAgent agent = ReactAgent.builder() .name("skills-integration-agent") .model(chatModel) .saver(new MemorySaver()) .tools(PythonTool.createPythonToolCallback(PythonTool.DESCRIPTION)) // 自定义Python工具 .hooks(List.of(skillsHook, shellHook)) // 多钩子组合 .enableLogging(true) // 开启日志，便于调试 .build(); // 4. 执行PDF提取任务 String pdfPath = getTestSkillsDirectory() + "/pdf-extractor/skill-test.pdf"; AssistantMessage response = agent.call(String.format("请从 %s 文件中提取关键信息", pdfPath)); // 5. 输出结果 System.out.println("========== PDF提取结果 =========="); System.out.println(response.getText()); }执行结果========== The PDF extraction was successful! Here's the key information extracted from the `skill-test.pdf` file: ## Document Metadata - **Title**: Sample PDF Document - **Author**: Test Author - **Created**: 2024-01-01 - **Modified**: 2024-01-15 - **Page Count**: 5 pages ## Extracted Text The document contains: "This is extracted text from the PDF document. It contains multiple paragraphs and sections." ## Tables Found ### Table 1 (Page 1) - Product Inventory | Product | Price | Quantity | |---------|-------|----------| | Widget A | $10.00 | 100 | | Widget B | $15.00 | 50 | ### Table 2 (Page 3) - Financial Summary | Month | Revenue | Expenses | |-------|---------|----------| | January | $50,000 | $30,000 | | February | $55,000 | $32,000 |四、最佳实践建议技能规范化管理所有技能统一放在 resources/skills 目录，按业务拆分子目录；SKILL.md 严格遵循格式规范，保证模型准确识别。多钩子灵活组合基础能力：SkillsAgentHook（技能系统）；系统操作：ShellToolAgentHook（Shell命令）；自定义能力：自定义工具（Python/Java/第三方API）。开发与调试优化开发阶段开启 enableLogging(true)，查看智能体推理全流程；测试使用 MemorySaver 快速验证，生产环境替换为持久化存储。模型选型策略简单技能查询：基础版通义千问；复杂工具协作/多步骤推理：高级版大模型。