读完这两份代码,我终于搞懂了 Agent 的 Skills 到底是什么

缘起:一个让我停下来的问题

前段时间,Anthropic 官方开源了一个仓库叫 anthropic/skills,与此同时,我在读 shareAI-lab/learn-claude-code 时看到它的 s07 章节专门讲 "Skill Loading"。

在此之前,我以为 Skills 就是"把 Prompt 拆成几段按需拼接"------听起来挺高大上,但骨子里就是一次字符串拼接的花活。直到我把这两份代码从头读到尾,才发现自己错得离谱:Skills 不是拼接,是一种上下文预算的经济学,是 Agent 世界的"懒加载"协议。

这篇文章不打算再泛泛而谈"Skills 是什么"。我想做一件事:带你把这两份官方级的资料读一遍,看清 Skills 的骨架、机制、边界,以及它为什么会以现在这种形态存在。

如果你只想收藏,那就带走这一句:

Skills = 一份约定的文件夹结构 + 一套按需扩展的加载协议。它不是"什么东西",而是"怎么组织"。

如果你想真的搞明白,往下读。


一、我最初的三个误解

在正式拆解之前,先说清楚我掉过的坑,也许你正在掉。

误解 1:Skills 就是把 Prompt 拆成几段?

这是最普遍的想法。既然模型上下文有限,那就把大 Prompt 拆成多个模块,用到哪段拼哪段,好像很合理。

错在哪?拆分 ≠ 按需加载。如果你拆完还是一次性全塞进 system prompt,token 一分没省。 Skills 机制真正解决的不是"拆分",而是"什么时候塞、塞多少、塞到哪里"。

误解 2:Skills 就是 Tool 的另一个名字?

Tool 是"可执行的动作"------read_filerun_bashhttp_get。Skills 是"可加载的知识 + 使用说明",它自己不干活,它告诉模型该怎么用现有的工具去干这件事

用一句话区分:

Tool 给模型双手 ,Skill 给模型方法论

一份 SKILL.md 里可能会写"处理 PDF 时,先用 pdfplumber 提取文本,如果失败再回落到 OCR"------它没有为模型增加任何新工具,它只是重新组织了模型使用工具的方式。

误解 3:Skills 是一个"框架"或"运行时"?

不是。Skills 是一份约定。它连一行必要的代码都没有。

看官方仓库 anthropic/skills 的目录结构就明白:

objectivec 复制代码
skills/
  pdf/
    SKILL.md
    scripts/
    references/
  skill-creator/
    SKILL.md
    references/
    scripts/

纯粹的 Markdown + 目录约定。运行时的加载逻辑写在你的 harness(比如 Claude Code)里,可以极其简单------learn-claude-code 的 s07 章节,用不到 30 行 Python 就实现了完整机制(下面就拆)。


二、先把官方定义摆在桌上

在开始拆代码之前,我更愿意先摆一份"元定义"。这段来自 Anthropic 官方 skill-creatorSKILL.md原文链接):

Progressive Disclosure

Skills use a three-level loading system:

  1. Metadata (name + description) - Always in context (~100 words)
  2. SKILL.md body - In context whenever skill triggers (<500 lines ideal)
  3. Bundled resources - As needed (unlimited, scripts can execute without loading)

翻译成人话:

  • 第一层 Metadata:名字 + 一句话描述,永远在上下文里,约 100 词。让模型知道"我有哪些技能可用"。
  • 第二层 SKILL.md 正文:技能被触发时才加载,理想控制在 500 行以内。
  • 第三层 Bundled resources :脚本、参考文档、模板文件、图片等,按需读取或直接执行,上限是无限的

三层加起来有一个统一的名字:Progressive Disclosure(渐进式披露) 。翻译得中二一点------洋葱式加载:外层薄如蝉翼始终在场,内层丰满但要用才剥开。

这个词你一定要记住。它是理解 Skills 的钥匙。整个机制所有的设计取舍,都是围绕它展开的。


三、用不到 30 行代码看穿 Skills 加载机制

理论说完了,看代码。以下代码来自 shareAI-lab/learn-claude-code/s07_skill_loading/code.py,我做了少量精简,核心逻辑一字未改。

3.1 启动时:扫目录,只登记 Metadata

python 复制代码
SKILL_REGISTRY: dict[str, dict] = {}

def _scan_skills():
    if not SKILLS_DIR.exists():
        return
    for d in sorted(SKILLS_DIR.iterdir()):
        if not d.is_dir():
            continue
        manifest = d / "SKILL.md"
        if manifest.exists():
            raw = manifest.read_text()
            meta, body = _parse_frontmatter(raw)
            name = meta.get("name", d.name)
            desc = meta.get("description", raw.split("\n")[0].lstrip("#").strip())
            SKILL_REGISTRY[name] = {
                "name": name,
                "description": desc,
                "content": raw,   # 完整内容留着,但先不注入 SYSTEM
            }

_scan_skills()  # 只在启动时跑一次

这一步最容易被忽略的关键点:扫描时虽然读了完整 raw,但注入到 SYSTEM 的只有 namedescription。完整内容存在内存的注册表里,等模型自己来"要"。

一句话概括这一层:目录先给你,菜单不上桌。

3.2 SYSTEM 组装:把目录塞进去

python 复制代码
def list_skills() -> str:
    if not SKILL_REGISTRY:
        return "(no skills found)"
    return "\n".join(
        f"- **{s['name']}**: {s['description']}"
        for s in SKILL_REGISTRY.values()
    )

def build_system() -> str:
    catalog = list_skills()
    return (
        f"You are a coding agent at {WORKDIR}. "
        f"Skills available:\n{catalog}\n"
        "Use load_skill to get full details when needed."
    )

SYSTEM = build_system()

生成出来的 SYSTEM 中,"技能目录"那一段大概长这样:

markdown 复制代码
Skills available:
- **pdf**: Extract, edit, and generate PDF documents...
- **skill-creator**: Create new skills, modify and improve...
- **code-review**: Perform structured code review...
- **agent-builder**: Design and scaffold new agent projects...
Use load_skill to get full details when needed.

这一小段目录的 token 消耗是恒定的------不管你有 4 个技能还是 40 个,只要一句话描述控制得好,代价都可以忽略。

3.3 运行时:load_skill 是一个由模型自己决定何时调用的工具

python 复制代码
def load_skill(name: str) -> str:
    skill = SKILL_REGISTRY.get(name)
    if not skill:
        return f"Skill not found: {name}"
    return skill["content"]

就这么几行。看似简单,但里面藏着三个关键设计。

① 走注册表查询,不走文件路径

SKILL_REGISTRY.get(name) 而不是 open(f"skills/{name}/SKILL.md")。为什么?防路径穿越 。如果模型调用 load_skill("../../../etc/passwd") 直接读文件,安全就崩了。走注册表就是把可访问集合硬编码成"启动时扫描过的合法技能"。

② 加载路径由模型决定,不是代码判断

没有关键词匹配,没有 embedding 相似度打分。就是模型在看到用户 prompt 后,自己判断"我需要 pdf 这个 skill",然后发出 tool_use 调用。这才是真正的"意图驱动加载"

③ 加载内容通过 tool_result 注入,不进 SYSTEM

这一点极其关键。加载完 SKILL.md 后,内容作为 tool_result 出现在当前对话的 messages 列表里,而不是 system prompt。这意味着:

  • 它可以随着上下文被压缩(后续章节的 Context Compact 会处理这件事)
  • 它可以随着长对话被淘汰
  • 下次需要再加载一次即可

技能内容是"上下文的一部分",不是"身份的一部分"。 这一刀切得极深。想想看:如果技能内容进了 system prompt,你就再也没法在一个长对话里"卸载"它了,也没法只压缩它。而放在 messages 里,它就是普通的对话数据,可读、可压缩、可淘汰。


四、上下文预算的经济学

理解 Skills 为什么长这样,得先理解一个残酷的事实:LLM 的 context window 是有价的。

看 learn-claude-code s07 里的一段对比表格(原文):

位置 时机 代价
1. 目录 system prompt 启动时注入 ~100 tokens/skill,每轮都带
2. 内容 tool_result Agent 调用 load_skill 时 ~2000 tokens/skill,按需

假设你的 Agent 有 20 个技能:

  • 全塞方案:20 × 2000 = 40,000 tokens,每次 API 调用都得带上。哪怕用户只问了句"今天天气怎么样",你也要为剩下 19 个用不到的技能付费。
  • Skills 方案 :20 × 100 = 2000 tokens 目录常驻,加上偶尔一次 load_skill 的 2000 tokens。一次对话通常只加载 0-2 个技能。每轮节省 90% 以上。

这就是为什么 Anthropic 在 skill-creator 里给出了一条硬约束:

Keep SKILL.md under 500 lines;超过就要"add an additional layer of hierarchy along with clear pointers about where the model using the skill should go next to follow up"。

翻译过来:正文别超 500 行,超了必须再分层,让模型知道下一步跳到哪个 references/ 文件里读。

learn-claude-code 还给出了一个更硬核的数字------Claude Code 内部实现(不是简化的教学版)里,目录的预算是上下文窗口的 ~1%,上限 8000 字符。也就是说,一个 200k context 的模型,分给"技能目录"的空间大约是 2000 tokens。这是一条工程上的天花板,不是随手拍的。

Skills 的整个存在,本质上是一个空间预算问题。它教你用最少的 token 让模型"看到最多可能性",然后让模型自己付出代价去展开它需要的那部分。


五、看一份真正的官方 Skill 长什么样

抽象讲得够多了,看一个真实的官方 Skill。这是 anthropic/skills/pdfSKILL.md 结构(我节选了 frontmatter 和开头几段):

yaml 复制代码
---
name: pdf
description: |
  Comprehensive PDF processing skill for extracting text, tables,
  and images from PDFs, editing existing PDFs, and generating new
  PDF documents. Use this skill whenever the user works with PDF
  files, mentions PDF processing, or needs to extract or modify
  PDF content.
---

# PDF Processing

## When to use
- Extract text/tables/images from PDF files
- Fill in PDF form fields
- Merge or split PDF documents
- Generate PDF reports from templates

## How to approach
1. Identify the operation type...
2. Prefer `scripts/extract_text.py` for text extraction...
3. For form filling, see `references/form_filling.md`...

拆开来看,有三个细节值得琢磨。

5.1 description 越具体越好,还要略微推销自己

注意上面这段描述里的 "Use this skill whenever the user works with PDF files, mentions PDF processing, or needs to extract or modify PDF content"。

这不是废话。skill-creator 官方文档里明确写道:

Note: currently Claude has a tendency to "undertrigger" skills --- to not use them when they'd be useful. To combat this, please make the skill descriptions a little bit "pushy".

模型有天然的"欠触发"倾向。所以描述里要主动列举触发场景,甚至用一点"带货"式的措辞,让模型扫过目录时觉得"哦这个场景归它管"。

写 description 的原则可以浓缩为一句话:"当且仅当用户说什么/做什么时,该用这个 skill",把这句话写在描述里。

5.2 目录结构长这样

objectivec 复制代码
pdf/
├── SKILL.md
├── scripts/
│   ├── extract_text.py
│   ├── merge_pdfs.py
│   └── fill_form.py
├── references/
│   ├── form_filling.md
│   ├── table_extraction.md
│   └── troubleshooting.md
└── assets/
    └── templates/
        └── report_template.pdf

三种子目录,各司其职(引自 skill-creator/SKILL.md):

  • scripts/ - Executable code for deterministic/repetitive tasks
  • references/ - Docs loaded into context as needed
  • assets/ - Files used in output (templates, icons, fonts)

区分标准:

  • 可以确定性执行 的 → scripts/。模型不用理解代码,只要知道"跑这个脚本就能得到 X"就行。脚本可以直接执行而不必先加载到上下文------这是省 token 的又一大招。
  • 需要模型阅读理解 的知识 → references/。深度文档、边缘情况说明、故障排除清单。
  • 用来做输出 的素材 → assets/。模板、图标、字体、样例文件。

5.3 多变体场景:domain organization

如果一个 skill 要支持多个变体(比如部署到 AWS/GCP/Azure),官方给的模式是:

bash 复制代码
cloud-deploy/
├── SKILL.md            # 通用工作流 + 变体选择逻辑
└── references/
    ├── aws.md
    ├── gcp.md
    └── azure.md

SKILL.md 只讲通用流程和"该读哪个 reference",具体变体的差异全部在 references/ 里。 模型根据用户上下文决定读哪一份。这样做的好处:一次加载只带一个变体的信息,不会为了讲 AWS 顺便把 Azure 的文档也塞进上下文。

这也是"渐进式披露"的又一层实践:主文件只负责路由,细节按需读取。


六、深入一点:Claude Code 里真实的 Skills 长什么样

learn-claude-code 的 s07 章节末尾有个折叠段,讲的是 Claude Code 源码里的真实实现,比教学版复杂得多。摘几点。

6.1 技能来源不止一个目录

真实的 Claude Code 从多个来源加载技能:

  • User skills~/.claude/skills/(用户全局)
  • Project skills.claude/skills/(项目局部)
  • --add-dir skills:命令行动态添加
  • Bundled skills:内置技能
  • Plugin skills:插件带来的
  • MCP skills:走 MCP 协议的远程技能
  • Legacy commands.claude/commands/ 下的老版本命令
  • Conditional skills :带 paths frontmatter,按文件路径条件激活

这一堆来源被聚合到一起,形成最终的技能池。用户级、项目级、动态添加,各有各的加载优先级。

6.2 Frontmatter 字段远不止 name / description

真实 Claude Code 支持的 frontmatter 字段:

字段 用途
name / description 显示名称和描述(触发核心)
when_to_use 明确的触发时机指引
allowed-tools 该技能可用工具的自动允许列表
context inline(默认)或 fork(作为子 Agent 运行)
model 模型覆盖(haiku/sonnet/opus/inherit)
hooks 技能级别的 hook 配置
paths 条件激活的 glob 模式
user-invocable 允许用户通过 /name 直接调用

尤其值得关注的两个:

  • context: fork :这个技能被触发后不是在主对话里展开,而是开一个子 Agent 单独去跑,跑完只把结论带回来。这解决了"技能内容会污染主对话上下文"的问题。
  • allowed-tools :技能自己声明"我需要哪些工具的自动放行",实现了权限与技能的绑定

6.3 加载路径的实际细节

真实实现里,模型调用的工具叫 Skill(不是 load_skill),输入是 skill: <name> + 可选 args。工具返回的 tool_result 展示文本仅仅是 "Launching skill: {name}"真正的 SKILL.md 内容通过 newMessages 机制注入到对话中,而不是塞进 tool_result 的文本里。

这个设计有个隐藏好处:从对话记录的可读性上看,"启动了 X 技能"是一条清晰的事件;技能内容作为对话数据存在,可以被后续的 context compaction 单独处理。

教学版把这两步合并成了"通过 tool_result 注入",是刻意的简化。理解这一点很重要------教学版是揭示原理的最小闭环,生产实现只是在这个闭环上层层加固


七、把 Skills 放回 Agent 的整体图里

理解了 Skills,还得知道它在 Agent 架构里坐在哪个位置。

learn-claude-code 的作者在 README 里给了一段非常犀利的总结:

Agent = Model (LLM) + a generalized operational environment (Harness).

Harness = Tools + Knowledge + Observation + Action Interfaces + Permissions

Skills 就是 Harness 里的 "Knowledge" 一层。它不属于 Tools,因为它不执行任何动作;它也不属于 Model,因为它是外挂的。

一张我自己画的关系图:

perl 复制代码
        ┌──────────────────────────────────────────────┐
        │                Agent Loop                     │
        │   messages[] → LLM → tool_use? → execute      │
        └────────┬───────────────────────┬─────────────┘
                 │                       │
     ┌───────────┴────────┐    ┌─────────┴─────────┐
     │       Tools        │    │      Skills        │
     │  (可执行的动作)     │    │ (可加载的方法论)     │
     │  read/write/bash   │    │  pdf, code-review   │
     │  glob/grep/browser │    │  skill-creator...   │
     └────────────────────┘    └─────────────────────┘
                                        │
                            ┌───────────┴────────────┐
                            │   Progressive Loading   │
                            │  Metadata → Body → Res  │
                            └────────────────────────┘

顺便把 Skills 和几个容易混淆的概念划清界限:

概念 定位 与 Skills 的关系
Tool 一次原子动作 Skills 指导模型如何组合 Tool
Sub-agent 一个独立的对话进程 Skills 可以选择 context: fork 借助 sub-agent 运行
MCP Server 一种远程能力协议 Skills 可以来自 MCP(远程技能)
Prompt Template 静态字符串模板 Skills 是"带触发条件的、可组织资源的" Prompt 载体
System Prompt 定义身份/规则 Skills 不进 system prompt,它进对话

八、我从中提炼的三条设计原则

把这两份代码来回读了几遍后,我给自己总结了三条可以复用的设计原则。不局限于 Skills,写任何"需要按需加载"的机制都用得上。

原则 1:元信息够狠,主体够薄

Metadata 层的每一句话都要能"帮模型做判断"。

❌ 反例:description: "PDF utility skill"

✅ 正例:description: "Extract text/tables/images from PDFs, fill forms, merge/split. Use whenever user mentions PDF files or PDF processing."

多写 30 个 token,可能就能让模型多触发这个技能 30% 的次数。Metadata 是黄金广告位,不是文件名。

同时正文要克制:500 行是官方给的软上限,超了必须再分层。理由不复杂------加载一次的成本,是每轮都付的

原则 2:加载路径靠约定,加载时机靠模型

Skills 的加载不是"if 关键词命中 then 加载",而是由模型自主决策。这背后是一种信任:

相信一个足够聪明的模型,看完目录后知道自己该拿哪个技能。

传统软件工程师看到这里可能会本能反抗:"这不就是把控制权交给模型了吗?出问题怎么办?"

出问题的兜底方案有三个:

  1. 明确的 description ------ 减少判断歧义。
  2. conditional skills(paths 字段) ------ 用文件路径这种确定性信号强制激活。
  3. user-invocable(/name ------ 允许用户手动拉起。

这三层从"模型主导"逐步退到"人主导",中间还留了一层"文件路径确定性兑现"。结论:把 90% 的选择权交给模型,把 10% 的确定性招子留给工程。

原则 3:内容进上下文,不进身份

这是整个 Skills 设计里最反直觉、也最重要的一条。

很多人的下意识会把"技能"当作 Agent 身份的一部分:你是一个会处理 PDF 的 Agent、一个会写代码的 Agent、一个会搜索的 Agent。但在官方机制里,技能内容是通过 tool_result 进入 messages[] 的。它们与模型本身无关,与当前任务相关。

这件事带来一个很大的工程好处:

  • 长对话时,旧技能可以被 context compact(上下文压缩)自然丢掉;
  • 不同轮次不同任务,技能可以自然地滞留、淘汰、重新加载;
  • 一个 Agent 可以共享很多不同领域的技能,而不会因为"身份臃肿"导致基座 SYSTEM 臃肿;
  • 技能可以在运行时动态发现、加载,而不需要重启 Agent。

将"能干什么"与"当下正在干什么"分开,一切都宽松了。 这也是为什么官方警告:不要把"你是一个处理 PDF 的助手"写到 system prompt 里------那里应该只写"你是一个开发者助手"。具体能力交给 Skills 机制去按需揭开。


九、与你相关的一些实际意义

读到这里你可能想问:知道这些对我有什么用?我列三个真实场景。

9.1 如果你在设计 Agent 产品

以前你可能把每个业务场景都写成一块 system prompt,结果就是一个巨大的、难以维护的提示词。学会 Skills 之后,你应该:

  • 主 system prompt 薄一点再薄一点,只写永久性的身份与规则。
  • 每个领域能力拆成一个 skill,写成 SKILL.md,让模型自己拉。
  • 重型知识拆到 references/,而不是写入主文件。
  • 确定性任务写成 scripts/,直接执行,不依赖模型推理。

你会发现产品能力能够线性扩展------新增一个场景只需要新增一个目录,不需要重新调优主 system prompt。

9.2 如果你在写 Prompt

你手上可能已经有很多"反复复制粘贴的 Prompt 片段"------代码审查模板、报告生成模板、数据分析流程。它们就是 Skills 的雏形。

下一步就是:把它们从剪贴板里取出来,写成 SKILL.md,配上一个醒目的 description,放到 ~/.claude/skills/.claude/skills/(如果你用 Claude Code)。从此你只需要对 Claude 说"写一份 XX 报告",它会自己拉上对应的 skill。

9.3 如果你在设计 harness / framework

从 Skills 里可以学到很多"对模型友好"的接口设计:

  • 提供列表能力(list_*),让模型浏览;
  • 提供加载能力(load_*),让模型得到完整上下文;
  • 列表便宜,加载贵,列表常驻 SYSTEM,加载仅对 tool_result。

拓展下去:文件、知识库、历史对话、外部数据,全都可以用同一套模式包装。这就是为什么我认为 Progressive Disclosure 不仅是 Skills 的机制,而是一种"面向上下文预算"的普适设计范式。


结尾:从"开发者"到"知识编排者"

写一个好的 Agent 产品,很大程度上不再是"写一个很长的 Prompt",而是"组织一套可拓展的知识与行为"。

当我读完 anthropic/skillslearn-claude-code 后最大的感受是:Anthropic 在教你怎么给 LLM 造一个"递归的知识系统"------Metadata 拐到 Body,Body 拐到 References,References 里还可以再拐到 sub-skill。每一层的代价不同,每一层的获得也不同。模型在树上自主行走,拿走自己需要的,丢下不需要的。

你的角色,从编写提示词的人,变成了"知识的园丁"。

最后给一个小作业:现在打开你自己的项目,看看有没有你反复写过好几次的 Prompt,反复粘贴过的风格指南,或者反复提醒模型的业务规则。

把其中一个,改写成一份 SKILL.md。支一个目录,写一个 frontmatter,把内容搬上去。

你会发现------它本来就应该长这样。


参考资料


如果这篇文章对你有帮助,欢迎点赞、收藏、关注。我会继续写 Agent 工程化方向的实战笔记。

相关推荐
明志数科2 小时前
具身智能全栈模型开源:从模型架构演进看数据需求的范式转移
人工智能·机器人
中微极客2 小时前
RAG评估工具2026横向测评:从手工到自动化
人工智能·自动化
三品吉他手会点灯2 小时前
嵌入式机器学习 - 学习笔记1.2.3 - 机器学习软件框架
人工智能·笔记·嵌入式硬件·学习·机器学习
LaughingZhu2 小时前
Product Hunt 每日热榜 | 2026-07-15
前端·人工智能·神经网络·react.js·搜索引擎·前端框架
TMT星球3 小时前
浦东新区与阅文集团携手,人工智能文创产业基地揭牌启用
人工智能·百度
repokey-Yao3 小时前
AI 如何看懂 FPGA 大工程?RepoKey Starter 使用记录
人工智能·chatgpt·fpga
意图共鸣3 小时前
《智能体三角模型白皮书》发布:意图共鸣科技如何定义私有知识域?
人工智能
潜龙95273 小时前
AI Agent评测体系实施规划
人工智能·ai agent评测实施
2601_956743683 小时前
上海AI Agent智能体开发选型:从工程架构看落地路径解析
大数据·人工智能·ai·架构·开发经验·上海
subin993 小时前
圣阳蓄电池企业口碑优势与核心运营策略FAQ解析
大数据·运维·人工智能·电源·ups