云计算百科
云计算领域专业知识百科平台

Day57|从0学习 Claude Code(七):我没把说明书全塞给它,用到哪本才翻哪本

苦猿的大模型日记 · 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 也确实读到了所有规范。但这个写法埋着一个问题,上一篇咱们刚算过一笔类似的账——它和分身要解决的是同一本账上的两页:上下文里到底该装什么。

上一篇解决的是"过程去哪":调查类的工具调用派给分身,白纸上烧掉,只带结论回来。这一篇解决"知识去哪":那些成套的规范文档,不该常驻,该用到才加载。

读完你会拿到三样东西:

  • SkillLoader 的完整实现:扫描技能目录、解析元信息、拼一张只印"菜名"的菜谱,总共四十来行
  • "目录常驻、正文按需"的两层注入机制:名称和描述进 system prompt,全文走一个叫 load_skill 的工具按需回流
  • 三个最容易想错的点:为什么做成工具而不是让模型自己去读文件;加载进来的内容到底进了哪(不是 system prompt);以及技能系统最常见的死法——description 写歪了,模型永远想不起来加载
  • 门槛不变:会 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 账单对比:全量塞入 vs 按需加载

    打个比方。这不像给新员工发一本员工手册——手册发一次,他自己保管。这像什么呢?像你每跟他说一句话,都先把整面书柜从头到尾念一遍。"今天改一下按钮样式"——念一遍书柜。"改好了你看看"——再念一遍书柜。

    员工会疯,账单会疯,唯一开心的是按 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……"——模型下一轮大概率就选对了。

    skill loading 架构流程

    再把 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 学进简历

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Day57|从0学习 Claude Code(七):我没把说明书全塞给它,用到哪本才翻哪本
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!