Learn-Claude-Code | 笔记 | Planning & Coordination | s05 Skills
Learn-Claude-Code | 笔记 | Planning & Coordination | s05 Skills
目录
前言
在上篇文章 Learn-Claude-Code | 笔记 | Planning & Coordination | s04 Subagents 中,我们介绍了开源项目 learn-claude-code 第四个章节 s04: Subagents 的内容,这篇文章我们继续跟着教程文档来学习规划与拆解后续内容,记录下个人学习笔记,和大家一起分享交流😄
Note:本篇文章主要学习记录教程第二部分 Planning & Coordination 中 s05: Skills 章节的内容。
github:https://github.com/shareAI-lab/learn-claude-code
reference:https://chatgpt.com/
1. s05: Skills
如果说 s04 解决的是 “复杂任务不应该全部堆在同一个上下文里做”,那么到了 s05,这一节开始进一步回答一个同样很关键、但性质又不太一样的问题:
当 Agent 需要掌握很多领域知识时,这些知识到底应该放在哪里?
前面几节我们已经逐步给这个最小 Agent 加上了越来越多的能力。s01 让它跑起来,s02 让它拥有一组结构化工具,s03 让它开始显式维护任务进度,s04 又让它学会把局部探索委派给子智能体,在新的 messages=[] 里独立完成,再把摘要带回父上下文。可以说,到这里为止,系统在 “行为组织” 和 “上下文管理” 这两个层面已经开始有了一点真实 Agent 的味道。
但前面几节依然默认了另一个前提:模型本身已经知道该怎么做事。也就是说,不管是调用 bash、读写文件、写 todo,还是生成子任务,前面这些章节主要都在解决 “如何组织执行过程”,而没有真正触碰一个更基础的问题:如果模型面对的是一个自己并不熟悉的领域流程,它应该从哪里获得这些专门知识?
比如,你希望这个 Agent 能做下面这些事情:
- 按仓库约定写 git commit
- 做代码审查
- 运行和分析测试
- 部署到指定环境
这些能力表面上看起来都可以用一句自然语言描述,但真正要让模型稳定遵循,背后通常都需要一整套具体工作流。比如 code review 不只是 “看代码并提意见”,而可能需要先拉取 PR diff、按文件逐个检查、重点关注 bug / security / style 问题,最后再按照某种格式写 review comment。换句话说,这些并不是通用常识,而是某种专门流程知识。
最直接的做法当然是:把这些知识全都塞进 system prompt。可问题也很明显 — 如果你有 10 个技能,每个技能正文 2000 tokens,那就是 20000 tokens 的常驻系统提示,而其中绝大多数内容在当前请求里根本用不到。文档里对这个问题说得非常直接:你希望智能体遵循特定领域的工作流,但如果把所有技能都塞进系统提示里会非常浪费,因为大部分技能和当前任务毫无关系。
所以 s05 想解决的问题,已经不再是 “任务怎么拆” “上下文怎么隔离”,而是:
不要把所有知识永久塞进 system prompt,而是让 Agent 在需要的时候,再去加载对应知识。
2. 问题
我们先从一个很朴素的问题出发:为什么不能直接把所有技能知识都写进 system prompt?🤔
从直觉上看,这种做法似乎很自然。因为 system prompt 本来就是最强的控制入口,把所有规则、所有工作流、所有最佳实践都塞进去,模型不就一直都能看到吗?这样一来,无论用户让它做 commit、review 还是测试分析,它都能随时参考那套规则。
但问题在于,system prompt 是一种 “常驻上下文资源”。只要你把内容放进去,它每一轮都会跟着一起被带上。也就是说,不管当前请求到底需不需要某个技能,那些技能内容都会一直占着 token,持续参与模型注意力分配。这种做法有两个很明显的坏处:
第一个坏处是 成本浪费。如果一个技能正文很长,而当前任务根本用不到它,那这些 token 就是在每一轮都白白支付。第二个坏处则更隐蔽,也更重要:系统提示越臃肿,模型越难聚焦当前真正相关的指令。因为 system prompt 本质上不是数据库,它更像是模型当前行为的背景设定,背景设定里东西太多,反而会让当前问题的信号变弱。
前面 s04 已经让我们意识到:不是所有探索过程都应该长期留在主上下文里;而 s05 则进一步告诉我们:不是所有知识也都应该长期常驻在 system prompt 里。有些知识其实和子任务很像 — 它们并不是一直都需要,而是 “在特定场景下才需要出现”。
所以这一节真正想解决的问题可以概括为一句话:
知识也应该像工具一样,按需获取,而不是默认常驻。
3. 解决方案
明确问题之后,s05 给出的解法非常漂亮,而且仍然延续了前几节一贯的工程风格:不去发明一套复杂的新协议,而是把 “知识加载” 也做成一个工具。
文档把这个设计总结成一个非常清晰的两层结构:
System prompt (Layer 1 -- always present):
+--------------------------------------+
| You are a coding agent. |
| Skills available: |
| - git: Git workflow helpers | ~100 tokens/skill
| - test: Testing best practices |
+--------------------------------------+
When model calls load_skill("git"):
+--------------------------------------+
| tool_result (Layer 2 -- on demand): |
| <skill name="git"> |
| Full git workflow instructions... | ~2000 tokens
| Step 1: ... |
| </skill> |
+--------------------------------------+
这就是 s05 最核心的架构思想:两层注入(two-layer skill injection)。
第一层是轻量层,也就是永远存在的技能目录。这里不会放技能正文,只会放技能名和简短描述,成本很低。它的作用不是 “让模型已经掌握技能”,而只是 “让模型知道有哪些技能可以调用”。
第二层则是按需层,也就是当模型真的需要某个技能时,通过 load_skill 工具把该技能的完整正文装进一次 tool_result 里,再送回当前上下文。
这个设计的妙处就在于,它没有把 “技能” 做成什么神秘的系统特权能力,而是继续沿用我们已经非常熟悉的那套 Agent 机制。模型先在 system prompt 里看到一份 “可用技能列表”,如果判断当前任务需要某个专门知识,就发起一次工具调用;程序执行这个工具,不是去操作文件系统,而是去读取对应的 SKILL.md 内容,再把完整正文作为 tool_result 回给模型。
也就是说,从模型视角看,技能并不是 “后台预装的一段隐形记忆”,而是一次按需请求后新出现在上下文里的信息块。
这一步变化的意义其实非常大。因为从这里开始,prompt 本身也开始被模块化了。前面几节主要是在模块化执行能力和上下文结构,而 s05 则开始模块化 “知识”。
4. On-Demand Skill Loading 流程图分析
这一节教程配了 6 张 On-Demand Skill Loading 的图,如果把它们连起来看,其实就是在解释这套两层注入架构是如何工作的。和 s03 的 Nag 图、s04 的 Context Isolation 图相比,这一组图更偏向说明 “知识是如何进入上下文” 的。

先看第一张图。顶部是 system prompt,旁边还特别标了一个灰色的 always present。system prompt 里面只列出了一组可用技能:
/commit/review-pr/test/deploy
右边的 token 柱状图显示大概是 120。下方说明写的是:
Layer 1: Compact Summaries
All skills are summarized in the system prompt. Compact, always present.
这张图对应的就是两层结构中的第一层。它想强调的是:system prompt 里并不是空的,但里面放的也不是技能全文,而只是一个非常便宜的 “技能目录”。也就是说,模型随时都知道有哪些技能存在,却不会因为这些技能的全部正文而承受巨大的上下文负担。
代码里的 SYSTEM 也确实是这样构造的:它调用 SKILL_LOADER.get_descriptions(),把所有 skill 的简短描述拼进 system prompt,而不是把 skill 正文放进去。

第二张图展示的是用户开始显式触发技能的场景。图里出现了一条:
User types: /commit
下方说明写的是:
Skill Invocation
The model recognizes a skill invocation and triggers the Skill tool.
这一步很关键,因为它说明技能不是自动加载的,也不是一上来就全部注入,而是需要模型先从第一层信息中识别出 “当前任务可能对应哪个技能”,然后主动触发 skill 工具。
也就是说,第一层的作用不是直接教模型怎么做,而是给模型一个 “可调用知识目录”。模型要不要调用、调用哪个 skill,依然是一次运行时决策。

第三张图就是整个 s05 最关键的转折点了。图里顶部仍然是那个简短的 system prompt 技能列表,但中间已经出现了一个具体 skill 的正文块:
SKILL.md: /commit
1. Run git status + git diff ...
2. Analyze all staged changes ...
...
同时右边的 token 柱状图从 120 增加到了 440。下方说明写的是:
Layer 2: Full Injection
The full skill instructions are injected as a tool_result, not into the system prompt.
这张图其实已经把整节课的核心设计讲透了。因为它明确说明:技能正文的注入位置不是 system prompt,而是 tool_result。 这意味着 skill 内容虽然会进入上下文,但它进入的方式和前面几节工具调用结果一样,属于一次普通的消息回填,而不是永久性地改写 system prompt。
代码里的 get_content() 也正是这样返回的:它不是返回裸文本,而是包装成 <skill name="..."> ... </skill> 的内容块,然后由 load_skill 工具通过 tool_result 送回模型。

第四张图继续说明这种注入方式的效果。图中 skill 正文块下方有一句黄色提示:
The Skill tool returns content as a tool_result message. The model sees it in context and follows the instructions. No system prompt bloat.
再下面的说明写的是:
In Context Now
The detailed instructions appear as if a tool returned them. The model follows them precisely.
这一点其实特别关键。因为它说明从模型视角看,这段技能正文并不会被区分成一种 “二等知识”。它看到的只是:当前对话上下文里刚刚多了一段结构化的、看起来非常像工具返回的指导内容。

第五张图展示了更进一步的情况。图中用户输入变成了:
User types: /review-pr
而下方同时出现了两个 skill 正文块:一个是 /commit,另一个是 /review-pr。底部说明写的是:
Stack Skills
Multiple skills can be loaded. Only summaries are permanent; full content comes and goes.
这一步说明技能不是一次只能加载一个,而是可以根据需要不断追加。也就是说,Layer 1 中那份常驻的技能目录始终都在,而 Layer 2 中的技能正文则是一个动态层:当前需要哪个,就加载哪个;如果一个任务连续需要多个技能,也可以一个一个叠加进当前上下文。

最后第六张图把整个效果总结了出来。图中 system prompt 里仍然只保留技能目录,而下方已经显示两个技能正文块同时被注入。底部说明写的是:
Two-Layer Architecture
Layer 1: always present, tiny. Layer 2: loaded on demand, detailed. Elegant separation.
这张图其实已经把 s05 整节课的全部价值讲完了。因为它说明 skills 机制真正解决的不是 “怎么多放一点知识”,而是 怎么优雅地把知识分成常驻层和按需层。前者便宜、稳定、低干扰;后者昂贵、详细、只在必要时出现。也正因为这样,模型既不会对可用技能毫无感知,也不会因为把所有技能正文都提前塞进 system prompt 而被拖垮。
完整动画演示如下图所示:

5. 工作原理(代码分析)
看完流程图之后,接下来我们正式进入 agents/s05_skill_loading.py。和前面几节一样,文件顶部先给出了这一节脚本意图的总结:
#!/usr/bin/env python3
# Harness: on-demand knowledge -- domain expertise, loaded when the model asks.
"""
s05_skill_loading.py - Skills
Two-layer skill injection that avoids bloating the system prompt:
...
Key insight: "Don't put everything in the system prompt. Load on demand."
"""
这段注释其实已经把 s05 的设计目标说得非常清楚了:按需知识(on-demand knowledge)。这里的关键词不是 “knowledge” 本身,而是 “loaded when the model asks”。也就是说,知识存在,但默认并不全部进入上下文,只有模型开口要的时候才加载进来。
前面 s04 的关键词是 “fresh context”,而 s05 的关键词则是 “load on demand”,两者本质上都是在节省上下文资源,只不过前者节省的是子任务过程,后者节省的是领域知识正文。
初始化部分和前面几节基本一致,依然是 .env、Anthropic 客户端、WORKDIR、MODEL 这些运行时准备逻辑。不过这一节新增了一个特别重要的路径:
SKILLS_DIR = WORKDIR / "skills"
这说明 skills 机制并不是把知识写死在 Python 代码里,而是正式开始引入一个外部知识目录 skills/。这点很重要,因为从工程角度看,只有当知识脱离代码本体,变成一种文件化资源,它才真正具备 “可维护、可新增、可替换” 的特性。也就是说,s05 不只是做了一个 load_skill 工具,它还第一次在项目结构上把 “知识库” 外置出来了。
这一节最核心的新组件,就是 SkillLoader。
class SkillLoader:
def __init__(self, skills_dir: Path):
self.skills_dir = skills_dir
self.skills = {}
self._load_all()
这个类的作用可以一句话概括:扫描 skills/ 目录下所有 SKILL.md,把它们变成系统可查询的技能表。
真正的扫描逻辑在 _load_all() 里:
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)}

上述代码把 skill 的最小组织单元定义得非常清楚:每个技能就是一个目录里的 SKILL.md 文件。系统会递归搜索所有 SKILL.md,把文件内容读出来,然后拆成两部分:前面的 frontmatter 元信息,以及后面的正文 body。
也就是说,一个 skill 从文件组织层面就已经天然分成了 “描述信息” 和 “详细内容” 两层,这其实正好对应了前面流程图里的 Layer 1 和 Layer 2。
接着看 _parse_frontmatter():
match = re.match(r"^---\n(.*?)\n---\n(.*)", text, re.DOTALL)
...
for line in match.group(1).strip().splitlines():
if ":" in line:
key, val = line.split(":", 1)
meta[key.strip()] = val.strip()
这里采用的是一个非常轻量的 YAML frontmatter 解析方式。它并没有引入完整 YAML 解析器,而是通过 --- ... --- 区域做简单切分,再一行行解析 key: value。
从严格性上看,这当然不是一个完整 YAML 实现,但对教程项目来说已经足够表达核心思想:skill 文件前面有一段元数据,后面是正文。元数据用于 system prompt 层的简短描述,正文用于按需加载时的完整注入。也就是说,skills 文件自己本身就在支持 “两层注入架构”。
有了技能表之后,接下来就要看第一层是怎么生成的。答案在 get_descriptions():
def get_descriptions(self) -> str:
if not self.skills:
return "(no skills available)"
lines = []
for name, skill in self.skills.items():
desc = skill["meta"].get("description", "No description")
tags = skill["meta"].get("tags", "")
line = f" - {name}: {desc}"
if tags:
line += f" [{tags}]"
lines.append(line)
return "\n".join(lines)

这段代码对应的就是前面图里的 Layer 1。它做的事情其实非常简单:遍历所有 skill,把 name、description,以及可选的 tags 拼成一串简短描述列表。
也就是说,system prompt 并不会知道 skill 正文内容,它只知道 “这里有哪些技能、每个技能大概是干什么的”。这就是为什么第一层 token 很低,因为这里提供的只是一个目录,而不是知识正文。
接着再看第二层生成逻辑,也就是 get_content():
def get_content(self, name: str) -> str:
skill = self.skills.get(name)
if not skill:
return f"Error: Unknown skill '{name}'. Available: {', '.join(self.skills.keys())}"
return f"<skill name=\"{name}\">\n{skill['body']}\n</skill>"
这一段可以说是整个 s05 的灵魂。因为它说明 skill 的完整正文是如何被真正 “打包” 进一次工具返回值里的。系统会按名称找到对应 skill,然后把 body 包进:
<skill name="...">
...
</skill>
这样的结构里返回。这个包装其实非常聪明,因为它既让 skill 正文保留了明确边界,又让模型很容易把这段内容识别成一次结构化注入。
更重要的是,如果 skill 不存在,还会给出错误信息和可用技能列表,这意味着 load_skill 也不是一个黑盒魔法,而是一个普通但设计得不错的工具接口。
有了 SkillLoader 之后,接下来就来看两层如何接进系统。
首先是 Layer 1 进入 system prompt:
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()}"""

这段 prompt 特别值得注意。它并不试图在 system prompt 里提前把所有 skill 正文讲完,而是明确告诉模型两件事:第一,你可以使用 load_skill 来获取专门知识;第二,下面是当前可用技能清单。也就是说,system prompt 这里扮演的角色更像是 “导航页”,而不是 “完整说明书”。这正对应了前面图里的第一层:始终存在,但尽量轻。
然后再看工具层。和前面几节一样,s05 依旧没有试图发明一套 “技能专用协议”,而是继续把技能加载这件事做成一个普通工具:
TOOL_HANDLERS = {
"bash": lambda **kw: run_bash(kw["command"]),
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
"load_skill": lambda **kw: SKILL_LOADER.get_content(kw["name"]),
}
这里的 "load_skill": ... 就是第二层的真正入口。也就是说,对运行时来说,skills 机制本质上并不神秘,它只是 dispatch map 中多出来的一项映射而已。
前面 s02 里我们说 “加工具 = 加 handler + 加 schema”,到了 s05 其实仍然完全成立,只不过这次新增的 handler 不是去操作文件系统或命令行,而是去操作知识库。换句话说,skill 不是系统提示的一部分,而是工具系统的一部分。
再往下看 TOOLS 数组:
{"name": "load_skill", "description": "Load specialized knowledge by name.",
"input_schema": {"type": "object", "properties": {"name": {"type": "string", "description": "Skill name to load"}}, "required": ["name"]}},
这说明从模型视角看,技能加载也完全是一种结构化调用。模型并不是 “隐式想起某个 skill”,而是要明确发起一次名字为 load_skill 的工具调用,并提供 skill 名称参数。也就是说,前面图中用户输入 /commit、模型再识别并触发 skill 工具,这件事在协议层面最终都会落成这样一种结构化动作。
最后再来看 agent_loop(),你会发现这一节最漂亮的地方,依旧是那个已经非常熟悉的 “底层 loop 基本没变” 的感觉:
def agent_loop(messages: list):
while True:
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return
results = []
for block in response.content:
if block.type == "tool_use":
handler = TOOL_HANDLERS.get(block.name)
try:
output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
except Exception as e:
output = f"Error: {e}"
print(f"> {block.name}: {str(output)[:200]}")
results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(output)})
messages.append({"role": "user", "content": results})
先看整体结构:最外层 while True 没变;每轮先调 client.messages.create(...) 没变;把 assistant 输出追加进 messages 没变;通过 stop_reason != "tool_use" 判断退出也没变;执行工具之后再把 tool_result 作为新的 user 消息加回上下文,仍然没变。
真正变化的地方,其实只有一点:工具集中多了一个 load_skill,而这个工具的返回值不再是命令输出或文件内容,而是一段完整的技能正文。也就是说,从底层控制流看,skills 机制和前面几节的工具机制根本是同一套框架。区别只在于,这次被加载进上下文的是 “知识”,而不是 “外部世界状态”。
这也正是 s05 最漂亮的地方。它表面上看引入了一个新的知识系统,好像事情突然复杂了很多;但真正落到 agent 核心骨架上,改动依然极其克制。底层 loop 完全没变,tool_use / tool_result 协议也没变,dispatch map 的结构也没变。
变化只是在 system prompt 里加了一层廉价的技能目录,又在工具层里加了一个 load_skill 入口,让昂贵知识在需要时才通过 tool_result 被送回上下文。这种设计风格和 s02、s03、s04 是完全一致的,也正因为这样,这个项目才会一直显得非常整洁:每一节都在加能力,但几乎从不推翻前面已经建立起来的核心结构。
到了 s05,整个系统已经不只是一个会调工具、会写 todo、会生成 subagent 的 Agent 了,它开始第一次长出一种更成熟的知识管理能力:哪些知识永远挂在背景里,哪些知识只在需要时短暂进入上下文。
博主在给定下面的提示词情况下:
I need to do a code review -- load the relevant skill first
想通过调试看看整个过程发生了什么,我们来具体分析下:
第一次 loop

模型第一次响应结果

工具第一次执行结果
第一次 loop 的行为其实非常标准,也非常能体现 s05 这一节的核心机制。从调试图里可以看到,这一轮 response.content[0] 先是一个 TextBlock,模型先明确说明自己要先为我们加载 code review skill;紧接着 response.content[1] 就出现了一个 ToolUseBlock,调用的工具名正是 load_skill,而且传入的参数也很明确,就是 name = "code-review"。
这一步非常关键,因为它说明 system prompt 中那层常驻的 skill 摘要列表确实起作用了。模型并不是一上来就已经拥有完整的 code review 工作流,而是先从 “可用技能目录” 中意识到存在一个叫 code-review 的技能,然后通过一次结构化工具调用,把这个技能的完整正文拉进当前上下文。
也就是说,前面流程图里说的那套 “两层注入” 在这里第一次真正落地了:第一层是 system prompt 里对 code-review 的简要描述,第二层则是当前这次 load_skill("code-review") 所返回的完整技能正文。
再看工具第一次执行结果,就会发现 results[0]["content"] 里已经不再是普通的命令输出或者文件内容,而是被包装成了:
<skill name="code-review">
...
</skill>
其中正文正是 code-review 这个 skill 的完整内容。它里面不仅明确写了这是一份 Code Review Skill,还给出了一整套结构化审查流程,比如先看安全性,再看正确性、性能、可维护性、测试等,同时还提供了具体检查项和建议命令。
第二次 loop

模型第二次响应结果
第二次 loop 非常有意思,因为它展示了 skill 加载完成之后,模型是如何 “消化” 这份技能正文的。从调试图里可以看到,这一轮已经没有再出现任何工具调用,response 只剩下一个纯文本回答。模型先说自己已经成功加载了 code review skill,然后开始询问我们接下来到底想 review 什么内容,比如:
- 想 review 哪些代码 / 文件
- 代码所处的上下文是什么
- 有没有特别关注的方向
我们继续给它提供下面的提示词:
I want you to review agents/s05_skill_loading.py
新请求第一次 loop

新请求模型第一次响应结果

新请求工具第一次执行结果
新请求第一次 loop 的行为也很符合预期。从调试图里可以看到,这一轮 response.content[0] 先是一个 TextBlock,模型说明自己要先读取 agents/s05_skill_loading.py 这个文件来理解其内容;紧接着 response.content[1] 就调用了 read_file 工具,路径正是 agents/s05_skill_loading.py
再看工具执行结果,可以看到返回的是 s05_skill_loading.py 文件内容的前面一部分,包括脚本头部注释、SkillLoader 的整体说明、两层 skill 注入的设计思路以及部分代码实现。也就是说,到这一轮结束时,模型已经同时拥有了两类信息:
- 上一轮加载进来的 code-review skill 正文
- 当前刚读到的 被 review 文件内容
如果一切按最理想情况推进,那么接下来它就应该基于这两部分信息直接进入正式的 code review 过程。
新请求第二次 loop

新请求模型第二次响应结果
从模型第二次响应结果来看似乎出了一些问题,模型并没有通过之前对话已经加载的 code-review skill 去 review 代码,而是想去找有没有一些 skill 能够解决这个问题。
也就是说,模型此时并没有 “想起” 自己前面其实已经通过 load_skill("code-review") 把 code-review 这份 skill 正文加载进上下文了,而是开始重新在 skills/ 目录里查找 SKILL.md,像是在重新确认有哪些 skill 存在、以及应该加载哪一个 skill 才能继续完成这次 review。
这个现象确实会让人有点困惑,因为从理论上看,整个上下文并不长,前面对话里也明确出现过一次完整的 load_skill 结果,如下图所示,模型本来应该有机会直接沿着那份 skill 去行动。

博主猜测可能是由于使用的 deepseek 模型本身能力较弱的原因。
这里重新读取文件加载 SKILL.md 的过程博主就跳过了,我们直接开始看加载完成后模型做的事情
新请求第五次 loop

新请求模型第五次响应结果
新请求第五次 loop 才真正开始进入 code review 的正文阶段。从调试图里可以看到,这一轮 response.content[0] 先是一个 TextBlock,模型明确说自己现在要对代码做一个快速的安全检查;紧接着 response.content[1] 就调用了 bash 工具,执行的命令大致是:
grep -n "password\|secret\|api_key\|token\|key" agents/s05_skill_loading.py
这一步非常有代表性,因为它正好和 code-review skill 正文里的第一大检查项完全对上了:
### 1. Security (Critical)
Check for:
- [ ] **Injection vulnerabilities**: SQL, command, XSS, template injection
- [ ] **Authentication issues**: Hardcoded credentials, weak auth
- [ ] **Authorization flaws**: Missing access controls, IDOR
- [ ] **Data exposure**: Sensitive data in logs, error messages
- [ ] **Cryptography**: Weak algorithms, improper key management
- [ ] **Dependencies**: Known vulnerabilities (check with `npm audit`, `pip-audit`)
```bash
# Quick security scans
npm audit # Node.js
pip-audit # Python
cargo audit # Rust
grep -r "password\|secret\|api_key" --include="*.py" --include="*.js"
SKILL.md 中明确写着,Code Review 的第一部分就是 Security (Critical),而且示例命令里就包含用 grep 去扫描 password、secret、api_key 等敏感关键词的做法。
可以看到模型没有先去做性能分析,也没有先去看命名风格,而是优先进入安全性检查,这正说明 SKILL.md 里的 structured approach 已经开始真正驱动模型的决策顺序。
后续就是根据 SKILL.md 不断完成 review 过程,这里就不一一展示了,我们直接看最终结果。
模型最终输出结果

模型最终输出结果
最终输出结果清楚地展示了 skill 对模型最终输出风格的影响。从图里可以看到,模型最后给出的并不是一段很随意的 “我觉得代码还不错” 式评论,而是一份相当完整、结构化很强的 code review 报告。
它先给出了整体 Summary,然后进入 Critical Issues,明确点出了几类严重问题,比如:
run_bash中的命令注入风险- 危险命令过滤仍然不完整
safe_path在路径安全上的潜在不足- 异常处理过于粗糙
- 输出长度硬编码
- Skill frontmatter 解析过于脆弱
接着又继续给出 Improvements、Positive Notes、Testing Considerations 和 Verdict 等内容。也就是说,最终输出的结构已经明显非常接近 code-review skill 里定义的 Review Output Format。这说明前面重新加载进来的 skill 正文,最终确实成功影响了模型的输出组织方式,而不只是影响了某一两个工具调用动作。
整个过程中终端的输出如下图所示:

从这次调试结果来看,skill 的确可以以 tool_result 的形式注入上下文,并最终影响模型后续行为与输出格式。
OK,以上就是 Skills 工作原理的完整分析了。
大家也可以试试下面这些 prompt,感受下按需技能加载之后,这个 Agent 在知识使用方式上会有什么变化:
1. What skills are available?
2. Load the agent-builder skill and follow its instructions
3. I need to do a code review -- load the relevant skill first
4. Build an MCP server using the mcp-builder skill
6. 相对 s04 的变更
| 组件 | 之前 (s04) | 之后 (s05) |
|---|---|---|
| Tools | 5 (基础 + task) | 5 (基础 + load_skill) |
| 系统提示 | 静态字符串 | + 技能描述列表 |
| 知识库 | 无 | skills/*/SKILL.md 文件 |
| 注入方式 | 无 | 两层 (系统提示 + result) |
7. 小结
如果说 s04 最大的贡献,是让我们看到 “不是所有任务都该在一个上下文里完成”;那么 s05 的贡献就是让我们进一步意识到:不是所有知识也都该永久放在 system prompt 里。
从教程文档来看,这一节最核心的结论其实非常明确:你当然可以把 git 约定、代码审查流程、测试规范、部署流程全都提前塞进系统提示里,但这种做法成本高、干扰大,而且大部分内容在当前请求中根本不会被用到;更好的方法是把知识拆成两层,第一层用便宜的技能目录长期挂在 system prompt 中,第二层则在模型真正需要的时候,通过 load_skill 工具把完整正文作为 tool_result 注入当前上下文。
文档和代码都明确体现了这一点:SkillLoader 扫描 skills/*/SKILL.md,frontmatter 提供 description 供第一层使用,body 则在 get_content() 中被包装成 <skill> 内容块,通过 load_skill 返回给模型。
所以,s05 真正的价值并不只是多了一个 load_skill 工具,也不只是把知识文件放进了 skills/ 目录,而是第一次把 “prompt 本身” 也做成了模块化系统。从这一节开始,这个项目已经不再只是一个会组织执行流程的 Agent,而开始真正长出一种更成熟的知识管理思维:常驻的是技能目录,按需出现的是技能正文;模型不用永远背着全部知识,但它永远知道可以去哪里取。
OK,以上就是本期想要分享的全部内容了。
结语
本篇文章我们围绕 s05 Skills 这一节,从问题出发,结合流程图与代码实现,完整梳理了 Agent 是如何通过 “按需加载” 的方式管理领域知识的。
通过两层注入架构,系统将技能拆分为 “轻量目录” 和 “完整正文” 两部分 — 前者始终存在,用来告诉模型 “有哪些能力可以使用”;后者按需加载,只在真正需要时进入上下文。这种设计既避免了 token 浪费,也让模型在复杂场景下能够更聚焦当前任务。
从工程视角来看,这一节最重要的意义在于:prompt 不再是一次性写死的文本,而开始演变为一种可组合、可加载、可管理的模块化系统。前面几节我们在模块化执行能力和上下文结构,而到了 s05,模块化的对象进一步扩展到了 “知识本身”。
也正因为如此,Skills 的引入标志着 Agent 系统的一个关键转变:从 “如何组织行为与上下文”,迈向 “如何组织知识”。模型不再需要在一开始就背负所有信息,而是可以在运行过程中动态获取所需能力,这使得系统在扩展性与可维护性上都迈出了重要一步。
下篇文章我们将来学习第三部分 Memory Management 中 s06 compact 章节的内容,敬请期待🤗
参考
更多推荐




所有评论(0)