☰
Agent Skills实战:给大模型封装一双能干活的手
2026/10/7 4:25:21 网站建设 项目流程

1. Agent真正缺的不是大脑,是一双能干活的手

前段时间跟一个做内容工具的朋友聊天,他兴致勃勃地跟我展示他们刚上线的AI助手,说模型多聪明、上下文多长、生成多流畅。我随手丢了个任务过去:"帮我把这个网页里的正文抓下来,去掉导航和广告,转成干净的Markdown存档。"结果助手回了一段很得体的歉意——它说它"非常理解"这个需求,然后给了我一篇八百字的操作指南,教我自己怎么手动复制粘贴。

问题出在哪?出在大多数Agent只有"嘴",没有"手"。

这就是agent-skills这个概念最近在圈子里越聊越多的核心原因。所谓的agent-skills,通俗讲就是一套让大模型真正具备执行能力的"技能库":把模型能理解的意图,翻译成它能调用的真实工具动作,再把这一整套动作封装成可复用、可插拔、可共享的标准化模块。你可以把它想象成给Agent配了一套乐高工具包:想让它整理网页,就给它"网页转Markdown"这块积木;想让它批量处理文件,就给它"文件批处理"这块积木。模型负责指挥,技能负责动手。

这个思路跟传统的function calling、tool use一比,差别还挺明显的。传统做法更像是"临时工"——模型现用现查,每个任务都要重新描述一遍工具怎么用、参数怎么传,调试一次痛苦一次。skills的思路则是"正式员工"——把一类操作沉淀成固定岗位说明书(SKILL.md),模型一看就知道这个技能是干什么的、什么时候该叫它、参数怎么给,调用成本和稳定性都有本质提升。

我后来自己动手把几个常用能力封装成了技能,跑了大概一个月的真实任务,这篇文章就把整个过程中的设计思路、踩坑经历和排查链路原原本本分享出来。如果你也在做Agent应用,或者正准备给自己的助手加一双"手",这篇应该能帮你省掉不少弯路。

2. 技能不是写个脚本那么简单,它是一套完整的"岗位说明书"

在动手封装第一个技能之前,我一直有个误区:以为所谓的skill就是把一段能跑的代码丢给模型就行。直到实际跑了几个任务才发现,模型需要的不只是"工具",而是"怎么用工具"的完整说明,否则它根本不知道该在什么时机调用、参数怎么填、结果怎么判断。

2.1 一个技能的标准四段式结构

我研究了市面上几个主流Agent技能项目的组织方式,发现做得好的技能包基本都是四件套:

组成作用类比
SKILL.md(技能说明书)告诉模型这个技能能干什么、怎么用、参数怎么定义岗位职责描述
可执行脚本真正干活的那段代码,Python、Shell、Node都行员工的双手
参数Schema定义入参格式、类型、必填项交接单
验证元数据(可选)技能版本、依赖、适用场景员工档案

这里面最容易被忽略,但也最关键的,是SKILL.md。它不是写给程序员看的文档,是写给模型看的"使用手册"。模型的推理能力再强,如果没有一份清晰、没有歧义的说明书,它就只能靠猜。

2.2 SKILL.md里最重要的三个字段

我踩过不少坑之后,总结出来三个字段是一定要写清楚的:

name(技能名)。看起来简单,但命名是有讲究的。我之前有个技能叫"web_extractor",模型在两个任务里分别把它理解成了"提取网页标题"和"抓取整站内容",后来改成"fetch_webpage_as_markdown",带上了动作和产出物,误调用的概率立刻降了很多。命名里最好直接包含"输入是什么、输出是什么",别搞文艺范。

description(何时使用)。这个字段直接决定模型会不会在正确的时机调用你。最忌讳写"这是一个网页抓取工具"这种废话。正确写法是给出明确的触发条件,比如"当用户请求将网页内容保存为Markdown格式,或需要提取网页正文、去除广告导航时使用。不适用于:解析本地PDF文件、抓取需要登录的页面"。把适用和不适用都写清楚,模型才能判断"该不该用"。

parameters(参数定义)。这里有个容易犯的错——按程序员的习惯用短变量名。我的第一个技能参数叫"q",想着是query的缩写,结果模型传参的时候时不时就会犹豫,甚至传错。改成"query_url"、"output_format"这种语义完整的命名之后,传参成功率肉眼可见地提高了。参数描述里还要写明格式要求,比如"必须是完整的http或https链接,不支持相对路径",这能省掉后面一长串的报错排查。

2.3 脚本是模型的手,但大脑始终是模型

还要强调一点:技能里的脚本不需要"聪明",需要的是"稳定"。你的脚本只是模型执行动作的通道,真正的判断、规划、文本理解都在模型侧完成。所以脚本的输入输出一定要简单粗暴:输入清晰明确的参数,输出结构化的结果(纯文本、JSON、或标准Markdown文件路径),别在脚本里做复杂的条件分支,更别让脚本自己"智能决策"。复杂逻辑放到模型那里,脚本就是一把螺丝刀,不是瑞士军刀,用好一个动作就够了。

3. 实战:把一个网页内容整理需求封装成可用技能

光讲结构有点虚,我拿一个真实跑通了的技能——"网页转Markdown整理归档"来完整走一遍。这个技能在我平时的工作流里出现频率最高,很多素材收集、内容存档的需求都会用到。

3.1 先想清楚边界:什么时候该用技能,什么时候让模型直接答

动手前得先划清边界。像"帮我总结一下这个网页讲了什么",这种事根本不需要技能,模型自己就能读URL内容然后总结。真正需要技能的场景是:要把网页变成什么产物——存成文件、转成结构化格式、批量处理——也就是需要"动手操作"的时候。

所以我在SKILL.md的description里是这样写的:

name: fetch_webpage_as_markdown description: 将用户指定的网页抓取为结构化Markdown文件,并保存到指定目录。 当用户需要存档、整理、下载网页内容为可编辑文本,或要求"把网页转成markdown"、 "保存这篇文章"时使用。不适合于网页内容已通过其他API获取的场景。 parameters: type: object properties: query_url: type: string description: 完整的网页链接,必须以 https:// 开头,不支持相对路径。 save_dir: type: string description: 保存文件的目录路径,默认 ./downloads default: ./downloads required: - query_url

这个description写出来的效果是:模型一旦识别到"存"、"转"、"下载"这类动作意图,就会调技能,否则它自己读内容直接答,不会打扰技能。这就是"边界感"。

3.2 脚本实现:稳定优先,别加花活

脚本我选Python,原因很简单:requests、BeautifulSoup、html2text这些库生态成熟,谁的环境里都能装,出问题的概率最小。核心逻辑其实短得可怜:

import sys, json, os, argparse import requests from bs4 import BeautifulSoup import html2text def fetch_and_convert(url: str, save_dir: str) -> dict: headers = {"User-Agent": "Mozilla/5.0 (compatible; AgentSkill/1.0)"} resp = requests.get(url, headers=headers, timeout=20) resp.raise_for_status() soup = BeautifulSoup(resp.text, "html.parser") for tag in soup(["script", "style", "nav", "footer", "aside"]): tag.decompose() # 尝试定位正文主体,找不到就退回全量 main = soup.find("article") or soup.find("main") or soup.body converter = html2text.HTML2Text() converter.ignore_links = False converter.body_width = 0 md = converter.handle(str(main)) os.makedirs(save_dir, exist_ok=True) slug = url.split("/")[-1][:80] or "index" path = os.path.join(save_dir, f"{slug}.md") with open(path, "w", encoding="utf-8") as f: f.write(f"# 来源: {url}\n\n{md}") return {"status": "ok", "path": path, "char_len": len(md)} if __name__ == "__main__": p = argparse.ArgumentParser() p.add_argument("--query_url", required=True) p.add_argument("--save_dir", default="./downloads") args = p.parse_args() result = fetch_and_convert(args.query_url, args.save_dir) print(json.dumps(result, ensure_ascii=False))

几个细节是试出来的教训,值得记一笔:

  • User-Agent一定要伪装。很多站点对requests裸访问直接403,带上浏览器UA之后再没遇到过这种问题。
  • 先剥掉script/style/nav/footer再转Markdown。不加这一步,html2text会把一堆导航链接和页脚信息也带进正文,输出文件脏得没法看。
  • 输出直接打JSON。我在设计技能时定了一条原则:脚本与控制台交互只用JSON,模型解析起来零成本。别打一堆可读性文本让模型去"猜状态",JSON里status字段一把梭,正确还是失败一目了然。

3.3 给模型"兜底":异常处理也要写在说明里

这一步是很多初做技能的人会忽略的。脚本虽然能正常抓取,但总会有例外情况:目标网页404了、反爬拦截了、超时了。这些异常脚本会抛给模型,模型如果没见过这种报错格式,就会开始胡言乱语。

我的做法是在SKILL.md最后加一节"错误处理注意事项",明确告诉模型:

当脚本返回 status 非 ok 时,把 error 字段原样转述给用户,不要尝试修复URL。 如果返回的是 404,告诉用户页面不存在;如果是 403,说明站点有反爬,建议用户换一个来源。

这一步等于给模型一份"售后处理预案",模型在异常状态下的反应,会从"自由发挥"变成"按流程走",稳定性能上一个台阶。

4. 第一次真实调用就翻车了:一条完整的排错链路

技能封装好了之后,我满怀信心地让它去抓一个平时常看的行业资讯页。结果第一跑就翻车了,而且翻得很典型。整个过程排查下来,我觉得这条链路本身就很有参考价值,放出来给大家当一个排错demo。

4.1 翻车现场:日志里躺着一行诡异的参数

我把任务发过去:"把这篇XX网的文章存成Markdown,放桌面。"Agent调用了fetch_webpage_as_markdown,日志里显示它传入的参数是这样的:

{ "query_url": "https://xxx.example.com/articles/2024/01/15/hello-world", "save_dir": "~" }

我桌面路径是 /Users/me/Desktop,可它传的是"~"。窗口没展开,脚本里的os.makedirs("~")直接在当前工作目录下建了个名为"~"的文件夹,文件没落地到桌面。我第一反应是脚本不健壮,得做路径展开。但想了想不对:问题根源不在脚本,在于SKILL.md里没有告诉模型"参数必须是绝对路径,不支持~等shell缩写"。

4.2 逐层回溯:不是脚本问题,是说明书有歧义

我要强调一个排查原则:Agent技能调用的错误,七成以上不在代码层,而在说明书层。代码报错只是表象,真正的问题是模型没得到足够清晰的约束。按照这个思路,我当时按三层逐步排查:

先排除脚本本身——单独用正确参数跑,能抓到,能存到,脚本没问题。这层排除之后,焦点立刻转移到模型调用环节。

再看参数来源——模型是从用户的那句话里提取的路径。用户的自然语言说的是"桌面",模型做了个"合理但不准确"的推断,把"~"当成桌面的缩写输出了。这不是模型的智力问题,是我的schema描述里没有规定"save_dir必须是绝对路径,且必须由真实工作目录推导"。

最后检查SKILL.md的parameters描述——老实说,我的parameters里save_dir的描述确实只写了"保存文件目录路径,默认./downloads",对路径格式完全没约束。说明书给了模型太大的自由裁量空间,它就按着"最省事"的方式发挥了。

4.3 修复:让说明书不给模型留自作主张的空间

定位到根因后,我把parameters里save_dir的描述改成这样:

save_dir: type: string description: 输出目录的绝对路径,必须先进行路径规范化(如将~扩为完整用户目录)。 如果用户表达的是"桌面"等常见目录,请映射为对应绝对路径。禁止传入"~"或"."等相对缩写。

同时,我还在脚本里加了一道保险:

save_dir = os.path.abspath(os.path.expanduser(args.save_dir))

两道保险下去,再跑同样的任务,Agent传的是 /Users/me/Desktop,脚本也能兜底处理异常输入。至此,这条排错链路才算完整走完。

4.4 这个坑后来反复出现过:泛化的教训

这个路径问题看着是个小case,但它背后的模式其实很常见。我后来又遇到过多例类似的"模型自作主张"翻车:

  • description里写了"提取文件标题",没说"提取的是HTML的title标签",模型擅自把h1当成标题。
  • 参数命名用了"target",模型在多个场景下分别理解为URL、文件路径、甚至文件夹名。
  • 技能说明里没写"处理完成后必须输出文件绝对路径",模型返回了"文件已保存"四个字,下游流程全断。

这类问题的修复思路完全一样:给模型的每一条约束,都要具体到无法二义理解的程度,宁可我写说明书时多花两分钟,不让模型调用时报错两小时。我把这条经验固化成了技能封装的checklist,每次新建技能之前先过一遍,后面翻车率确实降了一个量级。

5. 从单技能到技能链:Agent工程化绕不开的进阶问题

单个技能跑通只是第一步。真正在项目里实战,你会发现Agent一次任务往往需要连续调用多个技能,这时候技能与技能之间的配合方式,才是决定整个系统是否好用的关键。

5.1 技能组合的三种姿势,选错会后悔

我试下来,技能组合基本有三种方式,复杂度递增,灵活性也递增:

线性流水线。技能A输出是技能B的输入,一个接一个。比如"网页转Markdown"之后接"提取要点摘要"。这种姿势最简单,缺点是中间结果需要模型来回搬运,多一个环节多一次token消耗,也多点选参数出错的概率。

主技能嵌套子技能。在一个SKILL.md里声明可以调用其他技能。比如做一个"周报生成"的主技能,内部会调"读取工作日志"、"抓取项目进度页"、"生成结构化周报"三个子技能。模型拿到主任务的意图后,自己会去规划子技能的调用顺序。这种姿势适合任务本身有固定流程的场景。

纯模型自主编排。不预设组合方式,给模型一个较大的技能池,让它在每步动态决定下一步调什么。灵活性强但不可控,对模型能力和说明书质量要求极高。我目前只在探索性项目里用,正式工作流还是以有结构的设计为主。

5.2 技能之间的"交接":上下文比代码更脆弱

多技能协作最大的坑是上下文丢失。模型调完技能A拿到一堆JSON输出,在下一次决策时如果上下文窗口被压缩了,就可能在调技能B时传错参数。我处理这个问题的办法是:技能输出尽量自包含。每个技能的返回结果里带上"这份输出是什么、下一步建议做什么"的最小元信息。比如网页转Markdown返回的JSON里,除了path、char_len,还加上一句summary(用模型生成的一行内容摘要),这样下游技能拿到这个输出后,不用回查之前的对话也能理解上下文。

5.3 Skills和MCP的关系:很多人把这两个概念搞混了

聊到这里,很多人会问:说了半天,这个skill概念和最近炒得火热的MCP(Model Context Protocol)到底有什么区别?我用一句话总结:MCP是"插座标准",skill是"插头组合"。

MCP定义的是工具、资源和模型之间怎么通信、怎么暴露数据,是协议层的东西。而skill是业务层的东西,它规定的是"什么场景下调用哪些MCP工具、按什么顺序、怎么处理结果"。换句话说,MCP解决的是"模型怎么连上工具",skill解决的是"模型什么时候用哪些工具来完成任务"。

实际项目里两者不是竞争关系,而是配合关系。我通常的做法是:用MCP server把远程工具规范化暴露出来,然后在skill的SKILL.md里写明这个技能会用到哪些MCP工具。这样既保持了工具的独立性,又让技能行为可预测。

5.4 什么场景值得沉淀成技能,什么场景不值得

最后说点务实的建议。不是所有操作都值得封装成技能,我给自己定了一个"三次原则":一个操作如果同类型出现不到三次,别急着封装,让模型自己发挥就好;超过三次,才考虑固化成技能。

真正的skills思维,不是把所有东西都做成技能,而是知道哪些能力是高频复用的"肌肉记忆",把肌肉记忆固化成标准动作,剩下的随机应变交给模型。这个"度"把握好了,Agent工程化做起来会舒服很多。我自己跑了一个多月下来最大的感受就是:技能库的维护成本大头不在写脚本,而在打磨说明书。跟给新同事写交接文档一样,你把用词抠到多细,你之后返工就有多省。

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

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

立即咨询