
用 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 |
| Python | 3.13 系列 |
| OpenAI Python 库 | 3.6.0 |
| 模型 | 一个支持工具调用的对话模型 |
依赖文件里只写一行即可:
openai==3.6.0安装:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txtAPI 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 的 name 和 description,不返回正文。这是整套设计的地基——如果一上来就把所有 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: true、action: create 和 name。如果同名 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_listskill_viewskill_manage
并在系统提示里给出策略:正常回答时,先用 skills_list 看候选,再对真正需要的 Skill 调 skill_view。输入「API 是什么?请讲给初学者听」之后,模型在给出回答之前会依次调用:
skills_list {}
skill_view {"name":"explain-simply"}然后才生成符合手顺的回答。注意这里 Python 侧并没有把 explain-simply 写死,是模型从候选里自己选的。
第六步:回答之后跑一次 Review
正常回答结束后,再执行一次审查。审查侧收到的策略是:
- 先看 Skill 清单
- 有对应 Skill 就读取全文
- 只是小修小补就用 patch
- 需要整体重排就用 update
- 没有对应 Skill 就 create
- 没有值得留存的内容就什么都不存
需要说明的是,界面上可能显示成「后台审查」,但在这套源码里它并不是另一个进程的异步任务,而是正常回答结束后、在同一个程序里顺序执行的同步步骤。
没有可存内容时,审查返回 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.mdimportant-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」的方式拼起来。