苦猿的大模型日记 · Day57 · 从0学习Claude Code(七)Skill Loading-帮普通人把AI学进简历系列
前言:第七篇,给 Agent 一本菜谱,而不是一桌满汉全席
本篇干一件事:解决知识把上下文撑爆的问题。
先说场景。你的项目里多半有这些东西——一份 React 组件规范、一份 SQL 风格指南、一份 API 设计文档。你希望 Agent 干活的时候遵守这些规矩,最直接的做法是什么?全部拼进 system prompt:
SYSTEM = (
f"You are a coding agent. "
+ open("docs/react-style.md").read()
+ open("docs/sql-style.md").read()
+ open("docs/api-design.md").read()
)
能跑,Agent 也确实读到了所有规范。但这个写法埋着一个问题,上一篇咱们刚算过一笔类似的账——它和分身要解决的是同一本账上的两页:上下文里到底该装什么。
上一篇解决的是"过程去哪":调查类的工具调用派给分身,白纸上烧掉,只带结论回来。这一篇解决"知识去哪":那些成套的规范文档,不该常驻,该用到才加载。
读完你会拿到三样东西:
门槛不变:会 Python 基础语法、手上有一份能跑的 Agent 循环(工具分发、权限、hooks、清单、分身都在的那种)。直接开始。
PART 01:案发现场——system prompt 是个每轮都要重发的包裹
先把 system prompt 的机制账算清楚,你才知道那个拼字符串的写法亏在哪。
很多人对 system prompt 的理解停留在"开机设置":启动的时候设置一次,之后就存在那了。这个直觉是错的,而且错得很贵。
LLM API 是无状态的。你眼里的一场连续对话,在模型那边是几十次独立的调用:每轮都把完整的历史重新发一遍——messages[] 是历史,system 也是历史的一部分。system prompt 不是"设置一次",是"每一轮都全文重发一次"。
这带来一个直接后果:塞进 system prompt 的每一个字,不是存一次的仓储费,是每一轮都要重付的过路费。
来算笔具体的账。假设那三份文档加起来 1.5 万 token(规范文档写得细一点,这个数只多不少)。现在你的任务只是改一个 React 组件——真正需要的只有 React 规范那一份,SQL 风格指南和 API 设计文档全程用不上。
但它们在 system prompt 里。意味着:
- 第 1 轮调用,发 1.5 万 token 的文档
- 第 10 轮调用,还是发 1.5 万 token 的文档
- 这个任务干完 20 轮,那两份无关文档被重发了 20 次,白付 30 万 token 的输入
而且钱只是小事。更贵的是注意力:上下文窗口是有限的,被无关文档占掉的空间,本来可以放代码、放对话、放工具结果。窗口快满的时候,模型的表现会肉眼可见地往下掉——它要在一堆和当前任务无关的文字里,翻找真正相关的那部分。

打个比方。这不像给新员工发一本员工手册——手册发一次,他自己保管。这像什么呢?像你每跟他说一句话,都先把整面书柜从头到尾念一遍。"今天改一下按钮样式"——念一遍书柜。"改好了你看看"——再念一遍书柜。
员工会疯,账单会疯,唯一开心的是按 token 计费的 API。
那你可能会说:让模型自己去读文档不就行了,用 read_file,需要哪份读哪份。思路对了,但有个坑:模型怎么知道有这份文档? 路径它不知道,存在它不知道,"该去查一下规范"这个念头本身就起不来。你不能指望一个不知道书柜在哪的人自己去翻书。
所以问题变成了:得有个东西告诉模型"有哪些知识可用"(否则它想不起来),但又不能把全文给它(否则回到每轮重发)。目录要常驻,正文要按需。
这就是 Skill Loading。
PART 02:实现——四十行 SkillLoader,一张菜谱只印菜名
方案拆开就三个部件:技能长什么样、启动时怎么扫描、用的时候怎么取。
第一步:定技能的形态
一个技能就是一个目录,里面放一个 SKILL.md。文件头用 YAML frontmatter 写元信息,正文写完整指令:
—
name: code-review
description: Perform thorough code reviews with security, performance,
and maintainability analysis. Use when user asks to review code,
check for bugs, or audit a codebase.
—
# Code Review Skill
You now have expertise in conducting comprehensive code reviews.
Follow this structured approach:
## Review Checklist
### 1. Security (Critical)
– [ ] Injection vulnerabilities: SQL, command, XSS…
– [ ] Authentication issues: hardcoded credentials…
…
关键是 frontmatter 里那两行:name 是这道菜的名字,description 是这道菜的介绍。这两个字段就是全部的"目录信息"——启动时只取它们进 system prompt,正文一个字都不进。
第二步:启动扫描
class SkillLoader:
def __init__(self, skills_dir: Path):
self.skills_dir = skills_dir
self.skills: dict[str, dict[str, str]] = {}
self.scan()
def scan(self):
self.skills.clear()
if not self.skills_dir.exists():
return
for manifest in sorted(self.skills_dir.glob("*/SKILL.md")):
if not manifest.is_file():
continue
content = manifest.read_text(encoding="utf-8")
metadata, body = self.parse_frontmatter(content)
name = metadata.get("name", "").strip() or manifest.parent.name
description = (metadata.get("description", "").strip()
or body.split("\\n", 1)[0])
self.skills[name] = {
"name": name,
"description": description,
"content": content, # 全文留在注册表里,不进 prompt
}
scan() 干的事:glob("*/SKILL.md") 挨个找技能清单文件,解析出 frontmatter,登记进 self.skills 这个字典。
有三处防御式写法,值得停下来看一眼,因为它们决定了这套东西在真实项目里扛不扛造:
兜底一:name 缺了就用目录名。 manifest.parent.name——skills/code-review/SKILL.md 没写 name,那就拿 code-review 顶上。技能是别人写的、手滑漏了字段,系统不崩,降级可用。
兜底二:description 缺了就用正文第一行。 同理。目录里那一行介绍总得有东西填,拿正文标题凑合,好过空着——空着的目录条目,模型是看不懂的。
兜底三:content 整份存进注册表。 注意这个字段进了 self.skills,但从不进 prompt。全文躺在进程内存里,等点名。
第三步:菜谱和上菜
catalog() 负责拼菜谱——每个技能只出两样东西,名字和介绍:
def catalog(self) -> str:
return "\\n".join(
f"- {skill['name']}: {skill['description']}"
for skill in self.skills.values()
)
四份技能的菜谱长这样,就这几行:
– agent-builder: Build new coding agents with best practices…
– code-review: Perform thorough code reviews with security…
– mcp-builder: Create MCP servers with proper structure…
– pdf: Process PDF files, extract text and data…
注意上面那 1.5 万 token 的账——现在菜谱只花几十个 token,而且是固定开销,不随技能正文变长而变长。
然后是 system prompt 的组装:
def build_system_prompt() -> str:
return (
f"You are a coding agent at {WORKDIR}. "
"Use tools to solve tasks. Act, don't explain.\\n\\n"
f"Skills available:\\n{SKILL_LOADER.catalog()}\\n\\n"
"Use load_skill to read the full instructions when a skill applies."
)
固定指令 + 技能菜谱 + 一句"用到了就调 load_skill"。模型每轮都能看见这张菜谱,但它看到的只是菜名,不是整桌菜。
最后是上菜的 load():
def load(self, name: str) -> str:
skill = self.skills.get(name)
if skill:
return skill["content"]
available = ", ".join(self.skills) or "none"
return f"Error: Unknown skill '{name}'. Available: {available}"
按 name 查注册表,查到就返回全文。查不到呢?注意这个错误信息的写法——不是干巴巴一句 "not found",而是把可用技能列表一起带上。
这个手法你在权限那篇见过:拒绝信息本身也是模型的输入,写得有营养,模型就能自己改道。"react skill 不存在,可用的是 agent-builder、code-review……"——模型下一轮大概率就选对了。

再把 load_skill 注册成工具,六个工具的家族添了第七个:
TOOLS = [
# … 原有的 bash / read_file / write_file / edit_file / glob …
{"name": "load_skill",
"description": "Load the full SKILL.md content by skill name.",
"input_schema": {"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"]}},
]
TOOL_HANDLERS = {
# …
"load_skill": SKILL_LOADER.load,
}
到此实现完毕。循环一行没动——load_skill 就是个普通工具,模型调它和调 read_file 走的是完全一样的路:分发、执行、结果回流。这就是前面几篇攒下的架构红利:加一种新能力,不需要动心脏。
PART 03:边界——目录是菜单,不是书柜
实现简单,想明白边界不容易。这一节讲三个最容易想错的点。
想错一:为什么不干脆让模型自己 read_file?
既然技能就是磁盘上的文件,模型有 read_file,让它自己读 skills/code-review/SKILL.md 不就行了,何必专门造个工具?
三个理由,一个比一个硬。
第一,路径是不可靠的契约。 让模型自己读,等于要求它知道"技能放在 skills 目录下、每个技能一个文件夹、清单文件叫 SKILL.md"。哪天你把目录改名为 playbooks/、或者某个技能的清单换了个文件名,模型的知识就全部作废。而 load_skill("code-review") 走的是注册表查询——name 是启动时登记的键,和文件系统长什么样彻底解耦。技能随便搬家,调用方一个字不用改。
第二,不进目录,模型起不了"加载"这个念头。 这是最要命的一条。模型只有看见菜谱才知道有哪些菜。你要是只把技能文件扔在磁盘上、不告诉它,它连"这里可能有个规范该查一下"的意识都不会有——不是不会查,是不知道该查。这和 MCP 工具选择是同一个病:能力不进描述,模型就当它不存在。
第三,走工具就走监管。 还记得 hooks 吗?load_skill 是工具,所以它每被调用一次,log_hook 都会打日志,理论上也能被权限钩子拦。模型读了什么知识、什么时候读的,全部在线监管之内。让模型自己 read_file 绕开工具走磁盘?那你连它偷偷读了什么规范都不知道。
想错二:加载进来的全文,进了哪?
这是最反直觉的一点。load_skill 返回的 SKILL.md 全文,不是回到 system prompt——它以 tool_result 的身份进了 messages[],成为对话的一部分。
两张表看清这个设计:
| 名称 + 描述 | system prompt | 启动时 | 每轮都付(但很短) |
| 完整 SKILL.md | messages[] 的 tool_result | 调用 load_skill 时 | 调用之后的每轮 |
看明白了吗?按需加载省的不是"不付钱",是"从第一轮就付钱"变成"从用到才付钱"。 一旦加载,这份全文也会留在上下文里,后续每一轮照样重发。
那不还是亏?不。差别在于:改 React 组件的任务,SQL 规范从头到尾不会被加载,一次都不付;React 规范只在用到之后开始付。对比原来无差别全量常驻,该省的都省了。
这也是"目录常驻、正文按需"这条设计的精确含义:目录是每轮都要付的固定成本,所以必须瘦;正文是按需才付的变动成本,所以可以厚。
想错三:目录本身也是成本
顺着往下推一步:菜谱不是免费的。每个技能在目录里占一行,几十个 token。装十个技能,菜谱几百 token,每轮都付——可以接受。装一百个呢?菜谱几千 token,你又造了一个小号的 system prompt 问题。
所以技能不是越多越好。什么该做成 skill,什么不该,给你一张决策表:
| 每次都必须遵守的底线规则 | system prompt | 工作目录约束、输出语言、安全红线 |
| 成套的规范 / 流程 / checklist | skill(目录+按需加载) | 代码审查清单、部署流程、组件规范 |
| 一次性的项目信息 | 普通文件,让模型现读 | README、某个配置文件 |
判断标准就一条:这条知识是"某类任务才用"还是"所有任务都用"。所有任务都用的,进 system prompt,常驻不亏;某类任务才用的,做成 skill,点菜才上;只用一次的,根本不用登记,模型自己读文件去。

最后说那个最常见的死法:description 写歪,技能就等于不存在。 模型决定要不要调 load_skill,唯一依据就是目录里那行描述。写成 "Code review related stuff"——模型看不出这玩意什么时候该用,永远不点这道菜,你精心写的 500 行审查清单一次都没被加载过。description 不是注释,是推销文案:要写清楚这个技能干什么,更要写清楚什么时候该用它。
PART 04:实录——让它审代码,亲眼看它先翻菜谱再动筷
跑起来验证。python s07_skill_loading/code.py,我做了三个实验,每个都对着一个要验证的机制。
实验一:菜谱常驻吗?
先问一个最轻的问题:
s07 >> What skills are available?
它的回答里列出了四个技能的名字和用途。关键不在答案内容,在于它一个工具都没调——日志区干干净净,没有任何 load_skill 的记录。
这说明什么?菜谱在 system prompt 里,模型张口就能答,不需要任何 IO。这正是"目录常驻"的验证:菜单就摆在桌上。
实验二:会先翻菜谱再动筷吗?
上真任务:
s07 >> Review README.md, load the relevant skill first
盯着日志看戏:
[HOOK] load_skill({'name': 'code-review'})
第一步就是 load_skill("code-review")——它扫了一眼菜谱,判断"审代码该用 code-review 这道菜",先调工具把整份 SKILL.md 拉进上下文。tool_result 里回来的是完整清单:安全检查、性能检查、可维护性检查,一项一项。
然后它才开始读 README.md,并且按清单的结构逐项过:安全角度有没有问题、结构清不清楚、有没有过时的描述。出来的审查意见不是随笔,是照着菜谱做的套餐。
实验三:没用到就真不翻吗?
换个和任何技能都无关的任务,比如"把这个文件里的函数改个名"。
全程零次 load_skill 调用。Stop 钩子报的会话统计里,工具调用全是 read_file / edit_file 这类——模型没兴趣点菜,因为任务不需要。
三个实验合起来,"目录常驻、正文按需"这条机制就闭环了:知道有什么(不用翻)、用之前先查(查全份)、用不上不查(零成本)。
彩蛋实验:点错菜了怎么办?
我故意使坏:
s07 >> Load the react skill
项目里根本没有 react 技能。返回的是 PART 02 里那个精心设计的错误信息:
Error: Unknown skill 'react'. Available: agent-builder,
code-review, mcp-builder, pdf
下一轮,模型拿着这份可用列表自己改道了。没有崩溃,没有死循环——那条"错误信息要写得有营养"的设计,第一次实战就接住了。
收一笔账
到这里,这个 Agent 一共有了七个模块:心脏(agent 循环)、双手(工具分发)、神经(权限系统)、挂钩(hooks 扩展)、工单(TodoWrite)、分身(subagent)、今天的菜谱(skill loading)。
前五个解决的是"自己怎么把活干好"——干得动、干得安全、干得不忘。后两个换了个方向,把矛头对准上下文里到底装什么:分身把"过程"挪出去(调查的工具调用在白纸上烧掉),菜谱把"知识"管起来(正文点名才出场)。
一个管过程,一个管知识。但有一样东西它们俩都没管,而且正在悄悄变大——对话本身。每轮的回复、每个工具的结果,都在往 messages[] 里堆。任务干得越久,历史越长,每一轮重发的账单越厚。这东西怎么压?
下一篇,专门解决它。
结尾:知识的重量,应该由需要的时刻来承担
回到开头那三份规范文档。
它们没有消失,也没有被删减一个字——只是搬了家。从"每一轮都陪跑的 system prompt",搬到"点名才出场的 tool_result"。改 React 组件的那个任务,SQL 规范从头到尾没离开过磁盘;code-review 的五百行清单,只在你真的要审代码时才被翻开。
这其实就是知识管理的第一性原理,对 Agent 如此,对人也一样:
常驻的应该是对"有什么"的知晓,而不是对"是什么"的记忆。
你知道公司有份部署手册,知道放在哪、什么时候该查——这就够了。至于手册第一页写了什么,等真要部署的时候再翻开。把所有手册背下来的人,不是博学,是把记忆力浪费在了使用频率最低的地方。
模型也一样。它的"记忆力"就是你给的上下文——每一个 token 都是它要背的东西。上下文里最贵的,不是知识本身,是每轮都在重读、却一直用不上的知识。
互动时间:你的项目里有哪些文档适合做成 skill?是代码规范、面试题库,还是你自己的排查手册?最想聊的是这个——description 你会怎么写,才能让模型在正确的时机"点这道菜"?评论区见。
下一篇预告:「从0学习 Claude Code」第八篇——Context Compact,上下文压缩。菜谱省了知识的钱,分身省了过程的钱,但对话本身还在一轮轮变长:旧回复、旧工具结果堆在 messages 里,任务干得越久账单越厚。下一篇给历史"瘦身"——把早期的消息越压越薄,给新对话腾地方,而且压缩的时机本身,也值得好好设计。
— END —
苦猿 · 帮普通人把 AI 学进简历
网硕互联帮助中心





评论前必须登录!
注册