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.md2.3 安装依赖
pip install openai pyyamlopenai用来调 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 breakmax_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 技能,直接拷过来就能用,零修改。相当于给智能体建了一个共享的能力库,慢慢就能攒出一套自己的工具箱。