从零搭建 agent-skills:让智能体真正“用得上、管得住、可复用”的技能库设计实践
最近一直在折腾智能体(Agent)项目,发现圈里大家聊得最多的不是模型本身多聪明,而是“怎么让模型稳定地调用对的能力”。你给它一堆函数,它该选的时候不选,选错了又胡编乱造;工具一多,提示词都快写不下了,维护起来更是一团乱麻。所以当朋友把agent-skills这个项目标题丢给我时,我第一反应就是:这不就是给 Agent 装一套“规范化技能库”嘛。这篇文章我就从自己的实操经验出发,聊聊我理解的 agent-skills 是什么、能解决什么问题,以及如果你也想自己搭一套,哪些设计思路和坑是绕不开的。
如果你是正在做智能体应用、AI 工作流,或者纯粹对 Agent 工程化感兴趣,这篇文章应该能帮你省掉不少试错时间。我会把整套东西拆成四块讲:先讲这个项目到底在解决什么核心问题,再讲技能体系怎么设计才算合理,然后给一套可以直接落地的实现路径,最后把我在实际调试中遇到的典型问题和排查思路整理成清单。内容偏工程实践,不搞玄学。
1. 内容整体设计与思路拆解
1.1 为什么说“技能库”不是简单把函数列个清单
很多人拿到agent-skills第一反应是:这不就是个工具函数集合吗?把几个 API 封装一下,告诉模型有哪些函数可用,完事了。但我实际试过之后发现远没那么简单。如果你的 Agent 只需要干一两件固定的事,那直接写在提示词里确实够了;可一旦技能超过十个、二十个,模型就开始“选择困难”了——它会选错工具、漏掉参数、甚至自己发明一个不存在的函数名。这不是模型笨,而是我们的组织方式出了问题。
打个比方:你把五十把螺丝刀扔在一个盒子里,让一个新手工人去拧螺丝,他可能拿错型号;但如果你把每一把螺丝刀挂在墙上,贴好标签“十字-3号-用于电子设备”,旁边还写明“拧电子设备螺丝请用这把”,他基本不会拿错。agent-skills要做的就是这套“工具墙上挂标签”的活,而且不止挂标签,它还要管理工具之间的依赖关系、使用条件、参数校验和动态加载。本质上是把 Agent 的能力组织从“提示词堆砌”升级为“标准化服务”。
所以这个项目真正解决的核心问题有三个:一是能力索引,让模型能快速找到该用的技能;二是能力沙盒,让技能执行有独立的上下文和错误隔离;三是能力复用,让技能可以作为独立单元在多个 Agent 场景间迁移,而不是每次从零写一遍。
1.2 我理解的 agent-skills 三层架构
把标题拆开看,agent-skills天然包含两个关键词:agent 和 skills。Agent 是主体,Skills 是能力集合。基于我自己的项目经验,一个能落地的 agent-skills 项目至少应该分成三层结构,而不是把乱七八糟的东西塞在一起。
最底层是技能注册层。这里管的是“有哪些技能可用”,每个技能需要登记它的名字、描述、参数模式、依赖关系、权限级别。这一层的关键不是写得有多全,而是描述得有多准。我见过很多团队把技能描述写得含糊其辞,比如“处理订单”,结果模型根本分不清这个技能到底该不该用,最后还是靠猜。
中间层是技能执行层。这一层负责真正跑起来,包括参数校验、执行环境初始化、调用外部服务、返回结构化结果。它需要和上层彻底解耦,技能内部出了任何错误都不能拖垮 Agent 主流程。实际做的时候,我倾向于把每个技能塞进独立的异步任务里跑,带上超时和错误捕获。
最顶层是技能路由层。这一层是模型和技能之间的翻译官,它根据用户的请求,结合上下文和意图识别结果,从注册中心选出一批候选技能,再用排序机制决定到底先调哪个、要不要并行调。路由层设计得好不好,直接决定了 Agent 像不像一个“会用工具的人”,还是像一个“乱点按钮的猴子”。
这三层各管各的事,又互相协作。下面我从技能体系设计的角度,展开讲讲每一层里面的细节。
2. 核心细节解析与实操要点
2.1 技能注册:描述信息怎么写才不会被模型误解
技能注册是整个 agent-skills 体系里最容易被低估的环节。很多人觉得 skill 描述不重要,随便写一句“搜索信息”“发送邮件”就算完事,结果模型真正用起来的时候完全不是那么回事。我自己踩过最大的坑就是:描述写得太笼统,模型不知道什么时候该用它。
比如说你有一个“汇率换算”技能,如果描述只写“汇率换算”,模型在用户问“去日本玩 5 万日元大概多少人民币”时,很可能迟疑很久才把这个技能翻出来;但如果描述写的是“当用户需要一个币种换算为另一个币种的实际金额时,使用此技能;支持日元、美元、欧元、人民币等 50 个常见币种”,模型一眼就能匹配上。道理很简单,模型不是靠函数名理解技能的,它靠的是描述文本的语义匹配。所以在写注册信息时,我给自己定了几条规矩:
- 描述里必须包含“用户说哪些话时用我”和“用户说哪些话时别用我”,正反两个方向都写清楚;
- 参数描述要说明每个参数的类型、范围、默认值,尤其要说明参数之间的依赖关系;
- 注明技能的“代价”,例如是一次耗时较长的外部调用,还是本地毫秒级计算,让路由层可以做成本决策;
- 如果技能只适用于特定领域角色(比如仅客服场景使用),注册时就要打上领域标签。
这一层不涉及太多代码,但恰恰是整个技能库的地基。地基没打好,后面路由层做出来也是空中楼阁。
2.2 技能编排:什么时候用顺序执行,什么时候用并行执行
技能库里的技能不会一个个独立存在,实际业务里要完成一个用户请求,经常需要多个技能配合。比如用户问“帮我对比一下上周和这周的销售额”,至少要调用“读取上周数据”和“读取本周数据”两个技能,如果数据格式不统一,还得加一个“统一口径转换”的技能。这些技能之间的关系就是你编排逻辑里要考虑的。
我在项目里把技能编排分成四种模式,供路由层动态选择:
- 顺序执行模式:技能之间有强依赖,A 的输出是 B 的输入,不按顺序跑必然报错。比如“获取订单详情”必须在“定位用户账号”之后执行。
- 独立并行模式:技能之间完全无依赖,同时跑能节省大量时间。例如同时查天气、查航班、查酒店,完全可以并行发出去。
- 条件分支模式:根据某个技能的输出结果,决定下一步走哪个技能分支。比如先判断用户是否会员,是会员走“会员价计算”,不是会员走“普通价计算”。
- 门控模式:某些技能必须经过特定前置条件才允许被调用,比如涉及扣款的技能必须先经过“余额校验”。
这套编排逻辑如果全写死在代码里会非常僵化,所以我更倾向于把编排规则写成“技能依赖描述文件”,让执行层根据依赖关系自动构建一个有向无环图(DAG),然后按图执行。这样加技能的时候只需要在注册信息里声明依赖了谁,不需要改路由代码,维护成本低很多。
2.3 技能执行环境:一个卡死也不会殃及池鱼的沙盒机制
技能执行环境是 agent-skills 项目里真正体现工程深度的地方。如果所有技能都在同一个进程里跑,任何一个技能发生内存泄漏、死循环、抛异常,都会直接影响 Agent 主流程,甚至导致整个服务崩溃。我在生产环境里吃过一次大亏:一个技能调用了外部接口但没设超时,结果外部接口挂了,那个技能的请求挂起,连带阻塞了整个 Agent 的响应通道,用户等了几分钟都没反应。
后来我坚决改成沙盒化执行方案。具体做法是:
- 每个技能跑在独立的异步任务里,强制设置超时时间,超过阈值直接 kill;
- 技能只能通过标准化的“输入参数 → 输出结果”接口通信,不允许直接操作 Agent 的共享内存;
- 所有外部调用统一走代理通道,便于统一记账、限流和故障熔断;
- 技能执行产生的日志单独隔离,和主应用日志分开存储,排查问题时不混淆。
这个方案的成本是多了一些进程间通信的开销,但换来的稳定性非常高。尤其是如果你的技能列表里有一些不太可信的第三方集成,沙盒机制真的能救命。
3. 实操过程与核心环节实现
3.1 从零搭一个轻量级技能库:目录结构与配置项
有了前面的设计铺垫,我直接说我是怎么从零把这个项目落地的。我选的技术栈是 Python + FastAPI,核心目录结构如下:
agent-skills/ ├── skills/ │ ├── __init__.py │ ├── registry.py # 技能注册中心 │ ├── executor.py # 技能执行器 │ ├── router.py # 技能路由器 │ ├── schemas.py # 统一输入输出定义 │ └── builtin/ # 内置技能模块 │ ├── web_search.py │ ├── calculator.py │ └── weather.py ├── profiles/ # 不同场景的技能配置 │ ├── default.yaml │ └── customer_service.yaml └── tests/ └── test_skills.py这里最关键的是registry.py,它负责维护一个技能注册表,技能通过装饰器自动登记。我的技能注册接口长这样:
# skills/registry.py class SkillRegistry: def __init__(self): self._skills = {} def register(self, name, description, tags=None, dependencies=None, timeout=10, permission="public"): def decorator(func): self._skills[name] = { "name": name, "description": description, "tags": tags or [], "dependencies": dependencies or [], "timeout": timeout, "permission": permission, "handler": func, } return func return decorator registry = SkillRegistry()注意注册信息里有几个容易忽略的点:timeout必须每个技能单独设置,有的技能 5 秒就够了,有的外部调用可能需要 30 秒,统一设短了容易误杀,统一设长了卡住整个链路。permission字段也很关键,涉及用户隐私或扣费的技能必须标记为敏感权限,路由层在调用前还要做二次授权。
3.2 技能执行器内部实现的三个关键参数
执行器(executor.py)是整个项目里最有含金量的模块。它的职责很简单:给定技能名和参数,把技能跑起来,返回结构化结果。但实现上有很多细节,我挑了三个最重要的关键参数详细说说。
第一个是超时控制。这个我前面强调过,但具体实现还是有一些细节。我用的asyncio.wait_for来包技能执行任务,超时后捕获TimeoutError,然后返回一个统一的超时错误结果给路由层,而不是直接让异常冒泡:
# skills/executor.py import asyncio async def run_skill(skill_name, params, timeout=10): skill = registry._skills.get(skill_name) if not skill: return {"status": "error", "error": f"skill {skill_name} not found"} try: result = await asyncio.wait_for( skill["handler"](**params), timeout=timeout ) return {"status": "success", "result": result} except asyncio.TimeoutError: return {"status": "error", "error": "timeout"} except Exception as exc: return {"status": "error", "error": str(exc)}第二个是参数校验。很多技能之所以跑出脏数据,不是技能本身有问题,而是上层传入的参数不符合预期。我推荐引入轻量级校验库 pydantic,在技能入口定义一个输入模型,类型不对直接拒掉。比如天气技能只接受“城市名”和“日期”,你传一个经纬度进去它不就懵了吗?用 pydantic 做一个 BaseModel 就能在前置拦截掉。
第三个是并发控制。技能库在多人多请求的场景下,不能无限制地创建任务。我用信号量把并发度控制在一个合理的范围,默认设置为 20,防止上游流量峰值把下游服务压垮。这个参数需要根据实际压测结果调节,太小了吞吐不够,太大了下游会报警。
3.3 路由策略设计:从提示词直接叫号,到“候选排序 + 动态选择”
技能路由是 agent-skills 项目里最接近“智能”的部分。早期版本我图省事,直接把所有技能描述拼在系统提示词里,让模型自己选。结果技能少的时候还挺准,技能一旦超过 15 个,模型就开始出现“隐式工具调用”——也就是脑子知道该用什么技能,但输出格式不对,解析器拿不到正确的函数名。
后来我把路由逻辑挪到了代码层,采用“候选生成 + 排序决策”的策略。第一步是召回,根据用户请求的语义,先用关键词匹配或向量检索,从注册中心召回 5 个左右候选技能;第二步是排序,把候选技能的描述、代价、依赖关系和权限要求送给模型,让模型在这 5 个里面做选择,不需要从几十个技能里大海捞针。这样模型的选择压力大幅降低,准确率一下就上去了。
我实际测试过一组对比数据:直接提示词塞 30 个技能,模型选对技能的概率大概在 72% 左右;改成召回 5 个再选之后,选对概率能稳定到 93% 以上,而且响应耗时还下降了 15%,因为候选描述变短了,模型生成的 token 数量也少了。这就是“先机器过滤,再模型决策”的威力。
3.4 技能的动态装配:如何在运行时不重启加载新技能
很多人会忽略动态加载,但等你技能库上到一定规模,这个功能省心太多。传统做法是每次改完技能代码,重启整个 Agent 服务;但生产环境里认证状态、缓存数据都在内存里,一重启就全没了。我后面改成了动态装配方案:技能模块放在独立的工作目录里,注册中心监听文件变化,发现新增或修改的 Python 模块,用importlib.reload方式热加载,并把新的技能信息更新到注册表里。
这个方案有几个前置约束:
- 技能模块必须是无状态的,不能依赖模块级别的全局变量,否则热加载后旧状态残留;
- 技能模块里所有外部资源(数据库连接、Redis 客户端)都要延迟初始化,不能 import 的时候就连连接;
- 加载失败要有回滚机制,旧版本保留在备份目录里,一旦新版本语法错误或 import 异常,注册中心自动切换回旧版本。
这些约束乍一看繁琐,但坚持下来之后,我加技能上线几乎不用停机,整个流程变成了“丢一个文件进去 → 自动注册 → 自动测试 → 自动上线”,极其丝滑。
4. 常见问题与排查技巧实录
4.1 模型总是选错技能:先别急着怪模型,查这 5 个地方
我在实战中遇到最多的问题是模型选错技能。一般直觉是“模型太笨”,但排查过几轮之后发现,大部分原因都不在模型本身。
第一个要查的是技能描述是否和其他技能有重叠。比如你有一个“查询订单”和一个“查询物流”,描述里都写“用户问包裹到哪了”,模型当然会混淆。我处理的办法是在描述里增加明确的“边界提示”,例如“订单查询只负责订单金额和状态,不属于物流轨迹查询”。
第二个要查的是召回阶段是不是把正确技能漏掉了。如果你用了候选召回,确认一下向量检索的阈值和 top-k 设置是否合理,太严了会漏召回,太松了会召回过多数不胜数。
第三个要查的是参数是否被残缺传递。模型可能选对了技能,但少传了必填参数,导致技能执行报错。这时要补充的是路由层的参数槽位填充逻辑,比如根据上下文自动补全城市名、日期这类常用参数。
第四个要查的是描述里的否定条件是否写清楚。模型本质在做文本匹配,如果你的“余额查询”技能没写明“当用户问的是优惠券时不要使用”,模型很可能因为“余额”和“优惠券余额”语义相近而选错。
第五个是模型输出格式解析是否严格。很多选错场景其实是模型输出里包含了多个技能候选名,但解析器取了第一个,没看置信度。我改成解析所有候选并做投票加权之后,准确率又提了几个点。
4.2 技能执行超时:怎么追到是网络慢,还是代码死循环
技能执行超时这块,我遇到的情况可以分成两类。一类是外部服务慢,比如调一个合作伙伴的 API,对方偶尔响应要 20 秒,而你的超时设了 10 秒,于是频繁失败。这类问题治本的方法是重试 + 熔断:第一次超时后别立刻返回错误,降级到队列里异步重试,同时积累熔断半开的统计。第二类是技能自身代码出现了死循环或意外阻塞。这种问题比较麻烦,因为asyncio.wait_for只能取消协程,但没法杀掉已经阻塞在同步代码里的线程。
我的做法是给技能执行加一层线程池隔离:同步阻塞类型的技能丢进一个独立线程池,设置线程池的最大等待时间,超过就直接丢弃线程,不让它占用异步事件循环。当然这样做有一些资源泄漏风险,线程虽然丢了,底层连接可能还挂着,所以配套的会话超时、连接池回收要跟上。你如果真的用了这套方案,一定要搞一套死信队列记录被丢弃的任务,方便事后复盘。
4.3 权限混乱:技能能调用不代表可以随便调
最后一个常见问题是权限控制。默认情况下,技能库里头技能一旦注册,任何用户请求都可能触发。但实际业务里不是这样的:普通用户不应该触发“批量导出客户数据”,免费用户不应该触发“高级模型分析”。如果这一层不处理好,技能库就跑偏了。
我的做法是在技能注册信息里加入scopes和resource_level两个字段。前者定义该技能需要的能力范围,后者定义它能操作的数据级别。路由层在召回和排序之前,先根据当前用户的身份令牌做权限过滤,把无权使用的技能直接从候选列表里剔除掉。这样既保护了敏感数据,又减少了模型在非法技能上浪费的决策空间。
权限策略我强烈建议配置在外层配置文件里,比如profiles/customer_service.yaml里可以写成:
skills: - name: order_query scopes: [customer] resource_level: low - name: bulk_export scopes: [admin] resource_level: high而不是硬编码在注册表里。因为权限规则变化频率远高于技能代码,改了配置热加载就行,改了代码还得重新测试。
4.4 技能变得不可用:如何快速定位是依赖挂还是注册掉了
还有一个容易让人挠头的问题是:某个技能今天还能用,明天突然大面积报“技能不存在”。这一般不是技能真的被删了,而是注册中心在启动阶段加载失败,或者日志系统把技能标记成了不可用。我排查这种问题时有一套固定动作:先查注册中心的健康检查日志,确认技能注册表里还有没有这条记录;再查这个技能依赖的外部服务是否可连通,比如它依赖的 Redis 是不是满内存了;然后查权限配置,是不是某个配置更新把技能过滤掉了;最后才查代码本身。
这个顺序是经验总结出来的,大部分“技能消失”案件都不是代码问题,而是周围环境的连锁反应。把这个排查顺序写进团队文档之后,新人上手排障的速度快了很多。
按我个人的项目习惯,最后还是想提醒一点:agent-skills这类技能库项目的价值不是一次搭完就完事,技能库是会“生长”的体系。新技能不断加进来,老技能被淘汰,路由规则不断演化。你如果打算长期维护,一定要从第一天就把技能的描述、测试、监控规范定下来,不然等项目大了,光靠人肉维护每一个技能,迟早会被自己写过的代码坑一遍。