Skills 自动加载机制:Agent 如何"读取"Skills
关键点:Skill 不是"自动读取"的,而是模型"主动调用"的。这个"看起来自动"的效果,来自三方协作 —— Harness(运行时)注入元数据,模型判断相关性,工具机制加载正文。
所属专题:Skills 协议 (skills·02)
Skills 自动加载机制:Agent 如何”读取”Skills
关键点:Skill 不是”自动读取”的,而是模型”主动调用”的。这个”看起来自动”的效果,来自三方协作 —— Harness(运行时)注入元数据,模型判断相关性,工具机制加载正文。
一、核心机制:三方协作
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Harness │ │ LLM │ │ Filesystem │
│(Claude Code)│ │ (模型) │ │ (skills目录)│
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
1. 扫描目录,读元数据 ─────────────────────────►│
│ │ │
2. 注入 skill 列表 ────►│ │
│ │ │
│ 3. 匹配 description │
│ (纯模型推理) │
│ │ │
│ 4. 调用 Skill 工具 ┌───────────┐ │
│◄───────────────────┤ tool_use │ │
│ └───────────┘ │
│ │
5. 读取 SKILL.md ─────────────────────────────►│
│◄─────────────────────── 返回正文 ────────│
│ │
6. 作为 tool_result 返回 ►│ │
│ │ │
│ 7. 按 skill 指引执行 │
二、分步拆解实现
步骤 1:会话启动时扫描目录
Harness(Claude Code CLI)启动时:
# 伪代码
def load_skills():
skills = []
for path in ["~/.claude/skills/", ".claude/skills/", plugin_dirs]:
for skill_dir in scan(path):
skill_md = read(skill_dir / "SKILL.md")
frontmatter = parse_yaml_header(skill_md)
skills.append({
"name": frontmatter["name"],
"description": frontmatter["description"],
"path": skill_dir, # 记住路径,正文按需读
})
return skills
关键:只读 frontmatter,正文完全不加载。
步骤 2:把元数据注入 system prompt
Harness 在构造 LLM 请求时,把 skill 列表塞进 system message:
<system-reminder>
The following skills are available for use with the Skill tool:
- brainstorming: Use before any creative work...
- systematic-debugging: Use when encountering any bug...
- test-driven-development: Use when implementing any feature...
- deep-research: When the user wants a deep, multi-source report...
</system-reminder>
这就是你在对话开头看到的 available-skills 那段的来源。
步骤 3:暴露 Skill 工具给模型
同时,Harness 通过 Function Calling 机制注册一个 Skill 工具:
{
"name": "Skill",
"description": "Execute a skill within the main conversation",
"input_schema": {
"type": "object",
"properties": {
"skill": {"type": "string", "description": "The name of the skill"},
"args": {"type": "string"}
}
}
}
现在模型知道:“我有一堆 skill,用 Skill 工具就能加载”。
步骤 4:模型自主判断
用户发消息后,LLM 收到:
[system prompt]
[skill 列表]
[available tools: Skill, Read, Bash, ...]
[user] 帮我 debug 这个 TypeError
模型内部推理:
“用户说 debug ——
systematic-debugging的 description 匹配 —— 我应该调用 Skill 工具加载它”
这一步没有任何代码在”判断”,是模型自己决定的。 这就是为什么 description 写得好不好,直接决定 skill 能否被触发。
步骤 5:模型调用 Skill 工具
模型输出一个 tool_use:
{
"type": "tool_use",
"name": "Skill",
"input": {"skill": "systematic-debugging"}
}
步骤 6:Harness 拦截、读文件、返回
Harness 收到 tool_use:
def handle_skill_tool(skill_name):
skill = find_skill(skill_name)
body = read_file(skill.path / "SKILL.md")
return {"type": "tool_result", "content": body}
正文作为 tool_result 塞回下一轮 LLM 请求。
步骤 7:模型按指引行动
现在模型的上下文里有了完整 skill 正文,接下来的回复就会按照 skill 里的步骤走。
三、“自动”的假象从何而来
很多人以为是”自动读取”,其实是这两件事的错觉:
- 元数据常驻 → 每次对话模型都”看见”所有 skill 存在
- 模型判断快 → 匹配 + 调用工具在一次响应里完成,用户感受不到中间步骤
真相:
- Harness 不判断相关性,它只做”扫目录 + 提供工具 + 读文件”
- LLM 不”自动加载”,它是主动调用 Skill 工具
- 整个机制的智能部分完全在模型的 in-context reasoning
四、类比:图书馆借阅
| 现实 | Skill 机制 |
|---|---|
| 图书馆入口的索引卡片墙 | system prompt 里的 skill 列表 |
| 你(读者)扫一眼卡片决定借哪本 | 模型判断哪个 skill 相关 |
| 拿卡片去柜台请馆员取书 | 调用 Skill 工具 |
| 馆员从书架取书给你 | Harness 读 SKILL.md 返回 |
| 你按书上的方法做事 | 模型按 skill 指引执行 |
没有一步是”自动”的,但整个流程流畅得像自动。
五、几个关键实现细节
1. 为什么 description 是”触发钥匙”
因为它是模型判断相关性的唯一线索(元数据阶段正文不可见)。写得模糊 → 模型不知道何时用 → skill 形同虚设。
好的 description 模板:
Use when [触发场景], before [某个行动]
2. 为什么可以扩展上百个 skill
- 100 个 skill 元数据 ≈ 5000 token,占上下文比例小
- 只有真正触发的才展开正文(几千 token)
- 空间复杂度 O(元数据),而非 O(全部内容)
3. 为什么显式 /skill-name 也能触发
用户输入 /brainstorming 时,Harness 会:
- 识别为 slash command
- 直接构造一个 tool_use 让模型执行 Skill 工具
- 相当于跳过模型判断,强制加载
4. Subagent 隔离
skill 里常写”如果你是 subagent,忽略这条”。因为 subagent 有独立上下文,它也会看到 skill 列表 —— 如果不加限制,会导致递归触发。
5. 系统提示注入
技术上,skill 列表通过 system-reminder tag 注入。这是 Anthropic 定义的一种特殊结构,模型被训练成”注意但不直接回应”这类消息 —— 保证 skill 列表不干扰对话流。
六、代码层实现要点(如果你要自己造)
若要在自研 Agent 里复刻这套机制,核心是四件事:
# 1. 启动时索引
skills = scan_skills_directory() # 只读 frontmatter
# 2. 每轮请求注入元数据
system_prompt += format_skills_list(skills)
# 3. 注册工具
tools.append(SKILL_TOOL_SCHEMA)
# 4. 处理 tool_use
def on_tool_call(name, args):
if name == "Skill":
return read_file(skills[args["skill"]].path / "SKILL.md")
就这么简单。复杂度全在两个非代码的部分:
- 写好 description(触发精准度)
- 写好 SKILL.md 正文(执行有效性)
七、能力归属:LLM vs Harness 的边界
一个常见的理解误区是:“LLM 自己会读取 skills”。更精确的说法是:LLM 出厂不知道什么叫 skill,是 Harness 工程赋予了 Agent 使用 skills 的能力。
1. LLM 出厂时只有”通用原语”
LLM 训练时其实拥有两个通用能力,是 skill 机制得以运作的基础:
| 能力 | 说明 | 谁提供 |
|---|---|---|
| Function Calling / Tool Use | 能输出结构化 tool_use 调用工具 | 模型训练时具备 |
| In-context Reasoning | 能根据 system prompt 里的信息做判断 | 模型固有能力 |
“Skill”这个概念根本不存在于模型训练里 —— 它是应用层构造出来的。Harness 做的是利用这两个原语,搭建一套”skill 协议”:
- 用 in-context reasoning → 让模型判断哪个 skill 相关
- 用 function calling → 让模型调用 Skill 工具加载正文
2. “自主”是模型的,“读取”是 Harness 的
“自主读取”其实是两件事的组合:
| 动作 | 谁做的 |
|---|---|
| 决定要不要用某个 skill | LLM 自主判断(推理) |
| 实际读取磁盘上的 SKILL.md | Harness 执行(模型没有文件系统权限) |
模型永远无法自己”读文件”,它只能请求 Harness 帮它读(通过 tool_use)。
3. 能力分层图
┌─────────────────────────────────────────────────────┐
│ LLM 出厂能力(通用原语) │
│ ├─ Function Calling │
│ ├─ In-context Reasoning │
│ └─ 文本生成 │
└─────────────────────────────────────────────────────┘
↓
Harness 工程在这些原语上加了一层协议
↓
┌─────────────────────────────────────────────────────┐
│ Skill 协议(Harness 定义) │
│ ├─ 扫描 skill 目录 │
│ ├─ 注入元数据到 system prompt │
│ ├─ 注册 Skill 工具 │
│ └─ 拦截 tool_use、读文件、返回内容 │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Agent 表现出的行为(用户视角) │
│ "Agent 自主读取并使用 skill" │
└─────────────────────────────────────────────────────┘
4. 一个反证
如果你直接调用 Anthropic API(不用 Claude Code 这类 Harness),把一段用户消息发给 Claude:
- 模型完全不知道你机器上有哪些 skill
- 也不会主动”搜索”任何 skill
- 因为没有 Harness 帮它扫目录、注入元数据、暴露 Skill 工具
这说明 “skill 能力” 确实是应用层(Harness)赋予的,不是模型自带的。
5. 精确版理解
LLM 出厂只有”通用推理 + 工具调用”两个原语,“skill 机制”是 Harness 工程用这两个原语搭出来的一套协议。模型负责”判断用哪个”,Harness 负责”实际加载”,二者合作产生了”Agent 自主使用 skills”的表现。
一句话把边界收紧:决策自主,执行代劳。
八、一句话总结
Skills 的”自动”,本质是 “元数据常驻 + 模型自主决策 + 工具化延迟加载” 三者的组合。 Harness 只提供机制(发现 + 加载),智能全在模型。
相关文档
- [[03-skills原理]] —— Skills 的基本原理与分层结构
- [[02-agent开发框架]] —— 主流 Agent 框架全景