5. Skill
5. Skill
5.1 原理
Skill 是可发现、可版本化、可按需注入的能力包。它通常包含:
- 元数据:名称、描述、触发条件、权限/工具需求;
- 指令:完成一类任务的操作协议;
- 资源:参考文档、模板、样例;
- 脚本/工具:确定性执行逻辑;
- 验证:检查输出是否符合契约。
Skill 的价值是渐进式披露。这里必须区分三个容易混淆的时刻:
- 发现时刻:扫描受控目录或远端目录,建立名称、描述、路径、来源和策略索引;
- 物化时刻:把
SKILL.md解析成内部对象,或在调用时重新读取正文; - 注入时刻:真正把完整指令作为带来源的上下文项交给模型。
“只在命中时加载 Skill”更准确地说是只在命中时把完整正文暴露给模型,不一定表示发现阶段完全没有磁盘读取。Claude Code 当前会在扫描本地 Skill 时读取并解析 SKILL.md,把正文保存在 Command 闭包中,但模型初始只看到轻量目录;Codex 发现阶段建立 SkillMetadata 快照,显式命中后又从发现该 Skill 的文件系统读取正文并构造注入项。两者都避免了“所有正文在会话开始进入上下文”,只是内存与 I/O 策略不同。
安全上不能把 Skill 当成可信代码:Skill 的 allowed-tools 是能力需求,不是最终授权;路径必须约束在允许的根目录;本地正文中的脚本扩展仍要经过工具权限;来自 MCP 的远端 Skill 更不能直接执行正文中的 shell 片段。Skill 指令不能绕过系统、开发者、Session 权限和沙箱策略。
5.2 加载策略全景图
图表加载中…
这张图的关键不是“读了一个 Markdown”,而是四个边界:目录边界决定模型知道哪些能力,选择边界决定本 Turn 激活哪些能力,授权边界决定正文允许申请哪些工具,恢复边界决定压缩或热更新后哪些状态还能继续使用。
5.3 Claude Code:从多来源目录到调用时正文注入
5.3.1 发现来源、并行加载与确定性去重
Claude Code 将 Skill 统一投影为 Command,但来源不止一个。getSkills 并行加载本地 Skill 目录和插件 Skill,再同步合并 bundled 与 built-in plugin Skill;任何一类失败都降级为空数组,不让非关键 Skill 阻断 CLI 启动:commands.ts:353、commands.ts:360、commands.ts:374。loadAllCommands 又把 bundled、built-in plugin、Skill 目录、workflow、plugin command、plugin Skill 和内建命令按确定顺序合并,并按 cwd 缓存昂贵的磁盘加载:commands.ts:445、commands.ts:449、commands.ts:460。
本地 Skill 的目录来源包括 managed、user、从当前项目向上发现的 project 目录、--add-dir 目录和旧版 commands 目录。这些来源并行读取,但合并时固定为 managed → user → project → additional → legacy;随后用 realpath 得到规范路径,以“先出现者保留”去除通过软链接或重叠父目录重复发现的同一文件:loadSkillsDir.ts:638、loadSkillsDir.ts:677、loadSkillsDir.ts:716、loadSkillsDir.ts:725。这说明并发只优化 I/O,不能改变优先级。
getCommands 没有把所有过滤结果永久缓存。磁盘加载结果可以缓存,但认证可用性和 isEnabled() 在每次调用时重新判断,因此登录或远端开关变化后不必重扫文件也能改变可见性:commands.ts:408、commands.ts:471、commands.ts:476。动态发现的 Skill 会在基础目录之后去重插入,防止同名动态项覆盖已有能力:commands.ts:479、commands.ts:491。
5.3.2 “读取正文”和“把正文给模型”不是同一步
扫描 /skills/ 时,加载器会读取 skill-name/SKILL.md、解析 frontmatter 与 Markdown 正文,再创建一个 Command:loadSkillsDir.ts:403、loadSkillsDir.ts:421、loadSkillsDir.ts:433、loadSkillsDir.ts:447。因此 Claude Code 当前的本地策略不是“调用前完全不读正文”,而是“发现时读取,调用时才展开”。createSkillCommand 把 markdownContent 保存在 getPromptForCommand 闭包中;只有真正调用时才追加 Skill 根目录、替换参数、${CLAUDE_SKILL_DIR} 与 ${CLAUDE_SESSION_ID},并返回模型可见文本:loadSkillsDir.ts:270、loadSkillsDir.ts:334、loadSkillsDir.ts:344、loadSkillsDir.ts:349。
模型可调用目录由 getSkillToolCommands 再次筛选:必须是 prompt 类型、没有禁止模型调用、不是 builtin,并且具有可用于选择的描述或 whenToUse;MCP Skill 则单独存放在 AppState,只有 loadedFrom === 'mcp' 的 prompt 才会并入 SkillTool 可调用集合:commands.ts:543、commands.ts:563、SkillTool.ts:77、SkillTool.ts:86。这就是“轻量目录”和“完整正文”的实际边界。
5.3.3 选择、权限与执行上下文
SkillTool 在调用前依次验证名称、存在性、disableModelInvocation 和 prompt 类型:SkillTool.ts:400、SkillTool.ts:412、SkillTool.ts:421。权限判断不是简单白名单,而是:先匹配 deny,再匹配显式 allow;如果 Skill 只包含经过审查的安全属性则自动允许,出现安全属性集合之外的新字段默认转为询问。这种“新增字段默认收紧”的设计避免未来扩展 frontmatter 时意外扩大权限:SkillTool.ts:470、SkillTool.ts:507、SkillTool.ts:526、SkillTool.ts:872。
调用后有两条正文执行路径:
context: fork:构造独立 Agent 上下文,使用自己的 Token 预算运行,父 Agent 只接收进度和最终结果:SkillTool.ts:118、SkillTool.ts:205、SkillTool.ts:622;- 默认 inline:通过
processPromptSlashCommand把正文变成元信息消息,并把allowed-tools、model、effort 作为当前上下文的受控修改:SkillTool.ts:635、SkillTool.ts:650、SkillTool.ts:729、SkillTool.ts:776。
协调者模式还做了一层上下文节省:主协调 Agent 只收到 Skill 名称、描述、使用条件和委派方式,不加载完整正文;实际 worker 调用时才获得正文和权限。这避免把执行型知识塞进只负责分发任务的上下文:processSlashCommand.tsx:827、processSlashCommand.tsx:837、processSlashCommand.tsx:850。
5.3.4 条件激活、MCP Skill 与热更新
带 paths 的 Skill 不会立即进入无条件目录,而是先存入 conditionalSkills;文件操作命中 gitignore 风格路径后才移动到动态 Skill 集合,并发出变更信号:loadSkillsDir.ts:771、loadSkillsDir.ts:787、loadSkillsDir.ts:985、loadSkillsDir.ts:1029。嵌套项目目录也可以在文件操作期间动态发现 Skill,使大型单仓不必在启动时扫描所有子树:loadSkillsDir.ts:818、loadSkillsDir.ts:853。
MCP Server 可通过 skill:// Resource 提供 Skill。连接成功后,Tools、Prompts、Resources 和 MCP Skills 并行发现,MCP Skill 再与普通 MCP Prompt 合并到 commands:client.ts:2344、client.ts:2346、client.ts:2349。安全差异是:loadedFrom === 'mcp' 的正文不会执行内嵌 shell 命令,而本地 Skill 可在普通权限机制下执行相应扩展:loadSkillsDir.ts:371、loadSkillsDir.ts:374。
证据边界需要说明:当前源码中的 mcpSkills.ts 是自动生成 stub,因此可以确认主连接链会在 Resources capability 存在时调用 MCP Skill 加载器、Resource 变化会使其缓存失效,也能确认最终 Command 的远端安全约束;但不能仅凭这份仓库断言 skill:// 的分页、URI 校验和 Resource 正文解析细节。工程评审时应把这些标为“调用链已验证、加载器内部实现未完整公开”,而不是把 stub 的空返回当成真实产品行为。
本地文件热更新通过 watcher 完成。它等待写入稳定、合并短时间内的连续事件,再清除 Skill 与 Command 缓存;动态 Skill 信号只清除上层 memoization,避免把刚发现的动态项一起清空:skillChangeDetector.ts:24、skillChangeDetector.ts:34、skillChangeDetector.ts:85、skillChangeDetector.ts:89。React 侧重新调用 getCommands 并替换当前命令目录,加载失败只记录错误,不中断会话:useSkillsChange.ts:24、useSkillsChange.ts:28。
5.3.5 压缩恢复为什么要按 Agent 隔离
Skill 调用时会注册 Hook,并把最终展开后的文本、来源和 agentId 写入 invoked-skills 状态:processSlashCommand.tsx:869、processSlashCommand.tsx:871、processSlashCommand.tsx:880。状态键由 agentId + skillName 组成,压缩后只恢复当前 Agent 已调用的 Skill,防止父 Agent 与子 Agent 的指令互相泄漏:state.ts:1501、state.ts:1510、state.ts:1530。因此恢复对象不是整个目录,而是“本任务已经激活且继续执行仍需要的正文”。
5.4 Codex:从 HostSkillsSnapshot 到 Turn 注入
5.4.1 Root 是发现与信任的基本边界
Codex 的 SkillRoot 不只是目录路径,还携带 scope、实际读取使用的文件系统、插件身份、namespace、插件根目录和 discovery mode:loader.rs:139。扫描设置最大深度 6、每个 root 最多 2000 个目录,并限制名称、描述和依赖字段长度,防止异常目录树与超长元数据拖垮发现阶段:loader.rs:94、loader.rs:97、loader.rs:106。
软链接策略按 scope 区分:User、Repo、Admin 可跟随目录链接,System 忽略目录链接;隐藏目录默认跳过:loader.rs:208、loader.rs:220。Agent Plugin 采用更严格的 DirectChildren 模式,规范化后的 SKILL.md 必须仍位于插件根目录内且是普通文件,否则直接丢弃,防止插件通过软链接把宿主其他文件伪装成 Skill:loader.rs:238、loader.rs:258、loader.rs:263、loader.rs:271。
5.4.2 并行扫描不能破坏优先级
不同 root 会并行扫描,并由共享 semaphore 和 MAX_CONCURRENT_ROOT_SCANS 限制并发;插件 root 还可复用快照缓存:root_loader.rs:59、root_loader.rs:67、root_loader.rs:90、root_loader.rs:118。扫描完成顺序是不确定的,所以代码先保留 root_index,并行结束后再恢复原始 root 顺序,然后进行确定性合并:root_loader.rs:116、root_loader.rs:122。
合并阶段当前按 Repo → User → System → Admin 排序,规范路径 first-wins 去重,最后再按 scope、名称、路径稳定排序:root_loader.rs:132、root_loader.rs:136、root_loader.rs:175、root_loader.rs:195。这里应按“当前代码的合并顺序”理解,不要凭 scope 名称自行推断覆盖关系。
SkillLoadOutcome 同时保留 skills、解析错误、disabled paths、root 映射、插件路径集合、每个 Skill 对应的文件系统和隐式调用索引:model.rs:24。enabled 与“允许隐式调用”是两道独立判断;disabled paths 变化时会重建隐式索引:model.rs:37、model.rs:42、model.rs:60。最终 HostSkillsSnapshot 是不可变快照,正文必须通过发现该 Skill 的文件系统读取,而不能假设都在本机磁盘:model.rs:98、model.rs:114。
5.4.3 Turn 内先解析依赖,再注入正文
显式选择先处理结构化 Skill path,再扫描文本中的 $skill-name;plain name 只有在不歧义时才解析,disabled path 和重复 path 被排除:injection.rs:173、injection.rs:175、injection.rs:184。这种顺序使 UI 结构化选择比自然语言猜测更可靠,也避免同名插件 Skill 被错误激活。
Turn 构建阶段先从用户输入和已提及 Skill 收集 required MCP Server。Skill 的 dependencies.tools 中类型为 MCP 的条目会加入依赖集合;如果 Skill 属于插件,插件声明的 MCP Server 也会加入:turn.rs:650、turn.rs:692、turn.rs:700、turn.rs:710。随后才进入 build_skills_and_plugins:先检查并处理缺失 MCP 依赖,再构造正文注入,说明依赖就绪必须早于 Prompt 固化:turn.rs:724、turn.rs:780、turn.rs:789。
缺失依赖安装只在受支持的第一方客户端和功能开关开启时运行;它比较当前 Runtime Server,按 canonical key 避免一个 Session 重复询问,用户同意后更新全局配置并立即刷新 MCP Runtime:mcp_skill_dependencies.rs:37、mcp_skill_dependencies.rs:50、mcp_skill_dependencies.rs:59、mcp_skill_dependencies.rs:232、mcp_skill_dependencies.rs:301。因此 Skill 声明的 MCP 依赖是“可解析、可安装的前置条件”,不是调用到一半才临时发现的隐藏失败。
5.4.4 正文注入、大小限制与重复消除
build_skill_injections 只处理最终提及的 Skill,并在注入阶段从对应文件系统读取正文;读取失败形成 warning,不会伪造空正文:injection.rs:75、injection.rs:82、injection.rs:92、injection.rs:126。Agent Plugin Skill 还会受到正文最大字节数限制,截断时产生用户可见警告,同时记录注入成功/失败指标:injection.rs:99、injection.rs:105、injection.rs:111、injection.rs:143。
Turn 最终把 Skill 转成 ContextualUserFragment。如果扩展层已经注入同一路径,legacy 注入路径会按规范化 path 过滤,避免同一正文重复占用上下文:turn.rs:809、turn.rs:850、injection.rs:36。Guardian reviewer 则完全不解释嵌入父会话记录中的 Skill/Plugin mention,因为这些文本属于不可信证据而不是用户的新请求:turn.rs:731。
5.5 两套 Skill 加载策略对比
| 维度 | Claude Code | Codex | 工程设计结论 |
|---|---|---|---|
| 内部目录对象 | 统一为 Command,MCP Skill 在 AppState 单独合并 | SkillLoadOutcome + 不可变 HostSkillsSnapshot | 目录对象要保留来源、策略、路径和错误,而不只是名称列表 |
| 本地发现 I/O | 扫描时读取正文并保存在 Command 闭包 | 发现时解析元数据,命中后按发现文件系统再次读取正文 | 渐进加载主要是渐进暴露,不应笼统等同于延迟磁盘读取 |
| 来源与优先级 | managed、user、project、additional、legacy 等按固定顺序 first-wins | root 并行扫描,恢复输入顺序后按 scope/path 去重与稳定排序 | 并发加载必须保留确定性优先级 |
| 条件激活 | paths 命中和文件操作可激活动态 Skill | enabled 与 implicit invocation 独立建模 | “存在”“可见”“可隐式调用”应是不同状态 |
| 模型选择 | SkillTool 目录 + 显式名称,检查 disableModelInvocation | 结构化 path 优先,再解析文本 mention,拒绝歧义 | 结构化选择应优先于名称猜测 |
| 执行上下文 | inline 或 fork;协调者只收到委派摘要 | Turn 中构造 ContextualUserFragment,可与插件注入合并 | 指令注入与独立 Agent 执行是两种不同成本模型 |
| MCP Skill | 从 skill:// Resource 发现,禁止远端正文执行 shell | Skill 元数据声明 MCP 依赖,必要时询问安装并刷新 Runtime | 远端知识与远端工具都必须受来源和依赖治理 |
| 更新与恢复 | watcher 清缓存;压缩时按 agent 恢复 invoked skills | Turn 使用不可变快照;扩展注入按 path 去重 | 热更新作用于后续目录,进行中的 Turn 应保持输入稳定 |
5.6 参考实现伪代码
function buildSkillCatalog(roots, policy): // 构建只包含当前会话允许能力的轻量 Skill 目录
indexedRoots = attachStableIndex(roots) // 给每个来源保存固定序号,避免并发完成顺序改变优先级
snapshots = parallelMapBounded(indexedRoots, scanAndParseMetadata) // 在并发上限内扫描目录并解析名称、描述、路径和依赖
ordered = sortByOriginalIndex(snapshots) // 恢复来源原始顺序,使合并结果可以稳定复现
safeEntries = rejectPathEscapeAndInvalidFiles(ordered) // 丢弃越出插件根目录、非普通文件或元数据超限的条目
deduplicated = firstWinsByCanonicalPath(safeEntries) // 用规范路径去掉软链接和重叠目录造成的重复 Skill
visible = filterEnabledAndAvailable(deduplicated, policy) // 根据禁用状态、认证、产品策略和条件触发状态过滤目录
return metadataOnlyIndex(visible) // 只向模型选择层暴露轻量元数据,不注入所有正文
async function activateSkill(selection, catalog, session): // 激活一个被用户或受控规则明确选中的 Skill
skill = resolveStructuredPathBeforePlainName(selection, catalog) // 优先按结构化路径匹配,普通名称只有唯一时才接受
require(skill != null) // 无匹配或同名歧义时立即失败,避免激活错误能力
require(session.policy.isEnabled(skill)) // 再次确认 Skill 未被当前 Session 或管理员策略禁用
permission = session.permissions.evaluateSkill(skill) // 按 deny、allow、安全属性白名单和询问顺序计算权限
require(permission.isAllowed) // 未授权时不能读取依赖、执行 Hook 或修改当前工具集合
missing = diff(skill.mcpDependencies, session.mcp.readyServers) // 找出 Skill 声明但当前 Runtime 尚未就绪的 MCP Server
dependencyDecision = await resolveMissingDependencies(missing) // 对缺失依赖执行安装询问、跳过或明确失败流程
body = await readFromDiscoveringFileSystem(skill.path) // 使用发现该 Skill 的文件系统读取正文,兼容远端执行环境
boundedBody = enforcePromptByteLimit(body, skill.source) // 按来源应用正文大小上限并保留截断警告
expanded = substituteApprovedArgumentsAndPaths(boundedBody, selection.args) // 只替换允许的参数、Skill 根目录和 Session 标识
if skill.source == "mcp": // 判断正文是否来自不可信的远端 MCP Resource
expanded = disableInlineShellExpansion(expanded) // 禁止远端 Skill 借正文触发本地 shell 执行
if skill.executionContext == "fork": // 根据声明决定使用独立 Agent 还是当前上下文
result = await runInIsolatedAgent(expanded, session.toolCeiling) // 在独立预算和不超过 Session 上限的工具集合中执行
else: // 处理默认的当前上下文注入路径
result = injectAsSourcedContextFragment(expanded, skill.path) // 把正文作为带来源的上下文片段加入当前 Turn
session.invokedSkills.record(skill, expanded, session.agentId) // 按 Agent 记录已调用正文,供压缩恢复和观测使用
return result // 返回注入结果或独立 Agent 的完成结果
5.7 出现异常时如何判断
| 现象 | 首先检查 | 典型原因 | 正确处理 |
|---|---|---|---|
| Skill 文件存在但目录里看不到 | 来源开关、scope、disable-model-invocation、条件 paths | 来源被策略关闭、只允许插件、条件路径尚未命中 | 查看加载错误与 disabled 状态,不要直接清空所有缓存 |
| 同一个 Skill 出现两次 | canonical path、插件 namespace、宿主扩展注入路径 | 软链接重复、重叠目录、legacy 与扩展同时注入 | 发现阶段按规范路径 first-wins,Turn 阶段按注入 path 再去重 |
| 修改 Skill 后仍是旧描述 | watcher、写入稳定时间、Command/Skill 两层缓存 | 文件事件未到达或只清了一层缓存 | 清除正确缓存并重建目录;进行中的 Turn 保持旧快照是正常现象 |
| 能看到但模型不能调用 | disableModelInvocation、隐式调用策略、权限规则 | 只允许用户显式调用、deny 命中、出现未知高风险字段 | 区分可见、可显式调用和可隐式调用,不要把三者合成一个布尔值 |
| 调用后没有完整正文 | coordinator/worker 角色、正文大小、读取错误 | 协调者只收摘要、Agent Plugin 被截断、远端文件系统读取失败 | 查看 warning 和注入指标,确认真正执行正文的是哪个 Agent |
| Skill 调用后工具仍不可用 | allowed-tools 与 Session 上限、MCP 依赖状态 | Skill 只是声明需求,Session 未授权;依赖未安装或认证失败 | 对权限取交集,并在 Prompt 固化前完成依赖安装/连接判断 |
| 压缩后 Skill 行为消失或串到别的 Agent | invoked-skills 的 agentId 与恢复预算 | 未记录展开后正文、恢复了全局目录、父子 Agent 状态混用 | 只恢复当前 Agent 已调用且仍必要的内容 |
5.8 高频工程检验题
问:Skill 和 Tool 有什么区别?
Tool 是可执行接口,契约是 schema → side effect/result;Skill 是解决某类问题的知识与流程包,可以调用多个 Tool。Skill 负责“怎么做”,Tool 负责“执行一个动作”。
问:Skill 的渐进加载是不是调用时才读取文件?
不一定。应拆成发现、物化和注入三个时刻。Claude Code 当前会在本地目录扫描时读取正文并保存在 Command 闭包,但只在调用时把正文加入模型上下文;Codex 通过不可变目录快照选择 Skill,注入阶段从发现它的文件系统读取正文。共同目标是避免未命中的完整正文占用上下文,而不是强制采用同一种 I/O 策略。
问:为什么 Skill 热更新不能直接修改进行中 Turn 的目录?
因为模型选择 Tool 或 Skill 时看到的是一个能力快照。如果中途替换描述、权限或依赖,执行阶段可能使用另一份契约。安全做法是让文件变化失效后续 Turn 的目录,当前 Turn 继续使用已捕获输入;确需立即生效时取消当前采样并重新构建上下文。
5.9 原理深化:Skill 是按需展开的能力生命周期
从两套源码可以把 Skill 还原成六个阶段,而不只是“读取一个 Markdown”:发现阶段扫描受控来源并建立索引;选择阶段结合显式提及、条件规则和产品限制筛选;依赖解析阶段确认 MCP、插件和权限前置条件;注入阶段把正文作为有来源、有大小上限的上下文项加入当前 Turn;执行阶段仍通过普通 Tool、权限与沙箱;恢复阶段在压缩、文件变化或 Session 恢复后,只重建仍需要的激活状态。
三个不变量可以串起全部实现细节:
- 确定性目录:并行扫描不能改变来源优先级,相同输入必须生成相同 Skill 目录;
- 能力不放大:Skill 声明、Hook 和 allowed-tools 都不能突破 Session 权限与工具上限;
- Turn 输入稳定:选择和正文注入一旦成为本 Turn 输入,热更新不应无声替换其含义。
因此,Skill 的工程本质是“可治理的上下文模块”。它既不是随意拼接的 Prompt,也不是能绕开 Tool 管线的插件代码。渐进披露同时服务于上下文效率、安全面收缩和 Prompt Cache 稳定性;目录、正文、依赖与恢复必须分别建模,才能在大型 Skill 生态中保持可解释和可诊断。
5.10 DeepSeek Harness:Skill 的发现与正文加载完全分离
packages/skill/skill 是 ctx.skills Service Definition,它只合并多提供方目录、根据 rank 和注册顺序解决同名项,并区分 modelInvocable 与 userInvocable;skill-filesystem 负责从磁盘解析候选,tool-skill 才向模型暴露目录和按需加载入口。目录中只有 name/description/whenToUse/source/provider/resourceBase,完整 Markdown 正文在 ctx.skills.get() 成功后才渲染为 <skill_content>。
工具调用与用户显式调用共用同一 renderSkillContent(),但后者会以带 skill-invocation source 的注入消息进入 Agent Inbox,因此“模型看到了哪份 Skill 正文”可以从 Session Log 重建。提供方返回的 disposer 与 Scope 一起决定目录失效,避免把动态 Skill 当成永久全局单例。