AB
AiBoss站
教程

用 Python 与 OpenAI API 实现 Agent Skills:SKILL.md 的保存、选择与复用

教程

用 Python 与 OpenAI API 实现 Agent Skills:SKILL.md 的保存、选择与复用

Agent Skills 把「这类任务该怎么做」写成外部文件 SKILL.md,让模型先看清单、再按需读取全文,回答后还能回头审查并更新这份手顺。本文用 Python 与 OpenAI API 搭一套最小实现,覆盖 skills_list / skill_view / skill_manage 三个部件,以及 create、patch、update 三种写入方式,并演示跨进程复用与回答后 Review 的完整闭环。

Agent Skills 要解决的问题很具体:把「某一类活儿该按什么步骤做」从模型权重里拿出来,写成一份外部文件,需要的时候再读进来用。它和 Memory、Tools 的分工不一样——Memory 管「记住的信息」,Tools 管「对外部世界的调用」,Skills 管「选哪套手顺、照着做」。本文用 Python 和 OpenAI API 搭一套最小可跑的 Skills 实现,覆盖清单查询、全文读取、新建与修改、回答后审查、跨进程复用这几件事。适合已经会用 OpenAI API 发一次普通对话请求、想给 Agent 加上「可复用手顺」这一层的开发者。

准备工作

环境与依赖

整套东西只需要 Python 和一个 OpenAI 的 API Key。参考环境如下,版本号只是记录当时验证用的组合,实际以你本机与官网当前信息为准:

项目说明
操作系统Windows 11
终端Windows PowerShell
Python3.13 系列
OpenAI Python 库3.6.0
模型一个支持工具调用的对话模型

依赖文件里只写一行即可:

openai==3.6.0

安装:

python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt

API Key 通过环境变量传入,不要写进源码:

$env:OPENAI_API_KEY = "你的密钥"

目录结构

Skill 以文件夹为单位存放,一个 Skill 一个目录,目录里放一份 SKILL.md。其余目录分别放源码、文档、日志和证据文件:

项目根目录\
├─ .venv\
├─ docs\
├─ evidence\
├─ logs\
├─ source\
│  ├─ skills.py
│  └─ skills_demo.py
├─ skills\
│  ├─ important-news\
│  │  └─ SKILL.md
│  └─ explain-simply\
│     └─ SKILL.md
└─ requirements.txt

两个源码文件分工明确:skills.py 负责 Skill 的保存、读取、更新;skills_demo.py 负责调用 OpenAI API,让模型自己挑 Skill,并在回答之后跑一次 Review。

SKILL.md 的格式

SKILL.md 由两部分组成:带名字和描述的头部,加上真正的手顺正文。头部用三条短横线包起来:

---
name: explain-simply
description: 把难懂的说法讲给初学者听
---

# 面向初学者的说明

1. 先用一句话说清它是什么意思。
2. 举一个身边的例子。
3. 补充必要的术语。
4. 最后用一句话收束要点。

关键认知:Skill 本身不是可执行的 Python 代码,它是一段写给模型看的文字,描述「这类工作该怎么推进」。程序要做的是把这段文字在合适的时机喂给模型。

操作步骤

第一步:实现 skills_list(),只返回清单

清单接口只返回每个 Skill 的 namedescription,不返回正文。这是整套设计的地基——如果一上来就把所有 Skill 的全文塞进上下文,Skill 一多就会失控。

实现思路是遍历 skills 目录下的子目录,读取每个 SKILL.md 的头部字段,拼成一个列表返回。清单里还可以带一个自举逻辑:如果初始 Skill 不存在,就在取清单时顺手创建它。验证时可以看到这样的过程:

Before: False
↓ skills_list()
↓ 自动创建 important-news
After: True

之后清单里会出现类似 important-news: 比较新闻并判断该以什么优先级提醒 这样的条目,说明清单只暴露了名字和描述。

第二步:实现 skill_view(name),按需读全文

模型看完清单、决定要用哪个 Skill 之后,再调用 skill_view(name) 取该 Skill 的完整正文。例如用户问「API 是什么?请讲给初学者听」,模型的动作序列是:

skills_list()
↓ 选中 explain-simply
skill_view("explain-simply")
↓ 读到手顺全文
按手顺组织回答

「先看候选、再读全文」这个两段式,是 Skills 实现里最值得保留的结构。

第三步:实现 skill_manage(),三种写入方式

Skill 的变更统一走 skill_manage(),内部支持三种动作。

create:新建 Skill。在保留已有 Skill 的前提下新增一个。返回结果里会带上 success: trueaction: createname。如果同名 Skill 已经存在,这套实现不覆盖,而是返回 Skill already exists

patch:只改一处。把正文里某一段旧文本替换成新文本,其余部分原样保留。例如把第 2 条手顺从「举一个身边的例子」改成「举一个身边的例子,并点出它和说明对象的共同点」,其他条目不受影响。

patch 有一个刻意收紧的约束:old_text 必须在正文里恰好出现一次。出现 0 次或多次都判定失败。这样能避免模糊替换改错位置。

update:整体替换正文。把已有 Skill 的正文整段换成新内容。它和 patch 的区别在于,你想保留的条目也必须写进新正文里。例如把 explain-simply 从 3 条手顺扩成 4 条:

1. 用一句话说明含义
2. 举身边的例子并点出共同点
3. 补充专业术语
4. 最后用一句话回顾要点

第四步:确认写入落到了磁盘上

Skill 的更新如果只留在当前 Python 进程的内存里,就毫无意义。验证方法是:跑完更新程序、把进程结束掉,再开一个新的 Python 程序去读 SKILL.md。预期看到:

Exists: True
↓ 读取已保存的 SKILL.md
↓ 确认更新后的 4 条手顺
reload check OK

这里验证的是「跨 Python 进程复用外部文件」,不是重启电脑之后的持久性,两者不要混为一谈。

第五步:让模型自己挑 Skill

前面几步都是 Python 侧直接调用。接下来换成由模型决策。把三个能力作为工具暴露给模型:

  • skills_list
  • skill_view
  • skill_manage

并在系统提示里给出策略:正常回答时,先用 skills_list 看候选,再对真正需要的 Skill 调 skill_view。输入「API 是什么?请讲给初学者听」之后,模型在给出回答之前会依次调用:

skills_list {}
skill_view {"name":"explain-simply"}

然后才生成符合手顺的回答。注意这里 Python 侧并没有把 explain-simply 写死,是模型从候选里自己选的。

第六步:回答之后跑一次 Review

正常回答结束后,再执行一次审查。审查侧收到的策略是:

  1. 先看 Skill 清单
  2. 有对应 Skill 就读取全文
  3. 只是小修小补就用 patch
  4. 需要整体重排就用 update
  5. 没有对应 Skill 就 create
  6. 没有值得留存的内容就什么都不存

需要说明的是,界面上可能显示成「后台审查」,但在这套源码里它并不是另一个进程的异步任务,而是正常回答结束后、在同一个程序里顺序执行的同步步骤。

没有可存内容时,审查返回 Nothing to save.。也就是说,「回答过」不等于「必须改 Skill」——审查会先判断有没有复用价值。

第七步:让 Review 真的改一次 Skill

在用户输入里追加一条希望长期沿用的规则,例如:解释缩写时,先给出正式名称和它的中文含义;如果不知道正式名称,不要猜。

正常回答结束后,审查会读取已有的 explain-simply,然后选择:

skill_manage
action = update
name = explain-simply

这次它选的是 update 而不是 patch,由审查方自己决定。工具返回成功不代表文件真的变了,所以审查结束后要直接打开 SKILL.md 核对。更新后手顺变成 5 条,开头新增了一条关于缩写处理规则的条目。

第八步:在新进程里复用更新后的 Skill

最后换一个新的 Python 进程,问一个没提过「先给正式名称」的问题,比如「SDK 是什么?请讲给初学者听」。执行时同样会先出现:

skills_list {}
skill_view {"name":"explain-simply"}

回答开头会按更新后的手顺给出正式名称与中文含义,随后依次是含义说明、身边例子与共同点、术语补充、最后一句话收束。

这里要诚实一点:模型本身也知道 SDK 的正式名称,所以仅凭回答文本,无法严格证明「没有这个 Skill 就一定不会这样答」。这次确认的是「读取更新后的 Skill,并在另一个问题上按该手顺作答」这一串动作成立。

一个完整示例

把上面的步骤串成一条最小可跑的链路。

1. 准备两个 Skill。skills\ 下建两个目录,各写一份 SKILL.md:

skills\
├─ important-news\
│  └─ SKILL.md
└─ explain-simply\
   └─ SKILL.md

important-news 的手顺描述的是:汇总新闻候选、对照记忆里的兴趣与已读记录、比较新鲜度与重要度、必要时取详情、只推送真正重要的那几条。explain-simply 的手顺就是前面那份面向初学者的四步说明。

2. 写 skills.py。提供三个函数:skills_list() 返回清单,skill_view(name) 返回全文,skill_manage(action, name, ...) 处理 create / patch / update。清单函数里带上初始 Skill 的自举创建。

3. 写 skills_demo.py。把三个函数包装成工具定义交给模型,系统提示里写清「先看清单、再读全文」的策略;正常回答结束后,再按审查策略跑一轮,决定是否写入。

4. 跑一次问答。输入「API 是什么?请讲给初学者听」,观察工具调用顺序,确认模型先列清单、再读 explain-simply,然后按手顺作答。

5. 观察审查结果。第一次这类问题通常返回 Nothing to save.

6. 追加一条规则再跑一次。在输入里加上缩写处理要求,观察审查选择 update,然后直接打开 SKILL.md 确认手顺变成 5 条。

7. 换进程验证复用。关掉程序,新开一个进程问「SDK 是什么?请讲给初学者听」,确认它仍然先列清单、再读 explain-simply,并按更新后的手顺作答。

注意事项

Skill 不等于可执行代码。SKILL.md 里写的是工作手顺,不是能跑的 Python。它靠模型阅读并遵循,而不是靠解释器执行。

清单与全文必须分开。一开始就把所有 Skill 全文交给模型,会随 Skill 数量增长迅速吃掉上下文。清单只给名字和描述,全文按需读取,这是这套结构能扩展的前提。

patch 的替换位置必须唯一。old_text 在正文中出现 0 次或多次都会失败。写 patch 时尽量选足够独特的片段。

update 是整体替换。想保留的条目必须写进新正文,否则会丢。

同名 create 不会覆盖。这套实现遇到已存在的名字直接返回 Skill already exists,需要改内容请走 patch 或 update。

Review 是同步的。虽然显示上像后台任务,但在这份源码里它跟在正常回答之后顺序执行,不是独立进程的异步处理。

important-news 只是文本。它的 SKILL.md 里提到了记忆和取新闻的工具,但这套 Skills 程序并没有真的接入这些能力。文件里写了什么,和程序能不能执行,是两件事。把 Memory、Tools、Skills 组合起来做新闻提醒判断,属于 Agent 层面的工作。

这套实现没有覆盖的分支。已经验证过的包括:手动 create、手动 patch、手动 update、跨 Python 进程重新读取、模型自主选择 Skill、按 Skill 作答、审查判定无需保存、审查触发 update、复用更新后的 Skill。尚未验证的包括:审查触发 create、审查触发 patch、遇到真正不认识的缩写时「不要猜」这条规则是否生效、重启电脑之后的复用、以及与 Memory / Tools 的集成。

这是教学用的简化实现。它没有做 Skill 名的严格路径校验,也没有备份机制,不具备生产环境所需的防护。真要上线,这些都得自己补。

版本与配额以官网为准。本文出现的库版本、模型名称、可用地区、调用配额都可能变化,动手前请核对官方当前信息。

三者的分工再强调一次。Memory 复用过去的信息,Tools 执行外部处理,Skills 挑选并复用工作手顺。Skills 单独一个组件并不会让 Agent 完整,最终需要按「取数据靠 Tools、做判断靠 Skills、记东西靠 Memory、整体调度靠 Agent」的方式拼起来。