news 2026/9/26 10:31:57

从零实现 Agent Skills:用 SKILL.md 给 AI 智能体装上可插拔技能包

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零实现 Agent Skills:用 SKILL.md 给 AI 智能体装上可插拔技能包

1. 为什么你的智能体提示词越写越长,还越来越不听话

做智能体应用的朋友大概率都经历过这个阶段:一开始系统提示词只有几十行,写着写着变成几百行,最后膨胀到两千多行。代码审查规则、Git 操作流程、文件整理规范、API 测试步骤全塞在一起,每次调用都要把这一大坨内容完整传给模型。

结果就是三个字:贵、慢、乱。贵在 token 消耗,哪怕用户只是想让模型帮忙写个 commit message,你也得把代码审查的完整规则一起喂进去;慢在模型要在海量指令里找重点,响应质量反而下降;乱在调试时根本分不清模型到底在按哪段指令行动。

Agent Skills 这个思路换了个角度解决问题:别一次性把所有能力都告诉模型,而是给它一份"菜单",需要哪个技能再现场加载哪个。启动时只加载几百字节的元数据,真正用到某个技能时才把完整指令拉进来。核心思想一句话——技能就是结构化的、按需加载的提示词模板。

这篇文章聚焦用 Python 从零搭建一个 Agent Skills 加载器,用 SKILL.md 描述技能元数据,结合 OpenAI function call 完成技能注册与调用。我会给出可复制的目录结构、SKILL.md 骨架和完整的 Python 注册代码,并演示新增技能后重启即生效的验证步骤。适合正在做智能体、被长提示词折磨、想搞明白可插拔技能包怎么落地的开发者。

2. 前置准备:TaoToken 接入与项目骨架

2.1 为什么用 TaoToken 做模型接入层

Agent Skills 的加载器本身不依赖特定模型服务,但你需要一个稳定的 OpenAI 兼容接口来跑 function call。TaoToken 提供的就是这种兼容层,模型对话、Coding Plan、API Keys 都在一个控制台里管理,接入代码基本不用改,换模型只改 model 字段。

如果你只是验证技能加载逻辑,用模型对话页面手动测几轮就够;如果要把这套 Skills 加载器长期用在编码助手或 Agent 工作流里,建议直接上 Coding Plan,省得每次调模型都单独算账。

2.2 目录结构

先建项目骨架,每个技能一个子目录,SKILL.md 放在里面:

mkdir -p agent-skills-demo/skills/code-review mkdir -p agent-skills-demo/skills/git-helper mkdir -p agent-skills-demo/skills/api-tester cd agent-skills-demo

最终结构长这样:

agent-skills-demo/ ├── main.py ├── skills_manager.py └── skills/ ├── code-review/ │ └── SKILL.md ├── git-helper/ │ └── SKILL.md └── api-tester/ └── SKILL.md

2.3 安装依赖

pip install openai pyyaml

openai用来调 function call,pyyaml用来解析 SKILL.md 顶部的 frontmatter。两个包都很轻,没有额外负担。

2.4 拿 API Key

去 TaoToken 控制台的 API Keys 页面创建一个 key,然后写进环境变量:

export TAOTOKEN_API_KEY="你的key"

接入文档里有完整的 base_url 和参数说明,照着填就行。base_url 用https://taotoken.net/api,不要带多余路径。

3. SKILL.md 骨架与技能元数据设计

3.1 一个 SKILL.md 长什么样

每个技能就是一个放在独立目录里的 SKILL.md 文件,由两部分组成:YAML frontmatter 写元数据,下面的 Markdown 正文写详细指令。

以代码审查技能为例,skills/code-review/SKILL.md:

--- name: code-review description: 审查 Python/JavaScript 代码,检查安全漏洞、PEP 8 规范和性能问题 version: 1.0.0 --- # 代码审查技能 你是一名资深代码审查员。 ## 重点关注 1. 安全性:SQL 注入、XSS 攻击、鉴权绕过 2. 代码质量:可读性、可维护性、DRY 原则 3. 性能:N+1 查询、内存泄漏、低效算法 ## 输出格式 - 总结:整体评估 - 关键问题:安全相关(如有) - 改进建议:优化建议 - 亮点:做得好的地方

3.2 description 字段是命门

模型就是靠 description 这行字判断"这个任务该激活哪个技能"。写得糊弄,模型就选得糊弄。

反例:帮助写代码——等于没写。

正例:审查 Python/JavaScript 代码,检查安全漏洞、PEP 8 规范和性能问题——覆盖场景一目了然。

3.3 结构化指令比大段散文管用

清晰的小标题、列表、预期输出格式,模型跟着走的依从度会高一个档次。要是写成一整段话糊脸上,模型大概率会选择性失忆。这一点在多个技能同时存在时尤其明显,因为模型需要在激活后快速抓住重点。

4. Python 加载器:发现、注册、激活、执行

4.1 核心类 SkillsManager

把发现、解析、激活三个动作封装成一个类,skills_manager.py:

import yaml from pathlib import Path from typing import Dict, List, Optional from dataclasses import dataclass @dataclass class Skill: name: str description: str path: Path content: Optional[str] = None metadata: Optional[Dict] = None def load_full_content(self) -> str: if self.content is None: skill_file = self.path / "SKILL.md" with open(skill_file, "r", encoding="utf-8") as f: self.content = f.read() return self.content class SkillsManager: def __init__(self, skills_directory: str = "skills"): self.skills_directory = Path(skills_directory) self.skills: Dict[str, Skill] = {} self._discover_skills() def _discover_skills(self): if not self.skills_directory.exists(): return for item in self.skills_directory.iterdir(): if item.is_dir(): skill_file = item / "SKILL.md" if skill_file.exists(): try: skill = self._parse_skill(skill_file, item) self.skills[skill.name] = skill except Exception as e: print(f"加载技能失败 {item}: {e}") def _parse_skill(self, skill_file: Path, skill_dir: Path) -> Skill: with open(skill_file, "r", encoding="utf-8") as f: content = f.read() metadata = {} if content.startswith("---"): parts = content.split("---", 2) if len(parts) >= 3: try: metadata = yaml.safe_load(parts[1]) except yaml.YAMLError as e: print(f"frontmatter 解析失败: {e}") return Skill( name=metadata.get("name", skill_dir.name), description=metadata.get("description", "无描述"), path=skill_dir, metadata=metadata, ) def activate_skill(self, skill_name: str) -> Optional[str]: skill = self.skills.get(skill_name) if skill: return skill.load_full_content() return None

关键点:_discover_skills只解析 frontmatter,正文不读。完整内容留在磁盘上,等activate_skill被调用时才加载。这就是懒加载,也是 Skills 省 token 的核心。

4.2 转成 OpenAI function call 工具

把每个 skill 转成一个可调用函数暴露给模型:

def get_skill_tools(self) -> List[Dict]: tools = [] for skill in self.skills.values(): tool = { "type": "function", "function": { "name": f"activate_skill_{skill.name.replace('-', '_')}", "description": f"激活 {skill.name} 技能。{skill.description}", "parameters": { "type": "object", "properties": { "context": { "type": "string", "description": "任务上下文", } }, "required": ["context"], }, }, } tools.append(tool) return tools

函数命名统一加前缀activate_skill_,调试时看日志一眼就知道发生了什么。

4.3 对话主循环

技能可以链式触发,比如 api-tester 激活后还要调 execute_python 去实际发请求。所以处理逻辑必须是个循环,直到某一轮模型不再返回 tool_calls:

max_iterations = 10 iteration = 0 while iteration < max_iterations: iteration += 1 response = self.llm_client.chat( messages=self.messages, tools=tools if tools else None, ) if response["tool_calls"]: self._handle_tool_calls(response) continue break

max_iterations是兜底保险丝,防止模型抽风循环调用把账单跑爆。

4.4 处理 tool_calls 的细节

def _handle_tool_calls(self, response: Dict): self.messages.append({ "role": "assistant", "tool_calls": response["tool_calls"], "content": response.get("content"), }) for tc in response["tool_calls"]: tool_name = tc["function"]["name"] if tool_name.startswith("activate_skill_"): skill_name = tool_name.replace("activate_skill_", "").replace("_", "-") skill_content = self.skills_manager.activate_skill(skill_name) tool_result = ( f"技能 '{skill_name}' 已激活,请按以下指令执行:\n\n{skill_content}" if skill_content else f"错误:找不到技能 '{skill_name}'" ) else: tool_result = f"错误:未知工具 '{tool_name}'" self.messages.append({ "role": "tool", "tool_call_id": tc["id"], "content": tool_result, })

第 3 步特别容易漏:把带 tool_calls 的 assistant 消息加进对话历史。漏了之后 OpenAI 会直接甩你一个报错,类似Missing required parameter: messages[1].tool_calls[0].type。

5. 验证请求:新增技能后重启即生效

5.1 写一个最小可跑的 main.py

import os from openai import OpenAI from skills_manager import SkillsManager client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url="https://taotoken.net/api", ) manager = SkillsManager("skills") tools = manager.get_skill_tools() print(f"已发现 {len(manager.skills)} 个技能: {list(manager.skills.keys())}") messages = [ {"role": "user", "content": "帮我审查这段代码:def add(a,b): return a+b"} ] response = client.chat.completions.create( model="gpt-4o-mini", messages=messages, tools=tools, ) choice = response.choices[0].message if choice.tool_calls: for tc in choice.tool_calls: print(f"模型选择激活: {tc.function.name}") print(f"参数: {tc.function.arguments}")

5.2 跑起来看结果

python main.py

预期输出:

已发现 3 个技能: ['code-review', 'git-helper', 'api-tester'] 模型选择激活: activate_skill_code_review 参数: {"context": "审查 add 函数"}

模型看到用户请求后,从工具列表里选中了 code-review 技能,说明 description 写得够清楚,模型能准确匹配。

5.3 新增技能验证热插拔

现在新建一个技能,不改任何代码:

mkdir -p skills/json-formatter

写skills/json-formatter/SKILL.md:

--- name: json-formatter description: 格式化、校验和美化 JSON 数据,处理嵌套结构和转义字符 version: 1.0.0 --- # JSON 格式化技能 你是 JSON 处理专家。 ## 能力 1. 格式化压缩的 JSON 2. 校验 JSON 合法性 3. 提取嵌套字段 ## 输出格式 返回格式化后的 JSON 和校验结果。

重启python main.py,输出变成:

已发现 4 个技能: ['code-review', 'git-helper', 'api-tester', 'json-formatter']

新增技能零代码改动,丢一个 SKILL.md 进去就完事。这就是可插拔技能包的落地方式。

6. 本篇常见错排查

6.1 启动就把所有技能加载到内存

不少人图省事,在_discover_skills里直接把所有 SKILL.md 全读进内存。这完全背离了 Skills 的初衷,等于又回到一次性加载所有指令的老路。元数据是元数据,正文是正文,必须分开处理。

6.2 tool_calls 格式不对

那个type: "function"和嵌套的function对象,该怎么嵌套怎么嵌套,别自作主张扁平化:

# 正确格式 { "id": "call_xxx", "type": "function", "function": { "name": "activate_skill_code_review", "arguments": "{...}" } }

6.3 技能激活后调模型忘了传 tools

技能可能还要调其他工具,每一次 LLM 调用都得把 tools 带上。一旦忘了,模型在执行技能指令的过程中就没法再调用其他工具,技能的能力就瘸了半边。

6.4 description 写得太虚

帮助处理代码这种描述,模型根本不知道该啥时候调。得写具体:做啥、适用哪类任务、核心能力有哪些。

6.5 技能粒度没把握好

一个技能对应一个领域,code-review、git-helper、api-tester 都是好例子。像 developer-tools 这种范围太大的,模型根本选不准。

6.6 frontmatter 解析失败

YAML 对缩进敏感,name:和description:后面要有空格,冒号别用中文。解析失败时_parse_skill会打印错误,但技能名会 fallback 到目录名,容易掩盖问题,建议启动时把解析异常直接抛出来。

7. 下一步:把技能包接进你的工作流

这套加载器不到两百行 Python,一个下午就能跑起来。建议先写一个技能试试水,感觉顺手了再逐步扩展。

如果你只是验证模型选择技能的逻辑,用模型对话页面手动测几轮最快;如果要把这套 Skills 加载器长期用在编码助手或 Agent 工作流里,直接上 Coding Plan 更省心,模型调用和技能管理都在一个地方;接入过程中遇到 function call 格式或 base_url 配置问题,去接入文档对照参数排查,API Keys 在控制台随时可以重新生成。

技能写多了之后,你会发现真正值钱的不是加载器代码,而是那些 SKILL.md 里沉淀下来的结构化指令。别人写的 api-tester 技能,直接拷过来就能用,零修改。相当于给智能体建了一个共享的能力库,慢慢就能攒出一套自己的工具箱。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 10:30:57

typescript-expert - typescript-cheatsheet

TypeScript 速查表 类型基础 // Primitives const name: string John const age: number 30 const isActive: boolean true const nothing: null null const notDefined: undefined undefined// Arrays const numbers: number[] [1, 2, 3] const strings: Array<strin…

作者头像 李华
网站建设 2026/9/26 10:29:18

五子棋AI自博弈推理加速116倍:C++与GPU优化实战

1. 从一局五子棋说起&#xff1a;为什么要死磕推理速度五子棋这东西&#xff0c;规则简单到用一张餐巾纸就能讲明白&#xff0c;但真要让 AI 通过自博弈把棋力练出来&#xff0c;计算量一点都不“简单”。我最初用 Python 写了个能跑的自博弈框架&#xff0c;逻辑上没毛病&…

作者头像 李华
网站建设 2026/9/26 10:29:14

OpenResearch实践指南:打造可复现、可协作的研究工作流

OpenResearch 这个词&#xff0c;最近在研究工具圈子里出镜率越来越高。有人把它理解成开放获取的学术运动&#xff0c;也有人拿它当标签&#xff0c;统称那些把文献、实验、笔记和发布流程全部开源的个人研究项目。我自己把这套思路折腾了大半年&#xff0c;从一个“把论文 PD…

作者头像 李华
网站建设 2026/9/26 10:29:01

ChatGPT Work 还是 Codex?需求、文档、代码三类任务的入口决策表

ChatGPT Work 还是 Codex?需求、文档、代码三类任务的入口决策表 [!NOTE] ChatGPT Work 与 Codex 不是简单的“一个写文档、一个写代码”,真正的分界在交付物、所需工具、执行环境和验收证据。 Work 更适合从目标出发组织多来源材料并形成可审阅成果;Codex 更适合进入代码库…

作者头像 李华
网站建设 2026/9/26 10:28:38

ax:面向智能体的Kubernetes原生调度范式

1. “ax”不是缩写&#xff0c;是新一代智能体调度范式的代号最近在技术社区和开源项目讨论里频繁刷到“ax”&#xff0c;尤其和Kubernetes、agentic、orchestration这些词绑在一起出现——它既不是某个被遗忘的Linux命令&#xff0c;也不是Chrome插件名&#xff0c;更不是Goog…

作者头像 李华