skill机制
SkillManager
SkillManager 是 skill 资源的”仓库管理员”——发现、存储、查询、格式化全部收敛到一个有状态对象里,让外面的代码(Agent)不需要知道 skill 来自文件系统。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class SkillManager: """skill 仓储(Repository):发现 + 按名索引 + 清单/调用文本生成。"""
def __init__(self, dirs: list[str | Path] | None = None): """构造即发现:None → 探测 <cwd>/.agents/skills;[] → 禁用;非空 → 显式目录。""" def _discover_dir(self, root: Path) -> None: """扫单一来源目录的一层子目录(不递归、不认根级 .md、跳过隐藏目录)。""" def _load_one(self, path: Path) -> Skill | None: """读单个 SKILL.md → Skill;任何失败/缺 description → None(静默)。""" def get(self, name: str) -> Skill | None: """按名查询(Repository 主查询)。""" def list(self) -> list[Skill]: """全部 skills,发现顺序。""" def format_prompt(self) -> str: """全部 skills → XML 清单块;空 → 空串(进 system prompt)。"""
def format_invocation(self, name: str, instructions: str = "") -> str: """按名取正文 → <skill> 包装 + 可选附言;未知名 → ValueError(列可用名字)。"""
|
如何加载skill
你传什么决定扫哪:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class SkillManager: def __init__(self, dirs=None): self.skills: dict[str, Skill] = {} # 先建空巢 if dirs is None: dirs = [Path.cwd() / ".agents" / "skills"] # None → 探测默认目录 for root in dirs: self._discover_dir(Path(root)) # 逐目录发现 ┌─────────────────────────────────┬───────────────────────────────────────┐ │ 传参 │ 行为 │ ├─────────────────────────────────┼───────────────────────────────────────┤ │ Agent(skill_dirs=None)(默认) │ 探测 <cwd>/.agents/skills,不存在则空 │ ├─────────────────────────────────┼───────────────────────────────────────┤ │ Agent(skill_dirs=["my_skills"]) │ 只扫 my_skills/ 这一个目录 │ ├─────────────────────────────────┼───────────────────────────────────────┤ │ Agent(skill_dirs=[]) │ 显式禁用,空 dict │ └─────────────────────────────────┴───────────────────────────────────────┘
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| Agent(skill_dirs=["my_skills"]) ← 触发点:Agent 构造一次 │ ▼ SkillManager(["my_skills"]) │ self.skills = {} │ ├── 来源目录: my_skills/ │ │ │ ▼ _discover_dir(my_skills) │ │ │ ├─ my_skills/code-review/ │ │ │ is_dir ✓, 非隐藏 ✓ │ │ │ code-review/SKILL.md 存在 ✓ │ │ ▼ _load_one(SKILL.md) │ │ │ read utf-8-sig ──OSError──→ None (丢弃) │ │ │ parse_frontmatter ──坏/无 YAML──→ ({}, 全文) │ │ │ description? ──缺──→ None (守门跳过) │ │ │ description="Perform thorough code reviews..." ✓ │ │ ▼ │ │ Skill(code-review, 正文, path) ──► skills["code-review"] │ │ │ ├─ my_skills/pdf/ │ │ └─ ... ──► skills["pdf"] │ │ │ ├─ my_skills/README.md ✗ 不是目录 → 跳过 │ ├─ my_skills/.hidden/ ✗ 隐藏目录 → 跳过 │ └─ my_skills/notes/ ✗ 无 SKILL.md → 跳过 │ ▼ self.skills = {"code-review": Skill, "pdf": Skill} │ ▼(下一步,非加载) agent 组装 system = 用户 prompt + SkillManager.format_prompt() 清单块
|
何时会加载 skill
发现加载(读文件到内存)只在 Agent 初始化时发生一次:
1 2 3
| self.skill_manager = SkillManager(skill_dirs, extra_dirs=self.plugin_manager.get_skill_dirs()) self.skills: list[Skill] = self.skill_manager.list()
|
也就是说:
- Agent(skill_dirs=[…]) 或 Agent()(默认 None 探测 .agents/skills)构造的那一刻,扫描目录、解析 frontmatter、构建 self.skills 列表
- 之后不再刷新——你改了某个 SKILL.md,要新开会话/新建 Agent 才生效(这正是设计决策 4:skill 配置变更新会话生效,刻意不做动态刷新)
正文进入模型上下文则是另一个时机:初始化只把清单放进去,正文完全不会在初始化时加载——只有宿主显式 invoke_skill 的时刻才进模型。所以”何时加载”的准确答案是:发现=初始化一次;正文=按需、宿主显式注入时才出现。
skill 的哪些内容会加入上下文
分两个阶段,内容完全不同:
阶段一:初始化,只加清单(很小)——每个 skill 只贡献 name + description,拼成 XML 块追加到 system prompt 尾部:
1 2 3 4 5 6 7
| <available_skills> <skill> <name>code-review</name> <description>Perform thorough code reviews...</description> </skill> <skill>...</skill> </available_skills>
|
正文(content)不进,frontmatter 其他字段(name、tags 等)不进。每个 skill 大约几十 token,这是渐进式披露的 token 经济学。
阶段二:invoke_skill 时,加正文(wholesale)——整个 正文 作为一个 user 消息进入上下文:
1 2 3
| <skill name="code-review" location="D:\...\code-review\SKILL.md"> 完整正文(frontmatter 以下的全部 markdown 指令) </skill>
|
注意:附件文件(skill 目录里的 references/ scripts/ 等)从不加载——v1 只认 SKILL.md 正文本身。frontmatter 里除 description 外的键(比如 name、tags)也从不进上下文。
如何调用某个 skill
唯一的调用入口是 Agent.invoke_skill(name, instructions=””):
1 2 3
| async def invoke_skill(self, name: str, instructions: str = "") -> str | None: return await self.run(self.skill_manager.format_invocation(name, instructions))
|
调用方式(宿主侧代码):
1 2 3 4
| agent = Agent(llm=llm, tools=[...], skill_dirs=["my_skills"]) agent.invoke_skill("code-review", "重点看并发安全") # run('<skill name="code-review" ...>正文</skill>\n\n重点看并发安全') # → 模型按正文干活,返回最终文本
|
skill生命周期
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| ① 落地(用户/宿主写文件) my_skills/code-review/SKILL.md │ ┌── frontmatter ──────────┐ │ │ description: ... │ ← 唯一读取字段,缺了被守门跳过 │ └─────────────────────────┘ │ ┌── 正文 markdown ────────┐ │ │ 完整操作指令 │ ← 将来注入模型的 content │ └─────────────────────────┘ ▼ ② 发现(Agent 构造时,只此一次) Agent(skill_dirs=["my_skills"]) └─► self.skill_manager = SkillManager(["my_skills"]) │ dirs 三态:None 探测 .agents/skills / [] 禁用 / 显式目录 ▼ _discover_dir(my_skills) │ iterdir 一层 │ ① 非目录 → 跳过 ② 隐藏目录(.开头) → 跳过 │ ③ 无 SKILL.md → 跳过 ▼ ③ 解析 + 守门(_load_one)
① 落地(用户/宿主写文件) my_skills/code-review/SKILL.md │ ┌── frontmatter ──────────┐ │ │ description: ... │ ← 唯一读取字段,缺了被守门跳过 │ └─────────────────────────┘ │ ┌── 正文 markdown ────────┐ │ │ 完整操作指令 │ ← 将来注入模型的 content │ └─────────────────────────┘ ▼ ② 发现(Agent 构造时,只此一次) Agent(skill_dirs=["my_skills"]) └─► self.skill_manager = SkillManager(["my_skills"]) │ dirs 三态:None 探测 .agents/skills / [] 禁用 / 显式目录 ▼ _discover_dir(my_skills) │ iterdir 一层 │ ① 非目录 → 跳过 ② 隐藏目录(.开头) → 跳过 │ ③ 无 SKILL.md → 跳过 ▼ ③ 解析 + 守门(_load_one) read_text(utf-8-sig) ────读失败(OSError)───► None(静默丢弃) │ ▼ parse_frontmatter(text) │ 无 --- 头 / 闭合缺失 / 坏 YAML / 非 dict → 全部降级为 ({}, 全文) ▼ meta.get("description") ────缺失 / 空────► None(守门跳过) │ description 存在 ▼ Skill(name="code-review", description="...", content="正文", file_path=...) │ ▼ ④ 入巢 + 清单可见 self.skills["code-review"] = Skill # key = 目录名,dict 保持发现顺序 │ ├─► format_prompt() ──► XML 清单(name+description) │ 如 <available_skills><skill><name>... ──► 进 system prompt(常驻) │ ▼ ⑤ 调用(宿主显式,模型无自助通道) agent.invoke_skill("code-review", "重点看并发") │ ▼ skill_manager.format_invocation("code-review", "重点看并发") │ get("code-review") │ ──── 未知名 ────► ValueError(列可用名字)★ 唯一显式失败 ▼ <skill name="code-review" location="D:\...\SKILL.md"> 正文 markdown </skill>
重点看并发 │ 包装成一个 user 消息 ▼ ⑥ 执行(进入模型上下文) Agent.run(包装文本) │ user 消息追加进 messages ▼ llm.chat(messages, tools) ──► 模型看到正文指令 ──► 干活(可再调工具)──► 返回最终文本
|
阶段 ①②③ 合称”加载”——磁盘 → 内存 Skill 对象。只在 Agent 构造时发生一次。每个失败点都静默(丢弃 None),不阻塞 Agent 启动。
阶段 ④ 是”渐进式披露”第一层——模型此刻只看到 name+description(清单),看不到正文。这是 token 经济学:每个 skill 在 system 里只占几十 token。
阶段 ⑤⑥ 合称”调用”——正文从内存进模型上下文。触发者是宿主(应用层代码 / 未来的 REPL),不是模型。format_invocation 是唯一”查不到就报错”的地方。
阶段 ⑦ 是关键边界:skill 生命周期不随会话走——Agent.reset() 清 messages 不清 skills;改文件必须重开 Agent。skill 是代码级配置(构造时定死),不是会话状态。
plugin
标准plugin的结构
1 2 3 4 5 6 7 8 9 10 11 12 13
| my-plugin/ ├── .claude-plugin/ │ └── plugin.json # Plugin 清单(可选,不需要它也能自动发现组件) ├── skills/ # Agent Skills(自主调用或通过 /skill-name) │ └── my-skill/ │ └── SKILL.md ├── commands/ # 旧版:改用 skills/ 代替 │ └── custom-cmd.md ├── agents/ # 自定义 agents │ └── specialist.md ├── hooks/ # 事件处理程序 │ └── hooks.json └── .mcp.json # MCP 服务器定义
|
| 目录 |
位置 |
目的 |
.claude-plugin/ |
插件根 |
包含 plugin.json 清单(如果组件使用默认位置,则可选) |
skills/ |
插件根 |
Skills 作为 <name>/SKILL.md 目录 |
commands/ |
插件根 |
Skills 作为平面 Markdown 文件。为新插件使用 skills/ |
agents/ |
插件根 |
自定义 agent 定义 |
hooks/ |
插件根 |
hooks.json 中的事件处理程序 |
.mcp.json |
插件根 |
MCP server 配置 |
.lsp.json |
插件根 |
用于代码智能的 LSP server 配置 |
monitors/ |
插件根 |
monitors.json 中的后台监视器配置 |
bin/ |
插件根 |
在启用插件时添加到 Bash tool 的 PATH 的可执行文件 |
settings.json |
插件根 |
启用插件时应用的默认设置 |
架构全景
在我们的框架中,底层的 SkillManager(技能)、SubagentManager(子代理)以及 MCP 客户端已经非常成熟。
因此,PluginManager 的定位不是重写一套执行引擎,而是作为一个“标准插件包的扫描、校验与解构分发器”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Claude Code 格式插件目录包 (例如 .agents/plugins/my-plugin/) ├── .claude-plugin/plugin.json (或 .plugin/plugin.json) ├── skills/ (或根目录 SKILL.md 单技能简写) ├── agents/ └── .mcp.json │ ▼ ┌───────────────────┐ │ PluginManager │ (扫描目录、解析 Manifest、提取组件路径) └─────────┬─────────┘ │ ┌───────────────────────────┼───────────────────────────┐ │ │ │ ▼ ▼ ▼ get_skill_dirs() get_subagent_dirs() get_mcp_config_paths() [Path(".../skills")] [Path(".../agents")] [Path(".../.mcp.json")] │ │ │ ▼ ▼ ▼ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ SkillManager │ │ SubagentManager │ │ MCP 客户端扩展 │ │ (通过 extra_dirs) │ │ (通过 extra_dirs) │ │ (按需加载 MCP) │ └───────────────────┘ └───────────────────┘ └───────────────────┘
|
PluginManifest和PluginAuthor
1. PluginAuthor:格式抹平器与作者归属元数据
Plugin对象
一、Plugin 在系统中的定位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ┌────────────────────────────────────────────────────────┐ │ Plugin 实体对象 │ ├────────────────────────────────────────────────────────┤ │ │ │ 1. 插件名称 (name) │ │ 2. 物理目录 (path: Path(".../my-plugin")) │ │ 3. 插件身份证 (manifest: PluginManifest) │ │ 4. 启用开关 (enabled: True/False) │ │ │ │ 【动态资源探测器 (计算属性 Properties)】 │ │ • skills_dir ──► 自动探测 skills/ 或 SKILL.md │ │ • agents_dir ──► 自动探测 agents/ 目录 │ │ • mcp_config_path ──► 自动探测 .mcp.json 配置文件 │ │ │ └────────────────────────────────────────────────────────┘
|
两个核心任务
Plugin 这个类主要完成了两件极其关键的工作:
任务 1:从文件夹一键加载插件(Plugin.from_directory 工厂方法)
你只要给它传一个文件夹路径(比如 Path(“.agents/plugins/code-review”)),它会自动完成一整套容错加载流程:
- 多级查找 Manifest:
先看有没有 .claude-plugin/plugin.json ➔ 再看 .plugin/plugin.json ➔ 最后看根目录 plugin.json;
- 安全读取与解析:
采用 utf-8-sig 编码读取(不怕 Windows BOM 乱码),解析成 PluginManifest 对象;
智能兜底(Fallback):
如果这个文件夹里根本没有 plugin.json(比如用户随手建的本地测试目录),它绝不报错罢工,而是自动以文件夹名作为插件名,生成一个
默认的 PluginManifest。
任务 2:子资源动态解构与路由(三个属性探测器)
它不需要在内存里把所有文件都读出来,而是提供了三个极其轻量、即用即算的属性:
| 属性名称 |
作用与探测逻辑 |
返回结果 |
plugin.skills_dir |
技能目录探测器 ①优先找 skills/ 子目录; ②若没有,找旧版兼容的 commands/ 目录; ③若根目录直接有 SKILL.md(单技能插件简写),直接返回插件根目录。 |
Path 或 None |
plugin.agents_dir |
子代理目录探测器 探测是否存在 agents/ 子目录。 |
Path 或 None |
plugin.mcp_config_path |
MCP 配置文件探测器 探测是否存在 .mcp.json 配置文件。 |
Path 或 None |
为什么要把这三个资源做成属性(Property)?
@property 的作用是什么?
简单一句话:@property 把一个“方法(函数)”,伪装成一个“只读属性(变量)”。
1. 没有 @property 时的写法(普通方法)
如果你不用 @property,你必须这样定义和调用:
1 2 3 4 5 6 7 8
| class Plugin: def get_skills_dir(self): return path
p = Plugin(...) dir_path = p.get_skills_dir()
|
2. 加上 @property 后的写法(计算属性)
1 2 3 4 5 6 7 8 9
| class Plugin: @property def skills_dir(self): return path
p = Plugin(...) dir_path = p.skills_dir
|
3. 为什么不直接在 init 里存一个普通变量,非要用 @property?
| 方案 |
__init__ 直接存变量 (self.skills_dir = ...) |
用 @property 计算属性(我们的方案) |
| 计算时机 |
在创建对象时写死了一次,如果磁盘文件后来变动,变量值就过时了。 |
即用即算(On-Demand)。每次有人访问 p.skills_dir 时,现场去磁盘快速探测一次,保证结果永远是最新的。 |
| 可读性 |
容易被外部代码不小心修改:p.skills_dir = "xxx" |
只读保护。外部代码如果试图修改 p.skills_dir = 123,Python 会直接报错拒绝,防止属性被意外污染。 |
PluginManager
第一阶段:三态扫描与路径判断
这一步的核心任务是:搞清楚用户想怎么加载插件,并找到所有候选文件夹。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| Agent 启动: Agent(plugin_dirs=...) │ ▼ 【检查 plugin_dirs 三态参数】 │ ┌───────────────────────┼───────────────────────┐ ▼ (plugin_dirs is None) ▼ (plugin_dirs == []) ▼ (plugin_dirs 为路径列表) ┌──────────────┐ ┌──────────────┐ ┌────────────────────────┐ │ 默认自动探测 │ │ 显式完全禁用 │ │ 遍历每个传入路径 p │ │ .agents/ │ │ 插件字典保持 │ └───────────┬────────────┘ │ plugins/ │ │ 为空,直接结 │ │ └──────┬───────┘ │ 束装配 │ ▼ │ └──────────────┘ 【_is_single_plugin(p)?】 │ │ │ ┌──────────────┴──────────────┐ │ ▼ (是单个插件) ▼ (是包含多个插件的目录) │ 直接作为单个目标 调用 _discover_from_dir(p) │ │ 遍历其下所有子文件夹 │ │ │ └─────────────────────────────────────┼─────────────────────────────┘ │ ▼ 进入【第二阶段:单插件加载】
|
第二阶段:单插件加载与防爆安检(_load_one_plugin)
这一步的核心任务是:把每一个找到的文件夹变成合法的 Plugin 对象,坏了也不崩。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 接收到目标文件夹路径: p │ ▼ ┌─────────────────────────┐ │ 调用 Plugin.from_dir(p) │ │ 尝试读取 plugin.json │ └───────────┬─────────────┘ │ ┌───────────────┴───────────────┐ ▼ (读取成功) ▼ (文件缺失或 JSON 损坏) 解析并提取 Manifest 触发智能兜底机制: (name, version, author...) 用目录名自动推断默认 Manifest │ │ └───────────────┬───────────────────────┘ │ ▼ 组装成 Plugin 实体 ┌─────────────────────────────┐ │ 放入 try...except 安全沙箱 │ └──────────────┬──────────────┘ │ ┌───────────────┴───────────────┐ ▼ (无致命错误) ▼ (发生异常/坏插件) ┌────────────────────────┐ ┌────────────────────────┐ │ 登记入库: │ │ 【单点故障隔离】 │ │ self.plugins[name] = p │ │ 打印 Warning 日志 │ └───────────┬────────────┘ │ 自动跳过,绝不崩溃主程序│ │ └────────────────────────┘ ▼ 该插件加载完毕,等待分发
|
第三阶段:资源解构与管家分发
这一步的核心任务是:把已加载的所有插件拆包,分给各个专业管家。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 所有插件已登记在 self.plugins 字典中 │ ┌───────────────────────────┼───────────────────────────┐ │ 1. 提取技能 │ 2. 提取子代理 │ 3. 提取 MCP 配置 ▼ ▼ ▼ get_skill_dirs() get_subagent_dirs() get_mcp_config_paths() 收集所有 skills/ 路径 收集所有 agents/ 路径 收集所有 .mcp.json 路径 │ │ │ ▼ ▼ ▼ 注入 SkillManager 注入 SubagentManager 注入 MCP 客户端扩展 (System Prompt (自动注册 task 委派 (自动建立 Stdio 进程 自动呈现技能清单) 工具,支持调用专员) 桥接外部工具) │ │ │ └───────────────────────────┼───────────────────────────┘ │ ▼ 主 Agent 获得所有插件能力, 装配完成,正式就绪!
|
plugin.json
1 2 3 4 5 6 7 8 9
| ┌────────────────────────────────────────────────────────┐ │ 一个下载或手写的插件文件夹: my-plugin/ │ └───────────────────────────┬────────────────────────────┘ │ ┌─────────────────────────────────┼─────────────────────────────────┐ ▼ ▼ ▼ 【第 1 优先级:官方标准】 【第 2 优先级:社区通用】 【第 3 优先级:极简根级】 .claude-plugin/plugin.json .plugin/plugin.json ./plugin.json (Claude Code 官方最新规范) (OpenHands/开源社区通用标准) (本地快速手写极简风格)
|
plugin.json 的标准格式是怎样的?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| { "name": "code-quality", "version": "1.2.0", "description": "一套用于代码审查、格式化和 Lint 检查的开发质量插件套件", "author": { "name": "Anthropic", "email": "support@anthropic.com", "url": "https://anthropic.com" }, "homepage": "https://github.com/example/code-quality", "repository": "https://github.com/example/code-quality.git", "license": "MIT", "keywords": [ "lint", "code-review", "python", "quality" ] }
|
| 字段名称 |
类型 |
是否必填 |
作用与含义 |
例子 |
name |
string |
强烈建议 |
插件的唯一代号。在 Claude Code 中它同时是技能的命名空间前缀(防止与其他插件重名)。 |
"code-quality" |
version |
string |
可选 |
版本号(默认 "1.0.0")。遵循语义化版本规范。 |
"1.2.0" |
description |
string |
可选 |
插件功能简介。在列出插件清单时展示给用户看。 |
"代码质量检查工具集" |
author |
object / string |
可选 |
作者信息。 既可以写成对象:{"name": "...", "email": "..."} 也可以写成纯字符串:"OpenHands <dev@openhands.dev>" |
"Anthropic <support@anthropic.com>" |
homepage |
string |
可选 |
插件的官方主页或文档网址。 |
"https://example.com" |
repository |
string |
可选 |
插件的开源 Git 代码仓库地址。 |
"https://github.com/..." |
license |
string |
可选 |
开源许可证类型。 |
"MIT" 或 "Apache-2.0" |
keywords |
array |
可选 |
标签列表 |
["test", "review"] |