项目可能同时维护前端规范、数据库约定、代码审查流程和文档模板。把所有说明全文塞进 system prompt 虽然简单,却会让每次模型调用都携带大量与当前任务无关的内容。
Skill Loading 使用两阶段加载:启动时只告诉模型“有哪些技能”,真正需要某项技能时,再通过工具读取完整说明。
skills/*/SKILL.md
│ 启动时扫描 name + description
▼
system prompt 中的技能目录
│ 模型判断某项技能适用
▼
load_skill(name)
│
▼
完整 SKILL.md 作为 tool_result 进入对话这是一种渐进式披露:先给模型选择所需的最小信息,再加载任务相关细节。
技能目录解决了什么问题
若系统有 30 个技能,当前任务只涉及代码审查,理想情况是:
- 模型能从名称和描述知道
code-review可能适用; - React、PDF、数据库等技能正文不进入初始上下文;
- 模型加载代码审查技能后,按完整流程执行;
- 未使用技能始终只占一条简短目录记录。
因此,目录不是单纯的展示列表,而是模型的路由信息。名称要稳定,描述要能回答“什么任务应该加载我”,又不能长到接近正文。
描述过于宽泛会让模型频繁误加载;描述遗漏关键触发条件,又会让技能永远不被选中。
当前目录结构只扫描一层
课程约定每个技能是一个直接子目录:
skills/
code-review/
SKILL.md
pdf/
SKILL.md
api-design/
SKILL.md扫描代码使用:
for manifest in sorted(self.skills_dir.glob("*/SKILL.md")):
...glob("*/SKILL.md") 只匹配 skills 下一级目录中的清单,不会递归查找任意深度。旧版实现曾使用过 rglob,但不能把旧行为当作当前契约。
sorted(...) 让发现顺序保持稳定,便于测试和生成可重复的目录。扫描时还会解析真实路径并检查它仍位于技能根目录下,用于拒绝指向目录外的符号链接清单。
路径检查只能保护扫描入口,并不是执行沙箱。技能正文随后可能引导 Agent 调用其他高权限工具,那些工具仍需各自授权。
Front matter 是机器可读的发现信息
一个最小 SKILL.md 可以这样写:
---
name: code-review
description: Review a code change for correctness, tests, and project conventions.
---
# Code Review
1. Read the repository instructions.
2. Inspect the actual diff.
3. Report findings with evidence.课程使用 YAML 安全解析器读取 front matter:
metadata = yaml.safe_load(frontmatter) or {}safe_load 不会构造任意 Python 对象,比 yaml.load 的不安全加载方式更适合处理文档元数据。但它不会自动完成业务校验,仍需确认:
- 顶层结果必须是对象;
name和description必须是字符串;- 名称符合允许的格式和长度;
- 文件与字段大小没有超过上限;
- 重复名称不会静默覆盖已有技能。
当前课程在名称缺失时回退到目录名,在描述缺失时回退到正文首行。这适合教学演示;生产系统更适合对公开技能执行严格校验,让错误尽早暴露。
为什么按名称查注册表
启动扫描后,Loader 建立内存注册表:
self.skills[name] = {
"name": name,
"description": description,
"content": content,
}加载时只查询这个注册表:
skill = self.skills.get(name)模型传入的 name 不会直接拼接为磁盘路径,因此诸如 ../../secret 的字符串不会被当作任意文件读取。未知名称只应返回可控错误和可用名称列表。
这条安全性质依赖具体实现。如果以后为了动态加载而改成 SKILLS_DIR / name / "SKILL.md",就必须重新加入名称白名单、路径规范化和目录包含检查。
System prompt 只放目录
Loader 将技能名称和描述格式化为目录:
- code-review: Review code changes for correctness and project conventions.
- pdf: Read and create PDF documents where layout matters.然后和固定指令一起构建 system prompt:
SYSTEM = (
"You are a coding agent.\n\n"
f"Skills available:\n{loader.catalog()}\n\n"
"Use load_skill when a skill applies."
)目录在启动时生成,当前教学实现不会在每一轮自动重新扫描。进程运行期间新增或修改技能后,已构建的 SYSTEM 和内存中的正文仍可能是旧版本。若需要热更新,应定义刷新时机、并发读写方式和会话版本语义,而不是悄悄改变正在运行会话的规则。
完整技能通过工具结果进入消息
模型调用:
{"name": "load_skill", "input": {"name": "code-review"}}handler 返回完整 SKILL.md,Agent Loop 再把它作为对应调用 ID 的 tool_result 加入消息历史。模型随后可以按正文中的流程继续工作。
这里有一个重要层级边界:技能正文来自工具结果,不会提升为 system message,也不能覆盖更高优先级的系统规则、用户授权或运行时权限。技能应补充任务流程,而不是获得绕过全局安全约束的能力。
同样,技能正文属于外部内容。安装来源不可信、仓库被篡改或技能文件遭到提示注入时,正文可能诱导模型读取凭据、执行破坏命令或忽略用户范围。因此技能系统需要来源治理,而不仅是 Markdown 解析。
按需加载到底节省了哪些 token
与“所有技能全文都放进 system prompt”相比,按需加载通常能减少初始输入:未使用技能只留下名称和描述。
但完整技能一旦作为 tool_result 进入 messages[],后续模型调用通常仍会携带这段历史,直到会话结束、上下文压缩或消息被裁剪。它不是“只计费一次后永久免费”。
实际成本可拆成:
每轮固定成本:技能目录
按需成本:已加载技能正文
持续成本:正文留在消息历史后的重复输入若目录本身很大,可以增加路由层、分组搜索或服务端检索;若技能正文很长,可以加载所需章节、使用缓存能力,或在任务阶段结束后压缩为保留关键约束的摘要。
是否真的节省 token,应通过请求日志和用量数据测量,而不是只凭架构名称判断。
Skill 不只是一个 Markdown 文件
教学示例把完整内容缓存在内存中,只返回 SKILL.md。现实中的技能还可能引用:
- 模板和示例;
- 可执行脚本;
- 领域术语或 API 参考;
- 图片、Schema 和测试夹具;
- 需要按任务选择的变体。
Loader 需要明确相对路径基于哪里解析,哪些资源可以读取或执行,以及是否允许技能跨目录引用。脚本执行尤其不能因为“来自技能”就自动获得信任,仍应经过与其他命令相同的权限、沙箱和审计入口。
好的技能正文还应说明触发条件、前置输入、操作步骤、停止条件和可验证产物。只写背景知识,往往不能稳定指导 Agent 完成任务。
重名、版本和组合冲突
当前注册表以 name 为键。两个目录声明同名技能时,后扫描到的条目会覆盖前一个,除非 Loader 主动检查。静默覆盖会让目录内容依赖排序,是应当在启动时失败的配置错误。
生产设计还要回答:
- 技能是否有版本及兼容范围;
- 会话记录加载的是哪个内容摘要或哈希;
- 技能更新后旧会话是否继续使用旧版本;
- 多个技能同时适用时按什么顺序加载;
- 两个技能指令冲突时由谁裁决;
- 技能来源、签名、审核者和更新时间如何记录;
- 删除技能后历史任务能否重放。
技能目录负责“发现”,不能独自承担依赖解析和信任治理。
几个 Python 实现细节
SKILLS_DIR = WORKDIR / "skills" 使用 pathlib.Path 的 / 运算符拼接路径,等价于 WORKDIR.joinpath("skills"),不是数学除法。
函数中的:
return {}, text返回的是二元 tuple。Python 中 tuple 由逗号形成,括号主要用于分组;单元素 tuple 才必须写成 (value,)。
这些语法知识有助于阅读 Loader,但真正决定系统质量的是发现契约、输入校验、权限边界和生命周期设计。
建议覆盖的测试
Skill Loading 至少应验证:
- 技能目录不存在或为空;
- 只发现约定的一层
*/SKILL.md; - 扫描顺序稳定;
- CRLF、缺失结束线和非法 YAML;
- 元数据不是对象、字段类型错误或内容超限;
- 名称与描述回退策略;
- 重复名称在启动时失败;
- 指向技能根目录外的符号链接被拒绝;
- 未知技能名不会触发文件路径读取;
- 目录只包含摘要,加载后返回完整且正确版本;
- 运行期间文件变化的刷新语义;
- 技能正文不能绕过全局权限;
- 多技能冲突、长目录和长正文的 token 成本;
- 来源校验、审计信息和恶意技能内容。
小结
Skill Loading 的核心是把“模型需要知道哪些知识存在”和“模型现在需要完整读取哪些知识”分开。启动时暴露简短目录,任务命中后再按稳定名称加载正文,可以避免所有技能长期挤在初始上下文中。
但按需加载不是免费的知识注入:加载后的正文仍会进入消息历史,技能内容也不会自动获得系统级信任。可靠实现还需要严格清单校验、重名检测、版本与刷新语义、资源路径边界、来源治理,以及所有执行工具共同遵守的权限控制。