
最近一直在鼓捣 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-extractordescription一句话说明能力用于模型做技能选择提取网页正文并转换为干净的 Markdown 文本version技能版本号方便追踪更新1.0.0trigger触发场景描述写得越具体越好当用户给出一个网页链接并要求总结、提取或整理内容时正文部分最重要的是示例调用。模型对示例的依赖程度远超我们的想象你在说明里写十句抽象描述不如给一组“输入这个参数跑这条命令得到这段输出”的真实样例。我在一个数据处理技能里试过两种写法有完整调用示例的版本模型按正确参数调用的概率约 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}, timeoutTIMEOUT) 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: frequest_failed: {exc}} # 用 readability 提取正文 doc Document(resp.text, urlurl) title doc.short_title() body_html doc.summary(html_partialTrue) # 简单清洗去掉多余空白和乱码 soup BeautifulSoup(body_html, html.parser) text soup.get_text(\n, stripTrue) 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, requiredTrue) args parser.parse_args() result extract_article(args.url) print(json.dumps(result, ensure_asciiFalse)) 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 的整体稳定性肉眼可见地涨了一截。技能包这件事说到底就是一遍遍打磨“模型能读懂的能力说明书”脚本反而不是最难的难在你要用语言把边界、流程、异常都交代明白。