☰
从零设计Agent技能包:让大模型按需读手册再干活
2026/10/7 13:56:49 网站建设 项目流程

最近一直在鼓捣 AI Agent 相关的工程化落地,和团队复盘时聊得最多的一个词就是 agent-skills。你如果做过基于大模型的工具调用,一定遇到过这种场景:工具定义越加越多,模型却开始“选择困难”,上下文动不动被函数说明撑爆,明明手里有工具,它偏要绕远路甚至直接幻觉。agent-skills 就是冲着这些问题来的,核心思路是把“工具”拆成“说明书 + 可运行脚本 + 测试样例”这样的技能包,让模型按需读手册、再干活。这篇文章,我不讲概念层面那些虚的,直接聊聊怎么从零设计一个标准技能包,怎么落地调试,以及我在实际项目里踩过的坑和摸出来的经验。

这篇文章适合三类人看:正在给智能体填工具、加能力而头疼的开发者;做 RAG、自动化工作流,想让模型更稳定地完成多步操作的工程师;以及所有好奇“Agent 技能化”到底是什么、想亲手试出个所以然的实践者。

1. 先搞懂 agent-skills:为什么“读技能”比“调工具”更省事

1.1 传统工具调用哪里不对劲

我最早做 Agent 功能时,走的还是老路子:把所有可用的函数一个个塞进 system prompt,告诉模型“你可以调用这些”,再维护一大份 JSON Schema。刚开始工具只有三五个,效果还不错。等工具数量上了两位数,问题就出现了。

首先是上下文被工具描述大量占用。一个像样的函数定义,从名称、参数类型到注释说明,随便写写就是两三百个 token。你挂 30 个工具,光说明信息就接近一万 token,真正留给任务推理的窗口反而变小了。其次是选择准确率下降,工具越多,模型越容易把参数格式写错,或者在一个需要调 A 工具的场景里莫名选中 B 工具。最后是迭代成本太高,每次新增一个能力,都要同步改全局 schema,任何一个字段写错,整条链路都会挂掉。这就好比餐厅菜单越印越厚,服务员反而记不住哪道菜是谁的拿手菜。

还有个容易被忽略的问题:传统工具调用模式下,模型对工具的“理解”依赖于系统提示词里那一小段描述,它并没有办法在运行前“翻一翻说明书”。一旦工具的细节非常复杂(比如一段需要多步骤处理的数据清洗流程),模型只能靠猜。工具不是不够多,而是能力的“颗粒度”和“可理解性”不匹配。

1.2 Agent技能包的核心思路

agent-skills 的思路是把能力做成一个个独立目录,每个目录里面放“给模型读的说明”(通常叫 SKILL.md,就是一本小手册)、“给机器跑的脚本”(Python、Node.js 都行)以及“样例数据和测试用例”。让模型在对话过程中按需发现、按需加载技能说明,而不是一开始就把全部工具塞给它。

你可以这么理解:过去你是在模型脑子里硬塞一整本电话簿,让它背下所有人的号码;技能包则不同,你给了模型一张索引卡,告诉它“遇到这类问题,去查某个抽屉里的手册,手册会告诉你怎么办”。模型只在需要时才打开手册,上下文自然更干净,技能本身的编写和更新也不影响其他模块。技能包里的脚本是真正执行操作的,而 SKILL.md 是帮模型判断“何时用、怎么用、输出什么”的。

这带来一个实际好处:能力的添加和删除变得像插拔模块一样。想加一个“网页正文提取”技能,新建一个目录,写好手册和脚本,就完事了,完全不用改动系统提示词和全局配置。对于经常需要增减业务能力的场景,这个体验是非常爽的。

1.3 什么时候该用、什么时候不该用

技能包不是银弹,我个人总结下来,适合技能化的能力通常有三个特征:

  • 逻辑相对固定:输入输出边界清晰,过程可以通过脚本自动化完成。
  • 单次调用成本较高:或者需要多步骤组合,值得让模型先“读说明”再执行。
  • 业务更新频繁:描述、参数、脚本要跟着业务持续迭代,需要独立版本管理。

反过来,如果你只有三四个极其简单的工具,技能包带来的额外目录组织成本反而有点多余;如果是延迟要求极高、需要同步返回结果的场景,技能包“先读说明再执行”的机制可能会增加一次决策时延。此外,所有需要强事务保证的动作,比如支付扣款、自动发邮件这类,我建议仍然走严格的审批流程,不要全权交给模型自动选择技能,安全边界不能省。

2. 如何设计一个规范的技能包

2.1 目录结构:让Agent和人一眼都能看懂

我给技能包定的目录规范比较固定,参考了不少社区实践,最终拍板是这样:

skills/ ├── article-extractor/ │ ├── SKILL.md │ ├── scripts/ │ │ └── article_extractor.py │ ├── assets/ │ │ └── sample_output.json │ └── requirements.txt

每个技能以一个独立的文件夹为单位,文件夹名使用小写连字符风格,比如article-extractor、weekly-report-generator。里面四个基本组成部分:

  • SKILL.md:给模型读的说明书,是技能包的灵魂。
  • scripts/:实际执行操作的代码,模型不会直接去读全部实现,它只负责按说明调用。
  • assets/:存样例输入输出、模板、参考文件,方便模型做格式对齐。
  • requirements.txt:声明脚本依赖,并固定版本。

这套结构的最大价值是人能看懂,模型也能理解。模型通过SKILL.md的索引定位到脚本调用方式,我们做代码评审时也可以只盯着一个目录检查,不用满工程找逻辑。

2.2 SKILL.md 的正确写法

SKILL.md 不是写给人看的 API 文档,它是写给语言模型的“操作手册”,语言风格要极度清晰、少铺垫、直接给指令。我习惯用 YAML frontmatter 放元信息,正文则按“什么时候用、输入输出、执行流程、示例、注意”的顺序组织。

先看元信息部分:

字段作用示例值
name技能唯一标识article-extractor
description一句话说明能力,用于模型做技能选择提取网页正文并转换为干净的 Markdown 文本
version技能版本号,方便追踪更新1.0.0
trigger触发场景描述,写得越具体越好当用户给出一个网页链接并要求总结、提取或整理内容时

正文部分最重要的是示例调用。模型对示例的依赖程度远超我们的想象,你在说明里写十句抽象描述,不如给一组“输入这个参数,跑这条命令,得到这段输出”的真实样例。我在一个数据处理技能里试过两种写法,有完整调用示例的版本,模型按正确参数调用的概率约 95%;只写抽象描述的版本,成功率掉到七成,而且经常自作聪明地补一个不存在的参数。

还要控制说明书长度。模型读技能手册也是在消耗上下文 token 的,所以 SKILL.md 尽量控制在一页以内。复杂内容使用“折叠式”写法:入口描述给足,详细规则放到assets/里的参考文件里,让模型在需要时进一步读取。

2.3 脚本与依赖的工程规范

技能里的脚本虽然是给机器跑的,但它的设计直接决定 Agent 的稳定性。我有几条强制规范,算是实践里压出来的底线:

  • 脚本必须能独立运行:不接受从 Agent 框架里注入的全局变量,所有输入通过命令行参数或环境变量传入,方便单独测试。
  • 输出必须是结构化格式:首选 JSON,并且字段名要稳定。模型是根据输出继续推理的,输出格式乱,后续步骤全乱。
  • 错误处理要完整:网络超时、参数非法、数据解析失败都要有明确错误码和可读信息,别让模型遇到一坨堆栈就懵。
  • 保持幂等:同样的输入反复执行,结果应该一致。这个特性特别关键,模型在犹豫不决时可能把同一个技能重复调用两三次。
  • 依赖锁定:requirements.txt里写明具体版本号,避免一段时间后依赖升级导致脚本崩溃。

顺带提醒一个新手常踩的坑:不要假设语言模型会去读你的源码。它对外部世界的认知只能通过 SKILL.md 和工具返回结果获得,因此脚本的接口、调用方式、返回格式必须在 SKILL.md 里写清楚,不能指望模型自己去看 code 猜用法。

2.4 和标准化工具协议的关系

现在不少平台开始推广标准化的工具协议,比如 MCP(模型上下文协议),它把工具封装成统一接口,模型可以通过协议发现和调用。技能包和这种协议并不冲突,反而可以互相配合。技能包可以理解为“组织能力和知识的形式”,协议是“能力传输和调用的管道”。你把一段技能包发布成符合标准协议的工具,理论上也能接入支持对应协议的客户端。我现在的做法是:底层用技能包做能力管理,对外暴露时按平台要求做适配层。这样既不绑定某一家框架,又能保留技能包目录清晰、版本独立的收益。

3. 从零写一个网页正文提取技能包

3.1 先定边界:输入字段、输出格式、失败策略

动手前先别急着写代码,把技能的行为边界定义清楚。我以“网页正文提取”这个技能为例,它的定位是“给 Agent 提供从任意 HTML 页面提取标题、作者、时间和正文内容的能力,输出供摘要、翻译或笔记使用”。

输入我只允许一个字段:url,必须是 http 或 https 协议。输出统一走 JSON:

{ "title": "文章标题", "author": "作者(可能为空)", "published": "发布时间(可能为空)", "body": "正文的 Markdown 文本", "error": null }

失败时则返回:

{ "title": "", "author": "", "published": "", "body": "", "error": "page_load_timeout" }

把失败策略写进 SKILL.md 非常重要。模型如果拿到了“读取失败”的明确信号,它会主动告诉用户“链接打不开”,而不是接着往下编一个总结。

3.2 搭建目录和 SKILL.md

按照前面定的规范,我创建目录skills/article-extractor/,并写好 SKILL.md。核心内容长这样:

--- name: article-extractor description: 提取网页正文并转换为干净的 Markdown 文本,用于总结、翻译、笔记整理等场景。 version: 1.0.0 trigger: - 用户提供了一个 http/https 链接,并要求总结、提取、翻译或整理该网页内容 - 用户在会话中粘贴了一个文章链接,并期望基于链接内容进行后续操作 --- # article-extractor 给定一个网页 URL,使用本地脚本获取 HTML,提取正文和核心元信息, 输出结构化的 JSON 数据供后续步骤使用。 ## 使用时机 - 推荐使用:用户针对某个具体网页链接进行内容总结、正文提取、要点整理。 - 不要使用:用户只是提到某个网站名字,没有给出具体文章链接; PDF、图片、音频等非 HTML 资源不属于本技能处理范围。 ## 输入参数 - url:必填,字符串,必须以 http:// 或 https:// 开头。 ## 执行方式 在终端执行以下命令: ```bash python scripts/article_extractor.py --url "<网页链接>"

脚本会将结果以 JSON 格式打印到标准输出。

返回格式

返回 JSON 包含:title(标题)、author(作者)、published(发布时间)、 body(正文 Markdown)、error(失败信息,成功时为 null)。

示例

输入: python scripts/article_extractor.py --url "https://example.com/post/1"

输出: {"title": "示例文章", "author": "张三", "published": "2025-01-10", "body": "# 示例文章\n\n这是正文……", "error": null}

注意事项

  • 如果页面需要 JavaScript 渲染才能显示正文,本技能可能提取不到内容, 此时应告知用户“该页面为动态渲染页面,无法直接提取正文”。
  • 输出 body 长度已被脚本限制在 50000 个字符以内,超长正文会被截断。
**注意**:我在示例里把触发条件写得非常具体,甚至列出了“不要使用”的场景。这能显著减少模型误调用。 ### 3.3 编写提取脚本 脚本我用 Python 实现,依赖 `requests` 抓取网页,`readability-lxml` 做正文提取,`beautifulsoup4` 做基础清洗。完整代码不贴了,核心逻辑是这样: ```python import argparse import json import logging import re import requests from bs4 import BeautifulSoup from readability import Document UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" TIMEOUT = 10 MAX_BODY_CHARS = 50000 def extract_article(url: str) -> dict: if not url.lower().startswith(("http://", "https://")): return {"title": "", "author": "", "published": "", "body": "", "error": "invalid_url"} try: resp = requests.get(url, headers={"User-Agent": UA}, timeout=TIMEOUT) resp.raise_for_status() except requests.exceptions.Timeout: return {"title": "", "author": "", "published": "", "body": "", "error": "page_load_timeout"} except requests.exceptions.RequestException as exc: return {"title": "", "author": "", "published": "", "body": "", "error": f"request_failed: {exc}"} # 用 readability 提取正文 doc = Document(resp.text, url=url) title = doc.short_title() body_html = doc.summary(html_partial=True) # 简单清洗,去掉多余空白和乱码 soup = BeautifulSoup(body_html, "html.parser") text = soup.get_text("\n", strip=True) text = re.sub(r"\n{3,}", "\n\n", text) text = text[:MAX_BODY_CHARS] return { "title": title, "author": "", "published": "", "body": text, "error": None, } def main(): parser = argparse.ArgumentParser() parser.add_argument("--url", required=True) args = parser.parse_args() result = extract_article(args.url) print(json.dumps(result, ensure_ascii=False)) if __name__ == "__main__": main()

几个参数我特意说明一下,都是有目的性的:

  • TIMEOUT = 10:页面加载超过 10 秒直接放弃。Agent 场景下,单个步骤最好控制在几秒到十几秒,不能让等待拖垮整轮对话。
  • MAX_BODY_CHARS = 50000:正文截断上限。一篇长文章全文可能有十几万字符,全塞进上下文会挤占后续对话空间,截断到 5 万字符足够覆盖绝大多数文章,也留下了合理余量。
  • UA自定义:不少网站对裸requests请求返回 403,设置一个常规浏览器的 User-Agent 能显著提高抓取成功率。

脚本还有个细节:错误通过 JSON 里的error字段返回,而不是抛异常让调用栈裸奔。模型拿到一坨 traceback 基本就是傻了,返回一个明确错误码反而好处理。

3.4 联调与回归

技能写完不能直接挂上去,先本地验证三种代表性页面:普通技术博客(静态 HTML)、包含大量推荐位和广告的新闻页、需要动态渲染的主流资讯站。前两类静态页面,提取效果通常都很好,标题、正文都比较干净。第三类大概率提取失败,这时候SKILL.md里的“注意事项”就起作用了,模型会知道返回“动态渲染页面无法提取”。

为了后续回归,我习惯在assets/sample_output.json保留一组标准样例输出,每次改完脚本跑一遍对比,防止“这次改好了 A 页面,却把 B 页面弄坏了”。这一步看起来不起眼,实际维护时救命。

3.5 让模型“该出手时才出手”的说明书技巧

用了一阵子之后,我最大的感悟是:模型对技能的选择,99% 取决于SKILL.md里trigger写得好不好。别写“当需要提取网址内容时”这种废话,把它细化到场景级别:

  • 写具体动作:给出链接并要求总结、要求翻译、要求整理要点。
  • 写负面清单:前面示例里专门加了“不要使用”场景。
  • 给示例最有效:哪怕描述里只给一条“用户说:帮我看看这个链接讲了什么”的触发示例,模型的判断准确率都明显上了一个台阶。

如果还是出现误触发,别急着怀疑模型,先回头检查说明书里是不是存在歧义。我调过的绝大多数问题,最后都证明是手册写得不够“像人话”。

4. 常见问题与排查技巧实录

4.1 模型不加载技能:多半是描述写得像简历

有次我写了个月报生成技能,模型死活不调用。我去看description,写的是“根据数据生成规范月报”,其实挺清楚的,但后来发现问题出在触发词:团队内部习惯说“出一下月报”“把这个月的数理一下”,模型没见过这些说法,自然识别不出。后来我在trigger里加了一串真实场景语句,比如“用户说:出一下月报”“用户说:整理这个月的表现”,再测试,模型马上就能对上了。

排查思路总结:如果技能在需要时没被加载,先检查trigger里有没有包含真实用户可能说的话。不要用你脑子里理想化的说法,要用你从聊天记录里捞出来的原始说法。

4.2 脚本输出把上下文撑爆

技能刚上线时,我把网页正文全量输出,一个三万字的长文直接灌进对话,模型后续回答开始“前言不搭后语”,因为上下文快塞满了。后来所有输出型脚本都加MAX_*上限,正文按需截断,只保留对当前任务最关键的信息。比如总结场景,我会在脚本里加一个--mode summary参数,让脚本自己生成摘要,而不是先把全文丢给模型。

核心原则:技能返回给模型的是“下一步决策需要的最小信息集”,不是“所有信息”。能脚本处理的事情绝不要消耗模型 token 去二次处理。

4.3 环境不一致导致“在我机器上好好的”

技能包很容易犯一个错:本机调试通过,发布到 Agent 运行环境就报ModuleNotFoundError。Python 环境、系统依赖、甚至网络访问策略不同,都能让技能挂掉。现在我要求所有技能必须带requirements.txt且锁定版本;发布前在干净环境里跑一遍脚本命令,确认能通过再接入 Agent。如果你的 Agent 运行在容器里,也可以把技能依赖打进镜像,而不是依赖宿主机已装好的包。

4.4 安全边界与权限克制

技能包虽然只是“读手册+跑脚本”,但它有能力执行真实操作,权限设计要克制。我踩过的具体场景是:一个内部数据查询技能,最初允许模型传任意 SQL,结果模型把不含条件的全表查询写了进去,差点把数据库拖垮。后来我给技能加了一层白名单校验,只允许指定表、强制加LIMIT,这才放心。

几条安全底线:

  • 最小权限:技能进程只授权它完成本职任务所需的权限,不要顺手给整个系统权限。
  • 输入校验:所有外部输入(URL、路径、SQL、文件名)都需要在脚本里做合法性检查。
  • 凭证保护:密钥、Token 一律走环境变量,不写进 SKILL.md 或代码仓库。
  • 审计日志:给技能加日志输出,记录每次调用的参数和结果,出问题时才能回溯。

4.5 一张问题速查表

现象可能原因处理办法
模型该用没用trigger和description写得太抽象加入真实场景语句示例,写清“不要用”场景
技能返回解析失败输出格式不规范或非 JSON统一 JSON 输出,字段固定,脚本单独测试
同一技能被重复执行缺少幂等设计或没有结果缓存脚本做幂等处理,必要时加一层调用缓存
上下文被输出撑爆返回内容没有截断脚本内部限制输出长度,尽量返回加工后结果
环境报模块缺失依赖没有锁定或未安装锁定requirements.txt,发布前干净环境验证
模型调用参数总是错示例调用不充分在 SKILL.md 里补完整命令示例和输出样例

4.6 如何度量一个技能到底“好不好”

技能好不好不能靠感觉,我目前会用三个指标评估:

  • 触发准确率:模拟 30 条真实用户语句,数一数模型在正确场景主动加载技能的比例。
  • 执行成功率:技能脚本在测试页面/数据上成功返回预期 JSON 的比例。
  • 下游任务完成率:技能返回结果后,Agent 能否基于它顺利完成最终任务,比如总结是否准确覆盖正文关键信息。

这三个指标每次迭代技能包时都跑一遍,数字往上走,说明改动有效;数字往下掉,就得回滚或检查手册是否引入歧义。这套对照起来,比“我觉得这次应该行”靠谱得多。

5. 关于技能包长期维护的一些体会

5.1 把技能当产品维护

技能包写多了你会发现,它本质上是个面向模型的小产品。需要版本号,需要更新日志,需要定期 review。我会在每次业务大版本迭代时,把技能包目录整体过一遍:哪些手册描述过时了,哪些脚本依赖旧接口,哪些技能已经好几个月没人触发干脆下线。技能数量不是越多越好,一个能用清单解决的问题,没必要挂五个重叠的技能上去。

5.2 技能之间的“选择冲突”怎么处理

有一次我同时维护“网页正文提取”和“网页信息采集”两个技能,它们的触发场景高度重叠,结果模型经常随机挑选一个,行为不稳定。处理办法是明确技能边界:一个负责长文正文提取,一个负责短信息元数据抓取,并在各自的 SKILL.md 里写明“如果用户是要……请使用另一个技能”。说明书之间互相指路,比单方面描述自己要做什么更有效。

5.3 一个值得养成的迭代闭环

我自己操作下来,比较顺畅的迭代方式是:每次 Agent 在真实使用中调错技能、参数出错或输出不可用,就把这个失败案例记下来,作为负样例复盘,然后修改SKILL.md和脚本,再跑一轮回归对比。这个闭环跟带新人有点像:犯错不可怕,可怕的是没有把错误转换成说明书里的修正条款。技能包好不好用,拼的就是这套迭代是否足够勤快。

最后再分享一个小技巧:给每个新技能写一句“什么时候别用我”。这句负面条件看着简单,但真的能挡住大量误触发。我后面所有技能都强制要求写这一项,Agent 的整体稳定性肉眼可见地涨了一截。技能包这件事,说到底就是一遍遍打磨“模型能读懂的能力说明书”,脚本反而不是最难的,难在你要用语言把边界、流程、异常都交代明白。

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

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

立即咨询