首页
看点啥
插画图片
首页 经济看点 learn-claude-code-s05_skill_loading.py

learn-claude-code-s05_skill_loading.py

2026-05-19 0

原址

learn-claude-code-s05_skill_loading.py

s05_skill_loading.py 讲的是:给 AI Coding Agent 增加“按需加载技能”的能力
核心思想是:不要把所有专业知识一次性塞进 system prompt,而是先只放技能名称和简介,等模型需要时再通过 load_skill 工具加载完整技能内容。 文件开头注释明确写了这个“两层注入”设计:第 1 层把技能名和简介放进 system prompt,第 2 层在模型调用 load_skill("pdf") 时返回完整技能正文。(GitHub)

这个文件解决的问题

普通 Agent 如果把所有规则、工具说明、PDF 处理方法、代码审查规范、MCP 构建指南都直接写进 system prompt,会有几个问题:

  1. prompt 变长,浪费 token

  2. 模型注意力被稀释

  3. 不相关知识也会干扰当前任务

  4. 技能越多,系统提示词越膨胀

所以这个文件实现了一个更像 Claude Code / Cursor / Agent Skill 的机制:

这就是注释里说的:“Don’t put everything in the system prompt. Load on demand.” (GitHub)

整体执行流程

可以理解成这 6 步:

objectivec
体验AI代码助手
代码解读
复制代码
启动程序 扫描 skills/ 目录下所有 SKILL.md 解析每个 SKILL.md 的 YAML frontmatter 把技能名称 + 简介塞进 system prompt 用户提出任务 模型判断是否需要某个技能,需要就调用 load_skill(name) load_skill 返回完整技能说明 模型根据完整技能继续完成任务

目录结构设计

文件注释里规定了技能目录大概长这样:(GitHub)

objectivec
体验AI代码助手
代码解读
复制代码
skills/ pdf/ SKILL.md code-review/ SKILL.md

每个技能一个目录,每个目录里有一个 SKILL.md
比如仓库里确实有这些技能目录:agent-buildercode-reviewmcp-builderpdf。(GitHub)

SKILL.md 的结构通常是:

yaml
体验AI代码助手
代码解读
复制代码
--- name: pdf description: Process PDF files... --- # PDF Processing Skill 具体操作说明……

比如 skills/pdf/SKILL.md 里就有 name pdfdescription Process PDF files...,后面才是完整的 PDF 处理说明。(GitHub)

核心代码解释

1. 初始化环境

ini
体验AI代码助手
代码解读
复制代码
load_dotenv(override=True) if os.getenv("ANTHROPIC_BASE_URL"): os.environ.pop("ANTHROPIC_AUTH_TOKEN", None) WORKDIR = Path.cwd() client = Anthropic() MODEL = os.environ["MODEL_ID"] SKILLS_DIR = WORKDIR / "skills"

这段做了几件事:

代码

作用

load_dotenv(override=True)

读取 .env 文件里的环境变量

ANTHROPIC_BASE_URL 判断

如果使用自定义 base url,就移除 ANTHROPIC_AUTH_TOKEN

WORKDIR = Path.cwd()

当前运行目录作为工作区

client = Anthropic()

初始化 Anthropic 客户端

MODEL = os.environ["MODEL_ID"]

从环境变量读取模型名

SKILLS_DIR = WORKDIR / "skills"

默认从当前目录下的 skills/ 加载技能

这些初始化代码位于文件中部,负责准备模型客户端、模型 ID 和技能目录。(GitHub)

2. SkillLoader:技能加载器

这是本文件最关键的类。

ruby
体验AI代码助手
代码解读
复制代码
class SkillLoader: def __init__(self, skills_dir: Path): self.skills_dir = skills_dir self.skills = {} self._load_all()

它的作用是:启动时扫描 skills/ 目录,把所有技能读进内存。
self.skills 是一个字典,用来保存所有技能。每个技能大概会被存成这样:

css
体验AI代码助手
代码解读
复制代码
{ "pdf": { "meta": {...}, "body": "...完整技能内容...", "path": "skills/pdf/SKILL.md" } }

SkillLoader 会在初始化时调用 _load_all(),扫描 skills_dir 下面所有 SKILL.md 文件。(GitHub)

3. _load_all():扫描所有技能文件

python
体验AI代码助手
代码解读
复制代码
def _load_all(self): if not self.skills_dir.exists(): return for f in sorted(self.skills_dir.rglob("SKILL.md")): text = f.read_text() meta, body = self._parse_frontmatter(text) name = meta.get("name", f.parent.name) self.skills[name] = {"meta": meta, "body": body, "path": str(f)}

这段逻辑很重要:

步骤

含义

if not self.skills_dir.exists()

如果没有 skills/ 目录,就直接返回

rglob("SKILL.md")

递归查找所有叫 SKILL.md 的文件

f.read_text()

读取技能文件内容

_parse_frontmatter(text)

拆分 YAML 元信息和正文

meta.get("name", f.parent.name)

优先用 frontmatter 里的 name,没有就用目录名

self.skills[name] = ...

存入技能字典

也就是说,技能名可以来自两种地方:

makefile
体验AI代码助手
代码解读
复制代码
name: pdf

或者来自目录名:

bash
体验AI代码助手
代码解读
复制代码
skills/pdf/SKILL.md pdf

相关扫描和存储逻辑在 SkillLoader._load_all() 里。(GitHub)

4. _parse_frontmatter():解析 YAML 头部

python
体验AI代码助手
代码解读
复制代码
match = re.match(r"^---n(.*?)n---n(.*)", text, re.DOTALL)

这行正则的意思是:

yaml
体验AI代码助手
代码解读
复制代码
从文件开头开始匹配: --- 这里是 YAML 元信息 --- 这里是正文

如果匹配不到 frontmatter:

arduino
体验AI代码助手
代码解读
复制代码
return {}, text

也就是:没有元信息,整个文件都当正文。

如果匹配到了:

csharp
体验AI代码助手
代码解读
复制代码
meta = yaml.safe_load(match.group(1)) or {} return meta, match.group(2).strip()

也就是:

部分

作用

match.group(1)

YAML frontmatter

match.group(2)

技能正文

yaml.safe_load(...)

把 YAML 字符串转成 Python 字典

.strip()

去掉正文首尾空白

所以 SKILL.md 实际被拆成了两部分:

css
体验AI代码助手
代码解读
复制代码
meta:name、description、tags 等短信息 body:完整技能说明

这就是后面“两层加载”的基础。(GitHub)

5. get_descriptions():生成第 1 层技能简介

python
体验AI代码助手
代码解读
复制代码
def get_descriptions(self) -> str: """Layer 1: short descriptions for the system prompt."""

这个方法生成放进 system prompt 的内容。

它不会返回完整技能正文,只返回类似这样的短描述:

less
体验AI代码助手
代码解读
复制代码
- pdf: Process PDF files... - code-review: Perform thorough code reviews...

代码里会读取每个技能的:

ini
体验AI代码助手
代码解读
复制代码
desc = skill["meta"].get("description", "No description") tags = skill["meta"].get("tags", "")

然后拼成一行文本。(GitHub)

这就是第 1 层:

6. get_content():生成第 2 层完整技能内容

python
体验AI代码助手
代码解读
复制代码
def get_content(self, name: str) -> str: """Layer 2: full skill body returned in tool_result."""

这个方法就是 load_skill 背后的真正逻辑。

如果技能不存在:

kotlin
体验AI代码助手
代码解读
复制代码
return f"Error: Unknown skill '{name}'. Available: ..."

如果技能存在:

swift
体验AI代码助手
代码解读
复制代码
return f""{name}">n{skill['body']}n"

也就是说,当模型调用:

scss
体验AI代码助手
代码解读
复制代码
load_skill("pdf")

工具会返回:

ini
体验AI代码助手
代码解读
复制代码
name="pdf"> 完整 PDF 处理说明……

这就是第 2 层:

相关逻辑在 get_content() 中。(GitHub)

System Prompt 是怎么组装的?

ini
体验AI代码助手
代码解读
复制代码
SKILL_LOADER = SkillLoader(SKILLS_DIR) SYSTEM = f"""You are a coding agent at {WORKDIR}. Use load_skill to access specialized knowledge before tackling unfamiliar topics. Skills available: {SKILL_LOADER.get_descriptions()}"""

这段就是把技能简介注入系统提示词。

注意:这里注入的是 get_descriptions(),不是 get_content()

所以 system prompt 里只有:

diff
体验AI代码助手
代码解读
复制代码
你是一个 coding agent 遇到陌生任务时可以用 load_skill 加载专业知识 当前可用技能: - pdf: ... - code-review: ...

不会一开始就把 PDF 的详细命令、代码审查清单、MCP 构建流程全部塞进去。(GitHub)

工具系统:比前面章节多了 load_skill

这个文件仍然保留了普通 Agent 工具:

工具

作用

bash

执行 shell 命令

read_file

读取文件

write_file

写文件

edit_file

替换文件中的指定文本

load_skill

按名称加载技能正文

工具处理函数在 TOOL_HANDLERS 里注册,其中 load_skill 对应:

objectivec
体验AI代码助手
代码解读
复制代码
"load_skill": lambda **kw: SKILL_LOADER.get_content(kw["name"])

也就是:模型调用 load_skill,实际执行的是 SKILL_LOADER.get_content(name)。(GitHub)

load_skill 和普通工具有什么区别?

这是这个文件最值得理解的点。

普通工具是“做事”的:

体验AI代码助手
代码解读
复制代码
bash → 执行命令 read_file → 读文件 write_file → 写文件 edit_file → 改文件

load_skill 不是直接做事,而是“给模型补知识”的:

体验AI代码助手
代码解读
复制代码
load_skill → 返回一段专业操作说明,让模型变得更会做某类任务

所以它更像是:

体验AI代码助手
代码解读
复制代码
知识工具 / 上下文注入工具 / 按需说明书加载器

举个例子:

用户说:

体验AI代码助手
代码解读
复制代码
帮我处理这个 PDF

模型看到 system prompt 里有:

arduino
体验AI代码助手
代码解读
复制代码
- pdf: Process PDF files...

于是模型可以先调用:

json
体验AI代码助手
代码解读
复制代码
{ "name": "pdf" }

然后 load_skill 返回完整 PDF 技能,比如如何用 pdftotextPyMuPDFreportlabpandoc 等处理 PDF。skills/pdf/SKILL.md 里确实包含读取、创建、合并、拆分 PDF 的具体操作说明。(GitHub)

safe_path():限制文件路径不能逃出工作区

python
体验AI代码助手
代码解读
复制代码
def safe_path(p: str) -> Path: path = (WORKDIR / p).resolve() if not path.is_relative_to(WORKDIR): raise ValueError(f"Path escapes workspace: {p}") return path

这个函数用于保护 read_filewrite_fileedit_file 这些文件操作。

它会把用户传入的路径拼到当前工作区,然后解析成绝对路径。
如果最终路径不在 WORKDIR 里面,就抛错。

比如:

bash
体验AI代码助手
代码解读
复制代码
正常:src/main.py 危险:../../etc/passwd

../../etc/passwd 解析后会逃出工作区,所以会被拦截。相关路径保护逻辑在 safe_path() 中。(GitHub)

run_bash():执行命令,但做了简单危险命令拦截

scss
体验AI代码助手
代码解读
复制代码
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"] if any(d in command for d in dangerous): return "Error: Dangerous command blocked"

它会拦截一些明显危险命令,比如:

bash
体验AI代码助手
代码解读
复制代码
rm -rf / sudo shutdown reboot > /dev/

然后用:

ini
体验AI代码助手
代码解读
复制代码
subprocess.run(..., shell=True, cwd=WORKDIR, timeout=120)

执行命令,最多跑 120 秒。输出会截断到 50000 字符。(GitHub)

不过这里要注意:这是教学版,不是生产级沙箱。

原因是:

  1. 它用了 shell=True

  2. 危险命令只靠字符串黑名单拦截

  3. safe_path() 只保护文件读写工具,不保护 bash 命令

  4. bash 理论上仍然能执行很多未被黑名单覆盖的危险操作

所以这个文件更适合学习 Agent 架构,不适合直接拿来跑不可信输入。

agent_loop():核心 Agent 循环

ini
体验AI代码助手
代码解读
复制代码
def agent_loop(messages: list): while True: response = client.messages.create( model=MODEL, system=SYSTEM, messages=messages, tools=TOOLS, max_tokens=8000, )

这里和前面的 Agent Loop 思路一样:

  1. 把历史消息 messages 发给模型

  2. 带上 system

  3. 带上 tools

  4. 等模型回复

  5. 如果模型要调用工具,就执行工具

  6. 把工具结果塞回 messages

  7. 继续循环

  8. 直到模型不再调用工具

判断是否继续的关键是:

kotlin
体验AI代码助手
代码解读
复制代码
if response.stop_reason != "tool_use": return

如果模型不是因为调用工具而停止,就说明它已经给出最终回答了,循环结束。(GitHub)

工具调用结果怎么回传给模型?

go
体验AI代码助手
代码解读
复制代码
results.append( { "type": "tool_result", "tool_use_id": block.id, "content": str(output), } ) messages.append({"role": "user", "content": results})

这段会把工具执行结果包装成 Anthropic API 需要的 tool_result 格式,再作为一条新的 user message 追加到历史消息里。(GitHub)

所以完整交互像这样:

scss
体验AI代码助手
代码解读
复制代码
用户:帮我处理 PDF 模型:我要调用 load_skill("pdf") 程序:执行 load_skill,拿到 PDF 技能说明 程序:把技能说明作为 tool_result 返回给模型 模型:读完技能说明后,再继续完成 PDF 任务

命令行入口

ini
体验AI代码助手
代码解读
复制代码
if __name__ == "__main__": history = [] while True: query = input("33[36ms05 >> 33[0m")

这部分让脚本变成一个命令行聊天程序。

执行后会看到类似:

体验AI代码助手
代码解读
复制代码
s05 >>

然后你输入问题,程序把你的输入追加到 history

bash
体验AI代码助手
代码解读
复制代码
history.append({"role": "user", "content": query}) agent_loop(history)

最后从 history[-1]["content"] 里拿出模型回复并打印。(GitHub)

和前面几个文件的关系

你前面看过的文件可以串起来理解:

文件

重点

s01_agent_loop.py

最小 Agent Loop:用户输入 → 模型回复

s02_tool_use.py

加入工具调用:模型可以调用 bash / read / write 等工具

s03_todo_write.py

加入任务规划 / Todo 管理

s04_subagent.py

加入子 Agent:主 Agent 可以把任务委托出去

s05_skill_loading.py

加入技能加载:模型可以按需加载专业知识

s05 的本质升级是:

体验AI代码助手
代码解读
复制代码
从“能调用工具” 升级到 “能根据任务加载专业操作手册”

也就是:

ini
体验AI代码助手
代码解读
复制代码
Agent = LLM + Tools + Memory/Context + Planning + Skills

这里的 Skills 不是模型本身的能力,而是外部维护的一组专业说明书。

这个文件最核心的设计思想

可以总结成一句话:

sql
体验AI代码助手
代码解读
复制代码
System Prompt 只放索引,完整知识按需加载。

更具体一点:

ini
体验AI代码助手
代码解读
复制代码
skills/ 目录 = 技能库 SKILL.md frontmatter = 技能索引 SKILL.md body = 完整技能说明 get_descriptions() = 注入 system prompt 的轻量索引 load_skill() = 按需读取完整技能 tool_result = 把技能正文塞回模型上下文

这个设计很像:

体验AI代码助手
代码解读
复制代码
书架目录 + 按需取书

不是一开始把所有书都摊在桌子上,而是先告诉模型:

体验AI代码助手
代码解读
复制代码
我有 PDF 技能、代码审查技能、MCP 构建技能……

等模型真的要处理 PDF 时,再把 PDF 那本“说明书”拿出来。

最后用一句大白话理解

s05_skill_loading.py 就是在教你做一个更聪明的 AI Coding Agent:

喜欢(0)

上一篇

51漫画免费登录页面入口首页-51漫画最新版本下载安装官网

51漫画免费登录页面入口首页-51漫画最新版本下载安装官网

下一篇

AI 热潮冲击供应链:苹果 DRAM 议价权削弱,重心转向保供而非最优价

AI 热潮冲击供应链:苹果 DRAM 议价权削弱,重心转向保供而非最优价
猜你喜欢