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, timeout10).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_k3): 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(reverseTrue) 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应用开发者接下来要长期面对的挑战。我个人在实际操作中的体会是不要先搭宏大的技能平台再填内容那是典型的自嗨式建设。正确的姿势是从一两个高频业务场景切入把技能做深做透让它在真实任务里创造可感知的价值然后再逐步扩大覆盖范围。技能库的演化路径本质上是个生态演化的过程太早追求全面往往什么都做不精。