Agent技能库设计指南:从SKILL.md到调度器的完整实践 1. 项目解读agent-skills 到底在解决什么问题先说说我为什么要碰这个方向。做智能体Agent应用的人应该都有同感模型本身的推理能力越来越强但真正让 Agent“干活”的是它能调用多少可靠的外部能力。把这个能力集合做成一整套可复用的技能体系就是我这次想聊的 agent-skills。这个标题拆开看很直白agent 指智能体skills 是技能。合起来的意思是给智能体建立一套可以被自由装载、调用、组合的技能库——不是把代码硬编码进 Agent 的 Prompt 或主循环里而是把“能做某件事”的能力抽象成独立技能单元让模型或开发者按需取用。我在实际项目中踩过不少次这个领域的坑比如技能参数传错、技能之间互相干扰、库大了之后模型反而不知道选哪个后来逐步沉淀出这套做法。它解决的问题很明确现在的 Agent 最怕两件事一是能力边界不清晰什么都会一点但什么都做得不深二是功能堆在一起改一个地方就要动整个系统。agent-skills 的核心价值就是让“能力”成为能被独立开发、独立测试、独立替换的模块同时让模型能通过结构化的技能描述学会在合适的时机调用合适的技能。适合谁来参考呢如果你正在做智能体产品不管是用商业框架还是自己写调度逻辑又或者你只是想把大模型接进某个重复性工作流里这篇文章里的思路和代码都值得直接照搬。下面我把整个设计思路、目录规范、调度机制、常见坑逐个拆开讲。2. 整体设计思路技能不是工具是“有说明书的能力包”我第一次做 Agent 的时候也把技能理解成“给模型多塞几个函数定义”。后来发现这种思路在 demo 阶段能跑一旦技能多起来就崩了。真正的 agent-skills 设计核心不是把功能封装成函数而是把功能封装成“带着使用说明的能力包”。2.1 技能和工具、插件、工作流的边界很多人分不清技能Skill、工具Tool、插件Plugin、工作流Workflow这几个词。我自己的理解是这样的工具最底层的能力单元比如“读取文件”“调用某个 API”。一个工具只做一件事。技能面向任务的一层封装通常由“工具组合 使用说明 执行策略”构成。比如“整理下载文件夹”这个技能需要用到文件列举、类型判断、移动文件、重命名可能还有去重逻辑这些靠一个工具完成不了但把它整体封装成技能模型只需要知道“调用整理文件夹技能”就行。插件更偏“产品级”的概念通常是一个技能的集合加上配置界面、依赖管理等外部约束。工作流强调固定顺序比如先做 A 再做 B 再做 C中间不太需要模型动态决策。agent-skills 取的其实是“技能”这层抽象。它的设计哲学很朴素给模型一个带说明书的能力包模型负责判断“什么时候用、怎么用”技能内部负责“怎么把事情做对”。这两个责任分清楚之后开发体验会顺畅非常多。2.2 为什么要把技能描述文档化这是 agent-skills 能真正跑通的最关键一步。模型不会读你的源码它只能读文字。所以技能的“说明书质量”直接决定调用准确性。我见过不少团队技能的函数名写得很优雅docstring 草草几行就丢给人运行了。结果模型经常猜错参数含义。后来我形成了一条铁律每个技能必须经过“不看源码只看说明书能不能正确调用”的测试。技能描述文档至少要包含这几块描述模块核心内容反例与正例技能名称动词开头的短语说清楚“做什么”反例“文件处理”正例“批量压缩指定目录下的图片”适用场景什么时候该用、什么时候不该用反例“处理文件时可用”正例“当用户需要释放磁盘空间且指定目录包含大量 jpg/png 时”输入参数参数名、类型、必填与否、取值范围、默认值、单位反例“path: 路径”正例“source_dir: string, 必填, 绝对路径, 需存在且可读”执行流程技能内部按什么顺序做什么关键边界条件是什么让模型知道“如果遇到只读文件会跳过还是报错”输出格式成功时返回什么失败时抛出什么结构化最好别让模型去猜“返回了一个字典”具体长什么样这段看似是文档功夫其实是整个技能库质量的基石。技能写得好不好不看代码多漂亮先看这份说明书能不能让一个第一次接触的模型正确使用它。2.3 我踩过的设计反例把“技能”做成“大函数”早期我犯过一个典型错误为了让 Agent 显得全能我把十几个相关功能塞进一个技能里参数列表长达十几个模型每次调用都在猜到底传哪几个。这个技能覆盖面是广了但调用准确率低得让人绝望。后面我把这个巨型技能拆成三四个小技能每个技能只负责一类明确任务参数控制在五个以内调用成功率立刻上来了。这个教训让我记住了 agent-skills 设计里的一条原则技能的粒度以“说明书一页讲得清”为准不以“覆盖更多功能”为准。还有一个设计细节技能之间允许互相调用但尽量保持单向依赖不要搞成环。比如“整理下载文件夹”技能内部可以调用“读取目录清单”技能但“读取目录清单”绝对不能反过来依赖“整理下载文件夹”。技能库里一旦出现循环调用模型的调度逻辑会变得极难排查。3. 技能库的目录结构与核心配置解析结构先行。技能库在文件系统上怎么组织直接决定了它好不好扩展、好不好维护、好不好被 Agent 感知。3.1 目录规范一技能一文件夹我建议采用一技能一文件夹的结构每个技能文件夹里固定放这几个东西skills/ ├── common/ │ ├── SKILL.md │ ├── scripts/ │ │ ├── analyze.py │ │ ├── summarize.py │ │ └── requirements.txt │ └── assets/ │ └── prompt_templates/ ├── pdf_convert/ │ ├── SKILL.md │ ├── scripts/ │ │ └── convert.py │ └── requirements.txt └── report_writer/ ├── SKILL.md ├── scripts/ │ ├── generate_report.py │ └── data_loader.py ├── templates/ │ └── weekly_report.md └── requirements.txtSKILL.md 是技能的身份证和说明书一个技能文件夹里没有它就等于没注册。scripts 目录放可执行脚本模块稍微多一点就用 Python 包结构组织但对外只暴露一个入口函数入口函数签名保持简洁。requirements.txt 单独拆出来是因为技能经常有第三方依赖装进全局环境容易冲突保留每个技能自己的依赖清单会省很多事。3.2 SKILL.md 的标准化写法我参考了社区里几种流行的智能体技能包格式最后沉淀出一套自己的字段规范每一行都尽量让模型“不加思考就能看懂”--- name: pdf_to_text description: 将 PDF 文件批量转换为纯文本。适用于从报告、论文、合同中提取正文内容。不适合处理扫描版图片型 PDF也不适合保留复杂表格结构。 version: 1.2.0 author: internal license: MIT --- # 技能说明 将输入路径下的 PDF 文件批量转换为 txt 文件输出到指定目录。 ## 使用场景 - 需要从 PDF 中提取纯文本内容用于后续分析、检索或摘要 - 需要批量处理多个 PDF 文件 ## 不适用的场景 - 扫描版 PDF无文字层需要 OCR 预处理 - 需要保留表格、图片位置信息的场景 ## 输入参数 | 参数名 | 类型 | 必填 | 说明 | |---|---|---|---| | input_dir | string | 是 | 存放 PDF 文件的目录绝对路径目录必须存在且可读 | | output_dir | string | 是 | 输出 txt 文件的目录绝对路径不存在会自动创建 | | page_start | int | 否 | 起始页码从 1 开始默认 1 | | page_end | int | 否 | 结束页码默认到文件末尾 | ## 返回格式 成功时返回 JSON 字符串{status: success, converted: 3, failed: []} 失败时抛出异常RuntimeError携带失败原因YAML 头里的 name 和 description 是用来给模型快速筛选的正文部分是给模型深入了解执行细节的。一份好的 SKILL.md要让模型在几百毫秒内决定“要不要用”在调用前几秒内搞清楚“怎么用”。3.3 技能装载配置哪些技能默认启用、哪些按需启用不是所有技能都要塞进每次对话的上下文里。技能描述再精炼一次塞三四十个模型照样会“选择困难”。我一般把技能分成三档常驻技能五六个以内和 Agent 核心任务强相关每次启动都加载比如“文件读取”“信息检索”“代码执行”。按需技能通过意图识别或关键词匹配触发加载例如用户提到“PDF”再加载 pdf_to_text。手动技能需要用户在界面上主动启用或者开发者事先在配置里指定适合权限敏感或在特定工作流里才用到的能力。这个分档策略在性能和使用效果之间取了一个比较实际的平衡。全量技能入库模型照样能在文档里找到它们区别在于它们的说明书是否进入模型的有效上下文窗口以及进入之后占多大权重。4. 核心实现技能调度与调用的完整链路目录和文档都准备好了接下来要解决的是“模型怎么知道用哪个、怎么调”。这层机制我会拆成几个环节来讲。4.1 技能清单的注入方式模型在启动时会收到一份“可用技能清单”来源就是扫描技能库目录解析每个 SKILL.md 的 YAML 头把 name 和 description 拼成一段文本。这个清单通常放在系统提示词System Prompt的固定位置让模型每次决策前都先“看一遍菜单”。代码可以这样实现import pathlib import yaml SKILLS_ROOT pathlib.Path(./skills) def load_skill_catalog(root: pathlib.Path SKILLS_ROOT) - list[dict]: catalog [] for skill_dir in root.iterdir(): manifest_path skill_dir / SKILL.md if not manifest_path.exists(): continue # 只解析 YAML 头部分 raw manifest_path.read_text(encodingutf-8) if not raw.startswith(---): continue # 简易分割 YAML frontmatter _, yaml_text, _ raw.split(---, 2) meta yaml.safe_load(yaml_text) catalog.append({ name: meta[name], description: meta[description], path: str(skill_dir), }) return catalog def build_skill_prompt(catalog: list[dict]) - str: lines [可用技能] for item in catalog: lines.append(f- {item[name]}: {item[description]}) return \n.join(lines)这样生成出来的菜单干净、统一模型只需要做“选菜”这一个决策不需要理解文件系统结构。4.2 调用格式约定不用函数调用也能稳定执行虽然各家大模型都支持函数调用Function Calling格式但我在实际测试里发现把技能调用设计成“模型输出一段固定格式的 JSON”这种方式在不同模型之间的兼容性反而更好。核心逻辑是模型在对话中输出一段特殊标记的 JSON比如{skill: pdf_to_text, params: {input_dir: /tmp/doc}}调度器解析这段 JSON校验技能是否存在、参数是否必填齐全把参数传给技能入口函数执行把执行结果返回给模型继续处理好处是即使某个模型不支持函数调用协议这套流程依然能跑。坏处是要自己写 JSON 解析和错误处理但只要封装一次后续所有技能都能复用。参数校验这一步我做得比较严格类型不对直接报错缺失必填参数就返回提示让模型补充。这个严格校验让模型慢慢学会了“先把参数准备齐了再调用”整体调用成功率反而提升了。4.3 技能冲突与优先级处理技能库大了以后经常出现描述相近的技能模型可能选错。比如“压缩图片”和“调整图片尺寸”压缩绕不开调整尺寸但目标不同。解决方案是在 SKILL.md 的“适用场景”里写得更具排他性同时在技能清单注入时做一个简单的关键词去重碰撞检测def find_conflicts(catalog: list[dict]) - list[tuple[str, str]]: conflicts [] for i in range(len(catalog)): for j in range(i 1, len(catalog)): a, b catalog[i], catalog[j] # 描述中相同的命名词超过两个视为潜在冲突 shared set(a[description].split()) set(b[description].split()) if len(shared) 2: conflicts.append((a[name], b[name])) return conflicts检测出来之后不是删除技能而是人工调整描述词让边界更清晰。还有一种策略是在调用结果不理想时把“上一轮选错了技能”这个反馈写回记忆里下次遇到类似问题自动优先另一个技能。这个机制我在记忆模块里做过一版效果不错后面可以单独开一篇。4.4 超时与中断处理技能执行不是总有确定性答案的。某些脚本可能运行几十秒某些外部 API 可能一直不返回。我给技能调度器设了两条硬性规则单次技能调用默认超时 60 秒超过就主动中断把部分输出写进上下文告诉模型“执行太久被中止了”技能执行期间 Agent 的主循环允许接收外部中断信号比如用户手动喊停。这两条规则听起来简单但在真实环境中救了我不止一次。没有超时控制的时候一个死循环脚本能把整个 Agent 卡死连对话回复都发不出来。加上超时之后最坏情况是技能执行失败模型还能根据错误信息继续尝试别的路径。5. 实操从零搭建一个“文件整理”技能光说理论容易飘我拿一个完整的实操案例把上面的内容串一遍。这个案例是“文件自动整理技能”场景非常常见很多人的下载目录和桌面都是重灾区。5.1 先定义技能边界这个技能要做什么扫描用户指定目录按扩展名和文件类型把散落文件移动到分类子目录里。不做什么不删除文件、不处理已经分类过的子目录内部、不处理超过用户指定大小的文件。这个边界定义我认为很有必要。一开始我的版本会自作主张把“重复文件合并”也做进去结果用户反馈“文件被移动后找不到了”体验很糟。技能专注做一件小事其他事留给别的技能这个原则在这个案例里体现得很典型。5.2 编写 SKILL.md--- name: organize_downloads description: 自动整理指定目录中的文件按扩展名分类移动到子目录。适用于桌面目录、下载目录、临时文件目录整理。不会删除任何文件不会扫描隐藏文件夹。 version: 1.0.0 --- # 技能说明 扫描指定目录下的文件按扩展名映射到分类规则在目录下创建分类子目录并移动文件。 ## 输入参数 | 参数名 | 类型 | 必填 | 说明 | |---|---|---|---| | target_dir | string | 是 | 需整理的目录绝对路径 | | move_hidden | bool | 否 | 是否处理隐藏文件默认 false | | dry_run | bool | 否 | 为 true 时只打印计划不实际移动文件默认 false | ## 返回格式 {status: success, moved: 12, plan: [{file: a.pdf, to: document/}]}5.3 脚本实现这个技能的核心逻辑不难难点在边界处理我直接给出核心代码import json import pathlib import shutil EXT_MAP { document: {.pdf, .docx, .doc, .txt, .md, .xlsx, .pptx}, image: {.jpg, .jpeg, .png, .gif, .bmp, .webp, .svg}, video: {.mp4, .mkv, .mov, .avi}, audio: {.mp3, .wav, .flac, .aac}, archive: {.zip, .rar, .7z, .tar, .gz}, code: {.py, .js, .ts, .java, .go, .rs, .c, .cpp}, } def organize(target_dir: str, move_hidden: bool False, dry_run: bool False) - str: root pathlib.Path(target_dir).resolve() if not root.is_dir(): raise RuntimeError(f目录不存在或不可读: {target_dir}) plan [] for item in root.iterdir(): if item.is_dir(): continue if item.name.startswith(.) and not move_hidden: continue ext item.suffix.lower() category other for cat, exts in EXT_MAP.items(): if ext in exts: category cat break dest_dir root / category if item.parent dest_dir: continue plan.append({file: item.name, to: category}) if dry_run: return json.dumps({status: success, moved: 0, plan: plan}, ensure_asciiFalse) moved 0 executed [] for item in plan: src root / item[file] dest_dir root / item[to] dest_dir.mkdir(exist_okTrue) target dest_dir / src.name # 同名冲突时加时间戳后缀 if target.exists(): target dest_dir / f{src.stem}_{src.stat().st_mtime_ns}{src.suffix} shutil.move(str(src), str(target)) moved 1 executed.append({file: item[file], to: target.name}) return json.dumps({status: success, moved: moved, plan: executed}, ensure_asciiFalse)几个容易被忽视的细节resolve()是为了防路径穿越避免模型传入../之类的相对路径同名冲突不加随机数而是加修改时间戳这样即使重复执行几次结果也不会乱隐藏文件默认跳过这也符合多数用户对“整理目录”的预期。5.4 接入调度器脚本写好后在技能入口处做一层薄封装让调度器可以按统一协议调用def skill_entry(skill_name: str, params: dict): if skill_name organize_downloads: return organize( target_dirparams.get(target_dir, ), move_hiddenparams.get(move_hidden, False), dry_runparams.get(dry_run, False), ) # 未来其他技能... raise KeyError(f未知技能: {skill_name})所有技能在这个函数里登记入口函数保持极薄不写具体业务逻辑只做参数解包和转发。这样不管调度器是走函数调用协议、还是解析模型输出的 JSON最终都是调到这同一个入口。6. 常见问题与排查技术盘点这节我想把实际运行中积累的坑集中梳理一下很多问题是文档里不会写的但非常影响体验。6.1 技能有说明但模型就是选错最典型的情况是用户说“帮我看看这个文件多大”模型却调用了“文件整理”技能。我排查后发现问题出在技能描述里“文件”这个词出现太频繁而“查看大小”这个意图没有一个技能明确覆盖。解决方法是补一个专门的“文件信息查看”技能并且在“文件整理”的 SKILL.md 里增加一句“仅用于移动文件不用于查看文件属性”。选错问题大多不是模型不行而是技能边界没写清楚。排查思路就一条先把用户请求原文和模型实际调用记录拉出来并排看找到是在哪一步开始偏离的然后针对性调整描述。调描述比换模型成本低得多效果也直接得多。6.2 参数类型对了但值明显不合理模型传了一个字符串类型的路径但路径指向一个只读文件系统或者不存在的位置。这类问题严格来说算“运行时错误”但可以通过技能内部校验来美化。我在技能入口统一加了 try-except把异常转成结构化消息返回给 Agent。比如上面 organize 函数里root.is_dir()失败时抛出 RuntimeError调度器捕获后返回给模型{status: error, error_type: invalid_path, message: 目录不存在或不可读: /nonexistent}这样模型能根据 error_type 判断是路径给错了还是权限有问题而不是面对一行堆栈无从下手。6.3 技能库膨胀后加载时间变长全量扫描目录和解析 YAML 在技能数量到 50 个以上时会有明显耗时。解决办法是把技能清单的扫描结果做成缓存只在 SKILL.md 的修改时间变化时刷新。另一个优化是懒加载加载技能清单只读 YAML 头不读正文只有模型决定调用某个技能时才重新读取完整 SKILL.md 并加载脚本依赖。这两个优化做完之后即使技能库一两百个启动时间也基本无感。6.4 技能依赖冲突两个技能一个依赖 pandas 1.x另一个锁了 pandas 2.x装进同一环境必炸。我们后来采用虚拟环境隔离每个技能包可以在配置里声明自己的环境调度器在子进程里执行技能脚本。这套方案稳是稳就是性能上有额外开销。如果项目规模不大低成本方案是把公共依赖统一锁版本技能内尽量只用标准库或兼容性好的依赖。6.5 调试明细缺失最让人头疼的是模型调用了技能但没人知道参数到底传了什么。我在调度器里做了一层调用日志每一笔调用都记录[2025-06-01 10:23:15] skillorganize_downloads params{target_dir: /home/user/Downloads, dry_run: true} result{status: success, moved: 0, plan: [...]} elapsed_ms18日志有了之后排查技能问题的时间至少砍半。建议把日志直接写到独立的 JSONL 文件里不要混在主程序日志里否则对话过程中会产生大量噪音日志回头翻很痛苦。7. 经验总结技能库设计的三条军规最后把这段时间反复试错得到的体会集中说一下吧。第一技能描述文档比技能代码更值得花时间。代码写得再好模型不知道什么时候该用等于没用。我后来每次新增技能先写 SKILL.md再写代码写完代码还要回过来校准描述。顺序反了很容易变成“先写功能、后补文档”补的文档质量通常不太行。第二技能的返回结构要早定标准。推荐统一 JSON 外壳里面带 status、plan、error_type 这些字段。这个标准定得越早后面写技能越省力。我之前因为几个技能返回格式不统一调试时反复看源码工作效率被拖得很厉害。第三技能库的演进一定要有版本意识。技能接口变了别只改代码要同步更新 SKILL.md 里的版本号和参数说明最好还有一个简单的 changelog。模型感知不到代码层面的变化但它会感知到描述和实际行为不一致这种不一致带来的问题是隐性的等到用户反馈“同一个功能有时候灵有时候不灵”再排查就麻烦了。如果把这套模式再延伸一下后面可以给技能加上质量评分、自动评估正确率、不同模型适配测试这些方向。技能库做扎实了Agent 的可靠性和可扩展性会同时上一个台阶。这次先分享到这里希望能给正在搭或准备搭 Agent 能力层的朋友一些可落地的参考。