1. 为什么“技能”正在成为Agent开发的新焦点
最近在梳理Agent相关的开源项目时,我注意到一个很有意思的命名趋势:越来越多的仓库开始用“skills”来命名,而不是之前常见的“tools”或“plugins”。“agent-skills”这个标题乍一看平平无奇,但如果你真正动手做过Agent应用,就会明白这个词背后代表着一场正在发生的范式转变。
早期做Agent的时候,大家喜欢把一切都塞进一个巨大的tools列表里。今天加一个查询天气的函数,明天加一个调用SQL的接口,后天又塞进一个文件读写的操作。刚开始只有几个工具时还好,但一旦工具数量超过二十个,问题就开始冒出来了:模型在选择工具时经常选错、上下文窗口被过长的函数定义塞满、同一个操作在多个工具中重复定义导致行为漂移。这些问题不是你写代码时能立刻察觉的,而是在实际跑了几个复杂任务之后才会像暗礁一样浮出来。
“skills”这个概念的提出,本质上是要解决工具的“原子化混乱”问题。如果把工具比作一个个孤立的动作,比如“拿起螺丝刀”“对准螺丝”“旋转手腕”,那么技能就是把这些动作组合成有意义的流程:“更换一颗螺丝钉”。对于Agent而言,skills不再是零散的函数,而是一组完整的、可持续复用的、带有明确输入输出契约和上下文的知识模块。这个差异看起来很微妙,但对模型的理解效率和执行稳定性影响非常大。
所以当我看到“agent-skills”这类项目时,第一反应就是:这是冲着Agent工程化的深水区去的。它要解决的不是“能不能调工具”,而是“如何让工具更好用、更智能、更可控”。这个标题适合三类人看:第一类是已经用LangChain、AutoGPT、Claude或其他Agent框架做过Demo,但发现实际落地效果不理想的人;第二类是在做Agent平台或Agent即服务产品,需要为开发者设计技能接口的人;第三类是纯粹对LLM应用层设计感兴趣,希望找到一套比“不断堆函数”更优雅的方案的人。
读完这篇文章,你会理解技能与工具在概念上的本质区别,会学到一套从定义、注册到调用的完整技能管理方案,更重要的是,你会知道那些没有写在README里的坑长什么样。
2. 设计思路拆解:从Tools到Skills的架构演化
2.1 工具原子化带来的三个真实困境
要理解skills为什么会出现,必须先搞清楚tools模式在真实项目中到底卡在哪里。我做过一个内部文档问答Agent,最初版本挂了十几个工具,覆盖权限校验、文档解析、向量检索、引用格式化等操作。表面上看一切正常,但跑了三天之后就发现三个难以忍受的问题。
第一个问题是上下文膨胀。每个工具定义在OpenAI API里会展开成一个较长的JSON Schema,十几个工具加起来就要占掉几千个token。看起来不多,但当你处理长文档、多轮对话时,这些token就是从上下文窗口里硬挤出来的空间。更尴尬的是,模型每次都只能看到全部工具的全量定义,哪怕本轮根本不需要用到其中十个。这个问题随着工具数量线性恶化,二十个、三十个工具时,提示词成本已经高到让人肉疼。
第二个问题是误选率上升。工具一多,它们之间的边界就开始模糊。比如一个工具叫“search_web”,另一个叫“fetch_url”,模型在需要检索网页内容时经常搞不清应该先用谁。表面上看这只是模型能力问题,但仔细研究会发现,根本原因是工具定义过于原子化,缺少上下文。检索网页和抓取网页明明是同一个任务的连续步骤,硬拆成两个独立工具后,模型就必须自己判断调用顺序,判断错了就全错了。
第三个问题是复用性差。原子化工具很难在项目之间迁移。这个项目里写的“parse_pdf_with_ocr”,换个项目可能就要改成“parse_image_with_ocr”再写一遍。时间长了,工具库变成了一个不断膨胀的垃圾场,每个工具单独看都没问题,但整体上没有人敢轻易动它们。
2.2 Skill的抽象层次:把“动作”升级为“能力”
Skills的核心思路是把抽象层次从“单一动作”提升到“岗位能力”。一个skill不是“调用某个接口”,而是“完成某个领域任务所需的一组推理链路和工具组合”。它不是让你把几个工具打包成一个函数那么简单,而是在打包的同时附带额外的指导信息,告诉模型在什么场景下用这个技能、遵循什么步骤、注意什么边界。
我推荐用一种三层结构来理解skill:底层是工具资源,中间是操作流程,顶层是场景语义。工具资源就是实际的函数或API调用,操作流程是对这个技能执行逻辑的自然语言描述,场景语义是这个技能在什么样的用户意图下使用。三层缺一不可。没有工具资源,skill就是空头支票;没有操作流程,模型拿到工具资源也不知道从哪下手;没有场景语义,模型在意图识别阶段就可能漏掉这个技能。
举一个具体的例子。假设你要做一个“合同风险检测”技能。工具资源可能有三个:合同文本解析器、法律条款知识库检索器、风险评分模型。操作流程是“先解析合同文本,再根据条款类型检索相关法规,最后生成风险评分和说明”。场景语义是“当用户上传合同或询问合同条款风险时启用”。如果把这些全部展开成原子化工具,模型就得自己推断“上传合同后应该先解析还是先检索”,而用skill封装之后,整个推理链条已经预先定义好了,模型要做的事情就只剩下按流程执行。
2.3 为什么说这是对Prompt工程的一种替代和升级
Skills的另一个重要价值在于,它在某种程度上替代了传统Prompt工程的脏活累活。以前调试Agent行为靠什么?靠改system prompt,加上“请你在使用工具前先确认XX”“如果遇到XX情况请优先使用XX工具”这类指令。效果有一点,但很脆弱,因为系统提示词是全局的,它会影响所有任务的判断,哪怕有些任务根本不需要这些指令。
Skill的出现改变了这种一刀切的做法。把行为指导从全局的system prompt中剥离开,内聚到具体的技能定义里。用哪个技能,相关指导才进入上下文,不用就不进入。这相当于从“给所有人发同一份长篇操作手册”变成了“每个岗位拿自己专属的SOP”。这样做的好处是降低提示词的全局污染,之前提到的上下文膨胀问题也大幅缓解,因为不需要激活的技能不会加载它的完整定义。
从这个角度看,skills不只是工程上的组织优化,更是对Agent控制策略的一种重新思考。它承认了一个现实:我们不太可能靠一段几百字的系统提示词精确控制一个复杂Agent的所有行为,但我们可以把一个复杂任务拆成一个个有边界的子技能,每个子技能内部足够简单,简单到模型不需要太多自由发挥就能得到可靠结果。
3. 核心细节解析:一个Skill的标准长相与注册机制
3.1 Skill定义文件的字段设计
如果只记住一个结论,那就是:skill不是一个函数,而是一个目录。目录里至少要包含一份描述元数据、一份执行入口、以及若干可选的参考资源。下面是我在实际项目里打磨过的目录结构,虽然不是唯一的答案,但经历了多次迭代后,我觉得这个结构在管理性和可读性之间比较均衡。
skills/ ├── contract_risk_detector/ │ ├── SKILL.md │ ├── executor.py │ └── references/ │ ├── law_keywords.json │ └── prompt_templates.md ├── web_researcher/ │ ├── SKILL.md │ ├── executor.py │ └── requirements.txt └── data_analyzer/ ├── SKILL.md ├── executor.py └── references/ └── chart_schema.jsonSKILL.md是核心,它承担了前面说的“场景语义”和“操作流程”两大职责。我见过的SKILL.md大多采用YAML frontmatter加Markdown正文的结构。frontmatter区域管理结构化元数据,正文区域用自然语言描述执行逻辑。一个典型的SKILL.md长这样:
--- name: contract_risk_detector description: 检测合同文本中的潜在法律风险,适用于合同审核、法律尽调等场景。 version: 1.0.0 author: legal-agent-team tags: [legal, contract, risk-analysis] trigger_keywords: [合同, 风险, 条款, 协议, 法律] input_schema: text: type: string description: 合同原始文本 clause_types: type: array items: string optional: true output_schema: risk_scores: type: object description: 各类条款的风险评分 suggestions: type: array items: string --- # 合同风险检测技能 当收到待检测的合同文本时,按照以下步骤执行: 1. 使用合同解析模块结构化提取文本中的核心条款。 2. 根据条款类型,检索法条知识库,获取对应法规依据。 3. 调用风险模型对条款进行风险评分,评分范围0-1,高于0.7标记高风险。 4. 输出风险报告,包含具体条款原文、风险等级、修改建议。 ## 注意事项 - 如果合同文本超过上下文限制,先分段解析,再合并结果。 - 如果条款类型不在知识库覆盖范围,标注“未覆盖”而非跳过。这个文件的精妙之处在于,它将结构化信息和自由文本指导结合在了一起。模型解析frontmatter可以快速判断这个技能管不管用、什么时候用,而正文部分则提供了执行时的推理参考。很多项目只写description字段不写正文,这是不对的,你会发现在复杂任务中,模型根本不知道具体该按什么顺序执行,最后又退化成拿工具乱试。
3.2 注册与发现机制:如何让模型在正确的时间找到正确的技能
Skill定义好了以后,下一个核心问题是发现机制。一个Agent可能装了几十个技能,模型每次收到用户消息后,是应该把全部技能的描述都塞进上下文吗?肯定不行,那样上下文膨胀问题又回来了。实际采用的是两阶段发现策略:先粗筛,再加载。
粗筛阶段发生在用户请求进入Agent调度器之后、正式调用LLM之前。我通常用一个小的嵌入式模型把用户意图转成向量,和所有skill的description向量做相似度比较,取出TopK个候选skill。取几个?这个需要根据上下文窗口和任务复杂度来定,我推荐3到5个。太少可能漏掉真正需要的技能,太多则上下文压力大。
粗筛完成后进入加载阶段,只有候选skill的SKILL.md才会被拼接到系统提示词或单独的技能指令块里。这样做的好处非常明显。假设你装了50个技能,每个skill的SKILL.md平均包含1500个字符的英文描述,如果全部加载,大约要消耗2万token以上;但使用粗筛后,每次只加载3到5个,token消耗直接降到十分之一。同时,因为模型只需要在这几个候选里做选择,误选率也明显下降。
我最早在LangChain里做这件事时用了独立的选择器类,后来切到自研框架后用了更简单的方式:维护一个技能特征向量库。技能数量不多时,直接在Python进程内用numpy计算余弦相似度就行,几十个技能的性能开销完全可以忽略;技能数量达到几百个时再考虑用专门的向量数据库。不要一开始就上重武器,这是一条很实用的经验。
3.3 版本、依赖与安全:容易被忽视的三个方面
Skills的版本管理很考验项目组织能力。一个技能不是写完就完事的,业务逻辑变了、底层模型版本换了、知识库内容更新了,都需要版本迭代。我建议在SKILL.md的frontmatter里强制维护version字段,并用Git标签管理版本历史。当技能升级导致行为变化时,尽量让新版技能兼容旧版的输入输出格式,否则下游依赖会一起爆。
依赖管理也是一个容易翻车的点。每个skill依赖的Python包可能不同,如果全部装进同一个全局环境,很快就会出现依赖地狱。我的方案是给每个skill一个独立的requirements.txt,然后在加载时用虚拟环境隔离运行。轻量级方案可以用subprocess调用,重型方案可以用Docker容器封装。
安全方面只说一条经验:不要轻易让技能执行LLM生成的任意代码。你可能会想,“我的skill能让Agent自如地写Python脚本并运行,多酷”,但实际生产中这一步会引入任意代码执行风险。如果非做不可,至少要把运行环境限制在一个权限受控的沙箱中。我在自托管Agent服务上吃过亏,最后把所有需要执行动态代码的skill全部迁移到了无网络的受限容器中。
4. 实操过程:从零搭建一个可复用的技能库
4.1 先定边界:哪些能力值得封装成Skill
动手之前要先想清楚一个问题:到底什么才值得封装成一个skill?不是所有工具函数都要升级成skill。我总结了一套筛选标准,满足其中至少三条的才值得封装:一,该能力在多个不同任务中被复用;二,该能力的执行链路不是一条直线,需要中间判断或分支处理;三,该能力对模型有“使用门槛”,比如需要特定步骤才能正确执行;四,该能力的输入输出边界比较清晰。
一个典型的正面例子是“网页深度调研”技能。它需要搜索关键词、打开多个链接、提取正文、去重、生成摘要,链路中涉及多个工具和多次判断,明显满足标准。一个反面例子是“计算字符串长度”的函数,它只需要一次调用,执行链路简单,几个token就能完成,封装成skill反而增加系统负担。
我见过很多新手项目犯的错误是“万物皆Skill”,把加减乘除都封装成技能,结果就是发现机制被大量低价值技能干扰,真正有用的技能反而在粗筛阶段被挤掉了。所以第一步先讲清楚边界,是非常有必要的。
4.2 实操步骤:定义、注册、调用三步走
下面用“网页深度调研”这个技能作为案例,走一遍完整流程。
第一步,在skills目录下新建web_researcher文件夹,并创建SKILL.md。这里的关键是description要写得准。不要写成笼统的“搜索并总结网页内容”,而要写成“当用户需要调研某个主题、获取多个网页信息并生成综合报告时使用”,这样粗筛阶段的向量匹配才能命中。
然后定义executor.py,这部分是实际执行逻辑。我用LangChain的工具装饰器风格来写,但核心逻辑其实是通用的:
from typing import List, Dict import requests from bs4 import BeautifulSoup def web_research(query: str, max_results: int = 5) -> Dict: """执行网页调研,返回结构化结果。""" # 1. 搜索关键词,获取候选链接 links = search_engine(query, max_results) # 2. 逐页抓取正文并清洗 contents = [] for link in links: try: html = requests.get(link, timeout=10).text text = extract_main_content(html) if len(text) > 100: contents.append({ 'url': link, 'title': extract_title(html), 'content': text, }) except Exception: continue # 3. 返回汇总数据,由LLM生成最终报告 return { 'query': query, 'results': contents, 'total_fetched': len(contents), } def extract_main_content(html: str) -> str: soup = BeautifulSoup(html, 'html.parser') for tag in soup(['script', 'style', 'nav', 'footer']): tag.decompose() return ' '.join(soup.get_text().split())[:2000] def extract_title(html: str) -> str: soup = BeautifulSoup(html, 'html.parser') return soup.title.string if soup.title else 'Untitled' def search_engine(query: str, max_results: int) -> List[str]: # 实际项目中可换成SerpAPI、Bing API等 # 这里用伪代码表示,重点突出流程而非具体实现 return [ 'https://example.com/article1', 'https://example.com/article2', ]第二步,把技能注册到中心管理模块中。我做了一个简单的SkillRegistry,相当于所有技能的总台账。每次新增技能时,需要把技能名、入口函数、标签、依赖等信息注册进来:
class SkillRegistry: def __init__(self): self._skills = {} def register(self, skill_meta: dict, entry_func: callable): self._skills[skill_meta['name']] = { 'meta': skill_meta, 'func': entry_func, } def search(self, query_vector, top_k=3): scores = [] for name, skill in self._skills.items(): desc_vec = embed(skill['meta']['description']) score = cosine_similarity(query_vector, desc_vec) scores.append((score, name)) scores.sort(reverse=True) return [name for _, name in scores[:top_k]] def load(self, skill_name): skill = self._skills[skill_name] return skill['meta']['description'], skill['func']第三步,在Agent的执行循环里把发现、加载、调用串起来。LLM拿到用户消息后,先抽取出用户意图向量,在注册表里找到TopK候选技能,然后将候选技能的SKILL.md描述注入系统提示词,让模型自己决定要不要调用以及调用哪个。这一版跑通之后,你会明显感受到模型在复杂任务上的选择准确率提升了,整个人机交互的体验也顺滑了。
4.3 参数选择与经验判断:TopK、阈值和技能数量怎么定
这三个参数是最容易困惑新人的地方。先看TopK。TopK决定每次注入多少个候选技能的描述,我推荐3到5之间。设太小容易漏掉真实需要的技能,设太大则失去粗筛的意义。对于上下文只有8K的模型,3个候选最稳;对于128K上下文的模型,可以放宽到5个。
再看阈值。向量搜索会有个相似度得分,低于某个值说明用户意图和任何技能都不匹配。我经验值是0.3到0.4之间,具体看你用的嵌入模型分布。低于阈值时不注入任何技能,让模型直接走通用对话或默认工具链路。
最后是总技能数。一个Agent项目里的技能数不要盲目追求多。我维护过一个25个技能的项目,已经感觉到维护成本很高了。每次更新某个技能时都得重新跑一遍全部回归测试,确保其他技能没有被影响。如果技能超过50个,强烈建议将技能按领域分组,在粗筛前先做一次领域分类,避免所有技能都参与向量匹配。
5. 实战中的坑:我在Agent Skills落地时踩过的雷
5.1 症状一:Agent明明加载了Skill描述,却不按Skill的逻辑执行
这是最常见的坑,也是让很多人觉得“Skills根本没用”的头号原因。排查后发现,大部分情况下问题出在技能正文的表述方式上。如果你SKILL.md里的步骤描述是“分析文本并输出结论”,模型照做的时候就会用最模糊的方式完成任务;正确写法是给出具体步骤序列和判断规则。
打个比方,你让一个新员工“整理一下客户资料”,他可能会按照自己的理解乱整理一通。如果你告诉他“先按客户ID排序,再按最近联系时间排序,最后把超过30天未联系的客户标记为流失风险”,他的执行就会精准很多。SKILL.md就是那个用来消除执行歧义的操作手册。把执行步骤写得足够具体,是技能有效性的第一要务。
5.2 症状二:多个Skill的功能重叠,Agent在它们之间反复横跳
当你的技能库膨胀后,很多技能的边界开始模糊,模型经常会选错甚至先调用A又调用B。我调试过一个案例:一个“文档摘要”技能和一个“文档问答”技能,它们都需要先读取长文档并解析语义,但任务结果一个是总结性文字、一个是问答式输出。模型在用户问“这篇文章讲了什么”时,选到了文档问答技能,给出的答案结构很别扭。
解决这种重叠靠两条手段。第一,在各自SKILL.md的description里写得非常精确,强调适用场景和排除场景;第二,在粗筛后的加载阶段加入一个“拒绝提示词”,告诉模型候选技能中哪些明确不适合当前任务,减少误选。在实验里,这个拒绝提示词能把选择准确率提升5到10个百分点。
5.3 症状三:技能上下文占用太高,输入窗口爆掉
有些skill定义里塞了大量示例和长指令,多加载几个就会把上下文撑爆。后来我学到一个技巧:把SKILL.md分为“常驻摘要”和“触发后全量”两部分。粗筛阶段的向量匹配和候选展示只使用frontmatter里的description和keywords,这是常驻摘要;真正被选中执行时,才将正文部分的完整步骤注入到执行上下文中。这一步优化之后,我的系统提示词体积降低了大概百分之六十,同时技能质量没有变化。
这个做法的本质是延迟加载。就像你点进一个店铺才会看到商品详情页,而搜索页只需要展示标题和简介就够了。千万不要把商品详情页的所有信息一股脑塞进搜索索引里。
5.4 症状四:技能内部逻辑更新后,旧会话还在用老定义执行
这是我自己踩出来的坑。有一次我更新了一项技能的判定规则,但发现正在进行的对话里依然使用旧规则,原因是我的会话对象在对话开始时就把完整技能描述快照进去了。LLM没有“重启加载”这个概念,已经写进上下文的内容不会自动刷新。
我的解决方案是给技能增加版本标识,并在每次执行前做一次版本检查。如果发现当前会话加载的技能版本和注册中心不一致,就重新注入新版本的定义,并提示一次“技能定义已更新”。这个方法没有完全解决问题,但至少能保证新任务不会用到旧逻辑。
6. 从Skill到Skill图谱:给Agent装上可演化的技能生态
写到这里,我想把视角拉高一点。技能管理和技术债管理有一个极其相似的地方:只做新增不做清理,迟早会把自己的系统拖垮。我在项目里摸索出来的经验是,大约每两个月要做一次技能盘点。
盘点要干三件事。第一件是清理,把最近两个月没有被触发过的技能拆掉或者归档,保留不代表一直在用,很多技能注册之后就再也没被模型选中过,留着只会干扰向量匹配结果。第二件是合并,把频繁一起触发且边界模糊的多个技能合并成一个复合技能。我合并过三个小技能,合并之后不仅命中率提高,维护工作量也下降了。第三件是补强,找到那些经常被触发但反馈质量不高的技能,根据用户反馈和下游指标迭代执行流程。
技能库不是一次性建设的一次性交付物,它更像一个植物园,需要持续修剪、移栽和引入新品种。今天Agent的能力边界很大程度上不再取决于底层模型有多强,而是取决于你给它装配了哪些可复用的技能,以及这些技能被组织得好不好。摊子铺得越大,对管理能力的要求也越高,这大概是Agent应用开发者接下来要长期面对的挑战。
我个人在实际操作中的体会是:不要先搭宏大的技能平台再填内容,那是典型的自嗨式建设。正确的姿势是从一两个高频业务场景切入,把技能做深做透,让它在真实任务里创造可感知的价值,然后再逐步扩大覆盖范围。技能库的演化路径本质上是个生态演化的过程,太早追求全面,往往什么都做不精。