今天上午,我花了几个小时拆解 Claude Code 的 Skill Pipeline 底层机理。
下午 13:36,我打开 Jarvis 的代码,心想:既然 Skill 是纯应用层的,那我也能做。
14:00,不到 30 分钟后,Jarvis 已经能自己创建 Skill、调用 Skill、管理 Skill 了。
这篇文章记录整个实现过程——不是教程,而是一个真实的「从理解原理到动手造轮子」的故事。
起因:逆向工程带来的信心
在今天之前,我对 Jarvis 的 Skill 系统心里没底。Phase 4(进化系统)虽然已经做完了——PatternDetector 能检测重复模式,SkillGenerator 能生成 Skill 草稿,SkillSandbox 能做安全验证——但有个关键缺环:Jarvis 怎么在对话中真正「使用」一个 Skill?
Phase 4 解决的是「创造 Skill」的问题,但不是「加载并执行 Skill」的问题。
今天的逆向工程给了我答案。Claude Code 的方案简洁到令人惊讶:
- 启动时扫描 SKILL.md 文件
- 提取 name + description
- 注入到上下文,让 LLM 自己决定是否加载
- 被选中的 Skill 全文加载,LLM 按指令执行
就这样。没有什么复杂的匹配算法,没有什么向量检索,就是文件扫描 + frontmatter 解析 + prompt 注入 + LLM 推理。
设计方案:三种思路,选最简的那个
理解了原理后,我和小龙虾坐下来讨论实现方案。核心洞察立刻就明确了:
Jarvis 已经有 file_read 工具了。所以最简方案就是——不需要新建任何 Tool,只需要一个扫描器和一处 prompt 改动。
我们列了三种方案:
| 方案 | 复杂度 | 新增代码 | 新增工具 | 思路 |
|---|---|---|---|---|
| A: OpenClaw 式 | ⭐ | ~80行 | 0 | 复用 file_read,只加扫描器 + 改 prompt |
| B: Claude Code 式 | ⭐⭐⭐ | ~200行 | 1个 meta-tool | Skill 元工具 + 双消息注入 |
| C: 混合式 | ⭐⭐ | ~120行 | 1个 use_skill | 专用工具读 SKILL.md |
我们选了方案 A——原因很简单:
- 零新工具:Jarvis 已经有
file_read,让 LLM 用现有工具读 SKILL.md 是最自然的 - 兼容性:Jarvis 用的是 OpenAI-compatible API(通过 CopilotX 代理),不是直接调 Claude API,meta-tool 方案兼容性不确定
- 哲学一致:与 Jarvis 「数码宝贝式」渐进进化的理念一致——先跑通最小可行版本,需要时再加
- 调试直观:System prompt 注入的内容你直接就能看到,meta-tool 还得拦截 API 请求才知道注入了什么
具体变动只有两处:
| 改动 | 内容 |
|---|---|
新建 src/skills/loader.py | 扫描 SKILL.md + 解析 frontmatter + 生成 XML 片段 |
改动 src/llm/__init__.py | 把静态 SYSTEM_PROMPT 变成动态属性,追加 <available_skills> |
为什么说这是「最简」?
| 特性 | 说明 |
|---|---|
| 零新工具 | 完全复用已有的 file_read |
| 零新依赖 | 只需标准库 re,不引入 PyYAML |
| 一个新文件 | src/skills/loader.py ~80 行 |
| 改动一处 | __init__.py 把静态 prompt 变成动态 |
| 向后兼容 | 没有 SKILL.md 时行为完全不变 |
| 复用 Skills | .claude/skills/ 下现有的 30+ 个 Skill 直接可用 |
方案定了,开干。
实现:193 行的 Skill Loader
核心代码在 src/skills/loader.py 中,只有 155 行(加上集成代码共 193 行)。
Step 1: 发现(Discovery)
def discover_skills(extra_dirs=None) -> list[SkillEntry]:
"""
从多个来源发现技能。
扫描顺序(后者同名覆盖前者):
1. ~/.claude/skills/ — 复用 Claude Code 的全局技能
2. ~/.jarvis/skills/ — Jarvis 专属全局技能
3. extra_dirs — 项目级 / 自定义路径
"""
sources = [
Path.home() / ".claude" / "skills", # 复用 Claude Code 技能
Path.home() / ".jarvis" / "skills", # Jarvis 专属
]
# ...
有个设计细节值得说说:我让 Jarvis 直接复用 ~/.claude/skills/ 目录。
为什么?因为我已经给 Claude Code 写了 30 多个 Skill(blog-writer、paper-writer、mcp-builder 等等),这些 Skill 的格式完全通用——YAML frontmatter + Markdown 正文。Jarvis 直接扫描、直接用,零迁移成本。
后者同名覆盖的设计也留了口子:如果我想给 Jarvis 定制某个 Skill 的行为,在 ~/.jarvis/skills/ 下放一个同名的就行。
Step 2: 解析(Parse)
def _parse_frontmatter(content: str) -> dict[str, str]:
"""
纯正则实现,不依赖 PyYAML。
只解析 name 和 description,足够用了。
"""
match = re.match(r"^---\s*\n(.*?)\n---", content, re.DOTALL)
if not match:
return {}
# 支持三种格式:bare value / "double quoted" / 'single quoted'
零新依赖是一个刻意的设计原则。Jarvis 的 pyproject.toml 里没有 PyYAML,我也不想为了解析两个字段就加一个依赖。用正则处理 name: value 和 name: "value" 完全够了。
Step 3: 注入(Inject)
def format_skills_prompt(skills: list[SkillEntry], max_chars=15000) -> str:
"""
生成注入 system prompt 的技能片段。
格式参考 OpenClaw 的 <available_skills> XML 结构。
"""
header = (
"\n\n## 技能 (Skills)\n"
"回复前,先扫描下方 <available_skills> 中每个技能的描述。\n"
"- 如果有技能明确匹配用户请求:先用 file_read 读取该技能的 path,"
"然后按其中的指令执行。\n"
"- 如果没有匹配的技能:正常回复,不要读取任何 SKILL.md。\n"
"- 每次最多读取一个技能。\n\n"
"<available_skills>\n"
)
# 每个 Skill 一行 XML: <skill name="..." path="...">{description}</skill>
注入格式我用了 XML,原因也是从 Claude Code 的逆向工程中学到的——Claude 对结构化 XML 的解析能力很强,比纯 Markdown 列表更可靠。
还有个 max_chars=15000 的截断保护。30+ 个 Skill 的描述加起来大概几千 token,但如果未来 Skill 数量爆炸到上百个,这层保护就有用了。
Step 4 & 5: 匹配 + 加载
这两步不在 loader.py 中——它们由 LLM 自己完成。
当 format_skills_prompt 的输出被注入到 system prompt 后,LLM 在每次对话中会:
- 看到
<available_skills>列表 - 根据用户输入判断是否需要某个 Skill
- 如果需要,调用
file_read工具读取对应的 SKILL.md 文件 - 拿到完整指令后,按指令执行
这就是 Jarvis 版的「延迟加载」——不用专门的 Skill tool,复用已有的 file_read tool。
集成到对话引擎
Skill Loader 写好后,集成到 JarvisLLMClient 只需要几行:
class JarvisLLMClient:
def _build_system_prompt(self) -> str:
"""动态构建 system prompt,包含已发现的技能列表。"""
prompt = self.SYSTEM_PROMPT
# 技能发现 + prompt 注入
try:
skills = discover_skills()
skills_section = format_skills_prompt(skills)
if skills_section:
prompt += skills_section
logger.info("已注入 %d 个技能到 system prompt", len(skills))
except Exception as e:
logger.warning("技能发现失败,跳过: %s", e)
self._system_prompt = prompt
return prompt
_build_system_prompt 在首次调用时执行一次技能发现,结果被缓存。如果需要热加载(比如用户刚创建了一个新 Skill),调用 reload_skills() 清缓存即可。
和 Phase 4 进化系统的闭环
到现在为止,Jarvis 的 Skill 闭环完整了:
~/.jarvis/skills/{name}/SKILL.md每个环节都有对应的代码模块,每个模块都有清晰的职责边界。
最让我兴奋的是从自动创建到自动使用的无缝衔接。PatternDetector 发现你经常做翻译任务 → SkillGenerator 自动提议一个翻译 Skill → 你确认后写入 → 下次你说「帮我翻译这个文档」,Skill Loader 自动发现它、LLM 自动加载它、然后按 Skill 里的指令执行。
Agent 自己给自己创造能力,然后自己使用这些能力。 这就是 Phase 4 进化系统的核心意义。
从自动创建到自动使用的无缝衔接——PatternDetector 发现你经常做翻译任务 → SkillGenerator 自动提议一个翻译 Skill → 你确认后写入 → 下次你说「帮我翻译这个文档」,Skill Loader 自动发现它、LLM 自动加载它、然后按 Skill 里的指令执行。
测试验证
安装最新版后测试:
cd Jarvis-Agent
poetry install
poetry run jarvis chat
三个核心功能全部跑通:
- ✅ 创建 Skill:通过对话或 PatternDetector 触发,生成 SKILL.md 写入
~/.jarvis/skills/ - ✅ 调用 Skill:在对话中说对应的触发词,LLM 自动 file_read 加载 SKILL.md 并执行
- ✅ 查看 Skill:
jarvis skill list显示所有已注册的 Skill,包括来源、状态、使用次数
还有一些高级功能:
jarvis skill enable/disable <name>:启禁 Skilljarvis skill delete <name>:删除(移到 .trash)jarvis skill test <name>:沙盒验证jarvis reflect:元认知报告,包含五维能力雷达
几个实现中的心得
1. 复用优于重造
Jarvis 的 Skill Loader 复用了三个东西:
- SKILL.md 格式——不发明新格式,用已经成为开放标准的那个
~/.claude/skills/目录——不重建 Skill 库,现有 30+ 个 Skill 直接可用file_read工具——不新增专用 Skill tool,这就是方案 A 的核心优势
能在 30 分钟内做完,就是因为不需要从零开始。
2. System Prompt 注入 vs. Meta-Tool
Claude Code 用 meta-tool 方案有它的道理——tool_use 机制给了模型更明确的「调用」语义,而且 Anthropic 对自家 API 的 tools 支持肯定更好。
但对于通过 OpenAI-compatible API 代理(如 CopilotX)调用的场景,system prompt 注入更稳定。这也是我们讨论时选方案 A 而不是 B 的关键原因——不同模型、不同代理对 tools 的支持参差不齐,但 system prompt 每家都支持。
3. XML 格式的结构化优势
<skill name="blog-writer" path="~/.claude/skills/blog-writer/SKILL.md">
Polly 个人博客写作助手,用于撰写技术博文...
</skill>
比起纯文本列表,XML 格式给 LLM 提供了更清晰的结构信号——name 是什么、path 在哪里、description 是什么,一目了然。这是从 Claude Code 的逆向工程中学到的经验。
4. 零依赖的 frontmatter 解析
不到 20 行的正则就搞定了 YAML frontmatter 解析。Skills 规范中 frontmatter 只有简单标量字段(name: value),不需要处理嵌套对象、数组等复杂结构,所以完全没必要引入 PyYAML。
5. 小龙虾的 Code Review
reload_skills() 方法清缓存" 等字符,注入时要做 &quot; 转义从理解到创造
回顾今天的历程:
- 上午:逆向工程 Claude Code 和 Copilot 的 Skill Pipeline,纠正了两个关于模型层的误解
- 13:36 → 14:00:从打开编辑器到全功能验证通过,不到 30 分钟
- 小龙虾 Code Review:三条建议全部采纳
总共约 193 行新代码(loader.py 155 行 + init.py 10 行 + LLM Client 集成 28 行)。
为什么能这么快?因为每一行代码都写在理解之上。我知道 Claude Code 为什么那样设计,我也知道我为什么做了不同的选择。不需要试错,不需要迭代,拆明白原理后一次写对。
Jarvis 的 Skill 系统和 Claude Code 的 Skill 系统,底层原理完全一样,但实现方式各有取舍。这就是逆向工程最大的价值——不是抄作业,而是理解出题人的思路后,用自己的方式解题。 这也是小龙虾 Review 时说的:“30 分钟从零到跑通,比‘一个下午’更有说服力。”她说得对。
系列文章:

