Code Agent 解剖(22):从零扩展——用 Markdown 写一个 Skill

工具和 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-review Skill:给 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 会:

  1. 调用 Bash(command="git diff --staged") 获取 diff
  2. 调用 Skill(name="gen-commit-msg", args="<diff 内容>") 加载指令
  3. 按照 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 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。

更多实用知识和有趣产品,欢迎访问我的个人主页

相关推荐
小饕1 小时前
llama.cpp 参数调优指南:Jetson 8GB 实战手记
人工智能·llama·大模型端侧部署
火山引擎开发者社区1 小时前
Viking AI 搜索 × SearchCLI:搭建售后助手,让搜索能力参与售后回答
人工智能
冬奇Lab1 小时前
一天一个开源项目(第210篇):herdr - AI 编程 Agent 的运行时底座
人工智能·开源·资讯
dadanhuang2 小时前
PyTorch深度学习与实践【04】【迭代周期、autograd、构建计算图、*params参数解包、.grad属性】
人工智能·pytorch·深度学习
JavaPub-rodert2 小时前
LangChain 从入门到 Agent 实战:用 Python 搭建一个真正能调用工具和知识库的 AI 助手
人工智能·python·langchain
米小虾2 小时前
告别逐 token 蹦字:扩散语言模型(dLLM)到底能不能终结自回归?
人工智能·llm
火山引擎开发者社区2 小时前
从多模态数据湖到 Agent 湖:Lance 的格式设计与实践|Lance Meetup 火热报名中
人工智能
一枚爱吃大蒜的程序员2 小时前
CSDN文章-注意力约束QLoRA教育大模型微调
人工智能·机器学习·语言模型·qlora·大模型微调·注意力约束
技术小事2 小时前
AI挖出6个curl漏洞 但另外23份是噪音
人工智能·网络安全·漏洞挖掘·cve·curl·ai安全