这两年做智能体应用的朋友,应该都有同一个体感:大模型本身的智商肉眼可见地上来了,但真让它按一套正经流程去干活,它经常卡在"知道"和"做到"之间。你问它"怎么批量处理一批PDF",它能给你写出非常完美的步骤说明,但你要让它真的把文件处理完、把结果整理好放到指定目录,十有八九会翻车。这个问题的核心,就是 agent-skills 这个方向要解决的:模型不缺通识知识,缺的是一套能把"知识"变成"可执行动作"的技能机制。
所谓 agent-skills,简单说就是给 Agent 挂上一套"技能包"——每个技能封装了一个特定领域的规则、操作步骤、依赖工具和输出格式。Agent 遇到对应任务时,先检索并加载技能,再按技能里定义的流程一步步执行,最后把结果汇总回来。这套思路现在已经有不少开源项目在落地,不管是 Claude 系的 Skills 机制,还是各种社区维护的 skill 仓库,本质上都是在做同一件事:给 Agent 装上一套可复用的"岗位说明书+工具箱"。这篇内容我就围绕 agent-skills 这个方向,把我自己从理论到落地、从踩坑到跑通的完整过程拆开讲一遍,适合正在做 AI Agent 开发、自动化流程设计或者研究大模型应用层的朋友做参考。
1. 内容整体设计与思路拆解
1.1 Agent 为什么缺的不是智商,是技能
先说个我自己的判断:现在的模型能力边界,已经远远超出了大多数人对它的使用方式。很多人觉得模型"不聪明",其实不是模型的问题,是你没有给它一套规范的动作流程。想象一下你团队来了个高智商实习生,聪明是真的聪明,一学就会,但你如果不给他一份 SOP、不告诉他公司内部用什么系统、什么格式交报告、找谁审批,他第一天大概率是坐在工位上发呆,或者自己发挥出一套完全没法用的流程。Agent 也是这样。
大模型在训练阶段学到的是通用能力——它知道什么是 PDF、什么是 csv、什么是"批量重命名",但它不知道你当前这个环境里 Python 版本是多少、文件放在哪个目录、输出格式需要符合什么规范、出错的时候该重试还是该放弃。agent-skills 要做的,就是把这些"环境相关的、流程相关的、规则相关的"信息,提前封装成一个个独立的技能模块。模型不靠记忆去猜,而是靠检索去加载,加载之后照着执行。
这个思路和 RAG 有点像,但本质不同。RAG 解决的是"模型不知道某件事"的问题,给模型补充事实性知识;技能体系解决的是"模型不知道该怎么做"的问题,给模型补充操作性流程。一个是补知识,一个是补动作。这也是 agent-skills 独立于 RAG、独立于通用 prompt 工程存在的根本原因——你可以用一段很长的 system prompt 告诉模型"你要怎么做",但真正复杂的多步骤任务,塞在 prompt 里既难维护、又难复用、还容易互相干扰,不如拆成一个一个独立的技能文件,按需加载。
1.2 技能系统的三要素与设计取舍
我拆解了目前主流的几个 agent-skills 项目,发现它们的核心设计都可以归纳成三个要素:
技能注册表:模型怎么知道当前环境里有哪些技能可用?不同项目做法不同,有的是扫描固定目录,有的是在代码里注册函数,有的是提供一个 manifest 文件。注册表的作用是让模型在决策是否调用技能之前,先能看到技能的全貌(名称、简介、适用场景),这一步决定了技能的"可见性"。
技能定义文件:这是每个技能的核心,通常是一份 markdown 或 json/yaml 格式的文档,里面写了技能的名称、描述、触发条件、使用步骤、注意事项、参数说明。模型在决定调用某个技能之后,会读取这份文档,然后按照文档里的指引去操作。定义文件的质量,直接决定了模型"会不会正确地使用技能"。
执行载体:光有文档不够,真正干活的是一段脚本、一个函数、或者一套 API 调用。文档负责指挥,执行载体负责动手。这两者之间通过约定的输入输出格式对接。
这三个要素的组合方式,决定了不同 agent-skills 项目的风格差异。有的项目倾向于"文档驱动",把技能定义写成人类和模型都能读懂的 markdown,执行载体是关联的外挂脚本,典型代表是 Claude 系的 Skills 机制;有的项目倾向于"代码驱动",用 python 装饰器直接注册函数,参数校验交给类型系统,典型代表是 OpenAI Agents SDK 那套 function_tool 体系。
从设计取舍上看,文档驱动的好处是门槛低、可维护性强、非开发者也能写技能;坏处是模型解析文档的过程有不确定性,文档写得不清晰就容易执行跑偏。代码驱动的好处是精确、可控、好调试;坏处是每个技能都要写代码,灵活度被限制在函数签名的框架里。我自己的项目里两种方式都在用,简单技能用文档驱动,复杂技能用代码驱动,这个后面实操部分细说。
2. 核心技能机制解析与实操要点
2.1 主流的三种技能定义方式
先把我见过的技能定义方式归个类,大家对照自己的技术栈选型。
文档驱动(SKILL.md 模式)
这种方式以 Anthropic 在 Claude Code、Claude Desktop 里推的 Skills 为代表。每个技能是一个目录,目录下有一个SKILL.md文件,文件开头是 YAML frontmatter,写name和description,后面正文写使用说明。目录里还可以放脚本、参考文档、示例数据。模型运行时通过 description 感知技能的存在,一旦判定任务匹配,就读取整个 SKILL.md 和关联文件来获取执行细节。
函数驱动(Tool Function 模式)
这是 OpenAI、以及大量 Agent 框架采用的方式。你直接用代码定义一个函数,函数的 docstring 描述用途,参数用类型注解和描述标注。框架会自动把函数转换成模型能理解的工具 schema,模型通过 function calling 机制来调用。这种方式的优点是完全可编程、参数校验严格、返回结果直接用代码处理;缺点是技能的"编写成本"高,不适合非开发者维护,而且函数的独立性差,不好做跨项目复用。
混合式(文档+脚本模式)
这是我在实际项目里最常用的一种。每个技能也是一个目录,里面有SKILL.md(给模型看的操作指南)和scripts/(真正执行的代码)。模型的调用路径是:先通过 description 判断要不要用这个技能 → 读取 SKILL.md 里的使用说明 → 按说明调用 scripts 里的脚本。混合式的好处是,模型不是直接执行代码,而是通过文档理解目的,再由它自己决定如何调用脚本,这比函数驱动更灵活,比纯文档驱动更可靠。
我用一个表格把这三种方式的差异整理了一下:
| 定义方式 | 代表机制 | 编写门槛 | 灵活性 | 调试难度 | 适合场景 |
|---|---|---|---|---|---|
| 文档驱动 | SKILL.md | 低 | 高 | 中等 | 流程型任务、非开发者维护 |
| 函数驱动 | function_tool | 高 | 低 | 低 | 确定性任务、开发者深度参与 |
| 混合式 | SKILL.md + scripts | 中 | 最高 | 高 | 复杂流程、需要模型自主编排 |
2.2 技能描述的质量决定调用准确率
这是整个 agent-skills 体系里最容易被忽视、但影响最大的一个环节。模型判定"要不要加载这个技能",唯一依据就是技能定义文件里的 description 字段。description 写得不清楚,技能写得再牛也没用——模型根本不会触发它。
我踩过的最典型的坑,是把 description 写得太泛。比如我早期写过一个数据清洗的技能,description 写的是"用于数据清洗"。看起来没什么问题,但实际跑的时候,模型在大多数场景下都认为"数据清洗"自己直接处理就行,不需要加载技能。后来我把 description 改成了:
--- name: data_cleaner description: 当用户提供 CSV/Excel 文件,且任务涉及缺失值处理、重复行去除、格式统一或异常值检测时使用。适用于本地文件路径,不适用于数据库查询或在线数据抓取。 ---改动后的效果非常明显。核心变化有三点:一是明确了适用的输入格式(CSV/Excel),二是列举了具体的操作范围(缺失值、重复行、格式、异常值),三是加了排除场景(不适用于数据库和在线抓取)。模型读到这个描述,匹配的准确率大幅提升。
写 description 的个人经验是:用动词开头描述使用时机,用列表列举能力范围,用排除句说明不适用边界。长度控制在 100-200 个字符之间,太长模型会忽略,太短区分度不够。这个经验不是玄学,是因为模型在做工具选择时其实就是一次语义匹配,你的描述和用户请求之间的语义距离越近,匹配概率越高。
另外一个细节是技能的命名。我看到很多项目里技能名用中文、或者用无意义的编号,这其实会影响匹配效果。技能名最好用英文蛇形命名,和 description 形成互补——description 负责"像日常语言",name 负责"像系统标识符"。比如一个处理周报的技能,name 叫weekly_report_generator,description 写成"当用户需要汇总本周工作、生成周报文档时使用"。两者职责分开,模型匹配的准确率最高。
2.3 技能依赖与执行环境隔离
技能写到后面一定会遇到依赖冲突的问题。我最早把所有技能的脚本放在同一个目录,共用同一个 Python 环境,结果某天给一个技能加了pandas新版本依赖,另一个技能立刻跑不了了。这种问题最隐蔽,因为报错信息往往是在某个深层函数里,单看错误完全想不到是依赖版本冲突。
后来我总结出一套相对稳健的环境隔离方案:
- 每个技能目录内尽量自包含:脚本使用的相对路径、相对导入,避免写绝对路径;
- 不同技能的 Python 依赖尽量收敛:能用标准库解决的就不用三方库,非要用的指定版本范围;
- 关键技能用独立虚拟环境或同一环境内的独立 conda env:这个视项目复杂度决定,轻量技能不推荐为每个技能建 env,管理成本太高;
- 环境变量统一注入:技能脚本里不硬编码任何密钥或路径,统一从配置文件读,宿主程序在启动子进程时注入环境变量。
环境隔离的权衡点在于:过度隔离会让技能库变得非常笨重,完全不做隔离又会不断踩依赖冲突的坑。我的判断标准是——低频维护场景不强依赖的脚本共用主环境,高频使用且依赖敏感的脚本单独包一层。这个标准一句话就能记住:看这个脚本改动的频率和依赖的重量级,改得越多、依赖越重,越该隔离。
3. 从零到一搭建技能系统的完整流程
3.1 选型:选择适合自己的 Agent 运行框架
如果你现在想上手 agent-skills,第一个问题就是选哪个运行框架。我不打算替你做决定,但可以把主流的几条路线的实际情况说一下。
路线一:Claude Code + skills 目录
这是上手最快的方式。Claude Code 支持一个 skills 目录,你把写好的技能文件夹放进去,Claude Code 启动时会自动扫描,模型在对话过程中会调用匹配的技能。这个路线的优点是完全不用写胶水代码,平时写 markdown 就能开发技能;缺点是运行环境相对封闭,技能调用过程你只有很有限的日志能看,调试要靠对话本身去试探。
路线二:OpenAI Agents SDK / 各类 Agent 框架
这套路线适合愿意写代码的人。你用 python 定义函数、装饰成工具,通过框架让模型调用。优点是可控制性极强——你可以定义复杂的参数校验逻辑、可以在函数内部做权限控制、可以精确掌握每次调用消耗的 token。缺点是从"想法"到"可用技能"的链路更长,每新增一个技能都要写一遍函数、跑一遍调试。
路线三:自研轻量技能引擎
如果你的项目不是围绕某个现成框架,而是有自己的业务系统,我更建议基于开源的 agent-skills 项目做改造。目前 GitHub 上有几个项目专门在做这类技能库,用统一的目录结构管理技能,通过标准接口对外暴露。这套路线的开发成本最高,但好处是技能不再绑死在某个具体的 Agent 框架里,将来换框架、换模型,技能库可以直接迁移。
从实际落地速度来看,我的建议是:如果你只是想验证技能机制、快速跑通流程,走路线一;如果你已经确定要投入做智能体产品,走路线三。路线二更适合中间状态——你愿意写代码,但又不想从头搭一遍技能调度逻辑。
3.2 创建第一个技能:一个完整的批量文件重命名示例
理论讲了这么多,接下来我完整演示一遍创建技能的过程。我用的是混合式结构,宿主环境用 Claude Code,技能实现用 Python。
先建目录结构:
skills/ rename_files/ SKILL.md scripts/ rename.py然后是SKILL.md的内容。这是整个技能里最核心的文件,它决定了模型怎么使用这个技能:
--- name: rename_files description: 当用户需要批量重命名本地目录中的文件时使用。支持按序号前缀重命名、批量查找替换、添加日期后缀。不适用于移动文件或修改文件内容。 ---接下来是正文部分,给模型看的操作指南:
# 批量重命名文件 ## 何时使用 用户明确提出需要批量重命名本地文件,或用户给定的任务中"重命名"是必要步骤时,应当使用本技能。 ## 核心步骤 1. 确认用户指定的目录路径,如果路径不存在,请先和用户确认。 2. 运行 `python scripts/rename.py --dir [目录] --mode [mode] --pattern [pattern]`。 3. 将脚本输出的结果(重命名的前后对照表)完整地展示给用户。 ## 模式说明 - `prefix`: 在文件名前添加序号前缀,如 `01_report.pdf`, `02_report.pdf` - `replace`: 批量查找替换文件名中的指定字符串,通过 `--old` 和 `--new` 参数指定 - `date`: 在文件名末尾添加当前日期,格式为 YYYYMMDD ## 注意事项 - 脚本默认采用安全模式,仅打印将要进行的操作,不实际执行。确认用户同意后,加上 `--apply` 参数才会真正执行。 - 重命名操作不可自动回滚,务必在动手前确认目标目录正确。然后是scripts/rename.py的简化实现:
import argparse import os from datetime import datetime def build_new_name(path, mode, old=None, new=None): name, ext = os.path.splitext(os.path.basename(path)) if mode == "prefix": return f"{name}{ext}" if mode == "replace" and old: return name.replace(old, new) + ext if mode == "date": suffix = datetime.now().strftime("%Y%m%d") return f"{name}_{suffix}{ext}" return None def collect_operations(directory, mode, old=None, new=None): def sort_key(p): return p.lower() files = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))] files.sort(key=sort_key) ops = [] for idx, f in enumerate(files): if mode == "prefix": new_name = f"{idx + 1:02d}_{f}" elif mode == "replace" and old: new_name = build_new_name(os.path.join(directory, f), mode, old, new) else: new_name = build_new_name(os.path.join(directory, f), mode) if new_name and new_name != f: ops.append((f, new_name)) return ops def main(): parser = argparse.ArgumentParser() parser.add_argument("--dir", required=True) parser.add_argument("--mode", required=True, choices=["prefix", "replace", "date"]) parser.add_argument("--old", default=None) parser.add_argument("--new", default="") parser.add_argument("--apply", action="store_true") args = parser.parse_args() if not os.path.isdir(args.dir): print(f"目录不存在: {args.dir}") return ops = collect_operations(args.dir, args.mode, args.old, args.new) if not ops: print("没有需要重命名的文件") return for src, dst in ops: marker = "[已执行]" if args.apply else "[待执行]" print(f"{marker} {src} -> {dst}") if args.apply: os.rename(os.path.join(args.dir, src), os.path.join(args.dir, dst)) if __name__ == "__main__": main()这个示例看起来简单,但里面包含了几个我在真实项目里总结的关键设计:
第一,安全模式默认开启。脚本默认只打印将要执行的操作,加--apply才真正改文件名。这个设计的出发点很现实——模型在执行脚本时,一旦理解错用户意图就直接改名,结果不可回滚。安全模式能给模型多一层"确认后再执行"的机会,大幅降低误操作风险。
第二,脚本输出永远是易解析的结构化文本。我特意让 stdout 输出每行的格式都包含固定标记([已执行]或[待执行])和原文件名 -> 新文件名的结构。模型读取这段输出时不需要"理解"散文式的描述,而是可以直接提取对照表给用户看。这是技能脚本和普通脚本最大的区别——普通脚本的输出是给人看的,技能脚本的输出是给模型看的,必须结构化。
第三,参数设计尽量扁平化。所有参数走命令行参数传入,不用配置文件,不用交互式输入。原因是模型在调用脚本时,交互式输入是一个灾难——它不知道什么时候该等待输入、什么时候该继续。一次性把参数都传完,脚本运行结束就退出,这是模型侧最容易处理的执行模型。
3.3 注册技能与完整调用验证
技能文件写好后,把它放到技能目录,重启宿主程序。以 Claude Code 为例,技能目录通常可以通过环境变量指定,你把上面整个rename_files文件夹原样放进去,重启后技能就会被自动扫描到。
验证流程上,我的习惯是先做一次最简单的对话测试:"帮我把/tmp/downloads目录下的文件按文件名排序加序号前缀重命名。" 这一步的核心目的是验证两个点:模型有没有正确触发技能(看它的回答里是否主动提到调用了rename_files),以及脚本能不能在目标目录正确执行。
如果模型没有触发技能,我一般从两个方向排查:一个是 skill 的 description 写得太泛或太窄,一个是宿主工具没正确加载技能目录。最简单的验证方式是直接在对话里问一句"你现在有哪些技能可用",如果模型列出的技能里没有 rename_files,那就是加载环节出了问题。
执行验证时,重点看模型是否遵守了"安全模式"的设计——正常情况下,第一次执行结果应该是"待执行"清单,模型会把它展示给用户并请求确认。如果模型直接执行了改名操作,说明它忽略了 SKILL.md 里的注意事项,这时候我会在 SKILL.md 的"何时使用"后多加一句"禁止在未获得用户明确确认时直接执行实际修改操作"。不要小看这个反复校准的过程,技能机制本身就是模型和定义文件之间不断磨合的结果,一次写不好很正常,迭代几次就能稳定下来。
4. 常见问题与排查技巧实录
4.1 技能未被触发
这是新手遇到最多的一个问题。现象很明确:技能文件放在正确位置,里面脚本也能独立运行,但模型就是"看不见"它。
排查方向按顺序来:先确认技能目录被正确加载,这个通过向模型询问可技能列表来验证;再检查技能目录里有没有其他同名技能抢占了识别,有的话改名;最后审视 description 的语义匹配程度。这里我要特别强调一个实战经验:技能目录名不等于技能名。有些项目靠目录名做技能标识,有些靠 SKILL.md 里的 frontmatter 的 name 字段,两个不一致很容易导致加载异常。
另外,不同宿主程序对技能目录的扫描策略不同,有的是启动时扫描一次,有的是每次对话都扫描。如果你修改了技能文件但模型行为没变化,第一个动作应该是重启宿主进程而不是反复调整描述内容。
4.2 脚本执行超时或崩溃
技能脚本最典型的崩溃场景有两个:一是脚本依赖的三方库没装,二是脚本里用了硬编码的绝对路径或固定文件名,在目标环境里找不到对应路径。
依赖问题的排查思路很直接,看宿主程序的报错日志,一般会准确告诉你 module not found 还是文件不存在。我常用的规避手段是在 SKILL.md 里明确写明脚本的运行要求,例如"需要已安装 Python 3.9+ 和 pandas",这样模型执行前可以先做一个环境检查。更稳妥的做法是在脚本开头做依赖检查,缺失时给出明确的安装提示。
超时问题在技能场景里更棘手。有些任务是天然耗时的,比如处理大文件、批量网络请求,模型可能因为长时间拿不到脚本输出而判定执行失败。我的经验是:任何可能耗时较长的操作,脚本里都要输出阶段性进度,每隔一段时间打印一行进度信息。这样模型能持续感知"脚本还在执行"而不是"脚本已经卡死"。
4.3 模型读不到或误解脚本输出
技能脚本的输出是模型理解执行结果的主要途径,所以输出格式非常重要。我踩过最典型的坑是脚本同时往 stdout 和 stderr 里写内容,结果模型只收到了 stdout 的日志,stderr 里的错误信息被框架吞掉了或显示成了平台的系统错误。排查时先确认框架有没有合并这两类输出,如果没有,脚本里所有用户需要看到的信息一律往 stdout 打印,错误信息也不要依赖 stderr,而是打印在 stdout 里并带上前缀避免混淆。
还有一个经常被忽略的细节:脚本输出的长度会被截断。模型上下文窗口是有限的,如果脚本一次性打印几百个文件的重命名清单,输出可能被宿主程序截断,模型只看到一部分结果。解决方式是控制输出格式,精炼并必要处合并。
4.4 技能权限与安全边界
这一条我放在最后但实际是最重要的一条。agent-skills 的本质是让模型能够执行任意代码,这本身就伴随着风险。你在电脑上装了一个不熟悉的技能包,模型按技能描述运行时,你实际上是让它有了执行本机命令的能力。
几点务实的建议:技能脚本运行前,先人工浏览脚本代码,确认没有执行不受信任的外部代码或访问敏感目录;技能脚本内的 API 密钥等敏感信息统一走环境变量注入,不硬编码在技能目录中;涉及删除、覆盖、批量修改这类不可逆操作时,脚本内设计确认机制。这些细则也许繁琐,但在真实项目中会帮你规避很多本可以避免的麻烦。
4.5 常见问题速查表
| 问题 | 常见原因 | 排查动作 | 解决方案 |
|---|---|---|---|
| 技能未被触发 | description写得太泛 / 技能目录未加载 | 问模型当前可用技能 | 重写description,加触发场景和排除条件 |
| 技能脚本报ModuleNotFound | 依赖未安装 | 看宿主程序错误日志 | 脚本开头做依赖检查并给出安装提示 |
| 脚本输出被模型误解 | stdout/stderr信息混杂或输出过长 | 手动运行脚本观察输出 | stdout只打印结构化数据,控制输出长度 |
| 模型忽略安全确认直接执行 | SKILL.md注意事项不够醒目 | 观察模型对话行为 | 在SKILL.md显著位置加禁止事项描述 |
| 技能间出现同名干扰 | 目录/技能命名冲突 | 检查技能目录 | 统一命名规范,技能名前缀化 |
5. 值得收藏的开源技能资源清单
5.1 各方向的开源项目收藏
抛开理论,我把我实际用过或长期关注的开源项目列一份清单,大家可以根据自己的场景直接采坑参考。
官方参考类:Anthropic 官方维护的 skills 示例仓库,里面包含了一批官方技能的样例,覆盖了 PDF 处理、数据分析、文件操作等常见场景。如果想把技能体系落到一个具体产品,这组仓库是理解"官方期望的技能写法"的最好模板。
社区聚合类:awesome-claude-skills 这类聚合仓库,收集了大量社区成员自制的技能,跨度很大,网络调研、API 调用、文档生成等方向都能找到参考。质量良莠不齐,但用来找灵感非常合适。
框架实现类:OpenAI Agents SDK 适合偏好代码驱动的开发者。它的设计思路和文档驱动正好形成对照——你不需要写 SKILL.md,但需要把每个技能实现为 python 函数。研究它可以帮助理解"function calling 派"和"技能文档派"的底层差异。
通用工具类:还有一类是面向终端操作的技能,让 Agent 读取、分析本机文件,这类技能在这个体系里相当实用。不过要注意,这类技能的权限面比较大,使用时要把安全审查做到位。
5.2 建设个人技能库的两条建议
第一,按领域分目录而非按功能平铺。我的技能库一开始是平铺的,放了几十个技能后,模型在检索时经常不知道优先匹配哪个。按领域拆成data_processing、file_management、content_generation等子目录后,每个技能被命中的概率显著提升。
第二,给技能做"配置页面"而非写死参数。很多技能需要根据项目、环境的不同调整参数,把这些参数抽成一个标准的配置文件,宿主读取技能时先读配置,运行时通过环境变量覆盖,能让同一个技能在多个项目里复用而无需复制一份改来改去。
结尾
最后聊点个人的实战感受。agent-skills 这套体系在我自己项目里跑了大半年,最大的体会是:技能本身不难写,难的是把技能的"描述边界"和"执行边界"理顺。描述边界解决的是模型什么时候该用这个技能,执行边界解决的是脚本在什么条件下安全运行。这两个边界理清楚了,一个技能放在哪里都稳定;理不清楚,技能就会变成薛定谔的调用——时灵时不灵。如果让我给一个最小可行的起步建议,那就是先拿一个高频、简单、有明确规则的任务(比如文件整理或格式转换),走一遍"定义文档-写脚本-挂载测试"的循环。你不需要一开始就构建一个庞大的技能库,一个技能跑通带来的体感冲击,比读十篇理论文章都强。跑通一个、沉淀一个、再扩展下一个,这是我认为上手 agent-skills 最靠谱的路径。