做Agent开发做了差不多一年,我发现自己反复在做同一件很蠢的事情:换一个项目,就把上一个项目里写过的工具函数、调用流程和提示词重新敲一遍。直到我把这些散落的东西整理成一个叫“agent-skills”的可复用技能库之后,开发效率和效果才真正上了个台阶。这篇文章把我自己的设计思路、落地过程和踩过的坑原原本本分享出来,给同样在做智能体开发的同行一个可以直接抄作业的参考。
1. 为什么我需要一个技能库:Agent开发里最容易被低估的一环
1.1 智能体开发里最磨人的不是大模型,而是工具函数
刚开始做智能体应用时,我一度以为核心难点是提示词和大模型能力调用。实际写多了才发现,真正消耗时间的是那些看着不起眼的工具函数:让模型查询数据库、读写文件、调外部接口、解析返回数据,每一个都需要你写清楚“这个函数是干什么的、参数是什么、什么时候该用”。模型本身并不知道你的代码里有哪些能力,你必须通过提示词或者某种注册机制把工具告诉它,一次忘记描述,Agent在关键步骤就会卡壳或者干脆瞎编。
如果你同时维护几个不同的Agent项目,问题会更明显。A项目里写好的数据查询逻辑,B项目里要用就得重新适配一遍;C项目里精心调优过的输出格式化函数,在D项目里被复制过去之后,改了几轮就变得面目全非。我意识到这不是代码复用的问题,而是缺少一套“智能体技能”的管理机制——技能不应该是散落在各个项目里的函数,而应该是可以独立描述、独立加载、独立复用的模块。
1.2 agent-skills到底想解决什么问题
我给自己定的目标是做一套“技能库”,它要同时满足几个苛刻的要求:
第一,技能与项目解耦。写好的技能放在独立目录里,任何Agent项目通过约定好的方式引入即可,不需要把技能代码复制到项目里。类似于把常用的工具函数打包成依赖,但比普通依赖库多了“面向模型的描述”。
第二,技能自带“使用说明”。每个技能不仅要包含可执行的代码,还要包含一段给大模型看的描述,说清楚这个技能的用途、适用场景、参数格式和返回值含义。这相当于给工具函数配了一份“模型友好”的说明书。
第三,支持技能的自由组合。复杂任务通常需要多个技能协同完成,比如“先搜索,再整理成表格,最后发送通知”。技能库需要让Agent能够按需调度多个技能,而不是每个技能都是孤立的功能点。
第四,热加载和增量扩展。我不想每次加一个新技能都要重新构建整个项目,最好是把新技能文件丢进目录里,代码就能自动识别并注册,Agent下次调用时直接可用。
这四个需求看着简单,真正实现起来涉及不少设计决策,我把完整方案拆开来说。
1.3 为什么不用现成的框架
做之前我也研究过市面上已有的工具调用方案和插件体系。有几类路径是可行的:一类是在提示词层面定义工具列表,让模型从给定的函数列表里选;另一类是走工具调用协议,把工具封装成标准的接口格式让模型对接。
这些方案各有优缺点。提示词层面定义最灵活,但上下文窗口占用高,技能一多就没法用了。工具协议标准化程度高,但学习成本和接入成本都偏高,而且很多协议是为大型平台设计的,个人项目用起来有点重。我想要的是一种“轻量级、以目录和文件为边界、本地优先”的方案,既能被模型通过简短说明理解,又不需要引入一整套复杂的运行时。于是agent-skills就按我自己的需求设计了:核心就是“一个技能一个目录,目录里放描述文件和可执行脚本,由加载器统一扫描注册”。
2. 技能库的整体架构:一个技能到底应该长什么样
2.1 技能定义的四个核心组成部分
在动手写代码前,我花了比较长时间琢磨“技能”的基础数据结构,这是整个项目的地基。
一个完整的技能在我的设计里由四部分组成:技能元信息、技能描述、执行代码、使用示例。元信息包括技能的唯一标识、版本号、作者和依赖关系,这部分主要是给开发者看的,用于管理和排查问题。技能描述是给大模型看的一段文本,需要说清楚“我是谁、我能做什么、什么时候用我”,这部分直接决定了Agent会不会在恰当的时机调用这个技能。执行代码是真正干活的逻辑,可以是Python脚本、Shell命令,也可以是调用其他服务的代码。使用示例则记录一两个典型的调用场景,既有给开发者看的注释,也有给模型看的示例对话。
这里有个比较关键的设计取舍:技能描述必须要短。模型在决策阶段会读取全部可用技能的描述,如果每个描述都写几百字,二十个技能就会占用大量上下文。我的经验是一个技能的描述控制在50-100字左右,重点突出“在什么场景下使用”而不是“实现的细节”。实现细节放在代码注释里,那是给人看的。
2.2 原子技能和复合技能的分层思路
随着技能数量增加,我又发现了一个问题:有些技能是高度通用的,几乎所有场景都要用;另一些技能则只服务于特定流程,需要依赖多个基础能力。如果把所有技能都平铺在一起,描述列表会越来越长,模型反而不知道该选哪个。
所以我把技能分成了两层:原子技能和复合技能。原子技能是不可再分的基础能力,比如“读取文件内容”“调用搜索引擎”“发送Webhook通知”“执行SQL查询”,它们的共同点是功能单一、参数简单、复用率高。复合技能则是把若干原子技能编排成一个完整流程,比如“每日信息汇总”这个复合技能,内部就包含读取订阅源、调用搜索补全、整理摘要、发送报告四个步骤。
这个分层带来一个直接好处:Agent决策时优先看复合技能,如果复合技能的描述匹配当前任务,就直接进入复合技能的执行流程,不再挨个挑原子技能。只有找到不匹配的复合技能时,才退回原子技能层面做组合。这种“先粗后细”的决策路径,既降低了模型的决策难度,也减少了上下文占用。
2.3 目录结构与加载约定
技能库的物理形态是一组遵循约定结构的目录。我的目录设计大致长这样:
agent-skills/ ├── skills/ │ ├── fetch_url/ │ │ ├── SKILL.md │ │ ├── main.py │ │ └── requirements.txt │ ├── db_query/ │ │ ├── SKILL.md │ │ ├── main.py │ │ └── config.yaml │ └── daily_report/ │ ├── SKILL.md │ ├── main.py │ └── workflow.yaml ├── loader/ │ ├── registry.py │ ├── executor.py │ └── metadata_parser.py └── cli.py每个技能独占一个子目录,目录名就是技能标识。SKILL.md存放面向模型的描述和参数文档,main.py是技能的主执行逻辑,requirements.txt声明这个技能需要的Python依赖。加载器启动时扫描skills目录下所有一级子目录,读取SKILL.md和代码文件,把它们注册到内存中的技能注册表,Agent运行时通过注册表查找和调度技能。
这套约定的核心原则是“约定优于配置”。我不需要维护一个手工编写的技能清单,加新技能只需要新建目录、写好文件、放到skills目录下,下一次启动加载器就会自动识别。少了一个维护点,就少了一个出错的机会。
3. 从零实现一个可运行的技能加载与执行链路
3.1 初始化项目与依赖选择
实现这个技能库我选择了Python作为主力语言,原因很简单:个人Agent项目里数据处理相关的工作大部分都用Python完成,而且技能库的“胶水逻辑”比较多,Python写起来最快。项目本身依赖很少,核心只有标准库加上两个轻量级依赖:PyYAML用于解析配置文件,requests用于技能里常见的HTTP调用。
我先把基础目录结构和几个核心模块搭起来。项目初始化不算复杂,唯一要注意的是“技能执行器”和“技能加载器”这两个模块职责要划分清楚:加载器只负责扫描和注册,不负责实际执行;执行器只负责接收技能名和参数、找到技能代码、运行并返回结果。混在一起的话,后面排查问题会非常头疼。
3.2 加载器:把目录变成注册表
加载器的核心逻辑很直白:遍历skills目录,对每个子目录尝试解析SKILL.md中的元信息,然后把技能对象注册到内存字典里。这里我做了几个关键设计决定。
SKILL.md我采用了简化的格式,不是为了解析复杂,而是为了让AI生成这个文件时不容易出错。一个典型的元信息头部我用YAML来写,方便加载器用PyYAML直接解析:
name: fetch_url version: 1.0.0 description: 抓取指定网页的文本内容并清洗HTML标签,适合在Agent需要读取网页信息时使用。 parameters: - name: url type: string required: true description: 目标网页的完整地址 - name: max_length type: integer required: false default: 3000 description: 返回文本的最大长度description字段是给Agent看的,parameters字段的作用则有两层:一是给模型说明参数名称和含义,二是给执行器做基本的参数校验。加载器读入这些字段后,会在技能注册表里生成一个技能对象,对象里包含元信息、可执行文件的路径、以及技能依赖列表。
加载器代码里有一个细节很值得提:技能扫描必须做“目录名白名单”过滤。如果某个目录里没有SKILL.md或者解析失败,加载器不应该直接报错崩溃,而应该跳过这个目录并给出警告。因为技能库里技能数量多了以后,个别技能出问题不应该影响整个库的运行。
3.3 执行器:让技能真正跑起来
执行器的设计我参考了进程隔离的思路。每个技能在独立进程中执行,而不是作为模块直接导入。这样做的好处有几点:技能崩溃不会拖垮主程序,技能间的全局变量不会互相干扰,而且可以方便地设置超时限制,避免某个技能死循环导致整个Agent卡住。
执行器通过subprocess.run调用python main.py,把参数以JSON格式透传进去,技能执行完把结果以JSON格式打印到标准输出,执行器捕获后解析并返回给上层。这个协议简单,但稳定性非常高,我用这套方案跑了大量测试没出过问题。
技能内部main.py的标准写法是:读取sys.argv里的JSON参数,处理业务逻辑,把结果按约定的结构输出到stdout。我给了一套模板,让写新技能的人照着填就行:
import json import sys def main(): params = json.loads(sys.argv[1]) url = params.get("url") # 这里是技能实际的业务处理逻辑 result = {"status": "ok", "content": "some data"} print(json.dumps(result, ensure_ascii=False)) if __name__ == "__main__": main()执行器在调用前会先做参数校验,检查必填参数是否齐全,缺失就直接返回参数错误。这个校验看起来多余,实际上非常有用,能拦住大量因模型调用参数格式不对导致的执行异常。
3.4 Agent如何决策调用哪个技能
技能库本身不负责Agent的决策逻辑,它只负责“提供候选技能和描述”。在实际Agent项目里,我会把注册表中所有技能的描述拼接成一段“可用工具列表”,塞进系统提示词中。模型根据用户问题判断应该调用哪个技能,然后输出一个结构化的调用意图,解析器再把它转发给技能执行器。
举个例子,当Agent的技能描述列表里有fetch_url和db_query两个技能,用户提问“帮我查一下年度销售数据”,模型就应该倾向于选择db_query而不是fetch_url。这里有个属于“贫瘠描述导致误选”的经典问题:如果db_query的描述只写“查询数据库”而没有写“适合查询结构化的销售、财务等数据”,模型很可能因为用户问题里出现了“查”字,而把它理解成查询网页。
我的解决办法是在description里明确写“适用场景”和“不适用场景”。不适用场景这个字段是很多技能库设计里容易忽略的,但它对降低误调用率非常有效。在描述里加一句“不适用于读取网页内容,如需读取网页请使用fetch_url”,模型的决策准确率能提高不少。
3.5 技能编排与工作流执行
当复合技能收到调用请求时,执行器会把控制权交给工作流模块。工作流模块读取workflow.yaml中定义好的步骤顺序,逐步调度对应的原子技能。这个设计让“写新技能”这件事变成了“组合已有能力”,而不是“不断写新函数”。
用我上面提到的daily_report复合技能举例,workflow.yaml的编排逻辑大致是这样的:
- 第一步调用fetch_url获取指定信息源的最新内容
- 第二步调用markdown_convert把抓取到的内容转为结构化格式
- 第三步调用summarize对内容做要点提取
- 第四步调用send_webhook把最终整理好的报告推到接收端
整个过程的代码量并不大,但比硬编码要灵活得多。想调整流程,改一下YAML配置即可,不需要动Agent的代码。我在实际项目里发现,这种“配置驱动”的编排方式价值远超预期,因为它让非技术角色的同事也能通过调整配置来改变Agent的行为。
4. 实操中的高频坑与排查办法
4.1 描述写得太抽象,Agent根本不调用
我在尝试让Agent自动调用技能库时,遇到频率最高的就是“技能就在那里,你明确描述了它,但模型不用”。原因出在描述语言上。最初我把技能描述写得很技术化,比如“db_query:封装了MySQL数据库连接池的读取操作接口”,这个描述对开发者友好,对模型却不够友好。模型需要知道的是“什么时候该用我”,而不是“我的底层实现是什么”。
解决方向是重写描述,把关注的焦点从“实现”挪到“场景”。我调整后的描述是:“db_query:适合用户询问订单、库存、销售额等结构化业务数据时调用,直接回答需要查表的问题。当问题涉及网页内容时,请改用fetch_url。”这是一句典型的“为模型写描述”的句子:说得清楚触发条件,也说得清楚不触发条件。
4.2 技能之间的循环依赖和隐式调用
复合技能多了以后,出现了一个比较隐蔽的问题:技能A内部调用了技能B,技能B在某些条件下又调用了技能A。执行时如果没加深度限制,就可能变成无限循环。
我的解决办法是在工作流执行器里增加两层防护:一层是步骤深度限制,默认最多允许10层嵌套,超过即报错;另一层是“当前执行链”的路径检查,如果同一个技能在一条执行链里出现两次,直接阻断并把详细调用路径打出来。这个功能让我排查出了好几个设计不合理的复合技能,算是技能库稳定性的关键保障。
4.3 技能越来越多,上下文空间被描述占满
技能库的注册表可以承载几十上百个技能,但Agent的上下文窗口是有限的。把所有技能描述都塞进系统提示词,初期问题不大,技能超过二十个就开始明显压缩对话空间。
我目前采用的做法是对技能做分组和热启用。把技能分成“默认启用”和“按需启用”两组。默认启用的技能控制在十个以内,描述总长度控制在800字左右,保证Agent最常用到的能力都在线。剩下的技能通过一个“技能搜索”的额外技能来发现:当Agent觉得默认技能不够用时,可以调用“技能搜索”查询技能库,传入关键词,返回相关的技能描述,然后Agent决定是否加载这个技能。
这个机制相当于给技能库加了一层“延后加载”,代价是增加了决策链路的复杂度,但换来的是更高的可用性和决策准确率。我在实践中明显感觉到,模型面对的候选技能越少,选对的概率越高。
4.4 常见问题速查表
| 症状 | 可能原因 | 排查思路与解法 |
|---|---|---|
| Agent知道有这个技能但从不调用 | 描述聚焦“实现”而非“场景”,触发条件不清晰 | 重写description为“当用户需要X时使用”,增加“不适用”说明 |
| 调用技能时参数报错 | 参数名和模型输出的字段名不一致 | 统一参数命名风格为snake_case,在parameters里写清name,不要写Synonyms |
| 技能执行超时 | 子进程网络请求无超时设置 | 在main.py里为requests设置显式timeout,并在执行器中设置总超时兜底 |
| 加载新技能后没有任何变化 | 技能缓存未刷新 | 确认加载器的扫描间隔,或手动触发一次全量重新扫描 |
| 技能返回数据格式不固定 | main.py没有统一输出协议 | 统一为JSON格式,包含status、data、message三层结构 |
| 多个技能字段冲突 | 技能之间使用了相同的命名但含义不同 | 在元信息里增加namespace字段,执行时用命名空间隔离 |
这些坑在初期都真实绊倒过我。提前写进文档里,后面写新技能时照着规避,比每次都踩一遍要省太多时间。
5. 从技能库到技能生态:几个值得做的进阶方向
5.1 技能自动发现与元数据索引
技能数量超过几十个以后,“把全部描述塞给模型”的做法已经不可行,必须引入检索机制。我在第四部分提到的“技能搜索”是一个最简单的形态,它本质上就是根据关键词做文本匹配。更进一步的方案是给技能描述做向量化索引,把Agent当前的任务描述也转成向量,做相似度检索。
我实际测试过这个方向:选一个嵌入模型给技能描述生成向量,存在本地向量数据库里,Agent决策时先用当前对话信息做一次相似度查询,召回Top5相关技能,再把这五个技能的描述拼进提示词。效果确实比全量塞描述和纯文本匹配都要好,决策准确率提升明显。代价是增加了一个外部依赖,技能库不再零依赖运行,维护复杂度也上了台阶。
如果项目对延迟不敏感且技能数量还未超过三十个,我建议先不要上向量检索,保持简单的关键词匹配即可。
5.2 技能评测与回归验证
技能库面临一个普通代码库不太常见的问题:同一个技能,在模型参数量升级、提示词调整或者上下文长度变化之后,调用成功率可能明显波动。所以我在后期给技能库加了一个评测机制。
做法比较朴素:准备一组标准测试用例,覆盖每个技能常见调用和边界输入,跑完自动统计“调用成功率”和“结果正确率”。每次升级基础模型版本、改动技能描述或者调整加载逻辑后,就完整跑一遍评测,对比前后指标变化。这个机制帮我在一次模型版本升级后及时发现了db_query技能的调用成功率从97%跌到81%的问题,原因是新版模型的参数输出格式变了,但我的元信息描述还在用旧格式的字段名。没有评测机制的话,这个问题大概率要在生产环境的真实调用里才能暴露。
5.3 团队协作时的技能规范
如果你的技能库要被不止一个人使用,规范的优先级会大幅提升。我踩过不少协作相关的坑,最后沉淀下来三条约定。
命名必须有实际含义且用连字符分隔,比如fetch_url、generate_excel,禁止用a1、temp这种临时名。技能内部不要把个人色彩带进来,路径最好统一用相对路径,依赖尽可能声明在requirements.txt里。更新技能版本时要在元信息里更新version字段,并在CHANGELOG里写一句说明改了什么,避免他人使用时不知道行为已变化。
这三条约定一点都不复杂,但让多人在同一个技能库上协作变得丝滑了很多。有一次团队同事修改了技能A的返回结构,我负责的技能B因为解析不到旧字段而调用失败,就是因为版本变更没有及时同步。后来强制要求“改动必须更新version”,这类问题就基本杜绝了。
6. 写在项目之外:我对智能体技能的几点观察
做agent-skills这个项目的过程,让我对“技能”在智能体系统里的定位有了更清晰的认识。技能本质上就是把模型的能力边界变成一个可以持续扩展的“积木集合”。模型本身不会变强,但通过不断充实技能库,你的Agent能完成的事情会越来越多,越来越复杂。
我越来越觉得,技能库的设计目标不是“把所有能力塞进去”,而是“让正确的技能在正确的时机被正确的调用”。很多人在搭建技能库时容易陷入堆数量的误区,结果技能描述列表太长,模型决策质量反而下降。好的技能库应该像是一个精干的工具箱:常用工具放在最上层,特殊工具分类收纳,并且每一个工具都贴有清晰的使用标签。
从投入产出比来看,个人或小团队做Agent应用,不需要一开始就追求复杂的基础设施。先把几个核心技能跑通,把“技能定义-技能加载-技能调用”这条链路理顺,再逐步扩展数量和类型,是更务实的路径。遇到瓶颈时,再按需引入检索、评测、编排这些进阶能力。
我自己的项目在引入这套技能库之后,新Agent应用的开发时间大概缩短了四成左右,尤其是不需要再从零写基础工具函数的那些部分。省下来的时间,基本都花在了更有意思的事情上:设计更复杂的Agent流程、优化交互体验、把模型的输出打磨得更好。
最后再分享一个实用的小技巧:给技能库写README时,多用一个“快速开始”的板块,把“如何添加第一个技能、如何测试已有技能、如何排查常见报错”用尽量短的篇幅写在最前面。这个文档往往不会被开发者重视,但在团队协作或者你隔几个月回来自行维护时,它是能救命的索引。