<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>潘锦的空间 &#187; skills</title>
	<atom:link href="https://www.phppan.com/tag/skills/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.phppan.com</link>
	<description>SaaS SaaS架构 团队管理 技术管理 技术架构 PHP 内核 扩展 项目管理</description>
	<lastBuildDate>Sat, 25 Apr 2026 00:56:17 +0000</lastBuildDate>
	<language>zh-CN</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=3.9.40</generator>
	<item>
		<title>Claude Code 的 SKILLS 技能渐进式披露实现原理解析</title>
		<link>https://www.phppan.com/2026/04/claude-code-ai-skills-source/</link>
		<comments>https://www.phppan.com/2026/04/claude-code-ai-skills-source/#comments</comments>
		<pubDate>Sun, 12 Apr 2026 03:47:23 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[ClaudeCode]]></category>
		<category><![CDATA[skills]]></category>
		<category><![CDATA[渐进式披露]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2490</guid>
		<description><![CDATA[SKILLS 和 渐进式披露 是 A 家最早提出来的方案，也是 OpenClaw 火了后大家一直讨论的哪个技能 [&#8230;]]]></description>
				<content:encoded><![CDATA[<section style="color: #000000;" data-tool="mdnice编辑器" data-website="https://www.mdnice.com" data-pm-slice="0 0 []">
<p data-tool="mdnice编辑器">SKILLS 和 <strong style="color: #0e88eb;">渐进式披露</strong> 是 A 家最早提出来的方案，也是 OpenClaw 火了后大家一直讨论的哪个技能好用很核心的强依赖的实现逻辑。</p>
<p data-tool="mdnice编辑器">如果把 Claude Code 的 skills 理解成一堆 prompt 文件，后面的很多设计都解释不通。</p>
<p data-tool="mdnice编辑器">从其源码实现来看，会发现它在解决的核心问题是：<strong style="color: #0e88eb;">怎么让模型保留足够强的技能召回能力，同时又不把常驻上下文撑爆。</strong></p>
<p data-tool="mdnice编辑器">这件事说穿了就是五个字：<strong style="color: #0e88eb;">渐进式披露</strong>。</p>
<p data-tool="mdnice编辑器">大概的逻辑是：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">先告诉模型「系统里存在 skills 机制」。</section>
</li>
<li>
<section style="color: #010101;">再告诉它「当前有哪些 skill 名称和简短说明」。</section>
</li>
<li>
<section style="color: #010101;">等它真的决定调用某个 skill 时，再把正文、权限、hooks、模型覆盖、附加工具权限这些重内容展开。</section>
</li>
<li>
<section style="color: #010101;">如果某些 skill 还和路径、目录、文件类型绑定，那就继续往后拖，拖到模型真的碰到对应文件时再激活。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这是一个优雅且干净的工程化设计。它没有发明一套复杂到难以维护的 skill runtime，也没有把所谓智能寄托在黑盒检索器上，而是先把「披露成本」这件事控制住。</p>
<p data-tool="mdnice编辑器">我们按工程实现往下拆：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">skill 在系统里到底被建模成什么</section>
</li>
<li>
<section style="color: #010101;">多来源 skill 是怎么统一装配的</section>
</li>
<li>
<section style="color: #010101;">渐进式披露具体分了哪几层</section>
</li>
<li>
<section style="color: #010101;">条件激活和动态发现是怎么接进文件操作链路的</section>
</li>
<li>
<section style="color: #010101;">inline 和 fork 两条执行路径分别解决什么问题</section>
</li>
<li>
<section style="color: #010101;">这套设计真正适合什么场景，代价又是什么</section>
</li>
<li>
<section style="color: #010101;">如果要在自己的 Agent 里复刻，最短落地路径应该怎么走</section>
</li>
</ul>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">一、先看 skills 在系统里被建模成什么</span></h1>
<p data-tool="mdnice编辑器">Claude Code 里，skill 最终会被统一建模成 <code style="color: #0e8aeb;">Command</code>，而且类型是 <code style="color: #0e8aeb;">prompt</code>。</p>
<p data-tool="mdnice编辑器">最核心的构造函数是 createSkillCommand：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">return</span> {
<span style="color: #c678dd;">type</span>: <span style="color: #98c379;">'prompt'</span>,
  name: skillName,
  description,
  hasUserSpecifiedDescription,
  allowedTools,
  argumentHint,
  argNames: argumentNames.length &gt; <span style="color: #d19a66;">0</span> ? argumentNames : <span style="color: #56b6c2;">undefined</span>,
  whenToUse,
  version,
  model,
  disableModelInvocation,
  userInvocable,
  context: executionContext,
  agent,
  effort,
  paths,
  contentLength: markdownContent.length,
  isHidden: !userInvocable,
  progressMessage: <span style="color: #98c379;">'running'</span>,
  userFacingName(): <span style="color: #e6c07b;">string</span> {
    <span style="color: #c678dd;">return</span> displayName || skillName
  },
  source,
  loadedFrom,
  hooks,
  skillRoot: baseDir,
<span style="color: #c678dd;">async</span> getPromptForCommand(args, toolUseContext) {
    ...
    <span style="color: #c678dd;">return</span> [{ <span style="color: #c678dd;">type</span>: <span style="color: #98c379;">'text'</span>, text: finalContent }]
  },
}
</code></pre>
<p data-tool="mdnice编辑器">这段代码说明有几个关键点：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">skill 不是特殊 runtime object，而是 <code style="color: #0e8aeb;">prompt command</code></section>
</li>
<li>
<section style="color: #010101;">skill 本体是 <code style="color: #0e8aeb;">getPromptForCommand()</code> 生成的一组文本 block</section>
</li>
<li>
<section style="color: #010101;">skill 可以带：</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">allowedTools</code></section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">model</code></section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">effort</code></section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">paths</code></section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">hooks</code></section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">context: inline | fork</code></section>
</li>
</ul>
</li>
<li>
<section style="color: #010101;">skill 的调用结果，不是「执行一段脚本」，而是<strong style="color: #0e88eb;">把 skill 展开成后续对话消息，或者 fork 成子代理执行</strong></section>
</li>
</ul>
<p data-tool="mdnice编辑器">如果我们自己做 Agent，建议参考。skill 不要单独发明一套 DSL runtime，直接把它抽象成「可延迟展开的 prompt 命令」就够了。</p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">二、skills 的来源有哪几类</span></h1>
<p data-tool="mdnice编辑器">skills 并不只来自一个目录。<code style="color: #0e8aeb;">getSkills()</code> 会把多个来源统一聚合。[commands.ts] commands.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L353</a>-L398</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">const</span> [skillDirCommands, pluginSkills] = <span style="color: #c678dd;">await</span> <span style="color: #e6c07b;">Promise</span>.all([
  getSkillDirCommands(cwd)...
  getPluginSkills()...
])
<span style="color: #c678dd;">const</span> bundledSkills = getBundledSkills()
<span style="color: #c678dd;">const</span> builtinPluginSkills = getBuiltinPluginSkillCommands()
</code></pre>
<p data-tool="mdnice编辑器">然后 <code style="color: #0e8aeb;">loadAllCommands()</code> 再把这些东西和 workflow/plugin/内建命令一起合并。[commands.ts] commands.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L445</a>-L469</p>
<p data-tool="mdnice编辑器">也就是说，skills 的来源至少有：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">bundled skills</section>
</li>
<li>
<section style="color: #010101;">磁盘上的 <code style="color: #0e8aeb;">/skills/</code></section>
</li>
<li>
<section style="color: #010101;">plugin skills</section>
</li>
<li>
<section style="color: #010101;">builtin plugin skills</section>
</li>
<li>
<section style="color: #010101;">兼容旧 <code style="color: #0e8aeb;">/commands/</code> 目录加载进来的 prompt commands</section>
</li>
</ul>
<p data-tool="mdnice编辑器"><strong style="color: #0e88eb;">SkillTool 根本不需要知道 skill 来自哪里</strong>。只要最后是 <code style="color: #0e8aeb;">prompt command</code>，就能走统一调用路径。</p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">三、skills 的「渐进式披露」分 5 层</span></h1>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">1）第一层：系统提示只声明「技能机制存在」</span></h2>
<p data-tool="mdnice编辑器">系统提示里不会把所有 skill 正文直接塞进去。它只给一个能力声明，告诉模型：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">用户说 <code style="color: #0e8aeb;">/&lt;skill-name&gt;</code>，其实是在指 skill</section>
</li>
<li>
<section style="color: #010101;">可以用 <code style="color: #0e8aeb;">SkillTool</code> 去执行</section>
</li>
<li>
<section style="color: #010101;">不要乱猜，只能调用列出来的那些</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这段在 [prompts.ts] prompts.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L353</a>-L401：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;">hasSkills
  ? <span style="color: #98c379;">`/&lt;skill-name&gt; (e.g., /commit) is shorthand for users to invoke a user-invocable skill. When executed, the skill gets expanded to a full prompt. Use the <span style="color: #e06c75;">${SKILL_TOOL_NAME}</span> tool to execute them. IMPORTANT: Only use <span style="color: #e06c75;">${SKILL_TOOL_NAME}</span> for skills listed in its user-invocable skills section - do not guess or use built-in CLI commands.`</span>
  : <span style="color: #56b6c2;">null</span>
</code></pre>
<p data-tool="mdnice编辑器">这一步只暴露了<strong style="color: #0e88eb;">机制</strong>，没有暴露<strong style="color: #0e88eb;">内容</strong>。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">2）第二层：只披露 skill 名称和短描述</span></h2>
<p data-tool="mdnice编辑器">真正给模型看的 skill 列表，是通过 <code style="color: #0e8aeb;">getSkillToolCommands()</code> 过滤出来的。[commands.ts] commands.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L561</a>-L580</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">return</span> allCommands.filter(
  cmd =&gt;
    cmd.type === <span style="color: #98c379;">'prompt'</span> &amp;&amp;
    !cmd.disableModelInvocation &amp;&amp;
    cmd.source !== <span style="color: #98c379;">'builtin'</span> &amp;&amp;
    (
      cmd.loadedFrom === <span style="color: #98c379;">'bundled'</span> ||
      cmd.loadedFrom === <span style="color: #98c379;">'skills'</span> ||
      cmd.loadedFrom === <span style="color: #98c379;">'commands_DEPRECATED'</span> ||
      cmd.hasUserSpecifiedDescription ||
      cmd.whenToUse
    ),
)
</code></pre>
<p data-tool="mdnice编辑器">这段有两个要点：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">只有 <code style="color: #0e8aeb;">prompt</code> 命令才能进 skill 列表</section>
</li>
<li>
<section style="color: #010101;">并不是所有 prompt command 都自动暴露，至少得满足可描述性要求</section>
</li>
</ul>
<p data-tool="mdnice编辑器">也就是说，<strong style="color: #0e88eb;">可执行集合</strong>和<strong style="color: #0e88eb;">对模型披露集合</strong>不是完全相同的。<br />
Claude Code 在这里收了一刀，避免模型看到一堆没有描述、无法判断用途的技能。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">3）第三层：列表本身还要走预算裁剪</span></h2>
<p data-tool="mdnice编辑器">skill 列表不是全量原文塞进 prompt，而是按预算压缩过的。核心逻辑在 [prompt.ts] tools/SkillTool/prompt.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L20</a>-L171。</p>
<p data-tool="mdnice编辑器">最关键的常量：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">export</span> <span style="color: #c678dd;">const</span> SKILL_BUDGET_CONTEXT_PERCENT = <span style="color: #d19a66;">0.01</span>
<span style="color: #c678dd;">export</span> <span style="color: #c678dd;">const</span> DEFAULT_CHAR_BUDGET = <span style="color: #d19a66;">8</span>_000
<span style="color: #c678dd;">export</span> <span style="color: #c678dd;">const</span> MAX_LISTING_DESC_CHARS = <span style="color: #d19a66;">250</span>
</code></pre>
<p data-tool="mdnice编辑器">以及格式化逻辑：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">return</span> <span style="color: #98c379;">`- <span style="color: #e06c75;">${cmd.name}</span>: <span style="color: #e06c75;">${getCommandDescription(cmd)}</span>`</span>
</code></pre>
<p data-tool="mdnice编辑器">和预算裁剪：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">if</span> (fullTotal &lt;= budget) {
  <span style="color: #c678dd;">return</span> fullEntries.map(e =&gt; e.full).join(<span style="color: #98c379;">'\n'</span>)
}
</code></pre>
<p data-tool="mdnice编辑器">如果超预算，就会：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">bundled skills 尽量保留完整描述</section>
</li>
<li>
<section style="color: #010101;">其它 skills 截断 description</section>
</li>
<li>
<section style="color: #010101;">极端情况下退化成只发 <code style="color: #0e8aeb;">- skill-name</code></section>
</li>
</ul>
<p data-tool="mdnice编辑器">这就是很典型的渐进式披露：<strong style="color: #0e88eb;">先给最小可用索引，不给正文</strong>。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">4）第四层：列表还是增量下发，不是每轮全量重发</span></h2>
<p data-tool="mdnice编辑器">技能列表通过 <code style="color: #0e8aeb;">skill_listing</code> attachment 发给模型。发送逻辑在 [attachments.ts] utils/attachments.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L2669</a>-L2752。</p>
<p data-tool="mdnice编辑器">核心逻辑：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">const</span> newSkills = allCommands.filter(cmd =&gt; !sent.has(cmd.name))
...
<span style="color: #c678dd;">for</span> (<span style="color: #c678dd;">const</span> cmd of newSkills) {
  sent.add(cmd.name)
}
...
<span style="color: #c678dd;">return</span> [
  {
    <span style="color: #c678dd;">type</span>: <span style="color: #98c379;">'skill_listing'</span>,
    content,
    skillCount: newSkills.length,
    isInitial,
  },
]
</code></pre>
<p data-tool="mdnice编辑器">这个 <code style="color: #0e8aeb;">sentSkillNames</code> 机制说明：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">第一次发的是初始批次</section>
</li>
<li>
<section style="color: #010101;">后面只发新增的 skill</section>
</li>
<li>
<section style="color: #010101;">resume 之后还会 suppress，避免重复污染上下文</section>
</li>
</ul>
<p data-tool="mdnice编辑器">然后 <code style="color: #0e8aeb;">messages.ts</code> 会把它包成系统提醒。[messages.ts] utils/messages.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L3763</a>-L3772</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">return</span> wrapMessagesInSystemReminder([
  createUserMessage({
    content: <span style="color: #98c379;">`The following skills are available for use with the Skill tool:\n\n<span style="color: #e06c75;">${attachment.content}</span>`</span>,
    isMeta: <span style="color: #56b6c2;">true</span>,
  }),
])
</code></pre>
<p data-tool="mdnice编辑器">很多 Agent 会每轮把所有 tools / skills 全量重发，Claude Code 显然在认真控 token。 当然，如果技能不多，也可以直接全量发，不要过早优化。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">5）第五层：真正的 skill 内容延迟到调用时才展开</span></h2>
<p data-tool="mdnice编辑器">直到调用 <code style="color: #0e8aeb;">SkillTool</code>，skill 的真实正文才会通过 <code style="color: #0e8aeb;">command.getPromptForCommand()</code> 生成。[SkillTool.ts] utils/processUserInput/processSlashCommand.tsx<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L869</a>-L920</p>
<p data-tool="mdnice编辑器">这里才会发生：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">$ARGUMENTS</code> 替换</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">${CLAUDE_SKILL_DIR}</code> 替换</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">${CLAUDE_SESSION_ID}</code> 替换</section>
</li>
<li>
<section style="color: #010101;">markdown 内嵌 shell 执行</section>
</li>
<li>
<section style="color: #010101;">hooks 注册</section>
</li>
<li>
<section style="color: #010101;">附加权限 attachment 注入</section>
</li>
<li>
<section style="color: #010101;">invoked skill 记录</section>
</li>
</ul>
<p data-tool="mdnice编辑器">换句话说，skill 的重内容、重权限、重上下文副作用，都是<strong style="color: #0e88eb;">按需加载</strong>。</p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">四、除了延迟加载，它还做了「条件激活」</span></h1>
<p data-tool="mdnice编辑器">这也是渐进式披露的重要一层，而且很多人会漏掉。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">1）带 <code>paths</code> frontmatter 的 skill，不会启动即暴露</span></h2>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">getSkillDirCommands()</code> 里会把 skill 分成两类：[loadSkillsDir.ts] loadSkillsDir.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L771</a>-L803</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">if</span> (
  skill.type === <span style="color: #98c379;">'prompt'</span> &amp;&amp;
  skill.paths &amp;&amp;
  skill.paths.length &gt; <span style="color: #d19a66;">0</span> &amp;&amp;
  !activatedConditionalSkillNames.has(skill.name)
) {
  newConditionalSkills.push(skill)
} <span style="color: #c678dd;">else</span> {
  unconditionalSkills.push(skill)
}
</code></pre>
<p data-tool="mdnice编辑器">然后 conditional skills 被先放进 <code style="color: #0e8aeb;">conditionalSkills</code> map，而不是直接进入模型可见集合。</p>
<p data-tool="mdnice编辑器">这意味着：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">你定义了某个 skill 只适用于 <code style="color: #0e8aeb;">*.tsx</code></section>
</li>
<li>
<section style="color: #010101;">它不会在项目启动时就干扰所有任务</section>
</li>
<li>
<section style="color: #010101;">只有模型真的碰到匹配文件时，这个 skill 才会被激活</section>
</li>
</ul>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">2）激活时机挂在文件操作上</span></h2>
<p data-tool="mdnice编辑器">FileRead / FileWrite / FileEdit 三个工具里，都有两步副作用：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">发现上层目录里的 <code style="color: #0e8aeb;">.claude/skills</code></section>
</li>
<li>
<section style="color: #010101;">激活匹配当前文件路径的 conditional skills</section>
</li>
</ul>
<p data-tool="mdnice编辑器">比如 FileReadTool：[FileReadTool.ts] /tools/FileReadTool/FileReadTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L575</a>-L591</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">const</span> newSkillDirs = <span style="color: #c678dd;">await</span> discoverSkillDirsForPaths([fullFilePath], cwd)
...
addSkillDirectories(newSkillDirs).catch(() =&gt; {})
...
activateConditionalSkillsForPaths([fullFilePath], cwd)
</code></pre>
<p data-tool="mdnice编辑器">对应的激活实现是 [activateConditionalSkillsForPaths] skills/loadSkillsDir.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L997</a>-L1058：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">const</span> skillIgnore = ignore().add(skill.paths)
...
<span style="color: #c678dd;">if</span> (skillIgnore.ignores(relativePath)) {
  dynamicSkills.set(name, skill)
  conditionalSkills.delete(name)
  activatedConditionalSkillNames.add(name)
}
</code></pre>
<p data-tool="mdnice编辑器">这一步非常像条件规则系统，而不是纯静态注册。<br />
效果就是：<strong style="color: #0e88eb;">技能集合会随着你读写哪些文件而变化</strong>。</p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">五、动态发现本身也是渐进式披露的一部分</span></h1>
<p data-tool="mdnice编辑器">除了 path-conditional activation，Claude Code 还支持<strong style="color: #0e88eb;">目录级动态发现</strong>。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">1）启动时只加载一部分 skill 目录</span></h2>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">getSkillDirCommands()</code> 启动时会加载：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">managed</section>
</li>
<li>
<section style="color: #010101;">user</section>
</li>
<li>
<section style="color: #010101;">project dirs</section>
</li>
<li>
<section style="color: #010101;">additional dirs</section>
</li>
<li>
<section style="color: #010101;">legacy commands</section>
</li>
</ul>
<p data-tool="mdnice编辑器">但它不会把所有嵌套目录里的 <code style="color: #0e8aeb;">.claude/skills</code> 一次性全扫出来。[loadSkillsDir.ts] skills/loadSkillsDir.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L638</a>-L804</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">2）当模型碰到某个文件时，再向上走目录树找嵌套 skill</span></h2>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">discoverSkillDirsForPaths()</code> 会从当前文件的父目录开始，一路往上走到 cwd，查找 <code style="color: #0e8aeb;">.claude/skills</code>。[loadSkillsDir.ts] skills/loadSkillsDir.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L861</a>-L915</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">while</span> (currentDir.startsWith(resolvedCwd + pathSep)) {
  <span style="color: #c678dd;">const</span> skillDir = join(currentDir, <span style="color: #98c379;">'.claude'</span>, <span style="color: #98c379;">'skills'</span>)
  ...
  <span style="color: #c678dd;">await</span> fs.stat(skillDir)
  ...
  newDirs.push(skillDir)
}
</code></pre>
<p data-tool="mdnice编辑器">而且还做了两个非常实用的约束：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">已检查过的目录不会重复 stat</section>
</li>
<li>
<section style="color: #010101;">gitignored 目录里的 skills 不会静默加载</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这个设计让：<br />
<strong style="color: #0e88eb;">技能跟着你进入子目录而出现，不跟整个仓库一起一次性曝光。</strong></p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">六、SkillTool 的调用链，实际上分 inline 和 fork 两条路</span></h1>
<p data-tool="mdnice编辑器">这是技能系统和普通 slash command 最大的不同之一。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">1）调用前校验</span></h2>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">SkillTool.validateInput()</code> 会做：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">去掉前导 <code style="color: #0e8aeb;">/</code></section>
</li>
<li>
<section style="color: #010101;">检查 skill 是否存在</section>
</li>
<li>
<section style="color: #010101;">检查是否 <code style="color: #0e8aeb;">disableModelInvocation</code></section>
</li>
<li>
<section style="color: #010101;">检查是否为 <code style="color: #0e8aeb;">prompt</code> 类型<br />
见 [SkillTool.ts] tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L355</a>-L430</section>
</li>
</ul>
<p data-tool="mdnice编辑器">关键逻辑：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">const</span> commands = <span style="color: #c678dd;">await</span> getAllCommands(context)
<span style="color: #c678dd;">const</span> foundCommand = findCommand(normalizedCommandName, commands)
...
<span style="color: #c678dd;">if</span> (foundCommand.type !== <span style="color: #98c379;">'prompt'</span>) {
  <span style="color: #c678dd;">return</span> {
    result: <span style="color: #56b6c2;">false</span>,
    message: <span style="color: #98c379;">`Skill <span style="color: #e06c75;">${normalizedCommandName}</span> is not a prompt-based skill`</span>,
  }
}
</code></pre>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">2）权限检查</span></h2>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">SkillTool.checkPermissions()</code> 很细，除了 allow / deny 规则，还会对「只有安全属性的 skill」自动放行。[SkillTool.ts] /tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L433</a>-L579</p>
<p data-tool="mdnice编辑器">这个设计的意义是：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">简单 declarative skill 不必每次都弹权限</section>
</li>
<li>
<section style="color: #010101;">带额外风险属性的 skill 要 ask user</section>
</li>
</ul>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">3）inline skill：展开成后续对话消息</span></h2>
<p data-tool="mdnice编辑器">默认分支会走 <code style="color: #0e8aeb;">processPromptSlashCommand()</code>。[SkillTool.ts] tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L635</a>-L644</p>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">getMessagesForPromptSlashCommand()</code> 干的事情很丰富：[processSlashCommand.tsx] utils/processUserInput/processSlashCommand.tsx<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L827</a>-L920</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">command.getPromptForCommand(args, context)</code> 得到真正 skill 正文</section>
</li>
<li>
<section style="color: #010101;">注册 hooks</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">addInvokedSkill()</code> 记录 skill 内容，供 compact 时恢复</section>
</li>
<li>
<section style="color: #010101;">从 skill 文本里再抽 attachment</section>
</li>
<li>
<section style="color: #010101;">增加 <code style="color: #0e8aeb;">command_permissions</code> attachment</section>
</li>
<li>
<section style="color: #010101;">生成一批 <code style="color: #0e8aeb;">messages</code></section>
</li>
</ul>
<p data-tool="mdnice编辑器">返回结构里最关键的是：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">return</span> {
  messages,
  shouldQuery: <span style="color: #56b6c2;">true</span>,
  allowedTools: additionalAllowedTools,
  model: command.model,
  effort: command.effort,
  command
}
</code></pre>
<p data-tool="mdnice编辑器">也就是说，inline skill 的本质是：<br />
<strong style="color: #0e88eb;">把 skill 变成一段新的上下文和权限修饰，然后让主对话继续跑。</strong></p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">4）fork skill：交给子代理跑，再把结果归还</span></h2>
<p data-tool="mdnice编辑器">如果 skill frontmatter 里声明 <code style="color: #0e8aeb;">context === 'fork'</code>，就走 <code style="color: #0e8aeb;">executeForkedSkill()</code>。[SkillTool.ts] tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L622</a>-L633</p>
<p data-tool="mdnice编辑器">它会：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">构造子代理上下文</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">runAgent()</code></section>
</li>
<li>
<section style="color: #010101;">收集 agent messages</section>
</li>
<li>
<section style="color: #010101;">抽取结果文本</section>
</li>
<li>
<section style="color: #010101;">最终返回 <code style="color: #0e8aeb;">{ status: 'forked', agentId, result }</code><br />
见 [executeForkedSkill] /tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L122</a>-L290</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这一步说明 Claude Code 已经把 skill 分成两类：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">知识/流程模板型 skill</strong>：inline 展开</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">工作委派型 skill</strong>：fork 子代理执行</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这个值得学一下。不是所有 skill 都应该展开在主上下文里。</p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">七、结果返回逻辑</span></h1>
<p data-tool="mdnice编辑器">为什么它也算渐进式披露的一部分？</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">1）inline skill 的 tool_result</span></h2>
<p data-tool="mdnice编辑器">很轻</p>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">mapToolResultToToolResultBlockParam()</code> 对 inline skill 的返回只是：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;">content: <span style="color: #98c379;">`Launching skill: <span style="color: #e06c75;">${result.commandName}</span>`</span>
</code></pre>
<p data-tool="mdnice编辑器">见 [SkillTool.ts] tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L857</a>-L862</p>
<p data-tool="mdnice编辑器">也就是说，tool_result 本身不承载 skill 的全部结果。<br />
真正有价值的内容在 <code style="color: #0e8aeb;">newMessages</code> 里，已经被送回主会话继续推理。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">2）fork skill 的 tool_result</span></h2>
<p data-tool="mdnice编辑器">直接带最终结果</p>
<p data-tool="mdnice编辑器">fork skill 返回的是：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;">content: <span style="color: #98c379;">`Skill "<span style="color: #e06c75;">${result.commandName}</span>" completed (forked execution).\n\nResult:\n<span style="color: #e06c75;">${result.result}</span>`</span>
</code></pre>
<p data-tool="mdnice编辑器">见 [SkillTool.ts] tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L848</a>-L855</p>
<p data-tool="mdnice编辑器">这是因为 fork skill 已经在独立上下文里把工作做完了，主线程要拿的是总结结果。</p>
<p data-tool="mdnice编辑器">所以在 Claude Code 里，skill 结果返回不是单一模式，而是：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">inline：返回「已加载 skill」，真正内容进主对话</section>
</li>
<li>
<section style="color: #010101;">fork：返回「子代理执行结果」</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这也是一种披露控制。<br />
不同执行语义，对结果暴露方式也不同。</p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">八、如何简要实现</span></h1>
<p data-tool="mdnice编辑器">一个新 Agent，如何简要实现 skills 的发现、召回、调用、结果返回？</p>
<p data-tool="mdnice编辑器">一个<strong style="color: #0e88eb;">够用、够短、能落地</strong>的最小设计，不追求和 Claude Code 一模一样，但核心思路一致。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">1）第一步：统一 skill 数据结构</span></h2>
<p data-tool="mdnice编辑器">最小结构建议这样：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">type</span> Skill = {
  name: <span style="color: #e6c07b;">string</span>
  description: <span style="color: #e6c07b;">string</span>
  whenToUse?: <span style="color: #e6c07b;">string</span>
  contentLoader: (args: <span style="color: #e6c07b;">string</span>, ctx: AgentContext) =&gt; <span style="color: #e6c07b;">Promise</span>&lt;<span style="color: #e6c07b;">string</span>&gt;
  allowedTools?: <span style="color: #e6c07b;">string</span>[]
  model?: <span style="color: #e6c07b;">string</span>
  effort?: <span style="color: #98c379;">'low'</span> | <span style="color: #98c379;">'medium'</span> | <span style="color: #98c379;">'high'</span>
  context?: <span style="color: #98c379;">'inline'</span> | <span style="color: #98c379;">'fork'</span>
  paths?: <span style="color: #e6c07b;">string</span>[]
}
</code></pre>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">contentLoader</code> 允许延迟展开</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">context</code> 决定 inline/fork</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">paths</code> 支持条件激活</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">allowedTools/model/effort</code> 支持 skill 级上下文修饰</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这和 Claude Code 的 <code style="color: #0e8aeb;">createSkillCommand()</code> 思路是一致的。[loadSkillsDir.ts] skills/loadSkillsDir.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L270</a>-L401</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">2）第二步：启动时只加载「索引」，不要加载正文</span></h2>
<p data-tool="mdnice编辑器">最简做法：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">扫描 skills 目录</section>
</li>
<li>
<section style="color: #010101;">解析 frontmatter</section>
</li>
<li>
<section style="color: #010101;">只把 <code style="color: #0e8aeb;">name / description / whenToUse / paths / context</code> 放进 registry</section>
</li>
<li>
<section style="color: #010101;">skill 正文不要此时进 prompt</section>
</li>
</ul>
<p data-tool="mdnice编辑器">示意：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">async</span> <span style="color: #c678dd;">function</span> <span style="color: #61aeee;">loadSkillIndex</span>(skillDirs: <span style="color: #e6c07b;">string</span>[]): <span style="color: #61aeee;">Promise</span>&lt;<span style="color: #61aeee;">Skill</span>[]&gt; {
<span style="color: #c678dd;">const</span> skills: Skill[] = []
<span style="color: #c678dd;">for</span> (<span style="color: #c678dd;">const</span> dir of skillDirs) {
    <span style="color: #c678dd;">for</span> (<span style="color: #c678dd;">const</span> skillFile of <span style="color: #c678dd;">await</span> listSkillFiles(dir)) {
      <span style="color: #c678dd;">const</span> raw = <span style="color: #c678dd;">await</span> readFile(skillFile, <span style="color: #98c379;">'utf8'</span>)
      <span style="color: #c678dd;">const</span> { frontmatter, content } = parseFrontmatter(raw)
      skills.push({
        name: basename(dirname(skillFile)),
        description: <span style="color: #e6c07b;">String</span>(frontmatter.description ?? <span style="color: #98c379;">''</span>),
        whenToUse: frontmatter.when_to_use ? <span style="color: #e6c07b;">String</span>(frontmatter.when_to_use) : <span style="color: #56b6c2;">undefined</span>,
        paths: <span style="color: #e6c07b;">Array</span>.isArray(frontmatter.paths) ? frontmatter.paths : <span style="color: #56b6c2;">undefined</span>,
        context: frontmatter.context === <span style="color: #98c379;">'fork'</span> ? <span style="color: #98c379;">'fork'</span> : <span style="color: #98c379;">'inline'</span>,
        contentLoader: <span style="color: #c678dd;">async</span> () =&gt; content,
      })
    }
  }
<span style="color: #c678dd;">return</span> skills
}
</code></pre>
<p data-tool="mdnice编辑器">这个阶段要学 Claude Code 的不是目录细节，而是<strong style="color: #0e88eb;">索引和正文分离</strong>。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">3）第三步：做一个「未发送 skill 集合」</span></h2>
<p data-tool="mdnice编辑器">这是渐进式披露的核心。</p>
<p data-tool="mdnice编辑器">维护一个 session 级状态：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">type</span> SkillDisclosureState = {
  sentSkillNames: Set&lt;<span style="color: #e6c07b;">string</span>&gt;
}
</code></pre>
<p data-tool="mdnice编辑器">每轮只发送新的：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">function</span> <span style="color: #61aeee;">getNewSkillListings</span>(skills: Skill[], sent: Set&lt;<span style="color: #e6c07b;">string</span>&gt;): <span style="color: #61aeee;">Skill</span>[] {
  <span style="color: #c678dd;">const</span> fresh = skills.filter(s =&gt; !sent.has(s.name))
  <span style="color: #c678dd;">for</span> (<span style="color: #c678dd;">const</span> s of fresh) sent.add(s.name)
  <span style="color: #c678dd;">return</span> fresh
}
</code></pre>
<p data-tool="mdnice编辑器">然后把它格式化成短列表，而不是全文：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">function</span> <span style="color: #61aeee;">formatSkillListing</span>(skills: Skill[]): <span style="color: #61aeee;">string</span> {
  <span style="color: #c678dd;">return</span> skills.map(s =&gt; <span style="color: #98c379;">`- <span style="color: #e06c75;">${s.name}</span>: <span style="color: #e06c75;">${s.description}</span>`</span>).join(<span style="color: #98c379;">'\n'</span>)
}
</code></pre>
<p data-tool="mdnice编辑器">这对应 Claude Code 的 <code style="color: #0e8aeb;">sentSkillNames + skill_listing attachment</code> 方案。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">4）第四步：把文件操作接成动态发现触发器</span></h2>
<p data-tool="mdnice编辑器">如果你也想要「技能跟着目录出现」，最小版本就是：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">用户或模型读/写/改文件时</section>
</li>
<li>
<section style="color: #010101;">从文件父目录往上走到 cwd</section>
</li>
<li>
<section style="color: #010101;">看有没有 <code style="color: #0e8aeb;">.agent/skills</code> 或 <code style="color: #0e8aeb;">.claude/skills</code></section>
</li>
<li>
<section style="color: #010101;">找到新目录就加载 skill index</section>
</li>
</ul>
<p data-tool="mdnice编辑器">示意：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">async</span> <span style="color: #c678dd;">function</span> <span style="color: #61aeee;">discoverSkillDirsForFile</span>(filePath: <span style="color: #e6c07b;">string</span>, cwd: <span style="color: #e6c07b;">string</span>): <span style="color: #61aeee;">Promise</span>&lt;<span style="color: #61aeee;">string</span>[]&gt; {
<span style="color: #c678dd;">const</span> dirs: <span style="color: #e6c07b;">string</span>[] = []
<span style="color: #c678dd;">let</span> current = dirname(filePath)
<span style="color: #c678dd;">while</span> (current.startsWith(cwd + sep)) {
    <span style="color: #c678dd;">const</span> candidate = join(current, <span style="color: #98c379;">'.agent'</span>, <span style="color: #98c379;">'skills'</span>)
    <span style="color: #c678dd;">if</span> (<span style="color: #c678dd;">await</span> exists(candidate)) dirs.push(candidate)
    <span style="color: #c678dd;">const</span> parent = dirname(current)
    <span style="color: #c678dd;">if</span> (parent === current) <span style="color: #c678dd;">break</span>
    current = parent
  }
<span style="color: #c678dd;">return</span> dirs
}
</code></pre>
<p data-tool="mdnice编辑器">Claude Code 的现成参考是 [discoverSkillDirsForPaths] skills/loadSkillsDir.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L861</a>-L915。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">5）第五步：做条件激活，而不是启动时全暴露</span></h2>
<p data-tool="mdnice编辑器">如果 skill 定义里有 <code style="color: #0e8aeb;">paths</code>，就不要一开始暴露。<br />
等碰到匹配文件时再激活：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">function</span> <span style="color: #61aeee;">activatePathScopedSkills</span>(
  pending: Skill[],
  touchedFiles: <span style="color: #e6c07b;">string</span>[],
): { active: Skill[]; remaining: Skill[] } {
<span style="color: #c678dd;">const</span> active: Skill[] = []
<span style="color: #c678dd;">const</span> remaining: Skill[] = []
<span style="color: #c678dd;">for</span> (<span style="color: #c678dd;">const</span> skill of pending) {
    <span style="color: #c678dd;">if</span> (!skill.paths || skill.paths.length === <span style="color: #d19a66;">0</span>) {
      active.push(skill)
      <span style="color: #c678dd;">continue</span>
    }
    <span style="color: #c678dd;">const</span> matched = touchedFiles.some(file =&gt; matchAny(file, skill.paths!))
    <span style="color: #c678dd;">if</span> (matched) active.push(skill)
    <span style="color: #c678dd;">else</span> remaining.push(skill)
  }
<span style="color: #c678dd;">return</span> { active, remaining }
}
</code></pre>
<p data-tool="mdnice编辑器">这就是 Claude Code <code style="color: #0e8aeb;">conditionalSkills -&gt; activateConditionalSkillsForPaths()</code> 的最小复刻。</p>
<hr />
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">6）第六步：调用 skill 时才真正加载正文</span></h2>
<p data-tool="mdnice编辑器">不要提前把 skill 正文塞到 prompt。<br />
调用时再做：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">async</span> <span style="color: #c678dd;">function</span> <span style="color: #61aeee;">invokeSkill</span>(
  skill: Skill,
  args: <span style="color: #e6c07b;">string</span>,
  ctx: AgentContext,
): <span style="color: #61aeee;">Promise</span>&lt;<span style="color: #61aeee;">SkillInvocationResult</span>&gt; {
<span style="color: #c678dd;">const</span> prompt = <span style="color: #c678dd;">await</span> skill.contentLoader(args, ctx)

<span style="color: #c678dd;">if</span> (skill.context === <span style="color: #98c379;">'fork'</span>) {
    <span style="color: #c678dd;">const</span> result = <span style="color: #c678dd;">await</span> runSubAgent({
      prompt,
      allowedTools: skill.allowedTools,
      model: skill.model,
      effort: skill.effort,
    })
    <span style="color: #c678dd;">return</span> { mode: <span style="color: #98c379;">'fork'</span>, result }
  }

<span style="color: #c678dd;">return</span> {
    mode: <span style="color: #98c379;">'inline'</span>,
    newMessages: [
      { role: <span style="color: #98c379;">'user'</span>, content: <span style="color: #98c379;">`[SKILL:<span style="color: #e06c75;">${skill.name}</span>]`</span> },
      { role: <span style="color: #98c379;">'user'</span>, content: prompt, meta: <span style="color: #56b6c2;">true</span> },
    ],
    allowedTools: skill.allowedTools,
    model: skill.model,
    effort: skill.effort,
  }
}
</code></pre>
<p data-tool="mdnice编辑器">这就是 Claude Code <code style="color: #0e8aeb;">SkillTool.call()</code> 的最小骨架。[SkillTool.ts] tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L581</a>-L863</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">7）第七步：结果返回必须分 inline 和 fork</span></h2>
<p data-tool="mdnice编辑器">直接照 Claude Code 的语义分两种：</p>
<h3 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;">inline</span></h3>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">返回一个轻 tool_result：<code style="color: #0e8aeb;">Launching skill: xxx</code></section>
</li>
<li>
<section style="color: #010101;">真正内容通过 <code style="color: #0e8aeb;">newMessages</code> 回到主对话继续推理</section>
</li>
</ul>
<h3 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;">fork</span></h3>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">返回最终结果摘要</section>
</li>
<li>
<section style="color: #010101;">子代理对话不污染主上下文</section>
</li>
</ul>
<p data-tool="mdnice编辑器">示意：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">type</span> SkillInvocationResult =
  | {
      mode: <span style="color: #98c379;">'inline'</span>
      newMessages: Message[]
      allowedTools?: <span style="color: #e6c07b;">string</span>[]
      model?: <span style="color: #e6c07b;">string</span>
      effort?: <span style="color: #e6c07b;">string</span>
    }
  | {
      mode: <span style="color: #98c379;">'fork'</span>
      result: <span style="color: #e6c07b;">string</span>
    }
</code></pre>
<p data-tool="mdnice编辑器">这一步是很多新 Agent 最容易偷懒的地方。<br />
要么所有 skill 都 inline，主上下文爆炸；要么所有 skill 都 fork，失去细粒度引导。</p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">九、小结</span></h1>
<p data-tool="mdnice编辑器">「skills 的渐进式披露」其实就是 Claude Code 在控制 prompt 成本和能力密度时最典型的设计之一。它真正解决的问题不是「怎么找到一个 skill」，而是「怎么在不把上下文撑爆的前提下，让模型知道自己有技能可用」。</p>
<p data-tool="mdnice编辑器">它背后的思路：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">先给索引</section>
</li>
<li>
<section style="color: #010101;">再给局部集合</section>
</li>
<li>
<section style="color: #010101;">再给真实正文</section>
</li>
<li>
<section style="color: #010101;">最后才给执行结果</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这是一个很像搜索引擎的设计：摘要、点击、展开、消费，而不是把整本书扔给你。</p>
<p data-tool="mdnice编辑器">以上。</p>
<p>&nbsp;</p>
</section>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/04/claude-code-ai-skills-source/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>OpenClaw 的 Skills 的实现和 Claude Code 不一样</title>
		<link>https://www.phppan.com/2026/03/openclaw-skilss-claude-code/</link>
		<comments>https://www.phppan.com/2026/03/openclaw-skilss-claude-code/#comments</comments>
		<pubDate>Sat, 21 Mar 2026 01:01:17 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Claude Code]]></category>
		<category><![CDATA[OpenClaw]]></category>
		<category><![CDATA[skills]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2479</guid>
		<description><![CDATA[OpenClaw 的 Skills，本质上不是一个「可调用工具」，它更像一套经过约束的运行手册：启动时把技能目 [&#8230;]]]></description>
				<content:encoded><![CDATA[<section id="nice" data-tool="mdnice编辑器" data-website="https://www.mdnice.com">
<p data-tool="mdnice编辑器">OpenClaw 的 Skills，本质上不是一个「可调用工具」，它更像一套经过约束的运行手册：启动时把技能目录扫描出来，压成一份 <code>&lt;available_skills&gt;</code> 清单塞进 system prompt，模型自己判断要不要选一个 skill，然后再通过 Read 工具去读这个 skill 的 <code>SKILL.md</code>。读完以后，没有任何独立执行器接管，还是在当前这条 session 的 tool-loop 里继续跑。</p>
<p data-tool="mdnice编辑器">Claude Code 走的是另一条路。它把 skill 做成了 tool，工具里负责校验、加载、执行，甚至可以放进一个新上下文里跑完，再把结果回传主对话。</p>
<p data-tool="mdnice编辑器">这两个实现方向，表面上都叫 skills，工程含义完全不一样。</p>
<p data-tool="mdnice编辑器">如果你是做 Agent 平台、企业内 Copilot、代码助手、任务执行器，这个差异不只是架构图上的审美问题。它会直接影响：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>prompt 预算怎么花</section>
</li>
<li>
<section>权限边界放在哪里</section>
</li>
<li>
<section>skill 的治理成本有多高</section>
</li>
<li>
<section>运行链路是可控还是失控</section>
</li>
<li>
<section>你后面想不想做隔离执行、审计、回放、灰度发布</section>
</li>
</ul>
<h1 data-tool="mdnice编辑器"><span class="content">1. OpenClaw 的 Skills，到底是怎么被「召回」的</span></h1>
<p data-tool="mdnice编辑器">很多人一看到 skills，第一反应是：是不是和 memory 一样，先走 embedding 检索，再把相关技能召回进上下文。</p>
<p data-tool="mdnice编辑器">不是。</p>
<p data-tool="mdnice编辑器">OpenClaw 的 skills 召回机制非常直接，甚至可以说有点「朴素」：<strong>扫描目录，生成目录清单，注入提示词，让模型自己选</strong>。整条链路分三段：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>发现</section>
</li>
<li>
<section>注入</section>
</li>
<li>
<section>读取</section>
</li>
</ol>
<p data-tool="mdnice编辑器">OpenClaw 反过来做了一件更工程化的事：先把 skill 列表显式给模型，再用 system prompt 约束它怎么选。</p>
<p data-tool="mdnice编辑器">这个机制的起点在 <code>src/agents/skills/workspace.ts</code> 的 <code>loadSkillEntries()</code>。</p>
<h2 data-tool="mdnice编辑器"><span class="content">1.1 技能发现：先扫目录，再谈调用</span></h2>
<p data-tool="mdnice编辑器">OpenClaw 会从多个 root 目录扫描 <code>SKILL.md</code>，然后合并成最终技能集。这里是有明确的覆盖优先级的：</p>
<p data-tool="mdnice编辑器"><code>extra &lt; bundled &lt; managed &lt; agents-personal &lt; agents-project &lt; workspace</code></p>
<p data-tool="mdnice编辑器">它的逻辑是：越靠近当前工作空间、越贴近用户项目的 skill，优先级越高。平台预置的 bundled skill 可以兜底，但项目级 skill 要能覆盖它。否则你做企业落地时会很难受，团队定制流程永远被平台内置逻辑压着打。</p>
<p data-tool="mdnice编辑器">从目录结构看，OpenClaw 支持两种 skill 形态：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>root 本身就是一个 skill，目录下直接有 <code>SKILL.md</code></section>
</li>
<li>
<section>root 下的子目录分别是 skill，每个子目录里有自己的 <code>SKILL.md</code></section>
</li>
</ul>
<p data-tool="mdnice编辑器">这样，团队在实际维护 skill 时，有两种典型组织方式：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>单一 skill 仓库，根目录就是技能内容</section>
</li>
<li>
<section>skills 集合仓库，每个子目录一个技能</section>
</li>
</ul>
<p data-tool="mdnice编辑器">都支持，落地阻力会小很多。</p>
<p data-tool="mdnice编辑器">它对 <code>SKILL.md</code> 做了体积限制，默认超过 256KB 就直接跳过。</p>
<p data-tool="mdnice编辑器"><code>SKILL.md</code> 本来就应该是高密度操作指南，不应该演变成一个什么都往里塞的大文档。你一旦允许 skill 文件无限变大，后面一定会有人把 SOP、FAQ、设计文档、事故复盘全扔进去，最后技能选择和加载成本一起爆炸。OpenClaw 在扫描阶段直接卡体积，其实是在替平台维护纪律。</p>
<h2 data-tool="mdnice编辑器"><span class="content">1.2 frontmatter 解析</span></h2>
<p data-tool="mdnice编辑器">给 skill 增加最小限度的结构化元信息</p>
<p data-tool="mdnice编辑器">扫描到 <code>SKILL.md</code> 后，OpenClaw 会读原文并解析 frontmatter。坏掉或者缺失的 frontmatter 会被忽略。</p>
<p data-tool="mdnice编辑器">这个决策也很对。</p>
<p data-tool="mdnice编辑器">OpenClaw 这里更像是「有就用，没有拉倒」。它承认 markdown 本体才是 skill 的核心，frontmatter 只是辅助控制面。这个姿态很适合 skills 这种增长很快、来源很多的资产类型。</p>
<p data-tool="mdnice编辑器">但这里也埋了一个工程 trade-off：frontmatter 被弱约束，意味着后续治理和平台能力扩展会受限。你今天只做 discovery 和 basic gating，这么玩没问题；你明天如果要做 skill 分类、依赖分析、版本兼容、批量审计，元信息松散会让成本陡增。</p>
<p data-tool="mdnice编辑器">OpenClaw 当前站在了「先把系统跑起来」的一边，没有走「先把规范做重」的路子。</p>
<p data-tool="mdnice编辑器">这意味着它更适合中小规模 skill 生态，或者说更适合个人助手这类工具，而不是一个强治理的企业级 skill marketplace。</p>
<h1 data-tool="mdnice编辑器"><span class="content">2. 不是所有 skill 都会进 <code>&lt;available_skills&gt;</code></span></h1>
<p data-tool="mdnice编辑器">扫描出来只是候选集，不等于模型能看到。</p>
<p data-tool="mdnice编辑器">OpenClaw 在注入 system prompt 之前会做一轮 gating。这个步骤比很多人想象得重要，因为它决定了模型到底暴露给了哪些能力。</p>
<p data-tool="mdnice编辑器">过滤逻辑里有几类条件。</p>
<h2 data-tool="mdnice编辑器"><span class="content">2.1 配置开关</span></h2>
<p data-tool="mdnice编辑器">最基础的 enable/disable</p>
<p data-tool="mdnice编辑器">如果某个 skill 在配置里被标成 <code>skills.entries.&lt;skillKey&gt;.enabled === false</code>，它就会被剔除。</p>
<p data-tool="mdnice编辑器">这是最基础的 kill switch。工程上没什么可争议的，必须有。不然你没法快速止血。某个 skill 写坏了、依赖环境挂了、被发现会引导模型做危险操作，没有一键下线能力，平台就不配上线。</p>
<h2 data-tool="mdnice编辑器"><span class="content">2.2 bundled allowlist</span></h2>
<p data-tool="mdnice编辑器">对内置技能单独管控</p>
<p data-tool="mdnice编辑器"><code>skills.allowBundled</code> 只约束 bundled 来源。</p>
<p data-tool="mdnice编辑器">这个细节很有意思。它说明 OpenClaw 把 bundled skills 当成一类特殊资产处理：平台自带，但不默认无条件信任。</p>
<p data-tool="mdnice编辑器">为什么这事重要？因为内置 skill 经常是平台演进中最容易「偷偷变多」的那部分。你今天打包 5 个，明天为了演示方便塞到 20 个，后天 prompt 里一大坨 descriptions，模型选 skill 的噪声越来越大。allowlist 的存在，本质上是在给平台预装能力上保险栓。</p>
<h2 data-tool="mdnice编辑器"><span class="content">2.3 eligibility 判断</span></h2>
<p data-tool="mdnice编辑器">按运行环境判断 skill 是否可用</p>
<p data-tool="mdnice编辑器">OpenClaw 会根据 <code>metadata.requires</code> 去判断 skill 能不能用，条件包括：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>二进制依赖是否存在</section>
</li>
<li>
<section>环境变量是否满足</section>
</li>
<li>
<section>配置是否具备</section>
</li>
<li>
<section>操作系统是否匹配</section>
</li>
<li>
<section>remote 平台是否匹配</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这个设计非常工程化。因为 skill 如果涉及真实操作，它一定和环境耦合。比如某个 skill 依赖特定 cli，或者要求某个 API token，或者只能在 Linux 跑。你如果不在注入前做 eligibility，而是让模型先看到 skill，再在执行时失败，用户体验会很差，模型行为也会变形：它会看到一个貌似可用的方案，实际一调就炸。</p>
<p data-tool="mdnice编辑器">OpenClaw 选择「<strong>先过滤，再暴露</strong>」，这是我非常认同的策略。因为对模型来说，看得见就等于潜在可用。你让它看见无效 skill，本质上是在制造认知噪声。</p>
<h2 data-tool="mdnice编辑器"><span class="content">2.4 <code>disable-model-invocation</code></span></h2>
<p data-tool="mdnice编辑器">系统内部保留，模型不可见</p>
<p data-tool="mdnice编辑器">如果某个 skill 标记了 <code>disable-model-invocation</code>，它不会进入 prompt 的 <code>&lt;available_skills&gt;</code>，但仍然存在于系统内部，可用于管理或校验。</p>
<p data-tool="mdnice编辑器">因为技能资产不只有「给模型用」这一种角色。你可能有些 skill 需要：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>用于测试</section>
</li>
<li>
<section>用于审核</section>
</li>
<li>
<section>用于运维校验</section>
</li>
<li>
<section>用于内部 pipeline</section>
</li>
<li>
<section>用于未来发布但暂不开放</section>
</li>
</ul>
<p data-tool="mdnice编辑器">OpenClaw 没把「存在」和「可被模型调用」绑死，这样 skill registry 才像个平台能力，而不是一个 prompt 拼装器。</p>
<h1 data-tool="mdnice编辑器"><span class="content">3. <code>&lt;available_skills&gt;</code> 才是 OpenClaw 的真正召回入口</span></h1>
<p data-tool="mdnice编辑器">很多人说 skills 被召回，其实这句话在 OpenClaw 语境里容易让人误解。</p>
<p data-tool="mdnice编辑器">真正被放进上下文里的第一层，不是 <code>SKILL.md</code> 内容，而是 <code>&lt;available_skills&gt;</code> 这个目录清单。它由 <code>buildWorkspaceSkillsPrompt()</code> 生成，注入到 system prompt。</p>
<p data-tool="mdnice编辑器">也就是说，模型最先看到的是技能目录，不是技能正文。</p>
<p data-tool="mdnice编辑器">这个做法也算是渐进式披露的一种实现。先给模型足够决定是否要读 skill 的最小信息，避免一开始就把所有技能全文灌进去。</p>
<h2 data-tool="mdnice编辑器"><span class="content">3.1 system prompt 里的强规则，决定了技能选择流程</span></h2>
<p data-tool="mdnice编辑器">OpenClaw 在 system prompt 里有一段明确的 Skills mandatory 规则，大意是：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>先扫描 <code>&lt;available_skills&gt;</code> 里的 <code>&lt;description&gt;</code></section>
</li>
<li>
<section>只有明确匹配一个 skill 时，才去读对应的 <code>SKILL.md</code></section>
</li>
<li>
<section>最多只读一个 skill</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这三条里，最重要的是后两条。</p>
<h3 data-tool="mdnice编辑器"><span class="content">3.1.1 只有明确匹配一个才读</span></h3>
<p data-tool="mdnice编辑器">这是在压制模型的泛化冲动。模型很喜欢「我觉得这个也相关，那个也相关」，然后多读几个，最后把自己卷进上下文泥潭。OpenClaw 用提示词硬性要求「明确匹配一个」才允许进入下一步，这本质上是在给模型加稀疏化约束。</p>
<p data-tool="mdnice编辑器">效果未必 100% 可控，但方向是对的。</p>
<h3 data-tool="mdnice编辑器"><span class="content">3.1.2 最多只读一个 skill</span></h3>
<p data-tool="mdnice编辑器">如果你做过基于 prompt 的 tool orchestration，就会知道一个常见死法：模型在一堆说明里来回跳，读完 A 觉得 B 也有用，再读 B，顺手 C 也看看，最后 token 花了不少，任务还没干。</p>
<p data-tool="mdnice编辑器">OpenClaw 这里直接规定 upfront 只能读一个 skill。我个人判断，这不是因为理论上最优，而是因为工程上最稳。</p>
<p data-tool="mdnice编辑器">它牺牲了一部分组合式技能能力，换来了几个非常实际的好处：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>token 消耗更可控</section>
</li>
<li>
<section>模型决策路径更短</section>
</li>
<li>
<section>skill 选择失败更容易定位</section>
</li>
<li>
<section>调试和回放更容易做</section>
</li>
</ul>
<p data-tool="mdnice编辑器">代价也很明确：如果任务天然需要多个 skill 协同，OpenClaw 当前机制会显得笨。模型要么自己在单个 skill 指导下绕着做，要么根本选不准。</p>
<p data-tool="mdnice编辑器">这也是它和 Claude Code 一个很大的分歧。Claude Code 把 skill 视作 tool，更容易天然支持复杂编排；OpenClaw 把 skill 视作可读说明，天然倾向于单次聚焦。</p>
<h2 data-tool="mdnice编辑器"><span class="content">3.2 技能过多时的降级策略：compact 和 truncated</span></h2>
<p data-tool="mdnice编辑器"><code>&lt;available_skills&gt;</code> 不是无上限注入的。技能太多时，OpenClaw 会降级成 compact 格式，甚至截断，并提示 <code>skills truncated</code>。</p>
<p data-tool="mdnice编辑器">这个点看起来像 prompt 小技巧，其实是很重要的预算治理。</p>
<p data-tool="mdnice编辑器">因为技能系统一旦跑起来，数量几乎只会越来越多。最初十几个 skill，description 全量塞进去还行；到几十上百个 skill，再这么搞，system prompt 很快会变成垃圾堆。OpenClaw 至少意识到了这个问题，所以做了两级退化：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>compact：保留更少信息</section>
</li>
<li>
<section>truncated：截断并明确提示</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这说明它把 prompt 当成有限资源，而不是无限容器。</p>
<p data-tool="mdnice编辑器">但实话实说，这个策略只是在「延缓爆炸」，不是根治。skill 数量继续增长以后，靠 compact/truncate 顶不住。因为问题不只是 token 多了，而是模型在目录里做决策的辨识难度会越来越高。description 再压缩，skill 之间的区分度也会变差。</p>
<p data-tool="mdnice编辑器">所以我对这块的判断是：<strong>OpenClaw 的 catalog 注入机制适合中等规模 skill 集，不适合无限扩张。</strong><br />
如果你团队未来真要做上百个 skill 的企业级平台，迟早要引入更分层的 catalog、分类路由、或者显式 selector，而不是靠一坨 <code>&lt;available_skills&gt;</code> 让模型裸选。</p>
<h1 data-tool="mdnice编辑器"><span class="content">4. 真正的 <code>SKILL.md</code> 是怎么进入上下文的</span></h1>
<p data-tool="mdnice编辑器">OpenClaw 的第二层加载是按需读取。模型先看到目录清单，选中 skill 以后，才通过 Read 工具去读对应路径的 <code>SKILL.md</code>。</p>
<h2 data-tool="mdnice编辑器"><span class="content">4.1 技能正文不是 system prompt 的一部分</span></h2>
<p data-tool="mdnice编辑器">system prompt 里只有规则和技能目录。真正的 <code>SKILL.md</code> 内容，是在模型发起一次 read tool call 后，作为 toolResult 追加进当前消息流。</p>
<p data-tool="mdnice编辑器">也就是说，skill 正文进入上下文的方式，和你用工具读一个普通文件没有本质区别。差别只是 system prompt 先规定了什么时候允许这样读。</p>
<p data-tool="mdnice编辑器">这个设计的好处非常明确：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>大幅降低初始上下文体积</section>
</li>
<li>
<section>让技能正文成为按需成本，而不是固定成本</section>
</li>
<li>
<section>skill 更新后无需改 system prompt 模板，只影响运行时读到的内容</section>
</li>
<li>
<section>便于把 skill 当文件资产管理，而不是 prompt 模板片段</section>
</li>
</ul>
<p data-tool="mdnice编辑器">坏处也很明确：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>skill 的执行效果更依赖模型有没有正确触发 read</section>
</li>
<li>
<section>skill 的权威性被弱化成一条 toolResult，而不是顶层指令</section>
</li>
<li>
<section>如果 toolResult 很长，后续上下文里它和普通读文件结果没什么层级差异</section>
</li>
</ul>
<p data-tool="mdnice编辑器">从实现角度看，OpenClaw 这里是很克制的。它没有发明一个 Skill Runtime，没有做一套专门的 skill 调度协议，就是借已有的 read 工具完成正文加载。</p>
<p data-tool="mdnice编辑器">这让系统保持简单，但也让 skill 更像「读到的一份说明书」，而不是「被激活的执行单元」。</p>
<h1 data-tool="mdnice编辑器"><span class="content">5. OpenClaw 的安全边界</span></h1>
<p data-tool="mdnice编辑器">既然 skill 是文件，模型要去读 <code>SKILL.md</code>，安全问题马上就来了：路径能不能逃逸？symbolic link 能不能把 skill 指到 root 外？模型能不能顺着 location 读到不该读的内容？</p>
<p data-tool="mdnice编辑器">OpenClaw 这里做了两层保护。</p>
<h2 data-tool="mdnice编辑器"><span class="content">5.1 第一层：扫描阶段的 containment 检查</span></h2>
<p data-tool="mdnice编辑器">在技能发现阶段，它会对 realpath 做 containment 检查，防止 symlink 把 skill 指到 root 外面。</p>
<p data-tool="mdnice编辑器">这是典型的「尽早失败」策略。我一直觉得文件系统相关的安全问题，能在发现阶段拦的，不要拖到执行阶段。因为一旦把异常路径放进 skill registry，后面每个环节都得假设它可能有问题，整个系统会变得很难推理。</p>
<p data-tool="mdnice编辑器">扫描时就把越界项挡掉，registry 内部的数据至少是干净的。</p>
<h2 data-tool="mdnice编辑器"><span class="content">5.2 第二层：Read 工具的 workspace root guard</span></h2>
<p data-tool="mdnice编辑器">模型真正发起读取时，Read 工具还会做 sandbox root 校验，禁止 <code>..</code> 或绝对路径逃逸 workspace root。</p>
<p data-tool="mdnice编辑器">也就是说，即使 discovery 阶段没出问题，真正读取文件时还有一层运行时防线。</p>
<p data-tool="mdnice编辑器">文件边界这事，单点防护永远不够。扫描阶段挡的是「注册进来的 skill 本身」，读取阶段挡的是「模型实际提出的路径参数」。二者保护的对象不一样。</p>
<p data-tool="mdnice编辑器">工程上要是只做第一层，你会被运行时路径拼接坑；只做第二层，你会把很多脏数据带进 registry，影响可观测性和调试。</p>
<p data-tool="mdnice编辑器">OpenClaw 在这块虽然不算复杂，但做法是标准的。</p>
<h1 data-tool="mdnice编辑器"><span class="content">6. Skills 的渐进式披露</span></h1>
<p data-tool="mdnice编辑器">OpenClaw 的 skills 机制，如果只看「枚举目录 -&gt; 注入 prompt -&gt; read 文件」，会有人觉得太朴素。真正值得注意的是，它和 memory 一样，贯彻了一套很明确的渐进式披露思路。</p>
<p data-tool="mdnice编辑器">这是上下文预算治理的逻辑。</p>
<p data-tool="mdnice编辑器">原则就两条：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>先给最小可用信息</section>
</li>
<li>
<section>确认需要后再扩大读取范围</section>
</li>
</ol>
<p data-tool="mdnice编辑器">在 OpenClaw 里，memory 和 skills 都是这么干的，只是路径不同。</p>
<h2 data-tool="mdnice编辑器"><span class="content">6.1 Memory 是先搜 snippet，再精读指定行段</span></h2>
<p data-tool="mdnice编辑器"><code>memory_search</code> 先返回短片段，带 path 和 line range，不是全文注入。底层还有 <code>SNIPPET_MAX_CHARS = 700</code> 的硬上限。如果后端预算紧，还会继续裁结果。</p>
<p data-tool="mdnice编辑器">之后再通过 <code>memory_get</code> 按 <code>path + from/lines</code> 去拉具体行段。</p>
<p data-tool="mdnice编辑器">这个流程是非常标准的 progressive disclosure：先定位，再精读。</p>
<h2 data-tool="mdnice编辑器"><span class="content">6.2 Skills 是先给目录，再只读一个 <code>SKILL.md</code></span></h2>
<p data-tool="mdnice编辑器">skills 这边没有向量召回，而是目录清单。模型先看 <code>&lt;available_skills&gt;</code>，只在明确匹配一个时才读正文，并且 upfront 最多一个。</p>
<p data-tool="mdnice编辑器">两条链路表面不同，本质一样：都在防止大块文本无脑灌进上下文。</p>
<p data-tool="mdnice编辑器">我为什么说这点值钱？因为很多团队做 Agent 系统时，最大的问题不是模型不会做事，而是上下文管理太粗糙。什么都想喂进去，最后 token 烧得飞快，模型还因为噪声太高做不准。</p>
<p data-tool="mdnice编辑器">OpenClaw 至少在架构层面承认了一件事实：<strong>上下文是预算，不是仓库。</strong></p>
<p data-tool="mdnice编辑器">这件事说起来简单，真正落实到工具语义、提示词规则、底层裁剪，很多系统做不到。</p>
<h1 data-tool="mdnice编辑器"><span class="content">7. OpenClaw 没有「独立 Skill 执行器」</span></h1>
<p data-tool="mdnice编辑器">在 OpenClaw 里，skill 读完之后，并没有一个单独的执行环境接管它。没有所谓「Skill Runtime」去解释 <code>SKILL.md</code>，也没有「新上下文执行 skill」这回事。它还是在同一个 <code>activeSession.prompt(...)</code> 的 tool-loop 里继续跑。</p>
<p data-tool="mdnice编辑器">链路大概是这样：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>system prompt 里给出 <code>&lt;available_skills&gt;</code> 和选择规则</section>
</li>
<li>
<section>模型决定某个 skill 匹配当前请求</section>
</li>
<li>
<section>模型调用 read 工具读取对应 <code>SKILL.md</code></section>
</li>
<li>
<section>toolResult 里带回 <code>SKILL.md</code> 文本</section>
</li>
<li>
<section>这个 toolResult 被追加到当前 session 的 messages</section>
</li>
<li>
<section>模型再次被调用，看到刚才读到的 skill 内容</section>
</li>
<li>
<section>模型按 skill 指导，继续发起后续 toolCall，比如 <code>exec</code>、<code>write</code>、<code>edit</code></section>
</li>
<li>
<section>直到输出最终回答</section>
</li>
</ol>
<p data-tool="mdnice编辑器">关键点：<strong>skill 内容只是同一消息流里多了一条 toolResult</strong>。</p>
<p data-tool="mdnice编辑器">这意味着什么？</p>
<p data-tool="mdnice编辑器">意味着 OpenClaw 的 skill 执行，本质上是「模型读了一份流程说明，然后继续在原对话里行动」。它没有新的边界，没有新的记忆隔离，没有新的权限域。真正的隔离，来自工具列表裁剪和文件读写沙箱，不来自上下文切换。</p>
<p data-tool="mdnice编辑器">这和 Claude Code 很不一样。</p>
<h1 data-tool="mdnice编辑器"><span class="content">8. Claude Code 的 skills，更像「可调用子程序」</span></h1>
<p data-tool="mdnice编辑器">Claude Code 的 skill 流程有几个特征：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>skill 是 tools 列表里的一个明确工具</section>
</li>
<li>
<section>模型会调用 Skill tool，而不是自己去 read 某个 markdown</section>
</li>
<li>
<section>skill prompt 的加载是 tool 内部动作</section>
</li>
<li>
<section>权限校验也主要发生在 tool 内</section>
</li>
<li>
<section>skill 可以在新上下文执行，执行完再把结果带回主对话</section>
</li>
</ul>
<p data-tool="mdnice编辑器">从工程抽象上看，Claude Code 的 skill 更像一个「子程序入口」。它有名字、有调用接口、有内部装载逻辑，甚至有上下文隔离能力。</p>
<p data-tool="mdnice编辑器">OpenClaw 没有走这条路。它把 skill 设计成「模型可读的文件」，由模型自己决定是否展开，并在原上下文里继续操作。</p>
<p data-tool="mdnice编辑器">这两个方向，没有谁天然高级，但适用面不同</p>
<p data-tool="mdnice编辑器">如果你要的是：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>更强的执行隔离</section>
</li>
<li>
<section>更容易做权限封装</section>
</li>
<li>
<section>更容易做结果回传和子任务边界</section>
</li>
<li>
<section>更适合复杂、多步骤、可复用的任务单元</section>
</li>
</ul>
<p data-tool="mdnice编辑器">Claude Code 那种 tool 化 skill 更合适。</p>
<p data-tool="mdnice编辑器">如果你要的是：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>实现简单</section>
</li>
<li>
<section>skill 作者门槛低</section>
</li>
<li>
<section>把 skill 当 markdown 资产管理</section>
</li>
<li>
<section>快速把流程知识接进现有 ReAct tool-loop</section>
</li>
</ul>
<p data-tool="mdnice编辑器">OpenClaw 这种文件化 skill 更实用。</p>
<p data-tool="mdnice编辑器">工程上不看理念，看代价结构。</p>
<h1 data-tool="mdnice编辑器"><span class="content">9. OpenClaw 为什么会做成这样</span></h1>
<p data-tool="mdnice编辑器">我猜它的设计动机是：<strong>尽量复用现有 agent runtime，而不是为 skills 单独发明一层执行框架。</strong></p>
<p data-tool="mdnice编辑器">你看它的做法就知道了：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>discovery 复用文件系统扫描</section>
</li>
<li>
<section>selection 复用 system prompt</section>
</li>
<li>
<section>loading 复用 read 工具</section>
</li>
<li>
<section>execution 复用原有 tool-loop</section>
</li>
<li>
<section>safety 复用 sandbox path guard 和 tool policy</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这是一套很节制的架构思路。好处是：</p>
<p data-tool="mdnice编辑器"><strong>实现成本低</strong> ：不用造新协议，不用加新消息类型，不用多一层 runtime。对一个正在演进中的 agent 系统来说，这非常现实。 <strong>Skill 作者认知负担小</strong>：本质上写一个 <code>SKILL.md</code> 就行。对于组织内部推广，这是巨大优势。因为真正阻碍 skill 规模化的，从来不是模型能力，而是作者生态能不能起来。 <strong>运行链路可观测性还不错</strong>：所有事情都发生在同一 session 里。read 了什么、接着调了什么工具、toolResult 长什么样，回放起来相对直观。</p>
<p data-tool="mdnice编辑器">但它的代价也有。</p>
<p data-tool="mdnice编辑器"><strong>代价 1：skill 缺少强执行边界</strong></p>
<p data-tool="mdnice编辑器">skill 本身不是 tool，没有独立权限面。你没法像封装一个函数那样，给 skill 绑定专属 schema、专属校验、专属 side effect 边界。最终还是模型在拿到 skill 文本后，自由地调用后续工具。</p>
<p data-tool="mdnice编辑器">这就意味着 skill 的约束力主要来自提示词，不来自执行器。</p>
<p data-tool="mdnice编辑器"><strong>代价 2：组合式编排能力弱</strong></p>
<p data-tool="mdnice编辑器">system prompt 里要求 upfront 最多读一个 skill，这对预算控制很好，对复杂任务不友好。多 skill 协同在这个体系里不是一等公民。</p>
<p data-tool="mdnice编辑器"><strong>代价 3：skill 的结果不可天然封装</strong></p>
<p data-tool="mdnice编辑器">Claude Code 那种「在新上下文执行 skill，再返回结果」，天然有个输入输出边界。OpenClaw 这里没有。skill 一旦展开，后续动作直接混进主会话消息流。你很难把它当作一个独立执行单元来治理。</p>
<p data-tool="mdnice编辑器"><strong>代价 4：对模型的选择质量要求高</strong></p>
<p data-tool="mdnice编辑器">因为没有独立 selector，也没有 tool-level dispatcher，skill 能不能选对，主要靠 <code>&lt;available_skills&gt;</code> 的 descriptions 和 system prompt 规则。这对 skill 描述质量要求很高。写得不清楚，模型就选歪。数量一多，问题更明显。</p>
<h1 data-tool="mdnice编辑器"><span class="content">10. 权限控制</span></h1>
<p data-tool="mdnice编辑器">OpenClaw 的思路是「先裁剪能力，再让模型行动」</p>
<p data-tool="mdnice编辑器">这一点我很喜欢，因为它比「调用时再临时拦截」更稳。</p>
<p data-tool="mdnice编辑器">OpenClaw 的权限控制，不是把所有工具都亮给模型，然后在调用某个危险工具时说不行。它更像是：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>先根据 policy pipeline 把不允许的工具从列表里移掉</section>
</li>
<li>
<section>再把剩下的工具暴露给模型</section>
</li>
<li>
<section>模型根本看不到被禁的能力</section>
</li>
</ol>
<p data-tool="mdnice编辑器">skills 这边也一样：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>不合格的 skill 不进入 <code>&lt;available_skills&gt;</code></section>
</li>
<li>
<section><code>disable-model-invocation</code> 的 skill 对模型不可见</section>
</li>
<li>
<section>Read 路径有 workspace root guard</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这种「先裁剪，再推理」的方式有个很大的好处：模型认知空间更干净。它不会围着一堆不可用能力打转，也不会生成大量被拒调用的无效动作。</p>
<p data-tool="mdnice编辑器">如果你做企业场景，这比事后 deny 好得多。因为用户只会看到 agent 做合理尝试，而不是不停撞墙。</p>
<p data-tool="mdnice编辑器">当然，它的代价是灵活性差一点。你没法在非常细粒度的时刻做动态放行，除非重新构建 tool list 或 skill list。但我认为这笔账是划算的。Agent 系统的第一优先级不是灵活，是可控。</p>
<h1 data-tool="mdnice编辑器"><span class="content">11. 从平台设计角度看呢</span></h1>
<p data-tool="mdnice编辑器">觉得比较好的点：</p>
<p data-tool="mdnice编辑器"><strong>第一，简单。</strong><br />
这个简单不是简陋，是尽量不新增系统概念。skill 就是文件，加载靠 read，执行靠原有 tool-loop。对演进中的 agent 框架来说，这是非常健康的选择。</p>
<p data-tool="mdnice编辑器"><strong>第二，预算意识强。</strong><br />
<code>&lt;available_skills&gt;</code> 目录注入、最多只读一个 skill、compact/truncated 降级，这些都说明它把 context 当稀缺资源处理。</p>
<p data-tool="mdnice编辑器"><strong>第三，权限思路对。</strong><br />
先过滤 skill 和 tool，再让模型行动。可见性先于可调用性，这很工程化。</p>
<p data-tool="mdnice编辑器"><strong>第四，适合知识流程化。</strong><br />
很多团队真正需要的，不是一个会自己发明流程的 agent，而是把组织内已有 SOP 结构化地喂给模型。OpenClaw 这套很适合干这个。</p>
<p data-tool="mdnice编辑器">一些局限：</p>
<p data-tool="mdnice编辑器"><strong>第一，规模扩展性一般。</strong><br />
catalog 注入机制天然不适合 skill 数量无限增长。它适合几十级别，不适合大规模 marketplace。</p>
<p data-tool="mdnice编辑器"><strong>第二，组合能力弱。</strong><br />
「最多只读一个 skill」对稳态有帮助，对复杂任务编排是限制。</p>
<p data-tool="mdnice编辑器"><strong>第三，skill 缺少运行时身份。</strong><br />
它不是 tool，没有明确的输入输出边界，也没有独立权限与审计单元。后续做深治理会比较难。</p>
<p data-tool="mdnice编辑器"><strong>第四，过度依赖模型选择。</strong><br />
一旦 descriptions 写得不好，或者目录过大，skill 选择质量会变成系统上限。</p>
<h1 data-tool="mdnice编辑器"><span class="content">12. 适用场景</span></h1>
<p data-tool="mdnice编辑器">在系统架构选型时，如果有以下的条件，可以优先选择 OpenClaw 的设计逻辑：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>团队已经有一套成熟的 tool-loop，想低成本引入 skills</section>
</li>
<li>
<section>主要目标是把流程知识、操作手册、领域步骤注入 agent</section>
</li>
<li>
<section>skill 作者很多，技术水平参差不齐，需要低门槛 markdown 入口</section>
</li>
<li>
<section>更看重实现速度和治理可落地，而不是复杂编排能力</section>
</li>
<li>
<section>skill 数量在可控范围内，不会迅速膨胀到几百个</section>
</li>
</ul>
<p data-tool="mdnice编辑器">有如下的情况时，就不太适用使用这种方案：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>skill 本质上是可执行业务子任务，需要独立生命周期</section>
</li>
<li>
<section>需要强隔离执行、单独审计、结果回传和失败恢复</section>
</li>
<li>
<section>任务天然需要多个 skill 协同编排</section>
</li>
<li>
<section>技能库规模会非常大，必须做复杂路由</section>
</li>
<li>
<section>权限模型要求细到「某个 skill 可以做 A 但不能做 B」</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这些场景下，我更倾向 Claude Code 那种 tool 化 skill，或者更进一步，直接走 subagent / workflow node / task runtime。</p>
<h1 data-tool="mdnice编辑器"><span class="content">13. 小结</span></h1>
<p data-tool="mdnice编辑器">如果只用一句话概括两者区别：</p>
<p data-tool="mdnice编辑器"><strong>Claude Code 的 skill 更像可调用子程序，OpenClaw 的 skill 更像按需展开的运行手册。</strong></p>
<p data-tool="mdnice编辑器">前者强调执行单元，后者强调上下文注入。<br />
前者天然适合隔离和编排，后者天然适合轻量接入和流程沉淀。<br />
前者的复杂度在 runtime，后者的复杂度在 prompt 和内容治理。</p>
<p data-tool="mdnice编辑器">OpenClaw 用极其精简的 Prompt 规则和现成的 Read 工具，低成本实现了 Agent Skills 标准。它够用，但也把长文本管理的压力，原封不动地推给了底层大模型的 Context Window。</p>
<p data-tool="mdnice编辑器">我个人对 OpenClaw 这套实现是认可的，前提是别把它想象成一个万能技能平台。它解决的是「如何在不重写 agent runtime 的前提下，把结构化流程知识接进模型执行链路」这个问题。这个问题它解得挺干净。</p>
<p data-tool="mdnice编辑器">但如果你要的是 Claude Code 那种「新上下文执行 skill、像子程序一样调用、跑完把结果带回来」，你该找的是子会话、subagent、任务编排层，而不是继续往 <code>SKILL.md</code> 上堆规则。</p>
<p data-tool="mdnice编辑器">以上。</p>
</section>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/03/openclaw-skilss-claude-code/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
