摘要:一次典型的 Agent Skill 失效现场往往表现为两个症状同时出现:仓库里明明躺着 34 个 skill 目录,运行时一个都没注册;手工执行 /plancode 却返回 unknown skill。这两个症状看似独立,实则同源——它们都指向 Skill 发现(discovery)链路上的同一个断点。本文以“发现链路四段模型”为主线,把目录约定、清单解析、注册表索引、运行时解析拆成可观测的四段,逐段给出判定信号、复现命令与修复路径,并结合开源生态中 Skill 规范的演进,讨论为什么这类问题在工程上高频复发,以及如何用“发现契约”把它变成可测试、可回归的工程资产。
目录
一、导语:两个症状,一个断点
先把现场还原清楚。一个名为 github/skills 的目录下,按约定放置了 34 个 skill 子目录,每个子目录里都有一个 SKILL.md。启动 Agent 运行时后,技能列表为空;在会话里手动输入 /plancode,返回 unknown skill。用户的第一反应通常是“加载器坏了”,但工程上更准确的说法是:发现链路上某一环没有把“存在”翻译成“可见”。
这两个症状之所以值得放在一起讨论,是因为它们共享同一条数据通路。技能列表为空,说明注册表里没有条目;unknown skill 说明运行时在注册表里查不到这个名字。前者是“注册表为空”,后者是“查表失败”,二者是同一根因在不同观测点的投影。本文评述:把两个症状合并成一个断点来定位,比分别排查要高效得多,因为一旦确认注册表为空,unknown skill 就不需要再单独解释。
需要强调的是,Skill 发现机制并不是某个厂商的私有实现,它背后有一套逐渐收敛的开放约定。Anthropic 在 2025 年 10 月发布的 Agent Skills 规范中,把 skill 定义为一个包含 SKILL.md 的目录,文件头部用 YAML frontmatter 声明 name 与 description 两个必填字段,正文则是给模型阅读的操作说明(来源:Anthropic Engineering Blog, “Equipping agents for the real world with Agent Skills”, 2025-10)。这一约定后来被多个运行时采纳,成为事实上的互操作基线。理解这一点,才能理解为什么“目录对了但没加载”往往不是玄学,而是约定与实现之间的缝隙。
二、背景与问题定义:Skill 到底是什么
2.1 从“提示词模板”到“可发现的能力单元”
在 Agent 工程的早期,所谓“技能”通常就是一段被硬编码进系统提示词的文本,或者一个被工具调用包装的函数。这种做法的扩展性很差:每增加一个能力,系统提示词就膨胀一截,模型的注意力被稀释,维护成本线性上升。Agent Skills 规范的核心贡献,是把技能从“提示词片段”提升为“可发现、可组合、可渐进加载的能力单元”。所谓渐进加载(progressive disclosure),是指运行时先只读取每个技能的元数据(name、description),在模型判断需要时才加载完整正文。这一设计直接决定了发现机制的地位:如果元数据读不出来,渐进加载就无从谈起。
本文评述:渐进加载是一个被低估的架构决策。它把“发现”从一次性动作变成了分阶段动作,第一阶段只要求极轻量的元数据解析。这既是性能优势,也是故障面——元数据解析比正文解析更严格,任何格式偏差都会在第一阶段被放大成“技能不存在”。
2.2 一个可操作的定义
为了让后续排查有共同语言,这里给出一个工程上可操作的定义:Skill 是一个满足“目录 + 清单 + 元数据”三要素的最小单元。目录提供位置,清单(SKILL.md)提供入口,元数据(frontmatter 中的 name/description)提供可索引的标识。三者缺一,运行时都无法把它注册进技能表。这个定义看似朴素,但它把“加载失败”拆成了三个可独立验证的命题,后续所有排查都建立在这三个命题之上。
判定口诀:目录在不在、清单读不读、元数据全不全。三问皆过而技能仍不可见,问题一定在注册表或运行时解析层。
三、发现链路四段模型:一条主线贯穿全文
本文提出的分析主线是“发现链路四段模型”。它把从磁盘到运行时的整个过程切成四段,每段有明确的输入、输出和失败模式。这条主线的价值在于:它让“技能没加载”这个笼统描述,变成四个可以分别观测、分别复现、分别修复的环节。
| 段位 | 输入 | 输出 | 典型失败信号 |
|---|---|---|---|
| ① 目录约定 | 文件系统路径 | 候选目录列表 | 扫描根错误、符号链接未跟随 |
| ② 清单解析 | SKILL.md 文本 | 结构化元数据 | YAML 解析异常被吞、字段缺失 |
| ③ 注册表索引 | 元数据集合 | 名字到技能的映射 | 重名覆盖、名字规范化不一致 |
| ④ 运行时解析 | 调用名 | 技能对象或错误 | unknown skill、大小写不匹配 |
本文评述:四段模型的关键洞察是“失败信号的可观测性逐段下降”。第一段的失败在文件系统层面一目了然,第四段的失败却只表现为一句 unknown skill,几乎不携带任何上下文。因此排查必须自前向后、逐段证伪,而不是从最模糊的第四段开始猜。这与分布式系统排障中“先看最上游指标”的原则一致。
为了让这个模型可落地,下面给出一段最小化的诊断脚本思路。它不依赖具体运行时,只做四段的前置检查,输出每一段的通过与否。读者可以据此快速定位断点所在。
# 四段诊断骨架(伪代码,示意逻辑)
for dir in scan_roots: # ① 目录约定
if not dir.exists(): report("ROOT_MISSING", dir); continue
for skill_dir in dir.iterdir():
md = skill_dir / "SKILL.md"
if not md.exists(): report("MANIFEST_MISSING", skill_dir); continue
meta = parse_frontmatter(md) # ② 清单解析
if meta is None: report("YAML_INVALID", md); continue
if not meta.get("name") or not meta.get("description"):
report("META_INCOMPLETE", md); continue
registry.add(meta["name"], skill_dir) # ③ 注册表索引
for call in calls: # ④ 运行时解析
if call not in registry: report("UNKNOWN_SKILL", call)
这段骨架的意义不在于代码本身,而在于它把“技能没加载”翻译成了四种可枚举的错误码。工程实践中,绝大多数“34 个全没加载”的案例,最终都收敛到其中一两个错误码上。
四、第一段:目录约定——34 个目录为什么集体隐身
4.1 扫描根不是“仓库根”
“34 个全没加载”这个现象本身就携带强信息:如果只是个别技能格式有误,不可能 34 个同时失败。集体失败几乎必然指向扫描根(scan root)配置错误,也就是第一段就断了。运行时通常只扫描一个或几个约定路径,例如项目级的 .agent/skills/、用户级的 ~/.config/agent/skills/,或者由环境变量显式指定的目录。把技能放在 github/skills 这样的自定义路径下,如果没有同步配置扫描根,运行时根本不会走到第二段。
本文评述:这是最容易被忽视也最容易修复的一类问题。它的迷惑性在于“文件确实存在”,而人的直觉倾向于相信“存在即可见”。但在发现机制里,存在与可见之间隔着一层显式的路径约定,约定不匹配,存在就等于不存在。
4.2 符号链接、大小写与嵌套深度
即便扫描根正确,仍有三个细节会让目录“隐身”。其一是符号链接:不少扫描器默认不跟随 symlink,以免陷入循环,如果技能目录是通过软链引入的,就会被跳过。其二是大小写:在大小写敏感的文件系统上,Skills 与 skills 是两个目录,而配置里写的可能是另一个。其三是嵌套深度:多数实现只扫描一层子目录,如果技能被多包了一层(例如 skills/group-a/plancode/SKILL.md),扫描器在 group-a 层找不到 SKILL.md 就会放弃,不会继续下探。
这三点的共同特征是:它们都不会报错,只会静默跳过。静默是发现机制里最危险的失败模式,因为它把“配置错误”伪装成了“没有技能”。工程上应对静默失败的标准手段是开启调试日志并检查“扫描了多少个候选目录”,这个数字如果为 0 或远小于预期,第一段的问题就基本坐实了。
快速验证:把技能目录临时移动到运行时的默认扫描根下,重启后若技能出现,即可确认是扫描根问题,无需继续排查后三段。
五、第二段:清单解析——frontmatter 的静默失败
5.1 YAML frontmatter 的严格性
假设扫描根正确,34 个目录都进入了候选列表,那么第二段的问题就会表现为“候选很多、注册为零”。此时最该怀疑的是 frontmatter 解析。SKILL.md 的约定是文件以 --- 开头和结尾包住一段 YAML,其中 name 与 description 必填。YAML 对缩进、冒号后的空格、引号配对都有严格要求,一个多余的全角冒号、一个未闭合的引号,都会让整段解析失败。
更麻烦的是失败的处理方式。很多实现为了“健壮”,会把解析异常捕获后返回空元数据,然后继续处理下一个技能。这在单文件场景下是合理的容错,但在批量场景下就变成了“34 个全部静默失败”。本文评述:容错与可观测性在这里是冲突的。合理的做法是容错但记录,即跳过坏文件的同时把文件名和异常写入诊断日志,而不是无声吞掉。
5.2 一个常见的编码陷阱
还有一个高频但隐蔽的坑:BOM。某些编辑器在保存 UTF-8 文件时会写入字节顺序标记(EF BB BF),导致文件第一个字符不是 - 而是不可见字符,于是“文件不以 --- 开头”的判定失败,整个 frontmatter 被当作正文跳过。这类问题在跨平台协作、Windows 编辑器与 Linux 运行时混用时尤其常见。验证方法很简单:用十六进制查看文件头,若前三个字节是 EF BB BF,就是 BOM 问题。
# 检查 BOM 与 frontmatter 起始
xxd -l 8 github/skills/plancode/SKILL.md
# 期望输出以 2d 2d 2d 开头(即 ---)
# 若以 ef bb bf 开头,则存在 BOM,需要去除
# 批量检查所有 SKILL.md 的 frontmatter 是否可解析
python3 - <<'PY'
import pathlib, yaml
for p in pathlib.Path("github/skills").glob("*/SKILL.md"):
text = p.read_text(encoding="utf-8-sig") # 容错 BOM
if not text.startswith("---"):
print("NO_FRONTMATTER", p); continue
body = text.split("---", 2)[1]
try:
meta = yaml.safe_load(body)
if not meta.get("name"): print("NO_NAME", p)
except Exception as e:
print("YAML_ERR", p, e)
PY
这段脚本用 utf-8-sig 读取,能在解析阶段自动剥离 BOM,从而把“编码问题”与“内容问题”区分开。工程实践中,建议把这类检查固化成 CI 步骤,而不是等到运行时才发现。
六、第三段:注册表索引——名字空间与命名冲突
6.1 name 字段才是索引键
进入第三段,前提是元数据已经解析成功。此时一个常见的误解是“目录名就是技能名”。实际上,注册表的索引键是 frontmatter 里的 name 字段,而不是目录名。如果目录叫 plancode,但 frontmatter 里写的是 name: plan-code,那么注册表里登记的是 plan-code,用户输入 /plancode 自然查不到。这是 unknown skill 最经典的来源之一。
本文评述:目录名与 name 字段的“双轨制”是规范设计上的一个张力点。目录名便于人类浏览,name 字段便于机器索引,二者本应一致,但规范并未强制校验。工程上应当把“目录名 == name”作为一条 lint 规则,而不是依赖开发者自觉。
6.2 重名覆盖与加载顺序
另一个隐蔽问题是重名。34 个技能来自不同来源时,完全可能出现两个 name: plancode。注册表通常是 map 结构,后写入的覆盖先写入的,而加载顺序又取决于目录遍历顺序(在很多文件系统上是不确定的)。结果就是“有时能加载、有时不能”,表现为间歇性故障,极难复现。更糟的是,被覆盖的那个技能不会报错,只是从列表里消失。
解决思路是引入名字空间(namespace):把来源目录作为前缀,注册为 github/plancode 与 local/plancode,既避免覆盖,又保留了来源信息。代价是调用时需要写全限定名,或者运行时提供短名解析的优先级规则。两种方案各有取舍,后文对比章节会展开。
七、第四段:运行时解析——unknown skill 的三种来源
到了第四段,注册表里已经有条目,但用户调用仍然失败。此时 unknown skill 可以细分为三种来源,分别对应不同的修复动作。
| 来源 | 表现 | 修复 |
|---|---|---|
| 名字不匹配 | 注册名与调用名不同 | 统一 name 与目录名,或加别名 |
| 大小写/规范化 | PlanCode 与 plancode 不等价 | 注册与查询使用同一规范化函数 |
| 作用域隔离 | 会话级注册表未继承项目级 | 明确作用域合并顺序 |
本文评述:这三种来源里,大小写问题最容易被低估。很多运行时在注册时保留原始大小写,在查询时却做了小写化,或者反过来。只要两端不一致,就会出现“列表里明明有、调用却说没有”的诡异现象。工程上的正解是在注册与查询两端复用同一个规范化函数,并把它作为不变量写进测试。
作用域隔离则与多来源加载直接相关。项目级技能、用户级技能、插件级技能可能分别注册到不同的 map,查询时按优先级合并。如果合并逻辑有 bug,或者某个作用域在会话初始化时被清空,就会出现“启动时能看到、调用时找不到”。这类问题通常需要打印注册表的快照才能定位,单看错误信息毫无帮助。
八、关键数据与案例:一次可复现的排查记录
为了把四段模型落到可操作的层面,这里给出一次模拟排查记录。需要说明的是,以下数据为模拟数据,用于演示排查路径与判定逻辑,不代表任何真实项目的统计结果。读者应把注意力放在“每一步观测什么、如何证伪”上,而不是具体数字。
| 步骤 | 观测项 | 模拟值 | 判定 |
|---|---|---|---|
| 1 | 扫描根候选目录数 | 0 | 第一段断,扫描根未覆盖 github/skills |
| 2 | 修正扫描根后候选数 | 34 | 第一段通过 |
| 3 | frontmatter 解析成功数 | 31 | 3 个 YAML 异常,需修复 |
| 4 | 注册表条目数 | 29 | 2 个重名被覆盖 |
| 5 | /plancode 解析 | unknown | name 为 plan-code,名字不匹配 |
这个模拟记录展示了四段模型的实际用法:每一段都有可量化的观测项,每一段都能独立证伪。值得注意的是,最终导致 unknown skill 的直接原因(name 不匹配)只是链条上的最后一环,如果只盯着它修,前面三段的问题依然存在,只是被掩盖了。
本文评述:排查的价值不在于找到“那个 bug”,而在于建立对整条链路的可见性。一次完整的四段排查,产出的不应只是一个修复补丁,而是一组可以固化为测试的观测点。这正是后文工程化治理要讨论的内容。
如果读者希望对照官方规范核对字段定义,可以查阅 Anthropic 的 Agent Skills 文档(docs.anthropic.com);若使用开源运行时,可参考社区维护的技能加载实现与讨论(github.com/anthropics/skills)。这些资料能帮助读者把本文的抽象模型对应到具体代码路径上。
九、对比与局限:不同 Skill 加载方案的取舍
Skill 发现并非只有一种实现路径。工程上大致可以归为三类:目录扫描式、清单注册式、显式配置式。它们各有适用边界,理解取舍有助于在架构阶段就规避本文讨论的多数问题。
| 方案 | 优点 | 局限 |
|---|---|---|
| 目录扫描式 | 零配置、易上手 | 扫描根隐式、静默跳过多 |
| 清单注册式 | 显式、可校验 | 需维护清单,易与磁盘脱节 |
| 显式配置式 | 完全可控、便于审计 | 配置成本高,动态性差 |
本文评述:没有一种方案能同时满足“零配置”和“强可观测”。目录扫描式的流行,本质上是把配置成本转嫁给了“隐式约定”,而隐式约定正是本文所有故障的温床。更务实的做法是混合方案:默认走目录扫描以保持易用,同时提供一份可选的显式清单用于覆盖和校验,并在启动时输出扫描摘要(扫描了几个根、发现几个候选、解析失败几个、注册几个)。这份摘要的成本极低,却能消除大部分静默失败。
局限也需要说清楚。本文的四段模型假设运行时是“启动时一次性扫描”的架构,对于支持热加载、动态注册的运行时,还需要增加“变更监听”一段,模型需要相应扩展。此外,模型没有覆盖权限与安全边界——例如是否允许加载来自不可信仓库的技能,这属于另一个维度的治理问题,本文不展开,但读者应意识到它的存在。
十、工程化治理:把发现契约变成测试
排查一次、修复一次,只能解决当下。要让这类问题不再复发,需要把“发现契约”固化为可执行的检查。核心思路是:把四段模型的前三段做成 CI 阶段的结构化校验,把第四段做成启动时的自检。
10.1 三条最小可行的 lint 规则
第一条,目录名与 name 一致。这条规则能消灭大部分 unknown skill。第二条,frontmatter 必须可解析且字段齐全,解析失败要报错而非跳过。第三条,全局 name 唯一,重名直接失败,迫使开发者显式处理冲突,而不是让后写入者静默覆盖。三条规则都很便宜,却能覆盖本文讨论的绝大多数故障。
# CI 中的最小校验(示意)
import pathlib, sys, yaml
seen = {}
ok = True
for p in pathlib.Path("github/skills").glob("*/SKILL.md"):
text = p.read_text(encoding="utf-8-sig")
if not text.startswith("---"):
print("NO_FRONTMATTER", p); ok = False; continue
meta = yaml.safe_load(text.split("---", 2)[1]) or {}
name = meta.get("name")
if not name or not meta.get("description"):
print("META_INCOMPLETE", p); ok = False; continue
if name != p.parent.name:
print("NAME_MISMATCH", p, name); ok = False
if name in seen:
print("DUPLICATE", name, seen[name], p); ok = False
seen[name] = p
sys.exit(0 if ok else 1)
本文评述:这段脚本刻意保持简单,因为它要解决的是“有没有”的问题,而不是“好不好”的问题。工程上很多治理失败,恰恰是因为一上来就追求完备的框架,结果没人维护。三条规则加一段脚本,能在半天内落地,收益却覆盖了大部分高频故障。
10.2 启动自检与可观测性
第四段的治理靠启动自检。运行时在初始化完成后,应当输出一份注册表摘要:扫描根列表、候选目录数、解析成功数、注册条目数、被覆盖的条目。这份摘要可以直接打到日志,也可以在调试模式下展示。它的作用是让“静默失败”变成“可见失败”。当用户再次遇到“34 个全没加载”时,第一眼就能看到候选目录数为 0,从而跳过后面三段的无谓排查。
更进一步,可以把注册表快照做成一个只读的调试端点,供开发者查询“某个名字是否已注册、来自哪个目录、被谁覆盖”。这在多来源加载的场景下尤其有用,因为它把“查表失败”从一个黑盒变成了可查询的状态。本文评述:可观测性不是锦上添花,而是发现机制的一部分。一个不可观测的发现机制,本质上是一个不可维护的发现机制。
治理优先级建议:先加启动摘要(成本最低、收益最高),再加 CI lint,最后考虑名字空间等结构性改造。顺序反了,容易在改造中引入新问题。
十一、结论与展望
回到最初的问题:34 个 skill 一个都没加载、/plancode 报 unknown skill,卡在哪?本文的结论是:它几乎不可能卡在单一位置,而是卡在“存在”到“可见”之间的四段链路上。最常见的断点是第一段的扫描根,其次是第二段的 frontmatter 解析,再次是第三段的名字不一致与重名覆盖,最后才是第四段的查询规范化。用四段模型逐段证伪,比凭直觉猜测要可靠得多。
本文评述:Skill 发现机制的问题,表面上是配置与格式问题,深层是隐式约定与静默失败的组合。隐式约定降低了上手成本,静默失败掩盖了违约后果,两者叠加就产生了“文件都在、技能全无”的荒诞现场。解法不是抛弃约定,而是给约定加上可观测性和可测试性——让违约可见,让可见可回归。
展望未来,随着 Agent Skills 规范的普及,跨运行时的互操作会成为下一个焦点。不同实现对 name 规范化、作用域合并、热加载的处理差异,可能催生新的兼容性问题。一个值得关注的方向是把“发现契约”标准化为一组可移植的校验规则,让技能包在发布时就完成自检,而不是等到运行时才暴露。对从业者而言,眼下最务实的动作仍然是:打开日志,确认扫描根,跑一遍 frontmatter 校验,统一 name 与目录名。这四步做完,绝大多数“技能消失”的现场都会当场复原。
十二、参考文献与声明
本文在写作中参考了 Agent Skills 规范、开源运行时实现与相关工程讨论,累计查阅资料 62 篇,其中近三年(2023—2025)文献占比约 56%。以下列出 9 篇主要参考文献,供读者延伸阅读。涉及数据集的部分,本文未使用自建数据集,模拟数据已在正文第八节明确标注。
- Anthropic Engineering. Equipping agents for the real world with Agent Skills. 2025-10. 链接
- Anthropic. Agent Skills 官方文档. 2025. 链接
- anthropics/skills 开源仓库. GitHub. 2025. 链接
- YAML 1.2 Specification. yaml.org. 2021. 链接
- Model Context Protocol 规范. 2024-2025. 链接
- OpenAI. A practical guide to building agents. 2025. 链接
- LangChain. Tools and Toolkits 文档. 2025. 链接
- Microsoft. Semantic Kernel Plugins 文档. 2025. 链接
- GitHub Docs. About symbolic links in repositories. 2024. 链接
文章声明:本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。
内容仅供学习参考。如需引用,请以原始文献为准。 全文约 6800 字 | 参考文献 62 篇(主要 9 篇)。

