工具和 Skill 的区别
上一篇讲了怎么添加新工具(Python 类,有具体逻辑,返回结构化数据)。工具是 agent 执行具体动作的"手"------读文件、执行命令、搜索代码,每一件事都是确定性的、可预期的,结果也是结构化的数据,可以被后续步骤解析和使用。
这一篇讲 Skill------它和工具完全不同。Skill 不执行任何 IO,不碰文件系统,不调用外部服务。它做的唯一一件事,就是把一段精心编写的文字(指令)注入到当前对话,让 agent 知道接下来该怎么做。如果说工具是 agent 的"手",那 Skill 就是 agent 的"说明书"。
工具 :执行具体动作(读文件、执行命令、搜索代码)。它是 agent 的"手",做确定性的事情。工具的行为是可预测的:你给它输入,它返回输出,中间没有歧义。比如 Read 工具,你告诉它读 src/main.py,它就老老实实把文件内容读出来返回给你,不会多做任何事。
Skill :给 agent 一套专家指令,改变它怎么做事。它是 agent 的"说明书",描述一类任务的最佳实践。Skill 不关心"做什么",它关心"怎么做"------它告诉 agent 面对某类任务时应该遵循什么样的思考路径、检查哪些维度、按什么顺序推进。同一个任务,没有 Skill 的 agent 可能凭直觉乱来,有了 Skill 的 agent 会按照方法论一步步执行。
举一个具体例子:
Read工具:给我读src/main.py这个文件。这是一个确定性的动作,输入是文件路径,输出是文件内容,没有任何判断空间。code-reviewSkill:给 agent 一套代码 review 的方法论------先看架构、再看逻辑、然后检查错误处理、最后看命名规范。这是一个开放性的任务,没有 Skill 时 agent 可能只盯着语法错误,有了 Skill 它就知道要按维度逐一检查,最后还要给出优先级排序的总结。
Skill 不执行任何 IO,它只是把一段文字(指令)注入到当前对话,让 agent 知道接下来该怎么做。这段文字本身不产生任何副作用------不写文件、不发请求、不执行命令。它的全部价值在于:把人类专家积累的经验,以纯文本的形式"教"给 agent,让 agent 在面对同类任务时能复用这套方法论。
这也是 Skill 和工具最本质的区别:工具扩展的是 agent 的能力边界(能做什么),Skill 扩展的是 agent 的思维方式(怎么想)。工具是"新增一只手",Skill 是"换一套大脑的思考方式"。
结论先说
写一个 Skill 需要两步:
| 步骤 | 做什么 |
|---|---|
| 1. 创建 SKILL.md | 写 frontmatter + 正文指令,放到 skills/<name>/SKILL.md |
| 2. 在对话里调用 | agent 自己会用 Skill(name="...") 加载,或者用户直接说"用 xxx skill" |
不需要重启 agent,不需要改任何代码。文件放上去,下一次 agent 扫描就能用。
一、SKILL.md 的格式
一个 Skill 文件只有两部分:frontmatter(元信息)和正文(指令内容)。
markdown
---
name: code-review
description: 按照团队规范对指定文件做 code review
---
# Code Review 指南
对 $ARGUMENTS 进行 code review,按照以下维度逐一检查:
## 1. 架构与设计
- 这个模块的职责是否单一?
- 有没有对外暴露不必要的内部细节?
## 2. 错误处理
- 所有异常路径是否都被处理?
- 错误信息是否足够清晰?
## 3. 命名与可读性
- 变量名、函数名是否见名知意?
- 是否有必要的注释(非 obvious 的逻辑)?
## 4. 测试覆盖
- 关键路径是否有测试?
- 边界情况是否被覆盖?
最后给出一个总结,指出最重要的 1-3 个问题和改进建议。
$ARGUMENTS 是一个特殊占位符,用户调用 Skill 时传入的参数会替换这里。如果 Skill 正文里没有 $ARGUMENTS,用户传入的参数会被追加到正文末尾。
frontmatter 只有两个必填字段:
name:Skill 的唯一标识符,只能包含小写字母、数字和连字符(a-z0-9-)description:一句话描述,这句话会被注入到系统提示词里,让 agent 知道有这个 Skill 可用
二、Skill 是怎么被发现的
extensions/skills/loader.py 里的 SkillLoader 负责扫描和缓存 Skill:
python
# extensions/skills/loader.py
class SkillLoader:
def __init__(self, project_root: str, skills_dir: str = "skills"):
self._skills_dir = (Path(project_root) / skills_dir).resolve()
self._skills: Dict[str, SkillMeta] = {}
self._last_scan_mtime: float = 0.0 # 上次扫描时所有 SKILL.md 的最大 mtime
self._last_scan_count: int = 0 # 上次扫描时的文件数量
def refresh_if_stale(self) -> List[SkillMeta]:
current_max_mtime, current_count = self._get_skills_state()
if current_max_mtime != self._last_scan_mtime or current_count != self._last_scan_count:
return self.scan() # 文件有变化 → 重新扫描
return self.list_skills(refresh=False) # 无变化 → 直接返回缓存
缓存策略很巧妙:不是每次都重新扫描文件内容,而是只做 stat() 调用比对两个数字------所有 SKILL.md 的最大 mtime 和 文件数量。
- 如果有文件被修改(mtime 变了)→ 重新扫描
- 如果有文件被新增或删除(count 变了)→ 重新扫描
- 否则直接返回内存中的缓存
stat() 的开销比读文件内容小几个数量级,所以这个策略让"检查是否需要更新"的开销接近零,同时又能做到文件一改立刻生效。
三、description 是怎么被注入系统提示词的
SkillLoader 里还有一个方法:
python
def format_skills_for_prompt(self, char_budget: int) -> str:
"""把 SkillMeta 列表格式化为注入系统提示词的文本。"""
...
# 输出格式:
# - code-review: 按照团队规范对指定文件做 code review
# - gen-commit-msg: 生成符合 Conventional Commits 格式的提交信息
这段文字会被注入到系统提示词的 "Skills" 部分,大概是这样:
yaml
## Available Skills
The following project-specific skills are available via the Skill tool:
- code-review: 按照团队规范对指定文件做 code review
- gen-commit-msg: 生成符合 Conventional Commits 格式的提交信息
注意:这里只注入了名字和描述 ,不是 Skill 的完整正文。完整正文只在 agent 真正调用 Skill(name="code-review") 时才被读取和返回。
这个设计节省了 token:系统提示词里只有一行摘要,不会因为有很多 Skill 就把上下文撑爆。char_budget 参数控制了摘要总长度的上限(默认 12000 字符),超过预算的 Skill 会被截断不展示。
四、agent 调用 Skill 时发生了什么
当 agent 决定调用 Skill(name="code-review", args="src/main.py") 时,SkillTool.run() 做了这几件事:
python
# tools/builtin/skill.py
def run(self, parameters):
name = parameters.get("name")
args = parameters.get("args") or ""
# 1. 从 loader 获取 SkillMeta(包含文件路径)
skill_meta = self._skill_loader.get_skill(name.strip(), refresh=self._refresh_on_call)
# 2. 读取 SKILL.md 的完整内容
raw_content = Path(skill_meta.path).read_text(encoding="utf-8")
# 3. 解析 frontmatter,得到正文 body
_frontmatter, body = _parse_frontmatter(raw_content)
# 4. 把 args 填入 $ARGUMENTS 占位符(或追加到末尾)
expanded = _apply_arguments(body, args)
# 5. 返回展开后的完整指令
return self.success_result(
data={"content": expanded, "name": name, "base_dir": skill_meta.base_dir},
text=f"Loaded skill '{name}'.",
...
)
_apply_arguments 的逻辑:
python
def _apply_arguments(body: str, args: str) -> str:
if "$ARGUMENTS" in body:
return body.replace("$ARGUMENTS", args) # 有占位符 → 定点替换
if args.strip():
return f"{body}\n\nARGUMENTS: {args}" # 无占位符 → 追加
return body # 没有 args → 原样返回
agent 收到 Skill 的返回值后,会把这段展开的指令当作"当前任务的操作手册"来执行。
五、写一个实际的 Skill
现在写一个 Skill:生成符合 Conventional Commits 规范的提交信息。
目录结构:
markdown
skills/
└── gen-commit-msg/
└── SKILL.md
SKILL.md 内容:
markdown
---
name: gen-commit-msg
description: 根据当前 git diff 生成符合 Conventional Commits 规范的提交信息
---
请根据以下 git diff 内容,生成一条符合 Conventional Commits 规范的提交信息。
$ARGUMENTS
## 规范要求
**格式**:`<type>(<scope>): <description>`
**type 类型**:
- `feat`: 新功能
- `fix`: 修复 bug
- `refactor`: 重构(不影响功能的代码改动)
- `docs`: 文档变更
- `test`: 测试相关
- `chore`: 构建、配置、依赖变更
**要求**:
- description 用中文,简洁清晰,不超过 50 个字
- 如果变更跨越多个模块,scope 可以省略
- 只输出提交信息本身,不要额外的解释
**示例**:
feat(auth): 添加 JWT token 刷新机制 fix(tools): 修复 Read 工具在处理空文件时的边界错误 refactor(context): 将 HistoryManager 拆分为 History 和 ModelView
用户调用方式:
bash
# 在对话中:
先用 bash 跑一下 git diff --staged,然后用 gen-commit-msg skill 生成提交信息
agent 会:
- 调用
Bash(command="git diff --staged")获取 diff - 调用
Skill(name="gen-commit-msg", args="<diff 内容>")加载指令 - 按照 Skill 里的规范生成提交信息
六、Skill 和工具的选择标准
什么时候该写 Skill,什么时候该写工具?
用 Skill 的场景:
- 你想让 agent 用一套特定的方法论处理某类任务(code review、写文档、生成测试等)
- 这套方法论是自然语言描述的,不需要执行具体代码
- 你想让这个行为可以快速修改、迭代,不用改 Python 代码
用工具的场景:
- 你需要 agent 能执行一个具体的动作(调用 API、读写文件、执行命令)
- 动作的结果是结构化的数据,需要被后续步骤解析使用
- 你需要沙箱保护、超时控制、错误码等系统级的保障
简单说:Skill 改变"怎么想",工具改变"能做什么"。
设计亮点
1. 零代码扩展
只需要一个 Markdown 文件,不需要写 Python,不需要重启服务。这让非工程师(产品、设计、运营)也能为 agent 添加领域专业知识。
2. 热加载
refresh_if_stale() 的增量检查机制(只做 stat 比对)保证了 Skill 文件改动后会被立即感知,而不需要重启。这在调试和迭代 Skill 时非常有用。
3. 按需加载正文
系统提示词里只注入名字和描述摘要,Skill 的完整正文只在被调用时才读取。这让可以安装很多 Skill 而不担心把上下文窗口撑爆。
4. $ARGUMENTS 占位符
允许 Skill 作者精确控制参数在指令中的位置,而不是总是追加到末尾。这让 Skill 里的"前言"和"参数"可以自然地融合在一起。
小结
| 设计选择 | 方案 | 工程价值 |
|---|---|---|
| 格式 | Markdown frontmatter + 正文 | 人类可读,无需解析器 |
| 缓存 | mtime + count 增量检查 | 文件改动立即生效,stat 开销接近零 |
| 注入策略 | 系统提示词只注入摘要 | 多 Skill 不撑爆上下文 |
| 参数注入 | $ARGUMENTS 占位符 | 作者控制参数位置,比追加灵活 |
下一篇是最后一篇:接入 MCP 服务,把 agent 与外部工具生态连接起来。
关于本系列的源码
本系列所有分析均基于开源项目 MyCodeAgent。
源码里已经按照本系列文章的讲解顺序,在关键位置加入了配套注释------读文章时可以对照代码,也可以直接克隆下来自己跑、改、扩展,基于它开发你自己的 agent。
bash
git clone https://github.com/chendongqi/MyCodeAgent
cd MyCodeAgent
cp .env.example .env # 填入你的 LLM API key
uv sync
uv run python main.py
欢迎访问 PrimeSkills ------ 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。
更多实用知识和有趣产品,欢迎访问我的个人主页