【LangChain4j 10】【Skills】

文章目录

  • 一、前言
  • [二、Skills 简介](#二、Skills 简介)
    • [1. 与 MCP 的区别](#1. 与 MCP 的区别)
    • [2. Skills 目录结构](#2. Skills 目录结构)
  • [三、Skills 创建](#三、Skills 创建)
  • [四、Skills 集成](#四、Skills 集成)
    • [1. Tool 模式](#1. Tool 模式)
      • [1.1 Skills 示例](#1.1 Skills 示例)
      • [1.2 Skills 说明](#1.2 Skills 说明)
      • [1.3 与 Tool Search 配合](#1.3 与 Tool Search 配合)
    • [2. Shell 模式](#2. Shell 模式)
  • [五、Skills 推荐](#五、Skills 推荐)
  • 六、参考内容

一、前言

本系列内容来源于 LangChain4J 官网,内容稍作改删,仅做个人笔记使用。

本系列使用 LangChain4J 版本:

xml 复制代码
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-bom</artifactId>
            <version>1.8.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

本系列完整代码地址 :langchain4j-hwl

系列文章合集:

  1. 【LangChain4j 01】【基本使用】
  2. 【LangChain4j 02】【AI Services】
  3. 【LangChain4j 03】【Agents and Agentic AI】
  4. 【LangChain4j 04】【Tools (Function Calling)】
  5. 【LangChain4j 05】【RAG】
  6. 【LangChain4j 06】【结构化输出】
  7. 【LangChain4j 07】【Guardrails】
  8. 【LangChain4j 08】【Observability】
  9. 【LangChain4j 09】【MCP】
  10. 【LangChain4j 10】【Skills】

需要注意,虽然本系列统一使用 1.8.0 版本,但是该篇内容是新版特性,所以本篇使用的是 langchain4j 的 1.12.2 版本。
需要注意,虽然本系列统一使用 1.8.0 版本,但是该篇内容是新版特性,所以本篇使用的是 langchain4j 的 1.12.2 版本。
需要注意,虽然本系列统一使用 1.8.0 版本,但是该篇内容是新版特性,所以本篇使用的是 langchain4j 的 1.12.2 版本。


本篇的 langchain4j-skills 引入版本如下:

xml 复制代码
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-skills</artifactId>
    <version>1.12.2-beta22</version>
</dependency>

Skills API尚处于实验阶段。API和相关行为在未来版本中可能仍会发生变化。
Skills API尚处于实验阶段。API和相关行为在未来版本中可能仍会发生变化。
Skills API尚处于实验阶段。API和相关行为在未来版本中可能仍会发生变化。


二、Skills 简介

Skills 是一种为 LLM 配备可复用、自包含行为指令的机制:

  • 一个 Skill 会打包:名称、简短描述、核心指令内容,以及可选的资源(参考文档、资产、模板等).
  • LLM 可以按需加载 Skill,初始上下文保持精简,仅在实际需要时才拉取详细指令.
  • 该功能完全遵循 Agent Skills 规范 设计。

Skills 可以说是渐进式披露思想的原生落地实现,整个 Skills 的设计完全围绕该思想展开,也是 Agent Skills 规范的核心设计。

关于 渐进式披露思想 在 【LangChain4j 09】【MCP】 中有过介绍,这里就不再赘述。

Skills 的加载流程完全复刻了渐进式披露的三层结构:

  • L1 元数据层 :Skill 的 name + description,初始就注入到系统消息中,告诉 LLM 有哪些可用的技能,仅占用少量Token,即使几十个技能也不会撑满上下文
  • L2 核心指令层SKILL.md 中的详细指令,仅当 LLM 调用 activate_skill 激活该技能时,才加载到上下文中------此时 LLM 才真正需要这些执行规则
  • L3 补充资源层 :Skill 的参考文档,仅当 LLM 调用 read_skill_resource 时,才加载对应的参考内容,按需读取,不需要提前加载

Skills 还实现了工具的渐进式披露:绑定到 Skill 的技能作用域工具,平时不会暴露给 LLM,只有 Skill 激活后,才会加入到 LLM 的工具列表中。

这避免了大量无关工具占满上下文,让 LLM 始终只看到当前任务需要的工具,解决了工具膨胀的问题。

1. 与 MCP 的区别

MCP(Model Context Protocol)是 AI 连接外部世界的 "通信协议"(通道),而 Skill 是 AI 执行特定任务的"能力单元"(方法)。 两者协同工作:MCP 提供 "手" 去操作外部工具,Skill 提供 "脑" 去指导如何完成任务。

  • MCP (模型上下文协议) :由 Anthropic 开源的标准化通信协议,用于 AI 模型(如 Claude)与外部服务(数据库、API、文件系统)安全、统一地交互。 比喻:AI 的USB-C 接口------ 统一连接标准,实现 "即插即用"。
  • Skill (技能) :面向特定任务的轻量级执行单元,将专家经验、SOP 流程、领域规则封装为可复用的 "操作手册"。 比喻:AI 的专业技能书------ 指导 AI "如何正确地完成某件事"。

2. Skills 目录结构

Skills 标准目录结构如下:

bash 复制代码
	skills/  # 【根目录】所有技能的总容器(目录名可自定义,建议统一为skills)
	├── docx/  # 【技能分类文件夹】专注于「Word(docx)相关操作」的技能集合
	│   ├── SKILL.md        # 【必需】docx类技能的核心定义文件(AI识别此技能的入口)
	│   ├── references/     # 【可选】docx技能的参考资源文件夹(AI可自动加载使用)
	│   │   └── tracked-changes.md  # 【参考资源】Word修订模式( tracked changes )相关说明、操作规范
	│   └── scripts/        # 【可选】docx操作相关可执行脚本文件夹(AI可调用执行)
	│       ├── docx_revision.py    # 【脚本文件】执行docx修订模式操作的Python脚本
	│       └── docx_format.py      # 【脚本文件】统一docx文档格式的Python脚本
	└── data-analysis/  # 【技能分类文件夹】专注于「数据分析相关操作」的技能集合
	    ├── SKILL.md        # 【必需】数据分析类技能的核心定义文件(AI识别此技能的入口)
	    └── scripts/        # 【可选】数据分析相关可执行脚本文件夹(AI可调用执行)
	        ├── data_process.py    # 【脚本文件】数据清洗、统计计算的Python脚本
	        └── data_visual.py     # 【脚本文件】生成数据可视化图表的Python脚本

Skills 的部分规范要求如下:

  • 目录规范 : 一个技能 = 一个独立文件夹,文件夹内必须有 SKILL.md 文件

  • 文件格式SKILL.md 开头用---包裹YAML 配置,定义技能名称、描述;配置下方的文本,就是 LLM 激活技能时执行的指令;

    markdown 复制代码
    ---
    name: docx
    description: 使用修订模式编辑和审阅Word文档
    ---
    当用户要求你编辑Word文档时:
    1. 必须使用修订模式,让修改可以被审阅
    2. ...(其余指令内容)
  • 资源自动加载:技能文件夹里,除了SKILL.md和scripts/目录下的文件,其余所有文件(如参考文档)都会自动变成 LLM 可按需读取的资源;

三、Skills 创建

LangChain4j 提供了三种创建 Skills 的方式,适配不同的部署场景

  1. 从文件系统加载 :使用 FileSystemSkillLoader 加载文件系统的 Skill。

    java 复制代码
    // 加载指定目录下的所有Skill
    List<FileSystemSkill> skills = FileSystemSkillLoader.loadSkills(Path.of("skills/"));
    
    // 加载单个Skill
    FileSystemSkill docxSkill = FileSystemSkillLoader.loadSkill(Path.of("skills/docx"));
  2. 从类路径加载 :ClassPathSkillLoader 的逻辑和文件系统加载器完全一致,区别是从类路径中解析 Skill 目录,适合 Skill 被打包进 JAR 包、存放在 src/main/resources 下的场景, 加载代码如下:

    需要注意 ClassPathSkillLoader 是在 1.13.0-beta23-SNAPSHOT 版本中才出现,在本篇中使用的是 1.12.2-beta22 版本,所以尚未引入该类。

    java 复制代码
    // 加载类路径下的所有Skill
    List<FileSystemSkill> skills = ClassPathSkillLoader.loadSkills("skills");
    
    // 加载单个Skill
    FileSystemSkill docxSkill = ClassPathSkillLoader.loadSkill("skills/docx");
    
    // 可自定义类加载器
    FileSystemSkill customLoaderSkill = ClassPathSkillLoader.loadSkill("skills/docx", myClassLoader);

    该加载器同样遵循和文件系统加载器一致的 SKILL.md 格式、资源加载规则、scripts/ 目录排除规则。

  3. 编程式创建 : Skill 不一定要基于文件,你可以从任意来源(数据库、远程API、运行时生成)创建 Skill,使用 Builder API 即可:

    java 复制代码
    // 基础的编程式创建
    Skill incidentSkill = Skill.builder()
            .name("incident-response")
            .description("生产故障排查的标准化运行手册")
            .content("""
    当生产告警触发时:
    1. 调用 `fetchRecentLogs(serviceName)` 获取最近5分钟的日志
    2. 调用 `checkServiceHealth(serviceName)` 获取当前健康指标
    3. 根据结果调用 `createIncidentTicket(summary, severity)` 创建故障单
    4. 如果严重等级为CRITICAL,额外调用 `pageOnCall(incidentId)` 通知值班人员
     """)
            .build();
    
    // 带自定义资源的编程式创建
    SkillResource toneGuide = SkillResource.builder()
            .relativePath("references/tone-guide.md")
            .content("使用温暖简洁的语言,避免专业术语")
            .build();
    
    Skill supportSkill = Skill.builder()
            .name("customer-support")
            .description("处理客户咨询的客服技能")
            .content("请严格遵循references/tone-guide.md中的语气要求...")
            .resources(List.of(toneGuide))
            .build();

四、Skills 集成

Skills 提供了两种完全不同的集成模式,适配不同的安全与控制需求:

1. Tool 模式

这是官方所推荐的模式,通过 Tool-based agents 的方式集成,安全可控。其安全性体现在:

  1. 磁盘访问完全隔离(零文件系统权限)

    • LLM 在推理时完全无法访问文件系统,不能读、写、删除磁盘上的任何文件;
    • 所有技能指令、资源文件(如配置、参考文档),提前加载到内存(如通过 FileSystemSkillLoader );
    • activate_skill/read_skill_resource 仅返回内存中的预加载内容,绝不直接读取磁盘。
  2. 工具调用「白名单锁死」(无任意代码执行风险)

    • LLM 只能调用你「显式注册」的工具,没有任何自主创造命令 / 代码的能力;
    • 不存在「任意代码执行」漏洞,LLM 无法执行未授权的逻辑;
    • 所有工具的功能、参数、返回值,100% 由你用 Java 代码定义
  3. 执行流程「固化绑定」(LLM 不能自由发挥)

    • LLM 不是自主决策完成任务,而是必须先激活技能 → 获取固定的分步指令 → 严格按指令执行;
    • 任务流程由你提前定义在技能中,LLM 只能遵循,不能随意修改流程。
  4. 工具权限「动态收敛」(最小权限原则)

    • activate_skill :对 LLM 来说 始终可用,仅用于加载技能指令,无其他权限
    • read_skill_resource : 对 LLM 来说 仅技能有资源时可用 ,仅技能有资源时可用
    • Skill-scoped tools :对LLM来说 仅技能有资源时可用,作用域仅限当前技能,用完即关闭
  5. 系统权限彻底隔离

    • LLM 没有任何操作系统原生权限(无 Shell、无命令行、无系统调用权限),所有操作必须通过你编写的 Java 工具代理执行。

Tool 模式的工作流程如下:

  1. 前置准备:System Message 提前向LLM展示所有可用技能的名称和描述,让LLM先 "知晓" 有哪些 Skills 可选,明确技能的核心用途。
  2. 触发条件:用户提出的问题需要特定技能才能解决时,触发技能调用逻辑。
  3. 技能激活 :LLM 调用activate_skill工具,指定需要的技能名称,从而获取该技能的详细指令。
  4. 任务执行:LLM 严格遵循激活技能后的指令完成任务,执行过程中可按需读取技能关联的资源文件(如参考文档),完成最终需求。

1.1 Skills 示例

下面我们给出一个 Skills 示例,该示例的流程是 【用户提问 → LLM 激活 Skill → 获取策略指令 → 按策略调用 MCP 工具 → 返回结果】

这里调用的 MCP 即在 【LangChain4j 09】【MCP】 中的 Http MCP 。

  1. 创建 resources/skills/database-ops/SKILL.md 文件,内容如下:

    java 复制代码
    ---
    name: database-ops
    description: 数据库运维技能,根据用户意图智能选择合适的 MCP 工具完成数据库查询、表结构查看、数据变更等操作
    ---
    
    # 数据库运维技能
    
    你是一个数据库运维助手,能够根据用户的自然语言描述,智能选择合适的 MCP 工具来完成数据库操作。
    
    ## 可用的 MCP 工具
    
    你可以使用以下工具:
    
    1. **listTables** - 列出数据库中所有表名,帮助了解数据库结构
    2. **describeTable** - 查看指定表的字段结构信息(字段名、类型、是否可空等)
    3. **executeQuery** - 执行 SELECT 查询语句,返回结构化结果
    4. **executeUpdate** - 执行 INSERT / UPDATE / DELETE 语句,返回受影响行数
    	
    ... 
    
    忽略剩下的内容
  2. 自定义一个 CompositeToolProvider 对象

    因为 1.12.2 版本的 AiServices 只能指定一个 ToolProvider,因此这里定义一个组合 ToolProvider。

    java 复制代码
    /**
     * 组合工具提供者
     * 将多个 ToolProvider 的工具合并到一个 ToolProviderResult 中返回,
     * 解决低版本 AiServices 不支持 toolProviders(ToolProvider...) 的问题。
     * 同时合并各 ToolProvider 返回的 immediateReturnToolNames
     *
     * @author haowl
     * @date 2026/4/3
     */
    @Slf4j
    public class CompositeToolProvider implements ToolProvider {
    
        private final List<ToolProvider> delegates;
    
        /**
         * 私有构造,通过静态工厂方法创建
         *
         * @param delegates 需要组合的 ToolProvider 列表
         */
        private CompositeToolProvider(List<ToolProvider> delegates) {
            this.delegates = Collections.unmodifiableList(delegates);
        }
    
        /**
         * 组合多个 ToolProvider 为一个
         *
         * @param providers 需要组合的 ToolProvider(至少传入一个)
         * @return 组合后的 ToolProvider 实例
         * @throws IllegalArgumentException 未传入任何 ToolProvider 时抛出
         */
        public static CompositeToolProvider of(ToolProvider... providers) {
            if (providers == null || providers.length == 0) {
                throw new IllegalArgumentException("至少需要传入一个 ToolProvider");
            }
            return new CompositeToolProvider(Arrays.asList(providers));
        }
    
        /**
         * 从列表创建组合 ToolProvider
         *
         * @param providers 需要组合的 ToolProvider 列表(至少包含一个)
         * @return 组合后的 ToolProvider 实例
         * @throws IllegalArgumentException 列表为空时抛出
         */
        public static CompositeToolProvider of(List<ToolProvider> providers) {
            if (providers == null || providers.isEmpty()) {
                throw new IllegalArgumentException("至少需要传入一个 ToolProvider");
            }
            return new CompositeToolProvider(new ArrayList<>(providers));
        }
    
        /**
         * 遍历所有委托的 ToolProvider,收集工具规格、执行器和立即返回工具名,合并为一个结果返回。
         * 单个 ToolProvider 异常不会影响其他 ToolProvider 的工具收集
         *
         * @param request 工具提供请求上下文
         * @return 合并后的 ToolProviderResult,包含所有 ToolProvider 提供的工具
         */
        @Override
        public ToolProviderResult provideTools(ToolProviderRequest request) {
            ToolProviderResult.Builder builder = ToolProviderResult.builder();
            Set<String> mergedImmediateReturnToolNames = new HashSet<>();
    
            for (ToolProvider delegate : delegates) {
                try {
                    ToolProviderResult result = delegate.provideTools(request);
                    if (result == null) {
                        continue;
                    }
    
                    // 合并工具规格和执行器
                    if (result.tools() != null && !result.tools().isEmpty()) {
                        builder.addAll(result.tools());
                        log.debug("从 {} 收集到 {} 个工具",
                                delegate.getClass().getSimpleName(),
                                result.tools().size());
                    }
    
                    // 合并立即返回工具名
                    // immediateReturnToolNames 的作用是:当 LLM 调用了这个集合中的某个工具后,工具的执行结果会直接返回给用户,而不是再送回 LLM 做进一步处理。
                    // 正常的工具调用流程是这样的: 用户提问 → LLM → 调用工具 → 工具返回结果 → 结果送回 LLM → LLM 生成最终回复 → 返回用户
                    // 如果工具名在 immediateReturnToolNames 中,流程变成:用户提问 → LLM → 调用工具 → 工具返回结果 → 直接返回用户(跳过 LLM 二次处理)
                    if (result.immediateReturnToolNames() != null) {
                        mergedImmediateReturnToolNames.addAll(result.immediateReturnToolNames());
                    }
                } catch (Exception e) {
                    log.error("从 {} 收集工具时发生异常,已跳过该 ToolProvider,异常信息:{}",
                            delegate.getClass().getSimpleName(), e.getMessage(), e);
                }
            }
    
            if (!mergedImmediateReturnToolNames.isEmpty()) {
                builder.immediateReturnToolNames(mergedImmediateReturnToolNames);
            }
    
            return builder.build();
        }
    }
  3. 创建 SkillsAssistant,加载 SKILL.md 文件 和 MCP 工具。

    java 复制代码
        /**
         * 构建 Skills + MCP 联合助手
         * 同时挂载 Skills 的 toolProvider(提供 activate_skill、read_skill_resource)
         * 和 MCP 的 toolProvider(提供 listTables、describeTable、executeQuery、executeUpdate)
         * LLM 激活 Skill 后,按照 SKILL.md 中的策略指令选择调用对应的 MCP 工具
         *
         * @param chatModel 聊天模型
         * @return SkillsAssistant 实例
         */
        @Bean
        public SkillsAssistant skillsAssistant(ChatModel chatModel) {
            // 1. 加载 MCP 服务
            McpTransport transport = StreamableHttpMcpTransport.builder()
                    .url(MCP_REMOTE_SERVER_URL)
                    .build();
    
            // MCP 工具提供者(提供数据库操作相关工具)
            ToolProvider mcpToolProvider = McpToolProvider.builder()
                    .mcpClients(DefaultMcpClient.builder()
                            .key("MysqlSkillsMcp")
                            .transport(transport)
                            .build())
                    .build();
    
            // 从文件系统加载 Skills(包含 database-ops 等技能)
            Skills skills = Skills.from(FileSystemSkillLoader.loadSkills(Path.of(SKILLS_DIRECTORY)));
            
    	//        // 编程式创建 Skill(等价于 SKILL.md 文件加载,但内容直接在代码中定义)
    	//        Skill databaseOpsSkill = Skill.builder()
    	//                .name("database-ops")
    	//                .description("数据库运维技能,根据用户意图智能选择合适的 MCP 工具完成数据库查询、表结构查看、数据变更等操作")
    	//                // 将 resources/skills/database-ops/SKILL.md 的内容粘贴在此
    	//                .content("""
    	//                        # 数据库运维技能
    	//                        ...
    	//                        """)
    	//                .build();
    	
            // 工具调用监听器,记录每次工具调用的名称和来源,用于区分 Skills 触发还是直接触发
            // 因为即使不通过 Skill 该逻辑也是能跑通,这里为了验证确实激活了 Skills 打印了下面的日志。
            ToolExecutedEventListener toolExecutedListener = event -> {
                String toolName = event.request().name();
                String arguments = event.request().arguments();
    
                if ("activate_skill".equals(toolName)) {
                    log.info("【Skills 触发】激活技能,参数:{}", arguments);
                } else if ("read_skill_resource".equals(toolName)) {
                    log.info("【Skills 触发】读取技能资源,参数:{}", arguments);
                } else {
                    log.info("【MCP 工具调用】工具名:{},参数:{}", toolName, arguments);
                }
    
                // 打印工具执行结果
                String resultText = event.resultText();
                log.info("【工具执行结果】{}", resultText);
            };
    
            return AiServices.builder(SkillsAssistant.class)
                    .chatModel(chatModel)
                    // 使用 CompositeToolProvider 组合 Skills 工具和 MCP 工具
                     // 因为当前版本只能指定一个 ToolProvider, 因此编写一个 CompositeToolProvider
                    .toolProvider(CompositeToolProvider.of(skills.toolProvider(), mcpToolProvider))
                    // 注册工具调用监听器,记录调用链路
                    .registerListener(toolExecutedListener)
                    .systemMessage("你是一个智能数据库运维助手。\n"
                            + "你可以使用以下技能:\n"
                            + skills.formatAvailableSkills() + "\n"
                            + "当用户的请求涉及某个技能时,请先使用 `activate_skill` 工具激活对应技能,"
                            + "然后按照技能指令中的策略选择合适的 MCP 工具完成任务。")
                    .build();
        }
  4. 执行流程大致如下:

    1. 当通过 HTTP 发起 请求时,请求参数如下(给出 DB 连接的信息,然后让 LLM 查询具体的数据):

      java 复制代码
      DB 连接信息 :"jdbc:mysql://localhost:3306/demo",   "username": "root",   "password": "root",
      查询 data_demo 表的数据
    2. LLM 先调用了 activate_skill,参数是 {"skill_name":"database-ops"},说明 LLM 识别到用户的请求属于数据库运维场景,主动激活了 database-ops 技能。

    3. activate_skill 返回了 SKILL.md 中的完整策略指令,LLM 拿到了这份「行为手册」。

    4. LLM 按照 Skill 指令中「场景三:查询数据」的策略,调用了 MCP 工具 executeQuery,执行了 SELECT * FROM data_demo

    5. MCP 工具返回了查询结果:两条记录。


1.2 Skills 说明

基于上面的内容,我们再扩展一些说明内容

  1. skills.formatAvailableSkills() 会生成标准化的 XML 格式的 Skill 列表,方便 LLM 解析,如下:

    xml 复制代码
    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <available_skills>
        <skill> 
            <name>database-ops</name>
            <description>数据库运维技能,根据用户意图智能选择合适的 MCP 工具完成数据库查询、表结构查看、数据变更等操作</description>
        </skill>
    </available_skills>
  2. 自定义 Skills 暴露给 LLM 的两个内置工具的「外观」,也就是 LLM 看到的工具名称、描述和参数说明。默认情况下,Skills 注册的两个工具长这样:

    工具 默认名称 默认描述
    激活技能 activate_skill 英文默认描述
    读取资源 read_skill_resource 英文默认描述

    通过 ActivateSkillToolConfigReadResourceToolConfig,你可以把它们改成:

    工具 自定义名称 自定义描述
    激活技能 load_skill "加载指定技能的详细指令"
    读取资源 read_doc "读取技能的参考文档"

    这样做的实际意义有两个:

    1. 让工具描述更贴合你的业务语境。比如你的场景叫「加载技能」比「激活技能」更直观,LLM 理解起来更准确,调用决策也更精准。
    2. 支持中文化。默认的工具名和描述都是英文的,如果你的 systemMessage 是中文,工具描述也改成中文,LLM 的理解一致性会更好。

    这种改动在功能上没有任何变化,只是改了 LLM 看到的「工具说明书」的措辞。如果你对默认的名称和描述没有不满,完全不需要配置这些。

    如下是代码示例 :

    java 复制代码
    Skills skills = Skills.builder()
            .skills(mySkills)
            // 自定义activate_skill工具的配置
            .activateSkillToolConfig(ActivateSkillToolConfig.builder()
                    .name("load_skill") // 自定义工具名,默认是activate_skill
                    .description("加载指定技能的详细指令")
                    .parameterName("skill_name")
                    .parameterDescription("要加载的技能的名称")
                    .build())
            // 自定义read_skill_resource工具的配置
            .readResourceToolConfig(ReadResourceToolConfig.builder()
                    .name("read_doc")
                    .description("读取技能的参考文档")
                    .skillNameParameterDescription("文档所属的技能名称")
                    .relativePathParameterDescription("要读取的文档的路径")
                    .build())
            .build();
  3. 你可以将工具直接绑定到某个 Skill 上,这些工具仅在 Skill 激活后才会暴露给 LLM,这样可以保持 LLM 的工具列表精简,避免无关工具干扰,同时保证技能专属工具仅在需要时可见。

    需要注意该部分内容在 本篇所使用的 1.12.2 版本中 尚未实装,并没有该部分 API 方法。

    支持三种绑定方式,也可以混合使用:

    1. 绑定@Tool注解的方法

      java 复制代码
      // 你的工具类
      class OrderTools {
          @Tool("根据ID校验订单是否合法")
          String validateOrder(String orderId) {
              // 校验逻辑
              return "valid";
          }
          @Tool("为订单扣款")
          String chargePayment(String orderId) {
              // 扣款逻辑
              return "charged";
          }
      }
      
      // 绑定到Skill
      Skill orderSkill = Skill.builder()
              .name("process-order")
              .description("端到端处理客户订单")
              .content("""
      处理订单的步骤:
      1. 调用validateOrder(orderId)校验订单
      2. 调用chargePayment(orderId)扣款
       """)
              .tools(new OrderTools()) // 绑定工具
              .build();
      
      // 也可以给从文件加载的Skill添加工具
      FileSystemSkill loadedSkill = FileSystemSkillLoader.loadSkill(Path.of("skills/process-order"));
      Skill skillWithTools = loadedSkill.toBuilder()
              .tools(new OrderTools())
              .build();
    2. 绑定ToolProvider :你也可以绑定整个 ToolProvider,比如将 MCP 服务的工具绑定到 Skill,仅在 Skill 激活后才暴露:

      java 复制代码
      // 加载MCP的工具,过滤出库存相关的工具
      ToolProvider mcpInventoryProvider = McpToolProvider.builder()
              .mcpClients(mcpClient)
              .filter((tool, client) -> tool.name().startsWith("inventory_"))
              .build();
      
      // 绑定到Skill
      Skill inventorySkill = Skill.builder()
              .name("inventory-management")
              .description="管理仓库库存"
              .content("使用库存工具检查库存、更新数量")
              .toolProviders(mcpInventoryProvider) // 绑定ToolProvider
              .build();
    3. 直接绑定工具Map :如果你需要完全自定义工具的定义和执行逻辑,可以直接传入 Map:

      java 复制代码
      // 定义工具规范
      ToolSpecification validateSpec = ToolSpecification.builder()
              .name("validateOrder")
              .description("根据ID校验订单")
              .addParameter("orderId", JsonSchemaProperty.STRING, "订单ID")
              .build();
      // 定义执行器
      ToolExecutor validateExecutor = (request, memoryId) -> {
          String orderId = parseOrderId(request.arguments());
          return validate(orderId);
      };
      
      // 绑定到Skill
      Skill orderSkill = Skill.builder()
              .name("process-order")
              .description("端到端处理客户订单")
              .content("调用validateOrder校验订单")
              .tools(Map.of(validateSpec, validateExecutor))
              .build();

    技能作用域工具的工作原理如下:

    1. Skill 激活前,LLM 只能看到 activate_skillread_skill_resource 工具,技能作用域的工具完全不可见
    2. 当 LLM 调用 activate_skill 激活 Skill 后,AI 服务会重新评估工具列表,技能作用域的工具会自动暴露给 LLM
    3. 这些工具会保持可见,直到 Skill 被手动停用

Tool Search 的作用是:当你注册了很多工具时,不把所有工具都塞给 LLM,而是根据用户的问题动态筛选出最相关的几个工具发送,减少 Token 消耗。

正常情况下,每次请求 LLM 时,所有通过 .tools() 注册的工具定义都会放进请求里。如果你有 50 个工具,每个工具的描述加参数可能占几百个 Token,光工具定义就要消耗上万 Token,而用户的问题可能只需要其中 2-3 个工具。

ToolSearchStrategy 就是解决这个问题的。它会在发送请求前,根据用户输入筛选出最相关的工具子集,只把这些工具发给 LLM。

通过 .tools() 注册的工具会被 ToolSearchStrategy 筛选,而通过 .toolProvider() 注册的工具不受影响,每次都会全量发送。

activate_skill 工具会被标记为始终可见,即使开启了 Tool Search,LLM 也始终可以调用它

代码示例如下:

java 复制代码
Skills skills = Skills.from(mySkills);

MyAiService service = AiServices.builder(MyAiService.class)
        .chatModel(chatModel)
        .tools(new MySearchableTools()) // 可被 ToolSearchStrategy 过滤
        .toolProvider(skills.toolProvider()) // 不可通过 ToolSearchStrategy 过滤
        .toolSearchStrategy(new SimpleToolSearchStrategy())
        .systemMessage("可用技能:%s,需要时先激活".formatted(skills.formatAvailableSkills()))
        .build();

2. Shell 模式

Shell 模式本身是实验性功能,执行本质上是不安全的。

Shell 模式的典型用途是:你想快速验证一个 Skill 的可行性,不想先写 Java 工具代码,直接让 LLM 用 Shell 命令跑通流程。等验证通过后,再把 Shell 命令逐步替换为正式的 Java 工具实现。


Tool 模式下,LLM 激活 Skill 后,按照指令去调用你注册的 Java 工具(比如 MCP 工具)。而 Shell 模式下,LLM 激活 Skill 后,直接执行操作系统的 Shell 命令来完成任务。

在 Shell 模式下,大模型仅配备一个 run_shell_command 工具,并通过 shell 命令直接从文件系统读取技能说明。该模式下没有 activate_skill 或read_skill_resource 工具------大模型会像人类开发者一样浏览技能文件。

run_shell_command 对于 LLM 来说始终可见,LLM 会运行 shell 命令来读取SKILL.md文件、资源文件并执行脚本。

两种模式的核心区别如下:

Tool 模式 Shell 模式
LLM 执行动作的方式 调用你注册的 Java 工具 执行 Shell 命令(bash/cmd
安全性 高,只能调用你预定义的工具 低,LLM 可以执行任意命令
适用场景 生产环境 实验、原型验证
需要写 Java 代码 是,每个动作都要有对应的工具实现 否,Skill 指令中直接写 Shell 命令

以上面查询数据库的功能为例 :

  • Tool 模式下,Skill 指令写的是「调用 executeQuery 工具」,LLM 去调用你注册的 MCP 工具。
  • Shell 模式下,Skill 指令可以直接写「执行 mysql -u root -p demo -e 'SELECT * FROM users'」,LLM 会通过 run_shell_command 工具直接在服务器上执行这条命令。

Shell 模式 的工作流程如下:

  1. 系统提示先告诉模型:有哪些技能、每个技能文件在服务器上的绝对路径。
  2. 用户提问需要用到某个技能时,
  3. 模型直接在系统里执行 cat 命令,去读对应目录下的 SKILL.md 指令文件。
  4. 读完指令后,模型继续调用更多 shell 命令一步步完成任务。

如果想要使用 Shell 模式,需要引入如下依赖:

xml 复制代码
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-experimental-skills-shell</artifactId>
    <version>1.12.2-beta22</version>
</dependency>

通过 Shell 模式加载的 Skills 必须通过 FileSystemSkillLoader 加载,如下

java 复制代码
ShellSkills skills = ShellSkills.from(FileSystemSkillLoader.loadSkills(Path.of("skills/")));

MyAiService service = AiServices.builder(MyAiService.class)
        .chatModel(chatModel)
        .toolProvider(skills.toolProvider()) // or .toolProviders(myToolProvider, skills.toolProvider()) if you already have a tool provider configured
        .systemMessage("You have access to the following skills:\n" + skills.formatAvailableSkills()
                + "\nWhen the user's request relates to one of these skills, read its SKILL.md before proceeding.")
        .build();

skills.formatAvailableSkills() 生成的 XML 会多包含一个 location 标签,如下:

xml 复制代码
<available_skills>
    <skill>
        <name>docx</name>
        <description>Edit and review Word documents using tracked changes</description>
        <location>/path/to/skills/docx/SKILL.md</location>
    </skill>
    <skill>
        <name>data-analysis</name>
        <description>Analyse tabular data and produce charts</description>
        <location>/path/to/skills/data-analysis/SKILL.md</location>
    </skill>
</available_skills>

Shell 模式下可以通过 RunShellCommandToolConfig 就是来调整 Shell 的执行环境,如:

  • workingDirectory:命令在哪个目录下执行
  • maxStdOutChars / maxStdErrChars:限制输出长度,防止命令输出太多撑爆 Token
  • name / description:自定义工具名和描述(跟之前 ActivateSkillToolConfig 的思路一样)
java 复制代码
ShellSkills skills = ShellSkills.builder()
        .skills(mySkills)
        .runShellCommandToolConfig(RunShellCommandToolConfig.builder()
                .name(...)                              // tool name (default: "run_shell_command")
                .description(...)                       // tool description (default: includes OS name)
                .commandParameterName(...)              // command parameter name (default: "command")
                .commandParameterDescription(...)       // command parameter description
                .timeoutSecondsParameterName(...)       // timeout parameter name (default: "timeout_seconds")
                .timeoutSecondsParameterDescription(...) // timeout parameter description
                .workingDirectory(...)                  // working directory for commands (default: JVM's user.dir)
                .maxStdOutChars(...)                    // max stdout chars in result (default: 10_000)
                .maxStdErrChars(...)                    // max stderr chars in result (default: 10_000)
                .executorService(...)                   // ExecutorService for reading stdout/stderr streams
                .throwToolArgumentsExceptions(...)      // throw ToolArgumentsException instead of ToolExecutionException (default: false)
                .build())
        .build();

五、Skills 推荐

下面是一些 Skills 推荐网站。

  • agentskills.to :目前最活跃的 Skills 市场,上面有社区发布的各种技能,涵盖前端设计、图片处理、RAG 管道、音乐生成等方向。
  • agskills.dev : 精选的 Skills 合集,按设计模式和能力分类
  • skillsmp.com : 聚合了 GitHub 上的 Skills 仓库,提供搜索和质量评分
  • agent-skills.app :社区驱动的 Skills 库

六、参考内容

  1. LangChain4J 官网
  2. 豆包
相关推荐
极客先躯5 天前
高级java每日一道面试题-2025年9月23日-企业集成篇[LangChain4j]-如何与现有的企业中间件集成(Kafka、RabbitMQ)?
java·中间件·java-rabbitmq·稳定性·可靠性·扩展性·langchain4j
豆豆豆大王13 天前
LangChain4j 中使用 RedisEmbeddingStore 指定知识库存储位置
langchain4j
大傻^17 天前
LangChain4j Spring Boot Starter:自动配置与声明式 Bean 管理
java·人工智能·spring boot·spring·langchain4j
大傻^17 天前
LangChain4j 核心抽象:ChatMessage、UserMessage 与模型无关设计
人工智能·rag·langchain4j
大傻^17 天前
LangChain4j Agent 模式:ReAct、Plan-and-Solve 与自主决策
人工智能·agent·langchain4j·自主决策
大傻^17 天前
LangChain4j RAG 核心:Document、Embedding 与向量存储抽象
开发语言·人工智能·python·embedding·langchain4j
大傻^18 天前
LangChain4j AI Services 深度解析:声明式 API 与接口驱动开发
人工智能·langchain·openai·langchain4j
大傻^18 天前
LangChain4j 企业知识库实战:PDF 解析、OCR 与文档加载器生态
人工智能·pdf·ocr·langchain4j
大傻^18 天前
LangChain4j 记忆架构:ChatMemory、持久化与跨会话状态
java·人工智能·windows·架构·langchain4j