把 AI Agent 从“能聊天”变成“能办事”,技能(skills)系统是绕不过去的一环。我在做 agent-skills 这个项目时,最深的感受是:很多团队把 skills 和 tools 混为一谈,模型明明有工具,却还是不知道怎么完成一整套任务。这篇文章我从第一性原理出发,拆解 skills 的本质,讲清楚它与工具、运行时外壳(harness)的分工,再给出一套可以直接照做的技能开发流程、分发方式和排错思路。适合正在搭建 agent 应用、准备设计技能体系,或者想快速上手技能开发的开发者阅读。无论你是前端、后端还是全栈背景,只要会看目录结构、能写简单脚本,就能跟上。
1. 从第一性原理拆解 agent-skills 的核心问题
1.1 大模型的能力边界决定了 skills 存在的必要性
先说一个很多人忽略的事实:大模型本质上是一个“按概率预测下一个词”的序列模型。它擅长的是推理、生成、归纳,但它并不天然知道你的内部系统里有哪些接口、这些接口要传什么参数、调用失败了该重试还是该降级。换句话说,模型是“会写邮件”的应届生,不是“已经登录公司邮箱并且知道通讯录格式”的老员工。
举一个实际例子。你让模型“给张三发一封项目周报邮件”,模型能立刻写出内容,但接下来要面对一连串操作问题:邮件服务用哪个 API?API 的鉴权 token 从哪来?发送成功怎么确认?发送失败是返回错误码还是抛异常?要不要自动重试?这些都没法靠“更聪明的模型”解决,只能靠外部工程手段去约束。
skills 解决的就是这个“最后一公里”问题。它把一套操作经验打包成模型可理解、可执行、可校验的单元。模型不需要记住每个接口的细节,它只需要知道“什么情况下选这个技能”“执行完技能会得到什么结果”。这个心智模型一旦建立,整个 agent 的任务完成率会有质的提升。
我在设计 agent-skills 项目时,先把目标定死:让模型面对一个新任务时,能像老员工翻工作手册一样,找到对应流程并按步骤执行,而不是自己瞎猜。这也是为什么 skills 要做得足够具体、足够自包含。
1.2 Skills 不是 Tools 的换皮:一套完整的“岗位 SOP”
很多人上来就问:skills 和 tools 到底有什么区别?我用一个类比解释:tools 是工具箱里的单个工具,比如锤子、螺丝刀;skills 是一份《书架安装手册》,它告诉你先拼哪块板、用哪把工具、每步怎么校验。
Tool 通常对应一个函数,输入输出都非常明确。比如get_weather(city),传城市名,返回天气 JSON。它不关心你在什么场景下用,也不负责判断“要不要用”。Skill 则是一个更高层的封装,它往往包含提示词、脚本、校验逻辑、资源文件,甚至多个小工具的组合。
从工程上区分,我习惯用这张表:
| 维度 | Tool | Skill | Harness |
|---|---|---|---|
| 粒度 | 单一操作 | 一组流程 | 运行时外壳 |
| 典型内容 | 函数、API 封装 | SKILL.md + 脚本 + 资源 | 模型调用、调度、安全策略 |
| 是否关心任务目标 | 不关心 | 关心 | 全局决策 |
| 失败处理 | 由调用方决定 | 内置校验与提示 | 全局重试策略 |
| 示例 | search_web(query) | travel_planning | Agent 主循环 |
Harness 往往是被忽略的词。它其实是 agent 的运行时外壳,负责加载模型、管理工具列表、调度技能、处理上下文窗口,以及执行安全策略。Skills 依赖 harness 运行,又独立于 harness 存在。这样设计的好处是:技能可以跨项目复用,harness 可以随时替换或升级。
在 agent-skills 项目里,我要求每个技能包必须回答三个问题:自己负责什么、自己依赖什么、自己输出什么。这三个问题写清楚了,模型才能在决策时快速命中正确的技能,而不是在 tools 列表里反复横跳。
1.3 一次完整调用里技能系统如何工作
把一次真实调用拆开看,技能系统的运行链路长这样:
- 用户提出任务,harness 组装上下文,把可用的技能清单(名称、描述、触发条件)交给模型。
- 模型基于任务语义,选择要调用的技能,并按照技能的调参要求填充参数。
- harness 校验参数,启动技能脚本。
- 技能脚本执行操作(读写文件、调用 API、分析数据),把结果以结构化格式返回。
- harness 把技能输出回填到上下文,模型继续推理,决定下一步是结束还是调用别的技能。
我早期踩过一个坑:技能脚本输出一大段纯文本,模型读完上下文就爆了,后续推理质量断崖式下降。后来我们把所有技能输出强制改成 JSON 结构,并在 SKILL.md 里声明“输出不得超过一定长度”,问题才缓解。
这个链路还揭示了一个关键点:技能系统的性能不只取决于模型,更取决于技能的描述质量和输出规范。描述写得模糊,模型就选错;输出写得啰嗦,上下文就浪费。所以下一节我重点讲架构上的关键约定。
2. agent-skills 的架构设计与关键约定
2.1 技能描述:写给人看的文档和写给模型看的文档不一样
技能描述是模型判断“何时使用”的唯一依据。我发现很多教程都把 SKILL.md 写成给人看的 README,这其实是个误区。README 注重展示和解释,SKILL.md 必须注重“可决策性”。
一个合格的 SKILL.md 至少要包含这五块:
- 技能名称与版本号
- 触发条件(明确说明什么场景下使用)
- 使用前置条件(需要哪些输入、哪些权限)
- 执行步骤(给模型的指导性流程)
- 输出格式与示例
- 不适用范围(避免误调用的关键)
我用一个实际技能包来展示。这个技能叫note_organizer,作用是把散落的 Markdown 笔记按主题分类并生成索引。
--- name: note_organizer version: 1.0.0 description: 整理目录下的 Markdown 笔记,按主题归类并生成索引摘要。 when_to_use: 当用户要求整理笔记、分类文档、生成文档索引时使用。 when_not_to_use: 当用户只是要求编辑某篇具体笔记内容时,不要使用本技能。 input: target_dir: 要整理的目录绝对路径 output: summary: 整理后的索引摘要,JSON 字符串 steps: 1. 扫描 target_dir 下所有 .md 文件。 2. 读取文件中的 frontmatter 标签字段,按主题归类。 3. 为没有标签的文件生成默认分类。 4. 生成索引文件 INDEX.md。 5. 返回 JSON: {"summary": "...", "index_path": "...", "total_files": n}注意when_not_to_use这一行。我最初写的技能描述里没有这行,导致模型在“帮我改一下某篇笔记的标题”这种场景下也去调用note_organizer,整理完把用户文件结构都改了。加上了负面清单后,误调用率下降了非常多。这个细节值得所有技能开发者重视。
2.2 一个技能包的目录结构与实现分层
规范的技能包,不只是一个脚本文件。我推荐的结构是:
note_organizer/ ├── SKILL.md ├── manifest.json ├── scripts/ │ ├── organize.py │ └── file_utils.py ├── assets/ │ ├── categories.yaml │ └── templates/ │ └── index_template.md ├── tests/ │ ├── test_organize.py │ └── fixture/ │ └── sample_notes/ └── README.md各文件职责:
SKILL.md:技能入口,给模型读,决定何时调用。manifest.json:给 harness 读,声明技能元数据、执行入口、权限需求。scripts/:实际执行逻辑,尽量做成分层模块。assets/:技能依赖的资源文件,比如分类模板。tests/:离线测试,保证技能脚本本身可独立验证。
manifest.json我常用这种结构:
{ "name": "note_organizer", "version": "1.0.0", "entry": "scripts/organize.py", "runtime": "python3", "permissions": ["read:target_dir", "write:target_dir"], "max_output_tokens": 800, "timeout_sec": 60 }说一个关键设计决定:为什么技能脚本要用独立进程而不是内联执行?我试过把逻辑直接写进 harness,开发时很爽,但上线后问题一堆。某个技能内存泄漏,直接带崩整个 agent;某个技能偷偷访问了不该访问的文件,权限根本拦不住。改成独立进程后,技能就有了进程级隔离,崩溃可以捕获,权限可以按目录限制,资源超限可以直接 kill。虽然多了一点进程启动开销,但在稳定性和安全性上的收益完全值得。
2.3 记忆、上下文预算与技能编排的三个原则
技能系统跑起来之后,第二个大问题是上下文管理。每次技能输出都要占用 token,而上下文窗口是有限的。我总结出三个工程原则,现在已经成为 agent-skills 项目的默认规范。
原则一:技能输出必须结构化且限长。脚本返回结果一定是 JSON,并尽量压缩字段。默认单个技能输出不超过 800 token,超过就被截断加摘要。这样做的好处是模型推理时只看到关键信息,不会被日志和中间过程淹没。
原则二:技能内部状态尽量自包含。一个技能不应该依赖上一次调用的“隐藏状态”。比如note_organizer每次扫描目录都是全新结果,不缓存中间状态。否则多轮对话里模型可能会带着一个过期状态的假设继续操作,非常容易出错。
原则三:多技能编排要预先定义优先级和冲突处理。当两个技能都能处理同一个问题时,模型应该依据什么决策?我通常会在 harness 层配置一个“技能冲突仲裁表”,明确哪类技能优先。比如“读取文件”类技能永远优先于“修改文件”类技能,“查询”永远优先于“写入”。这能减少很多意想不到的副作用。
关于长期记忆,我单独说一句。Skills 和记忆是两个层面的事:记忆负责保存“用户的偏好”“历史对话摘要”,skills 负责执行“当前怎么操作”。不要把历史数据直接塞进技能脚本里,更不要让技能脚本自己维护数据库。那样会让技能的复用性大打折扣。
3. 实操:从零开发并接入一个可用技能包
3.1 准备开发环境与初始化技能仓库
实操从环境准备开始。开发技能需要的东西不多,我一般只用 Git 和 Python 3.10+,再加一个虚拟环境。如果你的 agent 框架本身提供了 CLI,也可以用它生成模板。
我建议先建一个技能仓库,而不是直接在项目里四处散放脚本。集中管理的好处有两个:一是技能可以同时被多个 agent 项目复用;二是后续做版本发布、安全审查都方便。
用 agent-skills 项目的 CLI 初始化一个技能包:
mkdir agent-skills-repo cd agent-skills-repo git init python -m venv .venv source .venv/bin/activate agent-skills init note_organizerinit命令会生成上面提到的标准目录结构。我这人比较依赖模板,因为手写结构很容易漏掉tests/或assets/目录。模板相当于把最佳实践固化下来,新技能开发不需要重复思考目录设计。
初始化之后先提交一版空模板,再开始写内容。这样每个技能的 diff 记录会非常干净,后期回滚也方便。别等技能写完再 commit,中途改了什么根本看不出来。
3.2 编写一个“项目资料整理”技能的完整流程
模板有了,我们开始填充实际逻辑。我要写的organize.py做三件事:扫描目录中的 Markdown 文件、读取 frontmatter 标签、按标签归档并生成索引摘要。
先写脚本核心逻辑:
#!/usr/bin/env python3 """note_organizer: 扫描 Markdown 笔记并按标签分类。""" import argparse import json import os import re import sys from collections import defaultdict from pathlib import Path FRONTMATTER_PATTERN = re.compile(r"^---\s*\n(.*?)\n---", re.DOTALL) TAG_PATTERN = re.compile(r"^tags?:\s*(.+)$", re.MULTILINE) def parse_tags(text: str) -> list[str]: """从 Markdown frontmatter 中解析 tags 字段。""" match = FRONTMATTER_PATTERN.search(text) if not match: return ["uncategorized"] fm = match.group(1) tag_match = TAG_PATTERN.search(fm) if not tag_match: return ["uncategorized"] raw = tag_match.group(1).strip() # 支持 "[" 分隔的数组形式和逗号分隔形式 raw = raw.strip("[]").replace('"', "").replace("'", "") return [t.strip() for t in raw.split(",") if t.strip()] def scan_and_organize(target_dir: Path) -> dict: """扫描目录,按标签分类,生成索引摘要。""" if not target_dir.exists(): raise FileNotFoundError(f"目录不存在: {target_dir}") grouped = defaultdict(list) for file_path in sorted(target_dir.glob("*.md")): text = file_path.read_text(encoding="utf-8", errors="ignore") tags = parse_tags(text) or ["uncategorized"] primary_tag = tags[0] grouped[primary_tag].append(file_path.name) target_subdir = target_dir / primary_tag target_subdir.mkdir(exist_ok=True) if file_path.parent != target_subdir: file_path.rename(target_subdir / file_path.name) index_lines = ["# 项目笔记索引\n"] for tag, files in sorted(grouped.items()): index_lines.append(f"\n## {tag}\n") index_lines.extend(f"- {name}" for name in files) index_path = target_dir / "INDEX.md" index_path.write_text("\n".join(index_lines), encoding="utf-8") return { "summary": f"共整理 {sum(len(v) for v in grouped.values())} 个文件," f"分类 {len(grouped)} 个主题", "index_path": str(index_path), "total_files": sum(len(v) for v in grouped.values()), } def main() -> int: parser = argparse.ArgumentParser(description="整理 Markdown 笔记") parser.add_argument("target_dir", type=str, help="要整理的目录路径") args = parser.parse_args() try: result = scan_and_organize(Path(args.target_dir)) except Exception as exc: # 统一拦截,保证输出 JSON print(json.dumps({"error": str(exc)})) return 1 print(json.dumps(result)) return 0 if __name__ == "__main__": sys.exit(main())这段代码有几个设计点值得说。第一,输入路径从命令行参数传入,不硬编码绝对路径。如果脚本内部写死路径,换台机器、换个项目就没法复用。第二,所有异常统一在main()里捕获,以 JSON 输出错误信息。这样 harness 解析技能输出时不用猜测“这是报错还是正常结果”。第三,归档时把文件移动到标签子目录,这一步有破坏性,所以在 SKILL.md 中必须写明“仅用于整理指定目录”,避免模型误操作到用户其他目录。
3.3 接入 Agent 时的加载与验证步骤
技能写好之后,要接入到 agent 里。我通常分三步验证。
第一步,先脱离 agent 单独跑脚本,确认逻辑正确:
python scripts/organize.py ./test_notes这一步能过滤掉大部分低级错误。如果脚本独立运行都报错,后面再好的描述也没用。
第二步,把技能安装到 agent 的技能目录:
agent-skills install ./note_organizer --target ~/.agent/skills第三步,用一个最小 prompt 测试模型能不能正确选中技能。我的做法是写一个带明确任务的开场白,比如:
我现在有一个项目目录 /tmp/proj,里面堆了几十篇没有整理的 MD 笔记, 请整理这个目录并生成索引。然后用 trace 日志观察模型是否调用了note_organizer,调用的参数是否完整。这一步最耗时间的是调 SKILL.md 的描述措辞。模型如果选了其他技能,先不要急着改脚本,回到 SKILL.md 看触发条件写没写清楚、负面清单全不全。
另外一个容易被忽略的点是 token 消耗记录。每次测试都记一下技能调用前后的 token 变化,这样能在早期发现输出过长的问题。我在项目里加了一个简单的日志函数,每次技能返回后自动记录输入输出 token 差,超过预算就告警。
4. 技能分发、生态差异与多 Agent 协作
4.1 技能分发渠道与安装前的安全审查
技能开发完,总要给别人用。目前技能的分发渠道可以粗略分为四类:官方技能市场、第三方技能仓库、Git 仓库直接分发、企业内部私有共享。每个渠道的安全风险不同,我在实际使用中已经养成了条件反射式的检查流程。
| 来源 | 可信度 | 建议 |
|---|---|---|
| 官方技能市场 | 较高 | 仍需检查权限声明 |
| 第三方技能仓库 | 中低 | 先看 SKILL.md 和脚本,再安装 |
| Git 仓库直接分发 | 中 | 锁定版本号,核对 commit |
| 企业内部共享 | 中 | 走代码评审,做安全扫描 |
安装任何技能前,我强烈建议先做三件事:阅读 SKILL.md,确认它的触发条件是否过于宽泛;检查 scripts/ 目录下的脚本是否引用了不明外部网络地址;确认权限声明与实际行为一致,比如一个“整理文件”技能如果声明了“可写系统目录”,直接拒绝安装。
我在技能市场里看到过一些标称“自动挖洞”之类的技能包,这类安全敏感型技能风险极高,不仅可能破坏目标系统,还可能让使用者在法律和合规上陷入麻烦。如果你做 agent 项目,这类技能尽量不碰,更不应该引入到生产环境。
还有一个供应链问题:技能依赖第三方 Python 包。安装之前先看一下requirements.txt里的依赖,版本有没有锁定。依赖锁定能有效防止“今天能跑,明天突然挂了”的问题。
4.2 主流 Agent 框架的 Skills 形态差异
写技能时还要考虑一个现实:不同框架加载技能的方式不完全一样。我接触比较多的是 Claude Skills、Codex Skills,以及 GitHub 上以仓库形式存在的 Skills 集合。它们名称相似,但形态上有差异。
| 框架/形态 | 声明方式 | 如何被模型发现 | 执行环境 |
|---|---|---|---|
| Claude Skills | 每个技能一个目录,含 SKILL.md | 由 Agent 运行时加载目录并索引 | 本地 CLI 或桌面端 |
| Codex Skills | 通过配置目录加载,类似 Claude 风格 | 由 codex CLI 在启动时扫描 | 命令行环境 |
| GitHub Skills | 仓库内文档与自动化流程 | 主要在项目内作为工作流说明 | 取决于仓库类型 |
这些形态并不是互斥的。我在 agent-skills 项目里为了迁移方便,定了一条规则:技能核心逻辑只依赖标准 Python 库和文件系统,不调用任何框架独有 API。这样同一个技能包,放到 Claude 生态能跑,放到 codex 环境也能跑,最多在描述格式上做小幅适配。
如果你打算写通用技能,建议遵循“可迁移写法”:脚本入口用python3而不是特定虚拟环境;路径使用参数传递而不是环境变量;输出必须是 JSON;SKILL.md 保持相对独立。这套写法让我在切换框架时几乎没有做过二次开发。
4.3 多 Agent 场景下的技能共享与编排
单 Agent 场景下,技能系统已经够复杂。进入多 Agent 编排后,技能的所有权和调度问题会更突出。我跑多 Agent 时碰到过三个典型问题。
第一个是技能所有权。多个 Agent 共用一个技能目录,结果 A 更新了技能,B 还被旧版本缓存毒害。后来我改成按 Agent 锁定技能仓库版本,共享的技能统一走“技能发布-拉取”流程,不再每个人自己改本地文件。
第二个是并发冲突。两个 Agent 同时调用同一个有写操作的技能,文件互相覆盖。我的解法是:所有写操作技能都加一个lock_id参数,harness 在调度时保证同一个技能对同一目标只能串行执行。
第三个是任务路由。主 Agent 到底该把哪个技能派给哪个子 Agent?我在 agent-skills 项目里做了一个很轻量的技能发现表,每个 Agent 注册自己的技能白名单,主 Agent 根据任务关键词做路由。比如“整理笔记”这个技能只注册给文件处理 Agent,主 Agent 不会把它派给计算 Agent。
如果你用 Rust 写 agent 框架,技能可以设计成 trait 对象,在编译期就锁定技能接口,运行时动态加载具体实现。Rust 的所有权模型对技能并发执行的限制反而帮了大忙,很多 Python 场景下的共享冲突在 Rust 里编译阶段就被拦住了。
5. 实战排错与经验沉淀
5.1 技能调用了但结果不对:三层排查法
我在项目上线后统计过技能调用失败的案例,发现大部分问题都出在三个层次,所以整理了一套三层排查法。
第一层:模型有没有选中正确的技能。打开 trace 日志,看工具调用记录。如果模型根本没调用,说明 SKILL.md 的描述让模型犹豫了。这时候优先检查触发条件和负面清单。我在一个案例里发现,技能描述里用了“可以”这种弱语气,模型就把它当成可选项,经常跳过;改成“当且仅当用户明确要求整理时使用”之后,调用准确率立刻上来。
第二层:技能脚本本身是否正常。直接把脚本拿出来独立跑一遍,看退出码和输出。很多问题其实是路径不存在、权限不够、依赖缺失这些“普通编程问题”,和 agent 一点关系都没有。
第三层:输出有没有被模型正确使用。脚本输出了正确 JSON,但模型下一步没按 JSON 里的摘要走,这通常是输出格式里字段含义不够清晰。我会在 SKILL.md 的输出示例里加一行“字段解释”,让模型理解summary是给用户的摘要,index_path是后续可操作的文件路径。
下面是一张常见问题速查表,基本覆盖了我遇到的绝大多数情况:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 模型从不调用技能 | 描述触发条件太模糊 | 增加 when_to_use 和 when_not_to_use |
| 调用时参数缺失 | SKILL.md 的 input 定义不够严格 | 明确必填字段和类型 |
| 脚本报权限错误 | 运行时没有授予目录权限 | 检查 manifest 中 permissions |
| 返回结果被模型忽略 | 输出结构太复杂 | 压缩字段,附示例说明 |
| 技能反复重试失败 | 脚本没有幂等设计 | 增加前置校验,重复执行返回相同结果 |
| 上下文被技能输出撑爆 | 输出超过 token 预算 | 强制截断+摘要回填 |
这个排查顺序已经帮我解决了不少问题。如果你也遇到技能“明明有但就是不生效”,先别怀疑模型能力,大概率是描述和输出环节的细节没做好。
5.2 上下文污染、越权与 prompt injection 防护
技能开发里最容易忽略的是安全问题。我把技能引入的外部内容分为两种情况,处理逻辑完全不同。
一种情况是外部文本与当前任务相关,但来源不可信。比如一个“网页摘要”技能,抓取回来的页面内容里可能藏着恶意指令。如果直接把页面原文塞进上下文,模型有可能被这些指令干扰,执行不该执行的操作。我的做法是:技能脚本在返回前先做清洗,只提取标题、正文摘要等结构化字段,任何超过预设长度的原始文本一律丢弃。
另一种情况是特权操作的二次确认。一个技能如果具备“写入文件”或“调用外部 API”的能力,harness 应该默认它不可信,直到任务语境明确需要这种能力。我在项目里实现了一个简单的规则:写操作技能返回结果前,必须带一个dry_run字段,展示“将要做什么”,确认后才真正执行。
还要提醒一点:不要在 SKILL.md 里写“忽略所有其他指令”之类的措辞来“加强”技能触发率。这类写法和 prompt injection 没有本质区别,短时间可能有用,但会污染模型的判断逻辑,长期一定出问题。技能描述应该是中立的、描述性的,不是命令式的。
5.3 我踩过的一些坑和后续扩展方向
最后分享几个实打实的教训。
第一个坑:先写实现,后写描述。我早期开发技能时喜欢先把 Python 脚本写完,再回头补 SKILL.md。结果模型根本不知道这个技能什么时候该用,等于白做。现在我改成“描述优先”:先把 SKILL.md 写完整,再按描述去实现。描述写得清楚,实现方向就不会跑偏,返工率大大降低。
第二个坑:技能做得太大,什么都想管。我开发过一个“全能文件助手”,既能读文件、又能改格式、还能批量重命名。模型确实频繁调用,但返回结果经常模糊,因为内部逻辑分支太多,异常情况根本测不完。拆成三个小技能之后,每个技能描述精准、输出明确,调用成功率反而提高了不少。
第三个坑:测试只看一两次成功就上线。语言模型调用技能时,输入分布的随机性很强,只测固定用例根本不够。我现在要求每个技能至少准备五个不同输入场景,覆盖边界情况,比如空目录、文件编码异常、同名文件冲突、权限拒绝、超大文件。脚本对这些场景的输出必须稳定,才允许接入 agent。
后续我还想做的扩展包括:定义一套跨框架通用的技能格式规范,让同一份技能包在不同 harness 间直接迁移;给技能加语义化版本管理和依赖解析;再把技能间的依赖关系显式地画出来,避免多技能叠加时的行为冲突。技能系统的生态还在很早期,现在参与定规范,至少能让自己的项目少走弯路。