用 TinyAgent 从零实现 Skill 机制
作者:程序员马丁
Ragent AI —— 从 0 到 1 纯手工打造企业级 Agentic RAG,拒绝 Demo 玩具!AI 时代,助你拿个offer。
上一篇梳理了 Skill 的通用规范和行业框架实现——SKILL.md 是什么格式、渐进式披露怎么工作、Spring AI 和 LangChain4j 分别怎么做。这一篇把概念落地:用 TinyAgent 从零实现 activate_skill 模式,包括 SKILL.md 解析、技能专属工具隔离、DynamicToolProvider 动态注入,以及完整的 Demo 演示。
TinyAgent 对齐了行业主流的执行模式——LLM 调用 activate_skill(name) 加载技能指令到主上下文,同时解锁技能专属工具,然后在主循环中按指令调用业务工具。整个过程只有一个 LLM、一个对话上下文,没有子 Agent。
本项目中具体代码已上传 GitHub TinyAgent,大家 Clone 项目后,将代码分支切换到 1.11.x,默认主分支是最新代码。运行前复制
.env.example为.env,把自己的 API Key 填进去,默认阿里云百炼平台;.env已加入.gitignore,切分支时不会丢。
TinyAgent 怎么用 Skill
TinyAgent 对齐了行业主流的 activate_skill 模式——LLM 调用 activate_skill(name) 加载技能指令到主上下文,同时解锁技能专属工具,然后在主循环中按指令调用业务工具。在标准格式之上,TinyAgent 只新增了一个 tools 字段,做了一处简化。
1. 为什么需要 tools 字段
上一篇讲到 LangChain4j 有技能作用域工具(Skill-Scoped Tools)——绑定在 Skill 上的工具,激活前对 LLM 不可见,激活后才动态暴露。TinyAgent 也实现了这个机制,原因是:如果所有工具都可见,LLM 会跳过 activate_skill 直接调用业务工具,技能指令永远不会进入上下文。
打个比方,如果 LLM 从一开始就能看到 applyRefund,用户要求退掉订单 88231 时,LLM 很可能直接调 applyRefund——毕竟功能名称和描述就摆在眼前,何必多走一步激活技能?但这样一来,先查订单 → 验证状态 → 再退款的标准流程就被跳过了,技能指令永远不会被读取。
这就是鸡生蛋问题:LLM 不会调 activate_skill,因为它已经看到了想要的工具;技能指令不进上下文,因为 LLM 没调 activate_skill。

解决方案很直接——在 Skill 定义中声明专属工具,启动时从 LLM 可见列表中排除,激活后才动态注入:
queryOrder、applyRefund、queryLogistics这类流程专属工具放进技能的tools列表,激活前不可见searchKnowledge、getCurrentTime这类通用工具保持始终可见- LLM 初始只看到通用工具 +
activate_skill,必须先激活技能才能拿到业务工具
这和 LangChain4j 的 Skill-Scoped Tools 是同一个思路。区别在于 LangChain4j 通过
ToolProvider机制在每轮 LLM 调用前重新评估可见工具,TinyAgent 用更轻量的DynamicToolProvider接口在工具执行后一次性注入——实现不同,效果一致。
2. 我们简化了哪些规则
Skill 是单文件而非文件夹。官方规范中,一个 Skill 是以 SKILL.md 为入口的整个文件夹(可含 scripts/、references/ 等子目录)。TinyAgent 简化为 skills/ 目录下的单个 .md 文件,没有子目录结构。这是因为当前的两个技能(退款、查单)流程简单,不需要脚本和参考文档。生产环境如果技能复杂度上升,应该按照官方规范的文件夹结构组织。
3. 比特严选的两个 Skill 定义
退款处理技能:
<!-- resources/skills/process-refund.md -->
---
name: process-refund
description: >
退款处理技能:自动查询订单状态,验证退款资格,条件满足则提交退款申请。
适用于用户要求退货退款的场 景,无需手动分步操作。
tools:
- queryOrder
- applyRefund
---
# 退款处理
## 概述
你是比特严选的退款处理专员。当用户要求退货退款时,按照以下流程处理。
## 处理步骤
### Step 1: 查询订单
使用 queryOrder 查询用户提供的订单号,确认订单状态和商品信息。
### Step 2: 验证退款条件并处理
根据订单状态判断:
- 如果状态为已签收,使用 applyRefund 提交退款申请,退款原因使用用户描述的原因
- 如果状态不是已签收,直接告知用户当前订单状态,说明需要签收后才能申请退款
- 如果订单不存在,告知用户订单号可能有误
## 输出要求
处理完成后,给出简洁明了的处理结果。包含订单商品名称、退款单号(如有)和预计到账时间。
订单全流程查询技能:
<!-- resources/skills/order-inquiry.md -->
---
name: order-inquiry
description: >
订单全流程查询技能:查询订单详情,如果已发货则同时查询物流轨迹,
返回包含订单状态和物流进度的完整信息。无需分步调用。
tools:
- queryOrder
- queryLogistics
---
# 订单全流程查询
## 概述
你是比特严选的订单查询专员。当用户要求查看订单状态或物流进度时,按照以下流程查询完整信息。
## 处理步骤
### Step 1: 查询订单详情
使用 queryOrder 查询订单详情,获取商品名称、价格、订单状态、运单号等信息。
### Step 2: 查询物流(按需)
如果订单有运单号(trackingNo 不为空),使用 queryLogistics 查询物流轨迹。
如果订单没有运单号,说明可能尚未发货,只返回订单基本信息即可。
## 输出要求
查询完成后,给出一段完整的订单状态说明,包含商品信息、当前状态和物流进度(如有)。
frontmatter 有三个字段:name(kebab-case,符合官方规范)、description 是标准字段,tools 是 TinyAgent 扩展的——声明该技能的专属工具,激活前对 LLM 不可见。Markdown 正文就是技能指令——LLM 激活技能后读取这段正文,按步骤调用里面提到的工具。
4. Tool vs Skill:本项目的边界
在 TinyAgent 里,Tool 和 Skill 的区别如下:
| 维度 | Tool(工具) | Skill(技能) |
|---|---|---|
| 定义方式 | Java 类,实现 Tool 接口 | .md 文件,YAML 前置元数据 + Markdown 指令 |
| 发现方式 | 代码中手动注册到 ToolRegistry | 从 skills/ 目录自动扫描加载 |
| 执行方式 | 直接调用业务接口,无 LLM 参与 | LLM 在主上下文中读取指令,按步骤调用工具 |
| 灵活性 | 高(LLM 自由组合) | 中等(LLM 在指令约束下执行) |
| 新增成本 | 写 Java 代码、编译、部署 | 新建一个 .md 文件,无需编译 |
instructions和 Plan-and-Execute 的 Planner 提示词的区别:Planner 的提示词教 LLM 怎么拆解步骤(元能力),Skill 的 Markdown 正文直接告诉 LLM 具体该怎么做(领域知识)。前者教你怎么做规划,后者要求照着这份规划来执行。
Skill 数据类与核心组件
1. Skill 数据类
SKILL.md 文件加载后需要映射成 Java 对象。Skill 是一个纯粹的数据类:
@Data
@NoArgsConstructor
public class Skill {
private String name;
private String description;
private List<String> tools;
private String instructions;
}
用 Lombok 的 @Data 生成 getter/setter,@NoArgsConstructor 用于 Jackson 反序列化。和之前几篇的 Tool 接口不同,Skill 不实现 Tool 接口——Skill 是数据类,存放从 YAML 加载的配置;Tool 是行为接口,定义了 invoke() 方法。两者之间通过 ActivateSkillTool 桥接。
四个字段:name 和 description 来自 YAML frontmatter,tools 也来自 frontmatter——声明该技能的专属工具名列表,instructions 来自 Markdown 正文。tools 里列出的工具在技能激活前对 LLM 不可见,激活后由 DynamicToolProvider 动态注入。
2. 类关系一览

SkillRegistry:目录发现与注册
SkillRegistry 做三件事:从目录扫描 .md 文件、按 SKILL.md 格式解析、通过 buildAgentRegistry() 构建主 Agent 的工具注册表。
public class SkillRegistry {
private final Map<String, Skill> skills = new LinkedHashMap<>();
private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
public SkillRegistry() {
}
// 从指定目录扫描所有 .md 文件,按 SKILL.md 格式解析
public void loadFromDirectory(Path skillsDir) {
// ...... 遍历 *.md 文件,调用 parseSkillMd
}
/**
* 构建主 Agent 的工具注册表:
* - 收集所有技能声明的 tools,标记为技能专属
* - 只注册不属于任何技能的通用工具 + activate_skill
* - 技能专属工具在 activate_skill 激活后由 DynamicToolProvider 动态注入
*/
public ToolRegistry buildAgentRegistry(ToolRegistry baseRegistry) {
Set<String> skillScopedNames = new HashSet<>();
for (Skill skill : skills.values()) {
if (skill.getTools() != null) {
skillScopedNames.addAll(skill.getTools());
}
}
ToolRegistry agentRegistry = new ToolRegistry();
for (Tool tool : baseRegistry.getTools()) {
if (!skillScopedNames.contains(tool.name())) {
agentRegistry.register(tool);
}
}
agentRegistry.register(new ActivateSkillTool(this, baseRegistry));
return agentRegistry;
}
// ......
}
构造函数很简单——不需要任何外部依赖。SkillRegistry 只负责加载和注册,不参与执行。buildAgentRegistry() 的核心逻辑是分离通用工具和技能专属工具:先遍历所有技能的 tools 列表,收集技能专属工具名(如 queryOrder、applyRefund、queryLogistics),然后只把不在这个集合里的通用工具注册到主 Agent 的注册表。ActivateSkillTool 同时接收 this(用于查找技能定义)和 baseRegistry(全量注册表,用于激活时查找专属工具对象)。
1. SKILL.md 解析
SkillRegistry 扫描 skills/ 目录下的 .md 文件,按 SKILL.md 格式解析——提取 --- 之间的 YAML 前置元数据和 --- 之后的 Markdown 正文:
private Skill parseSkillMd(Path file) throws IOException {
String content = Files.readString(file);
String trimmed = content.strip();
int secondSep = trimmed.indexOf("---", 3);
String yamlPart = trimmed.substring(3, secondSep).strip();
String markdownBody = trimmed.substring(secondSep + 3).strip();
// YAML 部分解析为 Skill 对象(name、description、tools)
Skill skill = yamlMapper.readValue(yamlPart, Skill.class);
// Markdown 正文作为 instructions
skill.setInstructions(markdownBody);
return skill;
}

yamlMapper 是 new ObjectMapper(new YAMLFactory()),Jackson 的 YAML 扩展模块。Maven 依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
2. 两个注册表的关系
buildAgentRegistry() 构建的主 Agent 注册表不包含技能专属工具——它们被隔离到激活后才可见:

LLM 初始只能看到 3 个工具(2 个通用工具 + 1 个 activate_skill)。queryOrder