
1. 技能系统被大多数 Agent 项目低估的核心模块做 Agent 开发这几年我见过太多项目把精力花在模型选型、Prompt 调优上却在技能系统这一层仓促应付。结果就是看起来什么都能聊真正干活时什么都干不利索。今天想聊的agent-skills说白了就是解决这个问题的——它把 Agent 的能力拆解成可复用、可编排、可热插拔的技能单元让 Agent 从会说话进化到会做事。这个概念本身并不复杂。你可以把 Agent 想象成一个新入职的实习生脑子聪明大模型但什么业务都不熟悉。技能系统就是你给他的一本操作手册里面写清楚了遇到报销流程该怎么走客户投诉怎么分类数据报表去哪里拉。没有这本手册再聪明的实习生也得抓瞎有了它他才能按照标准化流程把活干漂亮。我后面会用一个真实项目来拆解agent-skills的设计与落地从技能定义、注册机制、调用编排到多技能协作和问题排查全程带着代码和配置讲。如果你正在做 Agent 应用开发或者想把现有的机器人、工作流升级成真正能自主完成任务的智能体这篇内容应该能给你一套可以直接抄作业的参考方案。2. 先把底层的设计逻辑搞清楚2.1 skill 到底是什么在进入实现之前我建议你先想清楚一件事skill技能在你的系统里到底占据什么位置。我见过不少团队的写法把技能等同于一段 Prompt交给模型让它自由发挥。这种做法不是不行但它有一个致命问题——不可控。agent-skills的核心理念是把技能定义为一个可被 Agent 感知、按约定调用、并且有明确输入输出边界的能力单元。它至少应该包含三层信息技能说明这段技能是干什么的解决什么问题在什么场景下适用调用接口技能需要什么参数返回什么结构出错时怎么反馈执行逻辑可以是代码函数、API 封装、脚本命令也可以是引导模型完成的子任务流程。我用一个生活化的例子帮你理解这三层你把一项技能想象成餐厅里的一道菜。菜单上的描述说明让客人知道这道菜是什么厨房下单时需要填写辣度、忌口调用接口后厨按照标准工序做出来执行逻辑。缺了任何一层这道菜都很难稳定复现。2.2 为什么技能要可插拔提到可插拔很多人的第一反应是方便扩展。但真正用过之后我发现可插拔的价值远不止于此。至少有三点在实际项目里非常关键隔离故障。某个技能出了问题不会拖垮整个 Agent。技能 A 调用的第三方 API 挂了技能 B、C、D 照常工作用户不会因为一个环节失败就什么都干不了。独立迭代。技能可以单独开发、单独测试、单独上线。团队里不同的人负责不同技能互不阻塞。这个优势在并行开发时尤其明显。权限控制。不同用户可以加载不同技能集。给普通用户开放基础查询技能给管理员开放配置类技能比在一个大模型系统里做细粒度权限要容易得多。我最初把一个技能写死在 Agent 的主流程里后来不断加需求代码越来越臃肿每次改动都要重新测试整条链路。后来才下决心重构把技能全部做成独立模块。这个过程让我意识到刚开始多花两天设计后面能省两个月的时间。3. 核心细节技能定义、注册与调度3.1 技能描述文件怎么写才不会被模型误解技能描述是整个系统里最容易被低估的部分。很多人觉得模型很聪明描述随便写写就行实际上模型的调用准确率高度依赖技能说明的质量。我踩过不少坑最后总结出一套相对稳妥的写法。先说结构。我给每个技能写一个 YAML 描述文件字段包括name、description、parameters、returns、examples。其中description是最关键的它决定了模型在什么场景下会想到调用这个技能。经验是描述不要写概念要写触发场景。比如name: get_weather description: 当用户询问某个城市的天气、温度、降雨概率或者想要了解出行是否需要带伞、 是否需要加衣服时使用这个技能。不要用于查询历史气候数据或预报未来一周 之外的信息。包含城市名称时直接传入用户只模糊提到这边时需要先通过 定位技能获取城市名再调用。 parameters: - name: city type: string required: true description: 城市中文名称如北京上海若用户只说了区县需要先补全省市这种写法在描述里把什么时候该用“什么时候不该用”“参数怎么补全”都说清楚模型就不容易误判。另外我建议在examples里放一到两个真实对话示例格式类似用户说→调用什么技能→传什么参数模型在推理时会参考这些示例准确率提升非常明显。3.2 注册中心让 Agent 知道自己会什么有了技能模块还需要一个地方让 Agent 知道我自己会什么。不同框架对这个模块的称呼不一样有的叫 Tool Registry有的叫 Skill Manager本质是一样的——注册中心。我会在 Agent 启动时扫描指定目录下的所有技能描述文件解析后生成一份清单。这份清单包含两个用途一是给模型看的把所有技能的描述拼接成一个总说明在每次对话时注入 System Prompt二是给调度器用的维护一个内存索引方便按名称快速定位到具体的执行函数。# 注册中心的核心逻辑示意 class SkillRegistry: def __init__(self): self._skills {} def register(self, skill: BaseSkill): self._skills[skill.name] skill def list_skill_descriptions(self): return \n.join( f[{s.name}] {s.description} for s in self._skills.values() ) def get(self, name: str) - BaseSkill: return self._skills.get(name)这里有一个细节容易忽略当技能数量超过 20 个之后把所有描述都塞进 Prompt 会占用大量 token而且模型反而开始混淆。我常用的做法是再加一层粗筛机制——先用一次轻量分类把用户意图粗分成几大类只把对应类别下的技能描述注入 Prompt。比如用户问天气就只注入天气、定位相关的技能描述而不是把报销、报表、邮件全都塞进去。这个优化不复杂但能显著提升模型在技能选择上的准确性。3.3 调度器它决定模型怎么选调度环节是技能系统的大脑。模型在对话中如何决定现在该调用哪个技能我的经验是使用 function calling 机制把技能描述转换成 JSON Schema 格式传给模型让模型输出结构化的调用请求而不是自然语言。以 OpenAI 格式为例{ name: get_weather, parameters: { type: object, properties: { city: { type: string } }, required: [city] } }这套机制的优点是模型天生就训练过如何输出这种结构化格式不需要你做太多事。但真实场景中模型的输出不一定完全合法比如参数缺失、类型错误、甚至编造了一个不存在的技能名。所以调度器里我坚持放三层校验Schema 校验参数是否符合定义必填项是否齐全技能存在性校验模型输出的技能名必须在注册中心里找得到业务前置校验比如用户权限是否足够、技能是否在维护状态、当前环境是否满足执行条件。这三层校验做完再把参数传给技能执行函数。执行结果返回后会作为上下文交给模型生成最终回复。4. 从零搭一套技能完整实操记录4.1 定义两个基础技能我直接用一个例子带你走通全流程。假设我们要做一个出行助手 Agent第一版需要两个技能一个查天气get_weather一个查航班get_flight。先写天气技能的代码骨架class BaseSkill: name: str description: str parameters: list [] async def execute(self, **kwargs): raise NotImplementedError class GetWeatherSkill(BaseSkill): name get_weather description ... parameters [...] async def execute(self, city: str): # 这里去调用真实天气服务 weather_data await fetch_weather(city) return { city: city, temperature: weather_data[temp], condition: weather_data[condition], }航班技能类似只是参数多一些。我在BaseSkill里尽量把公共逻辑收敛干净参数校验、日志埋点、异常捕获都在基类里做好子类只需要关心自己真正的业务逻辑。4.2 把技能注册进 Agent接下来把这个技能模块接入 Agent 主流程。为了演示方便我用的是 FastAPI 一个简单的 Agent 循环核心逻辑如下from agent import Agent, skill_registry # 注册技能 skill_registry.register(GetWeatherSkill()) skill_registry.register(GetFlightSkill()) # 初始化 Agent agent Agent( modelgpt-4o-mini, skillsskill_registry, system_prompt你是一个出行助手帮助用户查询天气和航班信息。 ) # 用户消息进来Agent 内部会完成意图理解、技能调用、结果汇总 response await agent.run(明天北京到上海有航班吗)Agent 内部的主循环大致是拿到用户消息 → 让模型判断是否需要调用技能 → 如果调用解析参数 → 调度执行 → 拿到结果再交给模型组织语言 → 返回最终回复。这个过程说起来简单实际跑的时候最常出问题的就是模型明明该调用技能却不调用和模型调用了技能但参数传错——这两种情况我在后面第 6 章专门讲。4.3 用配置驱动技能加载技能少的时候手动注册没问题但是技能一多人工注册容易漏、容易乱。我的做法是做一个基于目录扫描的加载器规定每个技能一个文件夹里面必须有skill.py和skill.yaml启动时自动发现并注册。skills/ ├── get_weather/ │ ├── skill.yaml │ ├── skill.py │ └── __init__.py ├── get_flight/ │ ├── skill.yaml │ ├── skill.py │ └── __init__.py加载器扫描目录、解析 YAML、动态导入模块把 skill 实例注册进中心。这样新增技能只需要新建一个文件夹不需要改任何主程序代码。这个模式推广到团队合作时非常省心每个同学只需要在自己负责的目录里开发互不影响。5. 多技能协作顺序编排与结果融合5.1 技能之间怎么组合单个技能的用法相对直接但实际业务里更多是多个技能配合完成一个复杂任务。还是拿出行助手举例用户问明天下午从杭州去深圳穿什么合适——这个问题表面上是问穿衣建议但实际上要先把航班查出来确认到达深圳的时间再查深圳明天的天气最后结合天气生成穿衣建议。这背后涉及三种协作模式顺序依赖技能 B 的输入依赖技能 A 的输出。比如先查航班拿到达时间再查天气。并行独立多个技能互不依赖可以同时执行节约时间。比如查航班和查酒店。条件分支根据某个技能的结果决定是否执行另一个技能。比如如果天气是雨天就再调用一个推荐室内活动的技能。我在系统里实现了一个轻量级的任务规划器它接收用户目标和可用技能列表让大模型先规划一个 DAG有向无环图形式的执行计划再按拓扑顺序执行。{ tasks: [ { skill: get_flight, params: {from: 杭州, to: 深圳}, next: [task-2] }, { skill: get_weather, params: {city: 深圳}, next: [] } ] }这里有一个很关键的点规划器自己也需要提示词来约束输出格式否则模型容易自由发挥。我给规划器的指令里明确规定了输出必须是 JSON 数组每一项必须包含skill和params不允许出现额外的说明文字。5.2 结果如何拼装多技能任务处理完之后还有一个结果融合的问题。简单的做法是把所有技能的结果塞进上下文让模型总结但在技能结果很多时这样会导致上下文过长而且模型容易漏掉细节。我的做法是把每一步执行结果先做局部处理比如天气返回的是结构化 JSON先用模板转成一句话深圳明天最高气温 28 度有阵雨航班返回多条结果就只挑最早和最晚两个班次摘要。处理完的结果以标准格式回传再交给模型组织成最终回复。这样既保留了关键信息又大幅减少上下文长度。5.3 把编排做成可观测的多技能协作排错时最烦的就是不知道执行到哪一步、卡在哪一步。我强烈建议从第一天就给编排系统加上完整日志每次调用技能之前记录输入参数执行之后记录返回结果和耗时技能执行异常时记录异常类型和堆栈模型规划出的任务 DAG 先落日志再执行。用 Python 的logging模块就可以实现不用引入额外框架。日志级别控制在 INFO 就能覆盖日常排错需求。这些日志不仅帮你定位问题还能沉淀下来做数据分析——比如发现天气技能调用失败率特别高就该去查对应 API。6. 实战中那些绕不开的坑6.1 模型死活不调用技能这是最常遇到的问题。排查思路不要一上来就调 Prompt先按顺序做三件事确认技能描述是否注入到了 System Prompt 里。有时候注册成功了但 Prompt 拼接逻辑漏了这一块模型根本不知道你有技能可用。确认技能描述里的触发场景是否写清楚。用户想看天气和用户想了解明天该穿什么听起来是一回事但模型可能只对后者触发查询天气的行为。我会在描述里把同类说法都列上。确认模型版本是否支持 function calling。某些轻量模型的 function calling 能力很弱表现在要输出 JSON 时格式混乱或忽略工具定义这种情况建议要么换模型要么走 ReAct 风格把工具显式写进对话流程。6.2 参数幻觉模型在生成技能参数时偶尔会编造出用户根本没提供的值。比如用户只问明天北京天气模型却自动把city填成了上海。这类问题最难防我的经验是两层防护参数缺失或不确定时调度器直接拒绝调用并把错误信息反馈给模型让它询问用户补充。不要默认填一个值。在技能描述里用如果用户没有明确给出这个参数必须向用户确认后再调用这句话对付参数字段多的技能尤其有效。6.3 技能内部错误被模型脑补掩盖技能执行时抛出异常代码里捕获了返回给模型一个技能执行失败请重试的信息。但模型拿到这个信息后可能会把它脑补成用户所在地区天气查询失败建议用户稍后再试——看起来回复合理但用户根本不知道原因。正确的做法是把异常信息结构化返回比如{ status: error, error_code: THIRD_PARTY_API_TIMEOUT, message: 天气服务超时请稍后重试或检查服务状态 }让模型直接转述这段 message不要自由发挥原因。同时在上层做一个重试策略临时性错误重试一次持续性错误直接降级到备用方案比如天气查不了就提示用户查看天气预报 App。7. 效率优化让技能系统更快更省7.1 减少重复调用的时间开销技能系统跑起来之后响应时间主要耗在几个环节模型推理、技能内部逻辑、外部 API 调用。模型推理没法大幅压缩但技能侧的优化空间很多。我做过一个比较有效的优化给技能执行结果加上缓存层。天气查询这种数据源变化不频繁的场景按城市 日期做 key缓存半小时命中率很高。再比如用户连续问北京天气那上海呢两个地方哪个冷实际上很多查询是重复的缓存能直接省掉一半外部调用。from functools import lru_cache lru_cache(maxsize128) def fetch_weather(city: str, date: str): # 外部 API 调用 ...7.2 技能并发执行的取舍并行执行技能能省时间但也不是越并行越好。一个是资源占用问题另一个是外部 API 的限流问题——你同时发出去 10 个请求可能被对方直接拒绝。我的经验是并行数量控制在 35 个以内并且给外部 API 调用统一封装了限速器rate limiter每秒最多 N 次请求。此外有依赖关系的任务不要强行并行先执行上游拿到结果再并行执行下游。这个调度逻辑在任务 DAG 里写清楚效果立竿见影。7.3 模型选择上的省 token 技巧技能描述和任务规划都是 token 消耗的大头。我在前面的粗筛机制基础上又加了一个技能描述模板缓存——同一类任务的技能描述不再每次重新拼 Prompt而是拼好一次缓存起来复用。实际跑下来单轮对话的 token 消耗能省下 20% 左右。当然这个优化的前提是技能描述在运行期不会频繁变更。如果你经常调整技能描述缓存反而会成为负担记得给缓存加上版本号或过期时间。8. 一步步彻底掌握 agent-skills构建一套agent-skills技能系统本质上是在给 Agent 建立一套可执行的常识库。在这篇文章里我完整回顾了技能描述、注册机制、调度执行、多技能编排、异常处理和效率优化的全过程。你不需要照着代码逐行抄更重要的是理解每一层设计到底在解决什么问题描述文件让模型知道技能的存在注册中心让系统知道技能的可选范围调度器让模型学会在合适的时机发起调用编排器让多个技能协同配合完成任务。我最想强调的是技能系统的复杂度会随着技能数量非线性增长。当你只有两三个技能时怎么设计都行但技能超过十几个描述文件的一致性、注册机制的收敛性、调度策略的稳健性每一环都会决定这个系统是越用越好用还是越用越乱。不要等技能多了再补设计在一开始就把底座打好。最后分享一个小技巧技能开发完不要只测正向流程一定要测模型故意调用错误技能的场景。比如用户查天气强行让模型调用航班技能验证你的调度器能不能挡得住这种误调用。这类防御性测试做够了技能系统到线上才会省心。我自己的项目中曾经因为在调度器里没有校验技能的适用场景导致用户抱怨查天气的时候给我推荐了一堆机票从那以后所有技能都强制加上场景校验再没有出现过类似问题。