☰
Agent技能库设计指南:从SKILL.md到调度器的完整实践
2026/10/13 16:47:19 网站建设 项目流程

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.txt

SKILL.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(encoding="utf-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”这种方式,在不同模型之间的兼容性反而更好。核心逻辑是:

  1. 模型在对话中输出一段特殊标记的 JSON,比如{"skill": "pdf_to_text", "params": {"input_dir": "/tmp/doc"}}
  2. 调度器解析这段 JSON,校验技能是否存在、参数是否必填齐全
  3. 把参数传给技能入口函数执行
  4. 把执行结果返回给模型继续处理

好处是,即使某个模型不支持函数调用协议,这套流程依然能跑。坏处是要自己写 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_ascii=False) moved = 0 executed = [] for item in plan: src = root / item["file"] dest_dir = root / item["to"] dest_dir.mkdir(exist_ok=True) 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_ascii=False)

几个容易被忽视的细节:resolve()是为了防路径穿越,避免模型传入../之类的相对路径;同名冲突不加随机数而是加修改时间戳,这样即使重复执行几次,结果也不会乱;隐藏文件默认跳过,这也符合多数用户对“整理目录”的预期。

5.4 接入调度器

脚本写好后,在技能入口处做一层薄封装,让调度器可以按统一协议调用:

def skill_entry(skill_name: str, params: dict): if skill_name == "organize_downloads": return organize( target_dir=params.get("target_dir", ""), move_hidden=params.get("move_hidden", False), dry_run=params.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] skill=organize_downloads params={"target_dir": "/home/user/Downloads", "dry_run": true} result={"status": "success", "moved": 0, "plan": [...]} elapsed_ms=18

日志有了之后,排查技能问题的时间至少砍半。建议把日志直接写到独立的 JSONL 文件里,不要混在主程序日志里,否则对话过程中会产生大量噪音日志,回头翻很痛苦。

7. 经验总结:技能库设计的三条军规

最后把这段时间反复试错得到的体会集中说一下吧。

第一,技能描述文档比技能代码更值得花时间。代码写得再好,模型不知道什么时候该用,等于没用。我后来每次新增技能,先写 SKILL.md,再写代码,写完代码还要回过来校准描述。顺序反了很容易变成“先写功能、后补文档”,补的文档质量通常不太行。

第二,技能的返回结构要早定标准。推荐统一 JSON 外壳,里面带 status、plan、error_type 这些字段。这个标准定得越早,后面写技能越省力。我之前因为几个技能返回格式不统一,调试时反复看源码,工作效率被拖得很厉害。

第三,技能库的演进一定要有版本意识。技能接口变了,别只改代码,要同步更新 SKILL.md 里的版本号和参数说明,最好还有一个简单的 changelog。模型感知不到代码层面的变化,但它会感知到描述和实际行为不一致,这种不一致带来的问题是隐性的,等到用户反馈“同一个功能有时候灵有时候不灵”,再排查就麻烦了。

如果把这套模式再延伸一下,后面可以给技能加上质量评分、自动评估正确率、不同模型适配测试这些方向。技能库做扎实了,Agent 的可靠性和可扩展性会同时上一个台阶。这次先分享到这里,希望能给正在搭或准备搭 Agent 能力层的朋友一些可落地的参考。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询