做AI Agent应用开发这段时间我踩得最深的坑就是Agent表面上的能力边界很大一旦落到具体场景里连查个数据再算个结果这种基础操作都经常翻车。后来我把思路从拼命塞提示词转向给Agent一套可复用的技能体系也就是围绕agent-skills这套方案把所有可执行能力拆成标准化的技能单元统一注册、统一调度、统一维护。实际跑下来效果比之前好了不止一个量级。这篇文章就把这套技能库的设计思路、核心实现、踩坑实录完整展开适合正在做Agent落地、搞工具调用、纠结提示词工程边界的人参考。不管你是用开源框架改造自己的项目还是想在现有Agent上补齐能力层都会用得上。1. 先搞清楚一件事agent-skills解决的是什么问题1.1 我的技能认知是怎么被刷新了一遍我之前写过一个挺复杂的Agent把所有业务逻辑都塞进系统提示词里。开始看着很灵活要什么功能就往提示词里加一段描述。结果等场景多起来提示词膨胀到几千字之后模型开始出现明显的注意力漂移用户问的事情稍微拐个弯模型就去调用无关的能力模块甚至自己瞎编一个不存在的接口。那时候我才意识到Agent的应用层不应该依赖模型从一大段文字里去领悟自己有什么能力。更靠谱的做法是把每个能力封装成一个独立、自描述的技能单元让模型像一个工具箱前的人那样先看工具标签再决定拿哪个工具。这就是agent-skills这个项目名的由来它不是某个单一功能而是一整套关于Agent技能的定义、注册、调度、维护的实践方案。这套思路的转变有一个关键前提现在的模型接口本身已经支持严格的结构化输出和函数调用能力。我们要做的不是教模型怎么干活而是给模型提供一份足够清晰、足够规范的能力清单。技能库本质上就是这份清单的结构化实现让Agent既能用好自己的推理能力又能稳定地调用外部系统。1.2 技能Skill和工具Tool的分界线很多文章把技能和工具混着说实际用起来完全是两码事。工具是Agent可以发起的一个具体外部调用比如搜索发邮件调数据库而技能则是围绕一个完整任务目标组织起来的能力单元它可能包含多个步骤、依赖多个底层工具、执行一段业务逻辑最后返回一个结构化结果。举个例子。一个查天气的技能底层要调用天气API这算工具但这个技能真正的价值在于它知道用户问明天出门要不要带伞时应该先把问题拆成城市、日期两个参数去调API拿到结果之后还要解读成建议带伞这种可读结论。这个拆参、调用、解读的完整过程才是技能。agent-skills的设计核心就在这里技能是工具之上的编排层而且是可声明、可复用、可测试的编排层。每个技能用一个描述文件定义清楚自己是谁、在什么场景下触发、需要什么参数、返回什么结果。Agent运行的时候只做一件事根据用户当前的输入从注册表里挑出最匹配的技能按定义好的参数契约去执行。我后面所有章节都围绕这个原则展开。2. 技能体系设计思路和关键选择2.1 技能拆分的颗粒度怎么定整个技能库能不能好用第一关是拆分颗粒度。拆得太粗一个技能里塞了七八种意图描述怎么写都覆盖不全模型经常误判拆得太细技能数量爆炸模型做意图匹配时选择太多命中率也会下降。我最后定的判断标准其实很简单一个技能只回答一类问题这类问题在用户表达里有两个以上的常见问法。比如查天气查汇率查股票各自独立是三个技能但换算单位这一个技能可以覆盖长度、重量、温度等子类型因为从意图角度看用户都在做数值换算这一件事只是单位体系不同。还有一条更重要的经验技能的粒度要和参数契约的稳定度挂钩。如果一个技能在不同调用场景下参数结构经常要变说明它还没拆到位。比如获取订单信息这个技能如果既要支持我上笔订单到哪了又要支持导出本季度所有订单这两件事的参数和返回结构差异太大硬放一起会导致参数校验逻辑极其别扭。拆成订单详情查询和订单批量导出两个技能各自清爽模型也更容易触发。2.2 描述文件的写法决定触发率技能描述文件是整个技能库质量的分水岭。同样一个查天气能力描述只写天气查询四个字和写清楚当用户询问任意城市当前或未来几天的天气情况、气温、降水概率、空气质量时使用本技能相比触发率能差出一大截。我实测过多次描述里是否给出了具体的触发场景举例是影响模型意图路由准确性的最大因素之一。我在agent-skills里统一规定每个技能的描述文件必须包含以下字段技能名称、一句话总结、何时使用when_to_use、何时不使用when_not_to_use、参数定义、返回值说明。其中何时使用和何时不使用这两块是多数人最容易忽视的地方。何时不使用尤其关键。模型在做意图匹配时如果没有反向排除条件很容易把相似场景全部归到一个技能下。比如你有一个产品推荐技能不写用户要求查看订单物流时不使用本技能模型就可能在物流查询场景里也触发推荐技能。这种反向约束写的时候多花一分钟线上能少挨好几次骂。2.3 参数契约是技能的生命线技能描述决定模型能不能找到正确的技能参数定义决定模型能不能把正确的参数传给技能。这两步哪个出错都没法用。我在参数设计上吃了不少亏总结出几条硬规则。第一每个参数都必须写清楚它的业务含义而不是只标类型。比如city这个参数描述写成城市名字符串远远不够要写成城市中文全称例如北京、上海、广州优先使用输入中的原词如无明确城市则不允许猜测填写。这种描述看着啰嗦但它能挡住模型瞎填参数。第二尽量用枚举值和默认值压缩参数空间。能枚举的不要让模型自由发挥天气单位就填摄氏/华氏币种就用标准代码表。可以不传的参数一定要给默认值不然模型不知道该填什么反而传一个错误值进来。第三参数结构里要区分业务必填和接口必填。有些参数技能内部可以自己推导就不该让模型传而用户没有明说但业务逻辑需要的参数技能内部要用合理策略去兜底不能在调度层就把任务拒掉。3. 从零搭建一个能跑的技能库3.1 目录结构和数据格式动手写代码之前先把技能库的目录规整好。我的做法是每个技能一个目录目录名就是技能标识里面放两个文件SKILL.md用来描述技能元信息skill.py或者skill.js用来放真正的执行逻辑。skills/ ├── weather_query/ │ ├── SKILL.md │ └── skill.py ├── currency_convert/ │ ├── SKILL.md │ └── skill.py └── unit_convert/ ├── SKILL.md └── skill.pySKILL.md我统一用YAML格式因为它可读性好又能方便地被程序解析。核心结构如下name: weather_query description: 查询指定城市当前或未来数日的天气信息包括温度、湿度、风力、降水概率和空气质量 when_to_use: - 用户询问天气、气温、是否会下雨、是否需要带伞、空气质量等情况时使用 when_not_to_use: - 用户询问历史天气统计报表、气候带划分、天气预报视频解说时不使用 parameters: type: object properties: city: type: string description: 城市中文全称例如北京、上海用户未指定城市时使用定位默认值 days: type: integer description: 预报天数1到5之间默认1 required: - city这套格式有两点好处。一是纯声明式的元信息可以被统一读取后续想接入调度框架、做技能发现、生成模型可读的技能清单都是同一套数据的复用二是技能新增不需要改任何其他模块的代码丢一个目录进去注册逻辑自动发现。3.2 技能注册与动态加载注册模块要解决的节点问题是怎么把技能目录变成运行时模型真正能用的函数列表。我的实现里用了一个统一注册表和一个动态加载器。注册表就是一个字典结构键是技能名值是技能元信息加执行函数引用。动态加载器做的事情是遍历skills目录逐个读取SKILL.md解析YAML再把skill.py导入进来绑定到一起。这样新增技能不需要改动调度器任何代码。import yaml import importlib.util from pathlib import Path SKILLS_DIR Path(skills) def load_skills(): registry {} for skill_dir in SKILLS_DIR.iterdir(): if not (skill_dir / SKILL.md).exists(): continue meta yaml.safe_load((skill_dir / SKILL.md).read_text()) spec importlib.util.spec_from_file_location( f{skill_dir.name}_impl, skill_dir / skill.py ) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) registry[meta[name]] { meta: meta, execute: module.execute, } return registry动态加载的好处是开发体验极好技能做到写一个跑一个不用反复重启整个服务。但它也有一个副作用就是后面要讲的缓存问题。这一小节代码只是注册骨架真正让这套体系跑起来的是下面要展开的意图匹配和调度逻辑。3.3 意图匹配与调度的完整逻辑技能加载完了核心问题变成用户输入一句自然语言怎么路由到正确的技能并抽出正确参数。现在主流做法是把注册表里的技能描述拼接成一段模型可见的工具清单让模型基于用户的当前输入去选择调用哪个技能。我直接用的这种结构化的工具调用方式把每个技能的元信息转换成模型接口要求的工具声明格式。tools_for_model [] for skill_name, skill in registry.items(): tools_for_model.append({ type: function, function: { name: skill_name, description: build_description(skill[meta]), parameters: skill[meta][parameters], } })这里有一个值得注意的细节给模型用的description并不仅仅是把SKILL.md里的description字段拷过去而是要把when_to_use和when_not_to_use一并写进去。我自己构造描述文本的时候会生成下面这种格式实测下来比单句描述的效果稳得多。当{ when_to_use }时使用本技能。注意当{ when_not_to_use }时不要使用本技能。调度层拿到模型的输出之后先判断模型是否选择了某个技能如果模型没有选任何技能说明用户的需求不在技能覆盖范围内这时候就走兜底逻辑——直接告诉模型技能库里没有能力处理不要硬编一个不存在的调用。如果模型选了技能下一步是参数校验。参数校验这一环极其重要。我见过很多线上事故都是模型传了个类型不对或者缺字段的参数技能内部没有做防御直接崩了。所以调度器在执行之前必须做一次schema校验不合格的请求要能自动重试一次让模型重新输出合理的参数组合。3.4 多技能协同时的上下文共享单个技能调用是入门级玩法真正的复杂场景是多个技能连续配合。比如用户问对比一下北京和上海今天的天气如果上海更适合出行就帮我订一张高铁票这里涉及天气查询和交通出行两个技能而且第二个技能的执行依赖于第一个技能的输出。agent-skills在这块的处理方式是维护一个技能间的上下文缓冲。具体做法是技能执行之后除了把最终结果返回给模型还把经过脱敏处理的关键中间结果写入一个上下文对象后续技能可以通过统一接口读取之前步骤的结果。这不等于让技能之间直接耦合而是把数据交换放在一个明确声明的层面上。context SkillContext() result_weather execute_skill(weather_query, {city: 上海}) context.set(weather_shanghai, result_weather) result_ticket execute_skill(train_ticket, { departure: 北京, destination: 上海, })这种编排方式设计时要注意一个问题技能之间共享的数据必须精简不要无脑把上一步的完整输出全部塞进上下文。数据越复杂下一步的参数提取准确率越差。我的经验是每个技能只往共享上下文里写入业务关键字段比如城市、日期、金额、数量之类。调用的链路长了之后这个习惯会帮你省掉大量排查问题的时间。4. 实录四个最典型的坑和排查过程4.1 模型死活不调技能描述歧义问题有一次我上了个文本翻译技能结果用户无论怎么问模型就是不触发它反而去走兜底逻辑直接用模型自身的能力做翻译。我一开始以为是模型不支持工具调用排查了半天。后来发现问题是技能描述里写了当用户需要将文本转换为其他语言时使用这个转换被我另一个单位换算技能的描述处理任何需要转换的场景给覆盖了。这就是描述交集冲突的典型例子。两个技能的描述存在语义重叠模型在路由时倾向选择语义范围更宽的那一个导致精确技能被冷落。修复方式很直接把两个技能各自的何时不使用写清楚翻译技能里加用户不涉及数值、单位、货币等度量标准转换时不使用换算技能里加不处理自然语言文本翻译。这类问题的排查思路永远是先把所有技能的描述一次性拉出来逐条找语义交集。4.2 参数传错缺省值设计问题另一个高频事故是模型把用户一句帮我看看明天天气里的明天解析成了字符串明天直接传给days参数。天气接口收到一个非法值整个调用链就断了。这类问题本质上不是模型笨而是参数定义没有给模型足够的诱导。后来我把days参数的定义改成这样描述里写明当用户提到明天、后天等相对时间时你需要先按当前日期计算出具体天数再填入数值不要填写明天这类文字。同时在参数schema里规定days的取值范围是1到5的整数超出范围模型就会自动修正。改完之后这个参数的错误率基本清零。参数定义这件事目标就是让模型想填错都难靠枚举、范围、默认值把自由发挥空间压缩到最小。4.3 技能打架优先级和路由冲突技能库规模到了一定程度必定会遇到多个技能同时匹配用户意图的情况。最典型的是查快递和查订单用户说我的快递到哪了和我的订单怎么样了模型经常分不清楚要么两个技能都不触发要么触发错了。我先试过在描述里加更细致的正反例效果有提升但不彻底。后来在agent-skills里加了优先级机制当模型给出的技能调用候选里有多个相似技能时调度器按技能相关性得分优先级加权来排序。优先级是人工配置的业务上更精确、更细分的技能优先级更高。这个机制上线后再遇到模糊意图至少不会选到完全无关的技能。4.4 改了技能不生效缓存与版本问题这算是我踩得最隐蔽的坑。用动态加载方式做技能库本地改完skill.py测试环境怎么测都是旧逻辑。一开始我以为是部署没生效后来发现是运行时的import缓存机制在作祟。技能模块第一次被导入后第二次再执行同样的import不会重新加载文件。这个问题有两个方向的解法。简单粗暴的方案是给技能模块文件加内容hash文件变化时强制从磁盘重新加载模块对象另一个方案是放弃动态加载改成每次发布时重启执行进程。我实际用的是前者在加载器里维护一个hash表做技能热更新。这算是一个典型的看起来能跑实则藏雷的地方越早想清楚越舒服。5. 技能库的评测与维护5.1 一套低成本可落地的评测流程技能库建起来之后最需要的是建立评测闭环。没有评测你永远不知道一次描述文件的改动到底是变好了还是变糟了。我的做法是维护一个评测集包含每个技能对应的多种用户说法至少覆盖正常表达、模糊表达、边缘表达三类。评测时自动化执行全套流程输入用户query让调度器给出技能选择结果和参数抽取结果再和预先标注的正确答案做对比。核心看两个指标意图命中率也就是选对了哪个技能参数完整率也就是必填参数有没有抽取齐全、类型是否正确。def evaluate(test_cases): hit_count 0 param_accuracy 0 for case in test_cases: route dispatch(case[query]) if route.skill_name case[expected_skill]: hit_count 1 param_accuracy compare_params(route.params, case[expected_params]) print( round(hit_count / len(test_cases), 2), round(param_accuracy / len(test_cases), 2) )这套评测跑一轮只需要几分钟成本很低但价值极大。每次改动技能描述或参数结构先跑一遍全量评测再决定要不要上线这是整个agent-skills实践里性价比最高的一步。5.2 后续还能怎么扩展技能库的方案演化空间很大。沿着我现在的方向至少有三个明确分支值得继续做。一是技能依赖图管理把技能之间的调用关系显式声明出来让调度器能自动编排多步流程而不是靠上下文手工拼二是技能质量的自动化评估把线上真实请求的日志回流到评测集里持续发现描述盲区三是把技能从函数执行升级为策略包每个技能里除了执行函数还能带上重试策略、降级策略、权限校验让Agent在复杂环境里更鲁棒。这些扩展不一定要一次全做选一个方向先落地就比空想强得多。我自己下一步会优先做技能日志回流因为只有数据才能暴露真实场景里那些写例子时想不到的边角情况。最后分享一点真实的体会技能库这个方案真正的门槛不在写代码而在整理边界。每定义一个技能都要想清楚它在什么场景下能做什么、不该做什么、参数可以是什么、不可以是什么。这些边界打磨得越精细Agent表现就越稳定。如果你正在做Agent能力层建议从小处入手先挑三四个高频场景固化成技能把评测闭环跑通再逐步扩展。技能数量的增长一定要服务于场景不是为了显得系统庞大而堆出来的摆设。