LangChain4j SKILL.md 编写实战:用 Skill 约束 LLM 正确调用工具
2026/9/15 15:34:58 网站建设 项目流程

LangChain4j SKILL.md 编写实战:用 Skill 约束 LLM 正确调用工具

【免费下载链接】langchain4jLangChain4j is an idiomatic, open-source Java library for building LLM-powered applications on the JVM. It offers a unified API over popular LLM providers and vector stores, and makes implementing tool calling (including MCP support), agents and RAG easy. It integrates seamlessly with enterprise Java frameworks like Quarkus and Spring Boot.项目地址: https://gitcode.com/GitHub_Trending/la/langchain4j

导读

本文以 LangChain4j 仓库中一个真实的技能(Skill)示例——using-process-tool为核心,逐行拆解SKILL.md的编写规范与执行机制。你将掌握:如何通过 YAML front matter 声明技能的名称与用途,如何在指令正文中用自然语言约束 LLM 的多工具调用顺序与参数约定,如何借助 references 资源实现"按返回码条件分派"的分支逻辑,以及技能从文件系统加载到activate_skill激活的底层调用链。阅读后你可以直接照着仓库中的示例,在自己的 LangChain4j 应用中编写出可被 LLM 正确执行、可被测试用例验证的 Skill 文件。

一、Skill 与 SKILL.md:LangChain4j 的 Agent Skills 实现

LangChain4j 在langchain4j-skills模块中实现了业界通用的Agent Skills 规范:把"如何正确使用工具"这类过程性知识从硬编码的 prompt 中剥离出来,打包成独立的、可复用的技能文件,让 LLM 按需"激活"并阅读。

技能的核心抽象是 Skill 接口,每个技能包含四个要素:

要素方法说明
名称name()技能的全局唯一标识,LLM 据此在候选技能中做选择
描述description()技能的简短说明,LLM 据此判断当前请求是否与技能相关
指令正文content()技能的完整操作说明(通常即SKILL.md文件正文)
附加资源resources()可选的参考资料、脚本、模板等,默认空列表

其中name()description()是 LLM始终可见的(会被注入系统提示),而content()resources()是 LLM按需读取的——这正是 Agent Skills 的核心设计:技能详情不占用每轮对话的上下文,只在 LLM 判定"我需要这个技能"时才被取回。

从源码结构看,技能文件遵循一条硬性约定:每个技能必须位于独立目录中,且目录内必须存在一个SKILL.md文件,文件头部必须包含声明namedescription的 YAML front matter 块。本仓库的langchain4j-skills/src/test/resources/skills/下提供了三个可直接研读的示例:

  • using-process-tool/:本文主角,演示多工具调用序列与返回码条件分派;
  • greeting-user/:演示如何让 LLM 先执行scripts/hello.py脚本再按结果文档继续(见 SKILL.md);
  • test-skill/:演示携带多个 references 资源与图片资源的技能。

二、完整示例拆解:using-process-tool 技能

本文的关联文档位于 langchain4j-skills/src/test/resources/skills/using-process-tool/SKILL.md,其完整目录结构如下:

using-process-tool/ ├── SKILL.md # 技能主文件(front matter + 指令正文) └── references/ ├── 17.md # process 返回码 17 时的分支指南 └── 25.md # process 返回码 25 时的分支指南

SKILL.md全文只有 13 行,却完整覆盖了技能文件的三个核心构件:front matter、指令正文、对 references 资源的条件引用。我们逐段拆解。

2.1 Front Matter:技能的元数据声明

--- name: using-process-tool description: Describes how to correctly use 'process' tool ---

---包裹的 YAML 块声明了两个必填字段:

  • nameusing-process-tool,技能的唯一名称。在 SkillsTest.java 中可以看到,Skills.from(skill)后调用formatAvailableSkills()的输出会包含该名称,而 LLM 调用activate_skill工具时必须传入与之一致的skill_name
  • descriptionDescribes how to correctly use 'process' tool,一句话说明技能用途。它会被放入系统提示供 LLM 预筛技能,因此应当写清"做什么 + 涉及哪个工具",方便 LLM 在用户请求与技能之间建立关联。

2.2 指令正文:编排工具调用协议

front matter 之下的正文是技能的指令内容,原文如下:

When user asks you to use the 'process' tool, you need to first call the 'generate' tool with 2 arguments: arg0 (surname) and arg1 (name).

When you have an id, call the 'process' tool with 3 arguments: arg0 (name), arg1 (id), arg2 (surname).

If 'process' tool returns code 17, proceed with this guide, if it returns code 25, proceed with this guide.

这段自然语言指令解决了 LLM 工具调用中最常见的两个问题:

1. 多工具调用顺序约束。指令明确规定了"先generate,后process"的先后关系,并将前一步的输出(id)作为后一步的入参。测试用例 SkillsTest.java 用 Mockito 验证了这一顺序:generate("Heisler", "Klaus")必须先于process("Klaus", 177, "Heisler")被调用。

2. 参数别名约定。注意指令中使用的参数名是arg0arg1arg2,而非语义化的nameidsurname。结合同文件中的测试工具定义可以推断:这是故意设计的场景——当工具的参数名本身"不可读"或容易混淆时(测试注释明确写道:"这些工具刻意使用通用名、不一致的参数与晦涩的返回值,只有加载了技能内容/references 后才能'理解'它们"),SKILL.md就充当了参数映射表,把语义(surname/name/id)与位置(arg0/arg1/arg2)一一对应起来:

调用参数语义
generatearg0surname(姓)
generatearg1name(名)
processarg0name(名)
processarg1id(由 generate 返回)
processarg2surname(姓)

3. 返回码条件分派。最后一行让 LLM 依据process的返回值决定下一步动作,并把决策细节"延迟加载"到 references 资源中——正文只保留最小分派逻辑,避免在每轮上下文里携带全部分支细节。

三、References 资源:条件分支的下钻文档

原文档中的两个相对链接指向技能目录下的 references 文件,从仓库根目录看,它们分别是 references/17.md 与 references/25.md。这两个文件同样精炼:

references/17.md

If 'process' tool returns code 17, you need to call the 'finish' tool. Do not call the 'reset' tool!

references/25.md

If 'process' tool returns code 25, you need to call the 'reset' tool. Do not call the 'finish' tool!

两文件形成了互斥的指令对:返回码 17 → 调用finish(明确禁止reset);返回码 25 → 调用reset(明确禁止finish)。这种"正面指令 + 反面禁令"的写法是技能文档的重要技巧——LLM 在面对两个语义相近的工具时容易混淆,明确的Do not call ...能显著降低误调用的概率。

当 LLM 需要读取该分支文档时,会调用由技能框架暴露的read_skill_resource工具。从 Skills.java 的构建逻辑可见,该工具携带两个必填参数skill_namerelative_path,且relative_path的参数描述会按技能资源自动生成(形如For example: references/\d+\.md,见测试断言 SkillsTest.java)。在测试的模拟调用序列中,LLM 正是通过read_skill_resource请求"skill_name": "using-process-tool", "relative_path": "references/25.md"来读取分支文档的(见 SkillsTest.java)。

四、从文件到 LLM:技能的加载与激活机制

理解了技能文件本身,再看它如何进入运行中的 LLM 应用。

4.1 加载:FileSystemSkillLoader / ClassPathSkillLoader

技能可以从文件系统或 classpath 加载。以文件系统为例,FileSystemSkillLoader.java 的loadSkills(Path directory)会扫描指定目录的直接子目录,仅把"包含SKILL.md"的子目录识别为技能;loadSkill(Path skillDirectory)则加载单个技能目录。若目录中缺少SKILL.md,会直接抛出IllegalArgumentException

加载过程的关键步骤(在ClassPathSkillLoader中实现,逻辑与文件系统版一致):

  1. 读取SKILL.md全文;
  2. 用 SkillLoaderCommon 解析 YAML front matter,提取namedescription,front matter 之外的部分成为content()
  3. 递归扫描技能目录下的其余文件作为resources()排除规则包括:SKILL.md本身、scripts/子目录下的文件、以及内容为空的文件(会被静默跳过)。

这套规则解释了using-process-tool目录为什么能只把references/17.mdreferences/25.md暴露为资源——它们恰好位于默认会被加载的路径下。

4.2 激活:activate_skill 与动态 ToolProvider

技能加载后通过Skills.from(skill)包装,并经由toolProvider()接入AiServices。从 Skills.java 的实现看,Skills会向 LLM 暴露两个管理工具:

  • activate_skill:必选。参数为skill_name,执行后把技能全文content()返回给 LLM。其默认名称、描述、参数名与默认值均定义在 ActivateSkillToolConfig.java 中(工具名activate_skill、参数skill_name),也可通过 Builder 自定义。
  • read_skill_resource:可选,仅当至少一个技能携带资源时才会注册。

ActivateSkillToolExecutor.java 展示了激活的执行逻辑:按skill_name查表,命中后把技能对象作为执行结果返回,同时把resultText设为技能全文,并写入名为activated_skill的会话属性(该属性在消息中流转,供后续轮次识别"哪些技能已激活");若技能名不存在,则返回错误信息并附上全部可用技能名。

更有意思的是Skills动态 ToolProvider设计:isDynamic()返回 true,意味着每次请求时提供工具集合是动态计算的。其核心逻辑是——技能专属工具(skill-scoped tools)只有在对应技能被激活后才暴露给 LLM,激活前 LLM 只能看到activate_skill/read_skill_resource两个管理工具。这一点在 SkillsTest.java 的多个测试中都有严格验证:激活前请求中不存在query_inventory等技能工具,激活后这些工具出现且不会重复;普通工具(如processgenerate)则始终可见,与技能激活状态无关。

4.3 接入 AI Service

参考 Skills.java 的 Javadoc 示例,接入方式如下:

Skills skills = Skills.from(FileSystemSkillLoader.loadSkills(skillsDir)); MyAiService service = AiServices.builder(MyAiService.class) .chatModel(chatModel) .systemMessage("You have access to the following skills:\n" + skills.formatAvailableSkills() + "\nWhen the user's request relates to one of these skills, " + "activate it first using the `activate_skill` tool before proceeding.") .toolProvider(skills.toolProvider()) .build();

其中formatAvailableSkills()会把所有技能输出为 XML 结构(<available_skills><skill><name>...</name><description>...</description></skill></available_skills>),注入系统提示后,LLM 便知道"有哪些技能可用、各自干什么、用哪个工具激活"。

五、测试用例验证:一条完整的技能执行链路

using-process-tool技能之所以设计得如此精妙,是因为它被 SkillsTest.java 中的should_activate_skill_and_load_resource测试完整驱动。该测试定义了一组"语义晦涩"的工具:

@Tool int process(String name, int id, String surname) { return 25; } @Tool int generate(String surname, String name) { return 177; } @Tool void finish() { } @Tool void reset() { }

测试用ChatModelMock模拟 LLM,逐步执行出如下的工具调用序列(与SKILL.md指令完全对应):

activate_skill {"skill_name": "using-process-tool"} → generate {"arg0": "Heisler", "arg1": "Klaus"} // 先查姓氏/名字 → process {"arg0": "Klaus", "arg1": 177, "arg2": "Heisler"} // 用 generate 返回的 177 作为 id → read_skill_resource {"skill_name": "using-process-tool", "relative_path": "references/25.md"} // process 返回 25,读取对应分支文档 → reset {} // 按 25.md 指引调用 reset → "Done."

最终断言严格验证了generate("Heisler", "Klaus")process("Klaus", 177, "Heisler")reset()恰好各被调用一次,且没有调用finish()verifyNoMoreInteractions),完整印证了"返回码 25 → 读 references/25.md → 调 reset 不调 finish"的分支逻辑。测试还提供了should_activate_skill_and_load_resource__programmatic变体,用Skill.builder()以纯编程方式构造等价技能,证明同一技能既能来自文件系统,也能完全在代码中定义。

此外,仓库中的greeting-user技能(SKILL.md)展示了另一类写法:指令正文用 bash 代码块指示 LLM 运行scripts/hello.py,再按references/processing-result.md处理输出——注意脚本位于scripts/子目录,正是加载器专门排除的资源目录,说明脚本设计为在技能侧通过指令执行、而不是作为文本资源读取。

六、编写高质量 SKILL.md 的实战建议

基于以上拆解,编写一个可被 LLM 可靠执行的技能文件,可以遵循以下清单:

  1. front matter 只声明两件事name用短横线连接的唯一标识(如using-process-tool),description用"功能 + 涉及工具"的一句话描述,便于 LLM 预筛。
  2. 正文写清楚"顺序 + 参数映射":多工具协作时,用步骤式语言规定调用先后;工具参数若语义不直观,用arg0 (surname)这类"位置 + 语义"的标注建立映射。
  3. 条件分支用 references 下钻:正文只写"if code 17 → [guide]",把具体动作放进references/N.md,通过read_skill_resource按需读取,节省上下文并保持主指令精简。
  4. 关键分支写反向禁令:对易混淆工具(如finishvsreset)同时给出Do not call ...的否定指令,降低 LLM 误判概率。
  5. 遵守目录约定SKILL.md放在技能目录根下,辅助文档放references/等子目录;脚本放scripts/会被加载器排除,不会被当作文本资源读给 LLM。
  6. 用测试验证全链路:参考SkillsTest的模式,用 mock 模型驱动activate_skill → 业务工具 → read_skill_resource → 分支动作的完整序列,并对调用顺序、参数值、分支选择做断言,确保技能指令与工具实现严格对齐。

七、结语

using-process-tool虽然只是测试资源目录下一个 13 行的 Markdown 文件,却浓缩了 Agent Skills 规范在 LangChain4j 中的全部核心机制:front matter 驱动的技能元数据、自然语言编写的工具调用协议、references 资源实现的按需分支读取、以及activate_skill/read_skill_resource双工具加动态 ToolProvider 的运行时编排。理解这个最小示例的每一行,就掌握了在 LangChain4j 中为 LLM 编写"说明书"的基本功——它让你的工具调用从"碰运气"变成"有据可依"。

【免费下载链接】langchain4jLangChain4j is an idiomatic, open-source Java library for building LLM-powered applications on the JVM. It offers a unified API over popular LLM providers and vector stores, and makes implementing tool calling (including MCP support), agents and RAG easy. It integrates seamlessly with enterprise Java frameworks like Quarkus and Spring Boot.项目地址: https://gitcode.com/GitHub_Trending/la/langchain4j

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询