做 Agent 开发这半年多我最大的一个感触是很多人的 Agent 做得不够聪明不是模型能力不行而是把技能封装成了提示词模板。你往系统提示词里塞了一大段你是一个文件整理专家你要把 /tmp/test 里的文件按时间戳重命名注意后缀遇到同名文件要加序号...然后 Agent 就开始一本正经地胡说八道文件名改不对目录搞错中途还经常失忆。问题出在哪儿就出在你用提示词描述了一个过程但 Agent 本质上是一个需要工具的实体。它需要的不是一条条指令文本而是一个可加载、可调用、可复现的能力单元。这也是 SKILL.md 这类体系出现的原因把技能变成一个带元数据、带脚本、带示例的目录包让 Agent 在需要的时候自动加载而不是把所有技能说明都堆在上下文里。这篇文章面向正在做 Agent 开发和智能体应用的朋友无论你是刚接触 Agent 框架还是在为自己的智能体设计技能体系都应该看一下 SKILL.md 到底解决了什么问题以及它和传统提示词模板之间那条关键的分界线。1. 为什么说提示词模板撑不起 Agent 技能1.1 提示词模板的真实困境上下文与维护的双重压力我先说说提示词模板最大的硬伤它占用的是系统提示词里的常驻空间。你可能写过一个功能很完整的提示词比如让 Agent 做网页内容提取把请打开 URL、获取正文、去除导航栏、保留标题、过滤广告全部写进去甚至把正则表达式都贴在提示词里。当时测着没问题但一旦 Agent 同时具备多个技能比如还要管日程、发邮件、查天气你把所有技能的详细说明都塞进去上下文立刻膨胀。模型每轮推理都要在几千 token 的技能说明里做注意力筛选结果就是典型响应速度下降、输出格式不稳定、更可怕的是一旦任务步骤多Agent 会突然丢掉某条提示词里的关键约束。另一个容易被低估的问题是维护成本。提示词模板本质上是一段给人看的说明文字只是交给了模型执行。当你要修改一个行为的细节比如把按时间戳重命名改成按日期 YYYY-MM-DD 建子目录再移动你需要在那一大段提示词里找到对应的一句改掉然后祈祷其他地方没有耦合依赖。现实中没有一个 Agent 系统的技能数量会停留在个位数随着技能变多这种内联提示词的相互干扰会变得非常致命。最后也是最核心的一点提示词模板没法保证确定性。你用语言描述对文件做去重Agent 可能理解为按大小去重也可能理解为按内容哈希去重。每次运行的结果都可能因模型输出的随机性而不同。而一个真正的技能应当是一个可以精确执行的过程不该依赖模型的临场理解。1.2 SKILL.md 的本质用工程思维给 Agent 造工具SKILL.md 的出现本质上就是把技能从描述文本升级成了可执行资产。一个 Standard Skill 不再是一段提示词而是一个目录里面有 SKILL.md给 Agent 看的说明书、scripts真正干活的脚本、assets示例资源以及依赖和元数据定义。Agent 在遇到任务时会先通过语义匹配找到合适的技能目录然后读取 SKILL.md 来理解何时使用、怎么使用再调用 scripts 里的代码执行具体操作。这个设计解决了我上面说的三个问题技能说明只在需要时才被加载进上下文而不是常驻技能的行为由代码保证而不是由模型的临场发挥保证技能的更新可以走通用的代码管理流程目录独立、版本可追踪不会牵一发动全身。我在实际测试里对比过两种方案。同一个从网页提取结构化联系人信息的需求用提示词模板实现时遇到不同结构的网页成功率大约在六到七成改成 SKILL.md 加 Python 脚本解析 HTML 后成功率稳定在九成以上。原因很简单提示词模板是让模型去理解一个任务SKILL.md 是让模型去调用一个已经写好的工具模型只需要负责判断什么时候调用、传什么参数剩下的脏活累活交给代码。这个思路和人类的工作方式高度一致你不会让一个实习生每次都用自然语言解释一遍 Excel 怎么去重你会给他一个按钮或者一个宏。另外要特别提一点SKILL.md 不是要把 Agent 变得机械化。相反它把 Agent 的创造性留给了真正需要创造性的部分——理解用户意图、拆解任务、决定调用顺序而把确定性留给了代码。这个分工非常重要理解了这一点你就知道为什么说别再把技能写成提示词模板。2. SKILL.md 的目录结构设计与选型思路2.1 一份标准 skill 目录长什么样我最早接触 SKILL.md 时最先关心的就是目录结构长什么样。其实它和编程里的一个模块module非常像本质上就是一个 self-contained 的文件夹。按社区里比较公认的规范典型结构是这样的skills/ └── web_fetch/ ├── SKILL.md ├── scripts/ │ ├── fetch_url.py │ └── extract_contacts.py ├── assets/ │ └── example_output.json └── requirements.txt每个 skills 主目录下面的子目录就是一项独立的技能。目录名建议用短横线分隔英文比如 web_fetch、file_organizer方便 Agent 做语义关联。核心的 SKILL.md 放在技能目录根下里面用 YAML frontmatter 和 Markdown 正文描述技能scripts 目录放实际执行的脚本assets 目录放示例输出或者输入样本如果脚本依赖外部库就用 requirements.txt 描述。我在实际项目里一般会多加一个 docs 子目录里面放比较详细的参数说明或者维护记录。这样做的原因是SKILL.md 是给 Agent 看的篇幅有限不可能面面俱到而 docs 是给人看的方便你自己或者团队成员日后维护。目录设计有个很容易忽略的细节一条技能职责要单一。不要做万能技能想着一个目录同时处理文件整理、网页抓取和 OCR 识别。技能越胖Agent 越容易误判——它可能在处理 OCR 任务时因为 description 里出现了读取图片中的文字而把一个网页技能加载进来。保持每个技能小而专就像一个工具库里的工具一样一个锤子只负责敲钉子。2.2 Frontmatter 里最容易被忽略的字段SKILL.md 的开头是 YAML frontmatter 区长得像这样--- name: web_fetch description: 在允许的站点中抓取网页内容并提取结构化信息。当用户需要获取网页数据、检查网页状态、提取页面联系人信息时使用。 allowed-tools: [] ---name 字段好理解就是一个唯一标识。这里我要特别强调的是 description 字段它决定了 Agent 到底能不能在正确的时机想起这个技能。很多人会把 description 写成抓取网页内容的技能太笼统了。一个比较好的写法是说清楚它做什么、在什么场景下用、以及不要拿它来做什么。我习惯用当用户需要 X 时使用的句式并且在后面加一句负向描述比如不要在处理本地文件时使用此技能。这个负向描述非常有效能大幅降低 Agent 在错误场景下误调技能的概率。allowed-tools 字段很多人不重视甚至直接留空。它的作用是限制技能内部可以调用的其他工具避免一个抓网页的技能突然拿到了执行 shell 的权限。安全无小事尤其是在技能会执行外部输入数据的情况下尽量缩小权限边界。我见过有人把 allowed-tools 留空Agent 在技能内部私自调用危险工具导致沙箱报错的案例这就是典型的权限没设好。2.3 SKILL.md 正文该怎么写写给 Agent 看的说明书正文部分其实是和 Agent对话的说明书它的对象是模型不是人。所以写法和写给人看的 README 完全不同。一份好的 SKILL.md 正文应当包含几个关键部分When to use the skill什么时候用进一步的触发条件说明覆盖 description 里的场景How to use the skill怎么用分步骤的操作说明但不需要把每个细节都写成文字具体的执行建议交给 scriptsExample prompt / workflow示例任务给出一个完整的输入输出例子方便 Agent 做 one-shot 参考Important notes关键注意事项比如脚本执行失败时请检查 URL 是否包含反爬参数不要反复重试之类。我见过有人把 SKILL.md 写成了一篇 3000 字的操作手册里面全是命令和截图。这其实是误区SKILL.md 不是文档库它是技能的入口配置。你把所有细节都写在正文里Agent 加载时照样会丢失重点。我的经验是正文控制在 60 到 120 行之间核心是把什么情况调用和调用后做什么讲清楚剩下的让脚本去完成。一个比较实用的写法是在 SKILL.md 里加一个最小可用示例。比如文件整理技能可以在正文里写当用户说出把 downloads 里的文件按扩展名分类时应该调用 scripts/organize.py参数 source_dir 传 downloads 路径输出是整理后的目录结构。Agent 拿到这个示例就知道参数怎么传、脚本怎么调用不至于自己猜。3. 实战拆解从零做一个本地文件整理技能3.1 技能定义与场景分析回到具体操练。我拿一个我自己打磨过的技能来拆本地文件整理助手。这个场景很典型几乎所有 Agent 系统都能用到而且用提示词模板做和用 SKILL.md 做差距极其明显。先分析需求用户经常会让 Agent 清理一个乱七八糟的目录比如帮我把下载目录整理一下。提示词模板的做法是让模型理解整理这个词然后靠模型自己决定规则比如按扩展名分类、按时间归档、删除临时文件。但这里风险就出现了——模型可能会把 .tmp 文件直接删除而用户其实想保留也可能把带有final字样的文件误判为最终版本。这些都是语义理解带来的坑。SKILL.md 的做法完全不同我们把整理规则固化成代码。比如按 MIME 类型建立一级目录文档、图片、视频、音频、压缩包、其他再按日期建立二级目录2025-03-20冲突时自动加序号。这个规则一旦写好任何一次执行都是确定性的。用户得到的不是Agent 理解的整理结果而是你事先定义好的整理结果。这两者的差异用过的人都知道有多重要。你在设计技能的时候第一步不是写代码而是先定义行为契约这个技能接受什么输入、产生什么输出、有哪些边界条件。我自己一般会在 SKILL.md 的草稿阶段就写好这些约束再把它翻译成 YAML 和正文。3.2 落地实现完整目录与核心代码这个技能的目录结构我这样搭skills/ └── file_organizer/ ├── SKILL.md ├── scripts/ │ ├── organize.py │ └── preview.py ├── assets/ │ └── example_tree.txt └── requirements.txt其中 SKILL.md 是这样写的--- name: file_organizer description: 将指定目录中的文件按 MIME 类型和日期整理到分类子目录中。当用户要求整理文件夹、归档文件、清理下载目录时使用。不要用于删除文件或修改文件内容。 --- # 文件整理技能 ## When to Use 当用户给出一个目录路径并要求整理、归类、归档其中文件时使用本技能。 如果用户只要求查看目录内容不涉及归档则不要调用。 ## How to Use 1. 使用 organize.py 脚本通过 --source 参数指定目标目录。 2. 脚本会读取所有文件并用 mimetypes 推断类型将文件移动到分类目录。 3. 如果目标目录已存在同名文件自动追加 _1、_2 序号。 4. 默认不删除任何文件如需预览可先调用 preview.py。 ## Example Flow 用户把 ~/Downloads 整理一下 Agent 执行 python scripts/organize.py --source ~/Downloads --dry-run 展示预览结果询问用户是否确认确认后去掉 --dry-run 重新执行。 ## Important Notes - 不要删除源文件整理动作仅移动文件。 - 隐藏文件.DS_Store、.tmp不处理保留在原目录。 - 脚本失败时检查目录是否存在、是否有读取权限不要重复执行超过两次。注意我在正文里刻意强调了默认不删除文件和隐藏文件不处理。这就是在用说明书约束 Agent 的行为边界。你如果不写这句话Agent 在整理时可能自作主张把 .tmp 文件清了用户找不回来。设定明确的负向边界是 SKILL.md 写作中最重要的一环。核心脚本 organize.py 的关键逻辑大致是import argparse import shutil import mimetypes from pathlib import Path from collections import defaultdict CATEGORIES { documents: [.pdf, .docx, .txt, .md, .xlsx], images: [.jpg, .jpeg, .png, .gif, .webp], videos: [.mp4, .mov, .avi, .mkv], audio: [.mp3, .wav, .flac], archives: [.zip, .tar, .gz, .rar], others: [], } def categorize(path: Path) - str: ext path.suffix.lower() for category, exts in CATEGORIES.items(): if ext in exts: return category return others def organize(source: Path, dry_run: bool True) - None: files [p for p in source.iterdir() if p.is_file() and not p.name.startswith(.)] plan defaultdict(list) for f in files: plan[categorize(f)].append(f) for category, items in plan.items(): target_dir source / category if dry_run: print(f[plan] move {len(items)} files to {category}/) else: target_dir.mkdir(exist_okTrue) for item in items: dest target_dir / item.name counter 1 while dest.exists(): dest target_dir / f{item.stem}_{counter}{item.suffix} counter 1 shutil.move(str(item), str(dest))我这里有意把 categorize 用扩展名映射表写死而不是交给模型去判断 MIME。原因是扩展名映射更可预测尤其在文件名没变的情况下不会有歧义。你完全可以按自己的场景定制映射表甚至加入用户自定义规则比如凡是包含 report 的文件统一放到 reports 目录。脚本还加了 --dry-run 预览模式这非常重要。任何涉及批量变更文件系统的操作都应该先给 Agent 一个预览的机会。预览模式不仅避免了误操作也在流程上给了用户一个确认节点体验会好很多。3.3 让你的 Agent 真正用起来接入与触发脚本和说明书写好之后还要让你的 Agent 框架能识别并加载这个技能。不同框架的接入方式略有差异但思路是共通的把技能目录注册到 Agent 的技能搜索路径里。在工程实现上Agent 运行时会扫描技能目录读取每个子目录下的 SKILL.md把 name、description 索引成可检索的表。当用户提出新的任务时Agent 会通过语义匹配从技能表里挑出最相关的一到多个技能再加载完整的 SKILL.md 正文作为上下文。这也是为什么 description 的写作质量会直接影响技能触发率——它就是这个技能的检索指纹。我测试过用不同描述写法时的触发率差异。比如 description 写成文件整理时用户说帮我归置一下桌面上的东西Agent 触发该技能的概率只有四成左右。而把它改成将目录中文件按扩展名和日期移动到分类子目录。当用户要求整理文件夹、归档文件、清理下载目录时使用。不要用于删除文件,触发率直接提升到八成以上。所以千万不要小看那几行 description它几乎决定了你这个技能会不会被埋没。触发之后Agent 会按 SKILL.md 正文的指引调用 scripts 下的脚本并把脚本输出作为结果反馈给用户。这里有个技巧尽量让脚本输出结构化内容比如 JSON 或清晰的缩进文本这样 Agent 才能准确理解脚本的结果而不是在一堆日志里找关键信息。我通常在脚本末尾 print(json.dumps(result, ensure_asciiFalse, indent2))Agent 解析起来又快又稳。4. 常见问题与排查技巧实录4.1 技能不被触发怎么办这是大家问得最多的一个。明明 SKILL.md 写得挺完整可 Agent 就是不用偏偏自己硬编。我的排查顺序一般是这样的第一步看 description 是不是太抽象。千万不要只写处理文件的技能要加入触发场景词和负向约束第二步看技能目录名是否和 description 一致。有些框架做索引时会用目录名做辅助匹配如果目录名叫 utilsdescription 里却叫文件整理匹配质量会打折扣第三步看是不是技能太多互相打架。如果同时存在 file_organizer 和 file_cleaner用户说整理时两个都可能被召回Agent 可能挑了能力不合适的那一个。解决方式是给每个技能加更明确的场景边界。我还试过一个非常有效的方法在 description 里写调用条件时明确如果任务包含 X优先选择本技能。这种优先级强提示能让 Agent 在多个技能候选时更快做决策。4.2 脚本报了错 Agent 改不明白怎么办Agent 调用脚本时经常因为参数错误、路径不存在、权限不够等原因报错。模型在看到错误后可能尝试修改入参重试但有时候会陷入死循环。我在 SKILL.md 的 Important Notes 里专门写了一条脚本失败时检查目录是否存在、是否有权限、参数是否合法最多重试两次仍然失败就向用户报告错误并询问是否使用其他方式。这一条能有效避免 Agent 卡在那个无限自我修正的循环里。另外我建议在脚本里做足参数校验让错误信息尽量可读。比如用 argparse 自带参数解析时如果目录不存在就直接返回标准错误码和清晰的中文说明而不是抛一个晦涩的 Python traceback。Agent 看到清晰错误日志后修复思路会明显更清晰。还有一个大坑是 Python 脚本和 Agent 框架的运行时环境不一致比如系统 Python 缺少依赖库脚本一跑就 ModuleNotFoundError。这个问题我能用 requirements.txt 缓解但更稳妥的是在脚本开头做一次依赖自检缺了就打印提示不让 Agent 满世界找原因。4.3 什么时候仍然需要提示词模板写了这么多 SKILL.md 的好我也得说句公道话提示词模板并没有完全过时。它适合以下场景技能逻辑简单到不需要任何脚本也不涉及外部系统调用纯粹是约束模型的输出风格或思考方式比如写文案时要用轻松口语化风格回答法律问题时要先声明不构成专业意见。这类软技能用提示词模板更轻量没必要搞成 SKILL.md 目录。但凡是涉及文件操作、外部 API、数据处理、批量任务、需要精确执行步骤的场景我强烈建议改用 SKILL.md 结构。我的判断标准很简单如果这个技能的行为可以写成一个确定性算法那就别让模型自由发挥如果这个技能的本质是风格或思维方式那可以继续用提示词。另外补充一句同一个 Agent 系统里SKILL.md 定义的硬技能和提示词模板定义的软技能是可以共存的。硬技能负责执行软技能负责表达两者不冲突。我现在的做法是把所有涉及系统操作的技能全部迁移到 SKILL.md把写作风格、回答语气等约束留在系统提示词里。这样既保证了执行力也保留了对话的温度。5. 我的个人体会从提示词作者变成技能工程师说句心里话从提示词模板切到SKILL.md 技能包这件事刚开始并不顺利。因为写提示词的门槛低改一行文字就能看到效果让人很有成就感而搭建技能要写代码、设计目录、写说明文档、还要处理各种权限和依赖问题慢多了。但当你把五六个技能都按 SKILL.md 规范整理好后你会明显感觉到 Agent 的行为变得有骨头了不再是那个每次都要靠临场发挥的演员而是一个带着工具箱的工程师。我印象特别深的一次是我把公司内部一个客户信息查询的 Agent 从纯提示词迁到 SKILL.md 加 API 脚本后原先每次会话平均要消耗近两千 token 在技能说明上之后几乎降到了零。效果还更稳定了因为查询逻辑交给了代码模型只负责理解用户提问、提取查询参数、把结果组织成回复。那个时刻我才真正明白技能这两个字的分量——它不是一段说给模型听的话而是一个可以交付、可以复用、可以审计的能力单元。最后再分享一个小技巧如果你刚好在做一个比较复杂的技能可以先写一个最简可行版本——只定义 SKILL.md 和一个打印 hello 的脚本跑通接入流程后再逐步填充逻辑。这样你能在第一时间发现索引、触发、权限配置上的问题而不是等到代码写完后才在一个繁琐的黑盒里调试到底哪里没对上。我每次新增技能都用这个流程节省了大量排查时间。
