我最近在维护一个叫 agent-skills 的实验项目核心思路特别简单把智能体的各种能力拆成一个个独立的“技能”每个技能就是一套“说明书 脚本 元数据”的打包组合。最开始这么做只是因为被逼得没办法——智能体加的功能一多所有函数堆在同一个工具列表里所有调用说明挤在系统提示词里很快就开始互相干扰、频繁选错工具。后来我发现用技能化的方式组织智能体能力不只是代码整洁的问题它直接改变了模型对能力的理解方式。这篇文章我就把 agent-skills 从需求拆解、技能定义、落地实现到复用切分整个过程讲透特别适合正在做智能体应用开发、或者维护一堆工具函数不知道该怎么整理的同学。1. 为什么我把智能体能力拆成“技能”而不是堆 Prompt 和工具1.1 从一次失败的“全家桶工具”改造说起我最早做智能体的时候习惯把所有能力写进一个tools.py每个函数就是一个“工具”然后在 system prompt 里列一行“你可以使用以下工具……”。刚开始只有三五个函数一切正常。等函数超过二十个问题就来了模型经常调用错参数有些函数功能相近它根本分不清该用哪个系统提示词也越来越臃肿每加一个工具都要修改长文本改完还容易影响已经稳定的其它指令。真正让我决定重构的是一次日志排查。某个任务里模型连续五次调用了同一个函数每次参数都是错的而且它完全没意识到应该换一个更合适的工具。我翻看提示词才发现那个工具的描述写得含糊和另一个工具在功能上高度重叠。我意识到靠平铺函数和一段提示词来管理能力天花板太低了。于是 agent-skills 这个项目就开始了。我想做的不只是“把函数封装得更干净”而是让每个能力单元自包含——模型看到它能自己判断“这个场景该用谁”开发者能把它单独打包、测试、迁移到另一个智能体里。1.2 技能化的三个底层变化技能化和普通工具函数封装表面看起来都是“一个函数 一段注释”本质上有三个不同第一从平铺函数变为按职责封装。传统工具列表是一个全局大集合所有函数并排放着技能则把每个能力放进独立目录目录里有说明文档、执行脚本、依赖清单。集合是“散装”的技能是“盒装”的。第二从“人写死调用规则”变为“模型按描述自主判断”。传统方式是人规定什么场景调什么函数技能化之后每个技能的说明文档就是给模型看的使用手册模型通过阅读SKILL.md里的描述和触发条件自己决定是否调用、怎么调用。第三从单项目私有变为跨项目可复用。技能是一个自包含的目录我把某个技能复制到另一个智能体里只要加载器认识这个目录结构它就能直接工作不需要再去改那台智能体的全局提示词。可以拿工具箱来类比平铺函数是把所有螺丝刀、扳手、钳子都扔在一个大抽屉里找什么全靠翻技能化是给每把工具贴上标签、写上用途和注意事项挂在工具墙上。模型拿到的不再是“一堆函数”而是一个“整理好的工具墙”。1.3 什么场景值得技能化什么场景别硬上并不是所有能力都值得做成技能我一开始在这个问题上纠结了很久。后来总结出三个判断标准值得技能化的场景通常满足三点功能上有明确边界比如“整理文件”“查询天气”“生成周报”都是完整子任务输入输出有清晰契约能够用参数和返回值描述需要被多个项目或角色复用至少预期用两次以上。反过来有些东西就不适合硬上。比如一个只有三行的字符串处理函数它只是某个大流程的内部实现细节做成独立技能反而增加加载成本和检索噪音。一次性的临时逻辑也不应该技能化技能的价值在于复用不复用的东西封装得再好都是浪费。如果拿不准我有个简单建议先让代码裸奔跑通确认逻辑稳定后再问自己“这个能力我下个月还会不会用到”。会用到就技能化不会就别动它。2. agent-skills 的技能定义骨架目录、元数据与说明文档的分工2.1 目录结构与文件分工agent-skills 里每个技能都是一个标准目录路径上由几类文件组成skills/ ├── file-organizer/ │ ├── SKILL.md │ ├── run.py │ ├── requirements.txt │ └── schema.json ├── web-fetcher/ │ ├── SKILL.md │ ├── fetch.py │ ├── requirements.txt │ └── schema.json └── manifest.json各个文件职责很明确SKILL.md给模型看的说明书包括元数据和步骤说明是整个技能的“入口”run.py给程序执行的实现脚本包含核心函数schema.json参数定义用 JSON Schema 描述输入参数的类型、必填项、取值范围requirements.txt当前技能依赖的第三方库顶层manifest.json技能库索引告诉加载器有哪些技能、各自入口文件在哪个路径。这样的分工核心是让“模型理解层”和“代码执行层”完全解耦。模型不需要读 Python 源码它只需要看SKILL.md就能学会使用这个技能执行层也不需要关心模型怎么理解它只负责按参数把活干完。两部分通过 schema 来对齐。2.2 SKILL.md 的元数据字段设计SKILL.md的开头是一段 YAML front matter我设计的关键字段有这几个--- name: file-organizer version: 1.2.0 description: 按规则整理指定目录中的文件将文件移动到按扩展名分类的子目录。 when_to_use: 当用户要求整理/归档/分类某个文件夹中的文件时使用适合处理下载目录、桌面、临时目录。 parameters: src_dir: type: string description: 需要整理的源目录绝对路径。 required: true dest_root: type: string description: 整理后文件存放的根目录默认与源目录相同。 required: false dry_run: type: boolean description: 为 true 时只输出将要执行的动作不实际移动文件。 required: false default: false dependencies: - python 3.10 --- ## 执行说明 ...name和version是硬性的description和when_to_use是模型做“该不该调用”判断的关键。我踩过的坑是早期只写了description没有when_to_use模型经常在某个技能完全不相关的对话里把技能调出来浪费 token 还混淆上下文。加了触发条件之后误触发率明显下降。参数定义全部放到 schema 里而不是只写在正文中是因为模型需要用结构化的方式理解参数而不是从一大段文字里“猜”参数。参数定义越清晰模型构造调用时就越不容易出错。2.3 正文部分教模型“怎么做”的高质量说明元数据之下SKILL.md正文是给模型的“操作手册”。我写正文时给自己定了几个规则第一用编号步骤描述执行过程而不是笼统一句话。比如“先检查源目录是否存在再列出所有文件按扩展名分组逐组创建目标目录并移动文件”。模型是推理模型给它步骤序列它就知道在调用前后该确认哪些前置条件。第二明确边界情况。比如“如果目标目录中已有同名文件不要直接覆盖先返回冲突列表让用户决定”。不写边界情况模型不知道技能遇到异常时该怎么做。第三给出调用示例。正文里我会写至少一个完整示例参数怎么填返回值长什么样模型拿到结果后怎么判断“成功还是失败”。这能大幅减少模型拼接参数时的自由发挥。2.4 为什么选 Markdown YAML而不是纯代码注册其实在一开始有人建议我直接用 Python 里的装饰器注册函数像写 Flask 路由一样写技能。我试过后面放弃了原因有三。Markdown 和 YAML 都是模型训练时见过非常多的格式大模型对这些文本结构的理解深度远高于对 Python 类装饰器的理解。技能的目标读者一半是模型格式必须优先选模型最熟悉的。其次文本格式天然适合 Git 做 diff 对比改了一个技能的描述提交记录里能看出来到底改了哪个字段代码注册的方式很难做到。第三文本格式没有运行时依赖任何语言实现的智能体加载器都能读取技能目录不需要跟特定编程框架绑定。所以这个设计不是“图省事”而是从模型认知、版本管理、跨语言兼容三个维度权衡后的结果。3. 一个真实技能落地文件整理工具的拆解与实现3.1 需求边界与参数设计我拿 agent-skills 里第一个正式技能file-organizer来说说完整落地过程。需求听起来很普通把一个杂乱目录里的文件按扩展名移动到对应子目录比如.pdf进documents/pdf.png.jpg进images。但“整理文件”这个词太含糊了如果直接实现成“把所有文件按扩展名放到各类文件夹”会有很多问题要不要处理隐藏文件要不要覆盖同名文件源目录不存在怎么办扩展名未知的文件放哪里我把这些疑问全部转化为参数和规则。最终参数定下来四个src_dir必填dest_root选填dry_run选填rules选填。rules是扩展名到目标子目录的映射不填时使用内置默认规则。设计成可选规则而不是写死规则是为了让同一个技能既能服务“下载目录整理”又能服务“项目构建产物整理”。参数设计时我特别注意一点不接受“整理所有文件”这种无边界指令的所有权由模型判断技能本身必须用输入参数把任务边界划清楚。否则模型随便传一个盘符根目录进来后面就不可控了。3.2 核心实现run.py的核心函数结构是这样的import os import shutil from pathlib import Path from typing import Dict, List, Optional DEFAULT_RULES { .pdf: documents/pdf, .docx: documents/word, .xlsx: documents/excel, .png: images, .jpg: images, .jpeg: images, .gif: images, .zip: archives, .py: code/python, } def organize_files( src_dir: str, dest_root: Optional[str] None, dry_run: bool False, rules: Optional[Dict[str, str]] None, ) - Dict: src Path(src_dir) if not src.is_dir(): return {ok: False, error: fsource directory not found: {src_dir}} root Path(dest_root) if dest_root else src merged_rules {**DEFAULT_RULES, **(rules or {})} moves [] conflicts [] for path in src.iterdir(): if not path.is_file(): continue if path.name.startswith(.): continue ext path.suffix.lower() target_rel merged_rules.get(ext, others) target_dir root / target_rel target_path target_dir / path.name if target_path.exists(): conflicts.append(str(target_path)) continue moves.append({from: str(path), to: str(target_path)}) if not dry_run: target_dir.mkdir(parentsTrue, exist_okTrue) shutil.move(str(path), str(target_path)) return { ok: True, dry_run: dry_run, moves: moves, conflicts: conflicts, }这里几个设计点是刻意为之的。返回值统一用字典带ok字段这样模型拿到结果后第一眼就能判断成败不用自己去字符串里找“完成”两个字。所有待执行动作先收集到列表再统一处理方便dry_run模式直接输出动作列表。对已有同名文件不覆盖而是进入conflicts列表把决定权交回给用户和模型而不是自作主张覆盖文件。这些细节都是我在真实整理文件时被坑过之后补上的。3.3 dry-run 为什么重要dry_run这个参数让技能变得可安全演练。它默认false但模型在调用时一般会先跑一次dry_run: true把动作列表展示给用户确认再跑正式执行。这个流程像极了命令行工具里的“试运行”最大价值是避免不可逆操作。我第一次写这个技能时没有 dry-run测试时直接把一个测试目录里的文件全挪走有几个文件因为目标目录在同目录下路径处理不当把目录结构都带乱了。加了 dry-run 之后至少每次操作前都能看到“将移动 42 个文件跳过 3 个冲突”能提前发现问题。3.4 注册与触发技能写好后需要让智能体知道它的存在。agent-skills 使用目录扫描注册法顶层manifest.json列出技能名称加载器读取后导入技能实现。import json import importlib from pathlib import Path def load_skills(skills_root: str): root Path(skills_root) manifest_path root / manifest.json with open(manifest_path, r, encodingutf-8) as f: manifest json.load(f) loaded {} for item in manifest[skills]: module_path root / item[entry] mod importlib.import_module(module_path) loaded[item[name]] { entry: mod, params: json.loads((root / item[schema]).read_text(encodingutf-8)), doc: (root / item[doc]).read_text(encodingutf-8), } return loaded模型看到技能列表时是通过description和when_to_use判断“要不要用、什么时候用”。所以注册阶段我还会把每个技能的元数据组装成一个 JSON供模型在对话中做工具选择相当于建立了一个“技能索引”。实际触发链路是模型收到用户请求检索技能索引发现“整理文件”请求匹配file-organizer按 schema 生成参数 JSON调用加载器执行脚本拿到返回的字典后再组织给用户的回答。这一步跑通之后我才算真正完成了第一个技能。4. 一套技能库如何在多个智能体间复用打包、加载与依赖处理4.1 打包与版本管理技能写出来之后最大的价值是复用。我维护的不止一个智能体有的做办公自动化有的做数据处理它们需要的能力有重叠。如果每个智能体都单独写一套维护成本会爆炸。所以技能库的打包复用是 agent-skills 的核心目标之一。每个技能都有自己的version整个技能库也有版本号。发布时打上 git tag比如v0.3.0语义化版本规则照搬通用实践破坏性变更升大版本新增能力升小版本只改文档和修复补丁升补丁号。技能之间如果存在依赖在元数据里写清楚比如“web-fetcher 依赖 http-client 的 1.x 版本”。复用时要避免的是“复制粘贴后各改各的”。我给每个技能设置了单一来源所有智能体通过同一个技能仓库引入技能而不是把技能文件拷贝进各自项目。这样上游修了 bug下游拉一下依赖就能同步。4.2 技能加载器的兼容处理不同智能体的运行环境可能不一样有的是 Python 后端有的是 Node 后端有的跑在云端函数里。技能库要保持可复用加载器就得做兼容。我在设计manifest.json时故意不写死加载逻辑只描述文件路径和格式。这样每个环境自己实现加载器但解析的是同一套技能定义。比如 Node 环境也会去读SKILL.md的 YAML front matter也会读schema.json只是执行脚本可能是run.js而不是run.py。技能说明文档是跨语言共用的执行脚本按语言区分manifest 里用entry字段指向不同入口。路径兼容也是一个大坑。Windows 和 Linux 的路径分隔符不同技能返回值里的路径如果写死正斜杠在 Windows 环境就会有问题。我的做法是内部统一用pathlib处理路径返回时转换成当前平台的标准格式。这个细节不处理技能换一台机器就报错。4.3 依赖冲突与安全边界技能复用最怕依赖冲突。技能 A 需要某个库的 2.x 版本技能 B 需要同一个库的 1.x 版本如果它们跑在同一个 Python 进程里会非常难搞。我在 agent-skills 里用两层方案缓解。第一层是显式声明依赖每个requirements.txt都写清版本第二层是把高风险技能丢进独立进程或容器里执行通过标准输入输出和主程序通信。比如 web-fetcher 这类要访问外部的技能跑在受限子进程里只允许访问指定网络范围防止它被恶意 URL 利用后拖垮整个智能体。安全边界是我后来补上的也是最不能省的一环。技能本质上是可执行代码一个技能库放进了恶意技能就等于把任意代码执行能力交给了模型。我在录技能时坚持三个原则不给非必要技能开放写权限路径参数强制校验禁止越出工作目录跨进程执行的技能设置超时和资源上限。5. 构建 agent-skills 过程中踩过的四个硬坑5.1 模型不触发技能description 写得太“像广告”我把最早的几个技能发到测试智能体里发现模型在明显应该调用技能的场景下总是选择自己硬回答。比如用户问“把我桌面上的图片都整理一下”模型回复“好的你需要整理吗请确认一下”完全没有调用file-organizer。排查下来问题出在description写得太泛了。我写的是“一个用于文件整理的工具”模型看不出它在什么场景下有优势。后来我在when_to_use里写上了非常具体的触发场景“当用户要求整理、归档、分类文件夹中的文件尤其是下载目录、桌面、临时目录时使用当用户提到图片、文档等按类型归类时优先使用。”改完之后模型在对应场景基本都会主动调用。这个坑的教训是给模型看的技术文档要站在“什么时候该用”的角度写而不是“我能做什么”的角度写。后者是广告语前者才是决策依据。5.2 技能抛错了智能体还在盲目重试早期run.py碰到错误就直接抛异常结果模型收到异常消息后没有意识到是逻辑错误反而认为“参数不对”然后换着参数重复调用连续试五六次浪费大量 token。修复方式就是我在文件整理技能里用的那套错误也走结构化返回值。异常信息放进error字段ok置为false。模型看到ok: false就知道这个技能真的执行不下去了应该向用户反馈错误而不是自己硬试。对技能实现者来说所有可预见的错误都要主动捕获并转成结构化结果对于完全不可预见的异常至少包一层顶层异常处理禁止裸抛。5.3 中文字符和带空格的路径被“好心”处理坏了我测试的时候遇到了一个典型问题在 Linux 下正常一放到 macOS 下含中文的目录名就乱码带空格的路径被拆成两截。初看以为是编码问题追下去发现是加载器在拼接命令时用了 shell 字符串而不是直接传参数列表。后来规定所有技能调用都不走 shell 字符串拼接参数必须通过 JSON 序列化传递函数内部用解析后的对象拿参数。路径统一交给pathlib处理绝不在系统命令里拼路径。这个改动之后中英文、带空格路径全部正常。5.4 技能库变大后启动加载慢得离谱技能从三个涨到十三个之后智能体启动时间明显变长。一开始每个技能都在启动时预加载读取文档、生成索引、导入执行模块结果一堆技能加起来拖慢了整个启动流程。我的解决思路是延迟加载与按需加载结合。启动时只解析元数据和文档不导入执行模块模型选中某个技能后真正调用时才去导入脚本。进一步优化是把不常用技能标记为optional默认不生成检索索引需要时先由模型通过特殊请求激活。这个优化把启动时间从秒级降到了毫秒级。5.5 安全问题技能是代码能执行任意操作最后一个硬坑是安全。技能一旦跑起来它就是有权限的代码。某个测试用技能在做文件操作时没有限定工作目录结果传入一个src_dir/的路径差点把目录结构都动掉。安全设计我分三层做参数校验层路径必须 resolve 后在工作目录内权限层技能声明自己需要的权限没有声明的权限默认拒绝运行层高风险技能跑子进程加沙箱限制文件系统访问和网络访问。安全不是开发后期补的功能它必须和技能加载器一起设计否则技能越方便就越危险。6. 再往前走一步技能编排与下一步计划6.1 让技能学会调用技能单技能跑通之后我开始尝试技能编排一个技能的输出作为另一个技能的输入。比如web-fetcher抓取网页内容然后交给markdown-converter转成标准文档再由file-organizer归档到指定目录。这个过程的实现不复杂关键是让一个技能返回的数据结构能被下一个技能直接消费。我在 schema 里增加了outputs字段描述返回值的结构和用途编排器根据输出字段做自动衔接。目前我做得还比较保守只编排两到三个技能的短链路但效果已经比“一个技能硬做所有事”稳定得多。6.2 从技能库到技能市场的畅想技能库的价值在于标准化标准化的下一步就是交换。我这两周开始给 skill 元数据加分类标签比如“办公”“网络”“数据处理”尝试让不同项目可以直接按标签检索并引入外部技能。如果能保证安全性一套公开的技能市场是可行的——开发者发布技能使用方按需引入版本由发布方维护两边都省力。6.3 我的一点真实体会从agent-skills这个项目里我体会最深的是智能体应用开发的复杂度不会因为模型变强而自动消失它只是从“写 Prompt 的复杂度”转移到了“设计能力模块的复杂度”。技能化是我目前找到的最顺手的一种复杂度管理方式它把模型认知、代码实现、版本管理、安全边界都收进了同一个结构里。如果你也要做类似的东西我的建议是别一上来就做通用框架。先写三到五个真实要用的技能跑通完整链路再回头看哪些部分重复出现把它们抽成公共层。过早抽象只会让你在想象的通用性里绕圈真实技能会告诉你该抽象什么。