手把手实现 Agent Skills 机制:让 AI Agent 真正复用工程经验
手把手实现 Agent Skills 机制:让 AI Agent 真正复用工程经验
📖 摘要:2026 年 GitHub Trending 上
agent-skills类仓库合计近 30 万 star,Skills 已成为 AI Agent 工程化的关键一层。但多数文章只做"仓库推荐",没人讲清它到底是什么、怎么自己实现。本文从工程角度拆解 Skill 的定义、两阶段加载机制与路由原理,并手撸一个可运行的 Python Skills 加载器,帮你把零散的"工程经验"沉淀成 Agent 可复用的能力模块。读完你将拥有一个即插即用的 Skills 框架雏形。🏷️ 关键词:AI Agent,Agent Skills,大模型工程化,技能加载,Prompt 工程
目录
一、背景与痛点
1.1 Agent 的"失忆"困局
我们给 AI Agent 接上工具、配上模型,跑通第一个 Demo 时都很兴奋。但一旦进入真实业务,立刻撞上一堵墙:经验无法沉淀。
举个例子:你在 user_service 项目里花了一下午,摸清了"如何用内部 SDK 拉取用户信息并做权限校验"。第二天新开一个会话,Agent 又从零开始翻文档、试错、踩你昨天刚踩过的坑。每一次新对话,都是一次"失忆重启"。
传统做法是把这些知识写进 System Prompt,或者塞进一份超长的 CLAUDE.md / AGENTS.md。但提示词越长,token 越贵、上下文越容易互相干扰,模型反而越容易"迷路"。我们需要一种"按需调用、即插即用"的能力模块机制——这就是 Skills 要解决的问题。
1.2 Skills 为什么突然火了
2026 年 8 月 GitHub Trending 周榜前 15 名里,至少 9 个直接涉及 Agent / Skills / 记忆 / 上下文。其中两个 Skills 仓库格外亮眼:
mattpocock/skills:工程师实战技能集合,208k+ star;addyosmani/agent-skills:生产级 AI 编码技能库,83.8k+ star,覆盖 Code Review、测试、部署等标准化 SOP。
开源社区的注意力,正在从"单体大模型"转向"Agent 工程系统"。Skills 就是这套系统里的能力模块层——它让 Agent 像人一样,把"我会做什么、怎么做"固化成可复用、可分享的文件。
1.3 Skills 不是什么(常见混淆)
讲实现前,必须先划清边界,否则很容易和下面几个概念搞混:
| 概念 | 它解决什么 | 和 Skills 的区别 |
|---|---|---|
| Prompt | 一次性指令 | 写死在对话里,不复用、不发现 |
| Tool / Function Call | 让模型调一个具体函数(如查天气) | Tool 是"动作接口",Skill 是"动作背后的方法论+流程" |
| Plugin | 平台级功能扩展 | 通常绑定特定产品,Skill 是普通 Markdown 文件,跨框架 |
| MCP Server | 标准化的工具连接协议 | MCP 解决"连什么",Skill 解决"连上之后怎么干" |
一句话:Tool 是手,Skill 是脑中的"操作手册"。 Skill 可以调用 Tool,但本身是一段结构化的经验指令。
二、核心原理
2.1 关键概念:一个 Skill 的最小结构
一个 Skill 本质上就是一个带元数据的 Markdown 文件,约定俗成叫 SKILL.md。最小结构长这样:
---
name: user-permission-check
description: 当用户要求校验某账号权限、判断角色、或拉取 user_service 用户信息时使用。
---
# 用户权限校验技能
## 步骤
1. 调用 user_service 的 GET /internal/v1/profile 拿到原始档案。
2. 用 permission-sdk 的 evaluate(uid, resource) 做策略判定。
3. 返回 { role, allowed, reason },allowed=false 时必须带 reason。
## 注意
- uid 缺失直接报错,不要猜测默认值。
- 示例数据,仅作演示,实际接口以内部文档为准。
关键是顶部那段 YAML frontmatter:
name:技能唯一标识,路由时用它定位。description:这是最重要的字段——它决定 Agent 在无数技能里能不能"想得起"这个技能。它必须是"触发场景"描述,而不是功能罗列。
正文是给 Agent 看的操作手册,可以放步骤、代码、反例、注意事项。
2.2 工作机制:两阶段加载
Skills 能省 token,靠的是一个核心设计:两阶段加载(two-phase loading)。
- 阶段一(元数据扫描):Agent 启动时,只读取每个
SKILL.md的name+description,汇总成一份极短的"技能清单"注入系统提示。这一段通常只有几十到几百字。 - 阶段二(按需加载):只有当 LLM 判断当前任务需要某个技能时,才去读取该
SKILL.md的完整正文,塞进上下文。
💡 为什么不能直接全加载?一个有 30 个技能的库,全文可能上万字。全塞进上下文既烧钱又干扰推理。两阶段加载让"知道有哪些能力"和"展开具体能力"解耦。
2.3 Agent 如何"发现→加载→执行"
整个闭环可以用一张流程串起来:
启动 ──► 扫描 skills/ 目录 ──► 注入 [name + description] 清单
│
用户任务 ──► LLM 判断该用哪个 Skill ──► 命中 name
│
──► 读取该 SKILL.md 全文 ──► 按手册执行 ──► 返回结果
"LLM 判断该用哪个 Skill"这一步叫路由(routing)。生产环境里通常用 LLM 做语义匹配,但也必须保留一个确定性的兜底(比如关键词匹配),防止模型抽风调错技能。
三、实战落地
3.1 环境准备
我们只需要 Python 3.10+ 和一个 YAML 解析库。代码保证可运行:
# 创建项目
mkdir -p my-agent/skills && cd my-agent
python -m venv .venv && source .venv/bin/activate # Windows 用 .venv\Scripts\activate
pip install pyyaml
目录约定:
my-agent/
├── skill_loader.py # 我们马上写的加载器
└── skills/
└── user-permission-check/
└── SKILL.md # 一个技能
3.2 定义 SKILL.md 规范
在 skills/user-permission-check/SKILL.md 写入 2.1 节的示例内容(已给出)。再建一个 skills/order-refund/SKILL.md 作路由对照:
---
name: order-refund
description: 当用户要处理退款、冲销、或查询 order_service 某笔订单的退款状态时使用。
---
# 订单退款技能
1. 先查 order_service GET /orders/{id} 确认状态。
2. 状态为 SHIPPED 不允许自动退款,转人工。
3. 调用 refund-sdk.apply(order_id, reason) 发起冲销。
3.3 实现 Skill 加载器(Python)
3.3.1 扫描元数据阶段
# skill_loader.py
import os
import yaml
from dataclasses import dataclass
from typing import Optional
SKILL_DIR = "./skills"
@dataclass
class Skill:
name: str
description: str
path: str
body: str = "" # 阶段二才填充
@classmethod
def from_file(cls, path: str) -> "Skill":
text = open(path, "r", encoding="utf-8").read()
fm, body = _split_frontmatter(text)
meta = yaml.safe_load(fm) if fm.strip() else {}
return cls(
name=meta.get("name", os.path.basename(os.path.dirname(path))),
description=meta.get("description", ""),
path=path,
body=body.strip(),
)
def _split_frontmatter(text: str):
"""只认 '---' 包裹的 YAML 头,避免误切正文里的分隔线。"""
if text.startswith("---"):
parts = text.split("---", 2)
if len(parts) == 3:
return parts[1], parts[2]
return "", text
class SkillRegistry:
"""两阶段加载:先扫元数据,按需再读正文。"""
def __init__(self, skill_dir: str = SKILL_DIR):
self.skill_dir = skill_dir
self._index: dict[str, Skill] = {}
def scan(self) -> "SkillRegistry":
"""阶段一:只把 name + description 装进索引(极省 token)。"""
self._index.clear()
for root, _, files in os.walk(self.skill_dir):
if "SKILL.md" in files:
skill = Skill.from_file(os.path.join(root, "SKILL.md"))
self._index[skill.name] = skill
return self
def describe_all(self) -> str:
"""生成注入系统提示的精简清单。"""
lines = ["可用技能(Skills):"]
for s in self._index.values():
lines.append(f"- {s.name}: {s.description}")
return "\n".join(lines)
def load(self, name: str) -> Optional[Skill]:
"""阶段二:按需加载完整正文。"""
return self._index.get(name)
def __len__(self) -> int:
return len(self._index)
3.3.2 按需加载正文阶段
注意 scan() 只读了 frontmatter,body 此时是空的。load(name) 返回的是同一个对象,但由于 from_file 已经把 body 填好了,这里其实阶段一就顺带拿到了正文——这没问题,因为正文不会进 describe_all() 的清单,仍只在被路由命中后才进入上下文。如果你想严格"阶段二才读磁盘",可以把 body 留空、load() 里再 open 一次即可,逻辑等价。
3.4 实现 Skill 路由器
路由负责"用户说一句话,该调哪个 Skill"。生产用 LLM 语义匹配,这里先给一个确定性兜底 + LLM 钩子的实现:
import re
def route_deterministic(task: str, registry: SkillRegistry) -> Optional[str]:
"""兜底路由:用 description 里的关键词做简单命中,防止模型抽风。"""
task_lower = task.lower()
for name, skill in registry._index.items():
# 取 description 里的名词片段做弱匹配(示例逻辑,仅作演示)
if any(k in task_lower for k in ["退款", "冲销", "订单"]):
if name == "order-refund":
return name
if any(k in task_lower for k in ["权限", "角色", "用户"]):
if name == "user-permission-check":
return name
return None
def route_by_llm(task: str, registry: SkillRegistry, llm_call) -> Optional[str]:
"""生产路由:把清单 + 任务交给 LLM,让它返回 name。"""
prompt = (
registry.describe_all()
+ f"\n\n用户任务:{task}\n只回复最匹配的技能 name,没有就回 NONE。"
)
name = llm_call(prompt).strip()
return name if name in registry._index else route_deterministic(task, registry)
💡 工程实践:永远保留确定性兜底。LLM 路由偶尔会返回不存在的 name 或乱编技能,用
name in registry._index校验 + 兜底,是生产稳定的关键。
3.5 运行与验证
if __name__ == "__main__":
reg = SkillRegistry("./skills").scan()
print(f"已扫描 {len(reg)} 个技能:")
print(reg.describe_all()) # 阶段一:只输出 name+description
task = "帮我查下 uid=10086 有没有访问报表的权限"
name = route_deterministic(task, reg) # 或 route_by_llm(...)
print(f"\n路由命中:{name}")
skill = reg.load(name) # 阶段二:按需取正文
if skill:
print(f"\n===== {skill.name} 正文 =====\n{skill.body}")
预期输出(节选):
已扫描 2 个技能:
可用技能(Skills):
- user-permission-check: 当用户要求校验某账号权限、判断角色、或拉取 user_service 用户信息时使用。
- order-refund: 当用户要处理退款、冲销、或查询 order_service 某笔订单的退款状态时使用。
路由命中:user-permission-check
===== user-permission-check 正文 =====
# 用户权限校验技能
## 步骤
1. 调用 user_service 的 GET /internal/v1/profile ...
阶段一只进了清单,正文直到"路由命中"后才出现——两阶段加载验证通过。
四、踩坑与优化
4.1 description 写多写少都是坑
⚠️ description 是 Skills 机制的命门。 写太短(“处理用户相关”)Agent 想不起来;写太长(把整个手册抄进去)既浪费清单 token,又让"按需加载"失去意义。
对策:description 只写触发场景,用"当用户……时使用"的句式,控制在 1–2 句话。正文才放步骤细节。
4.2 技能职责重叠导致路由冲突
⚠️ 两个 Skill 的 description 都写"处理订单",路由会随机命中,行为不可复现。
对策:技能之间用明确边界切分。比如"退款"和"查物流"必须拆成两个 Skill,description 互不覆盖触发词。定期用真实任务做路由回归测试。
4.3 安全与共享
💡 三件必须做的事:
- Skill 正文里绝不写密钥/Token,敏感信息走环境变量或 MCP Server。
- 执行高风险动作(删数据、发请求)前加 human-in-the-loop 确认点。
- 技能是普通 Markdown,天然可 git 管理、可跨框架(Claude Code / Codex / Cursor 都认
SKILL.md),团队共建时统一放skills/目录即可共享。
五、总结
2026 年 Agent 工程化的关键词是"分层收敛":MCP 管工具连接、A2A 管 Agent 间协作、Skills 管经验沉淀。本文讲的就是其中最容易被忽视、却最贴近开发者日常的一层——把"我会怎么做"固化成可复用、可路由、可按需加载的 SKILL.md。
我们手撸的 SkillRegistry 已经具备两阶段加载与确定性兜底路由,你只要往 skills/ 里不断加 SKILL.md,Agent 的能力就会像搭积木一样长出来,且每次新会话都"记得"昨天的经验。
后续方向:把 Skill 路由接上真实 LLM、给 Skill 加版本号与依赖声明、再和 MCP Server 组合——一个最小可用的 Agent 工程系统就成型了。
如果本文对你有帮助,欢迎点赞、收藏、关注~ 你打算先用 Skill 沉淀哪类工程经验?评论区聊聊你的方案。
更多推荐


所有评论(0)