
最近在做一个基于大语言模型的 Agent 项目一开始图省事把所有功能说明和参数规则全塞进 System Prompt 里。结果模型经常“选择性失明”该调工具的时候不调不该调的时候乱调改一个功能的提示词还会影响其他功能的稳定性。折腾几轮之后我下定决心把所有能力拆成独立的“技能块”用一套统一的框架去管理这就是我称之为agent-skills的实践项目。agent-skills不是某个开源库的名字而是一种面向 Agent 的能力组织方式把模型可以执行的每一个具体操作封装成带描述、带参数约束、带调用逻辑的独立技能单元再通过注册表统一管理。它解决的是 Agent 在复杂任务中“工具调用不可控、提示词互相干扰、新增能力成本高”这三个核心问题。适合正在用大模型 API 做落地应用的开发者、对 Agent 工作流有基础认知的产品经理也适合想把自己的 ReAct 原型改成真正可维护系统的朋友。这篇文章就沿着“为什么技能化 - 怎么设计技能块 - 怎么实现和调优 - 踩过哪些坑”这条线把整套思路和可复现的细节都摊开讲。1. 为什么需要技能化从“能力堆砌”到“技能库”1.1 原始 Agent 的痛点提示词不是万能的第一版 Agent 的逻辑很直白把工具列表 JSON 塞进 Prompt再加上一段自然语言说明“当用户需要查询天气时调用 weather_api需要计算时调用 calculator”。这个方案在小 Demo 里跑得通一旦工具数量超过十五个问题就接踵而至。模型开始混淆相似工具的功能比如把 “发送邮件” 和 “创建草稿” 搞混有时明明应该调用工具却自作主张答非所问更麻烦的是为了纠正某个调用错误而新增的提示词往往会污染其他工具的判断。这里有个本质原因大模型理解工具调用依赖的是工具名称和描述里的语义信号而不是我们编写的“指令优先级”。当所有工具的描述挤在同一个上下文里彼此之间会产生语义干扰。尤其当两个工具名称相似或描述相近时模型几乎必然踩坑。这就像一个人同时记住二十条交通规则每条都很重要但遇到具体情况时很容易记岔。1.2 技能Skill是什么一个边界明确的“能力单元”技能化的第一步是重新定义能力的边界。一个 Skill 不再是一行函数定义而是一个完整的包它包含一段面向模型的能力描述什么时候该用、什么时候不该用、一组带约束的参数 Schema、一个或多个底层工具函数的实现以及这条技能特有的调用前后处理逻辑。举个例子单纯的get_weather是一个工具接口但“查询当前天气”这个技能还可以包含参数里城市名的模糊匹配逻辑、超时重试策略、返回数据的格式化模板。模型只需要理解“用户问某地天气怎么样就调用这个技能参数填用户提到的地点”至于地点怎么解析、结果怎么整理全部藏在技能内部。这种封装让模型面对的决策空间大大缩小它不再需要理解几十个底层函数的差异只需要认识若干个语义清晰的技能。1.3 技能库带来的收益可控、可复用、可进化把能力从“提示词里的一段话”提升为“库里的一个单元”收益是直接的。第一是可解释性每个技能的触发条件、参数含义、输出格式都有文档和代码可查模型调用出错时能快速定位是描述问题还是实现问题。第二是复用性一个写好的技能可以被不同 Agent 项目直接引用不用每次复制粘贴再改一遍。第三是可独立迭代当某个技能需要升级时单独改这个技能做回归测试就行不影响其他技能。这套思路在设计上其实借鉴了传统软件的“高内聚、低耦合”原则。技能之间尽可能互相隔离技能内部尽可能自我完备。这样做的代价是前期要多花一点时间做拆分设计但后面每次新增能力、调整逻辑时省下的时间远超过前期投入。我自己把十几项能力拆成二十多个技能后Agent 的工具调用准确率从六成多提升到九成以上这个对比相当说明问题。2. agent-skills 核心结构拆解2.1 技能描述让模型“一眼看懂”的门面技术技能的第一个核心是描述文本。很多开发者在写描述时容易走极端要么一句话糊弄要么把实现细节全写进去。好的技能描述应该站在模型的角度回答三个问题这个技能是做什么的什么场景下应该调用它什么场景下不应该调用它反面案例是获取天气这种描述太抽象模型可能在任何与天气无关的对话里触发它。正面案例是技能名称: query_current_weather 技能描述: 当用户询问某个地点当前或近期的天气情况、气温、降水概率等信息时使用。用户可能使用口语化地名需要先归一化。该技能不适用于历史气候趋势分析也不适用于天气预报的长期预测超过7天。这段描述的重点是明确“什么时候用”和“什么时候不用”并给出边界条件。模型对否定性描述的敏感度很高加上一句“不适用于什么”能大幅减少误调用。2.2 工具定义结构化的参数 Schema技能的第二部分是工具定义通常用 JSON Schema 描述参数结构。这里的关键不是把参数列全而是把约束写准确。比如一个查天气的技能参数可以设计成{ type: object, properties: { location: { type: string, description: 用户提到的地点支持城市名、区县名如北京、上海浦东 }, date: { type: string, description: 查询日期格式YYYY-MM-DD缺省为今天 } }, required: [location] }注意date参数的描述里写了缺省值规则这防止模型在用户没提日期时非要编一个日期出来。参数描述同样要写“边界”比如枚举值、格式限制、缺省行为这些都是模型最容易犯错的地方。2.3 技能编排多个技能如何协同工作实际任务往往需要多个技能串联。比如用户问“明天北京开会穿什么衣服合适”Agent 需要依次调用query_current_weather、search_clothing_advice最后生成回答。技能编排要解决的问题是技能之间的调用顺序、条件判断和数据传递。我的做法是在技能库里加一层“编排路由”不把这个关系写死在 Prompt 里而是用代码控制。当 Agent 决定需要多个技能时按照一个简单的工作流引擎顺序执行先查天气拿到结果后把天气数据作为参数传给穿衣建议技能。每个技能的输入都可以引用上一个技能输出中的某个字段。这样模型不需要自己脑补技能间的数据流只需要选出用哪些技能具体顺序和参数映射由代码兜底。2.4 元数据与版本管理技能库的“体检报告”技能库规模变大后需要给每个技能加元数据创建时间、最后修改时间、调用次数、成功率、平均耗时。这些数据有两个用途一是排查问题二是评估技能质量。我用一个简单的 JSON 文件维护技能注册表大致结构如下{ skill_id: skill_query_weather, version: 2.1.0, entry_prompt: ..., schema_file: schemas/weather.json, handler: handlers.weather_handler, lang: zh, tags: [weather, daily], created_at: 2025-01-11, updated_at: 2025-02-03, stats: { calls: 128, success_rate: 0.94, avg_latency_ms: 380 } }版本管理特别重要因为技能描述和实现经常要微调。我把每次变更当作一个版本保留历史版本的回滚能力。这样当模型调用行为突然变差时可以直接用二分法定位是哪个版本引入的问题。3. 从零到一构建一个可用的技能库3.1 明确边界一个技能只做一件事动手写第一个技能前先做“任务清单”而不是“工具清单”。列出 Agent 要支持的所有用户意图再拆分为不可再分的最小操作。每个最小操作就是一个技能候选。比如“设置闹钟”这个意图下可能有“创建闹钟”“修改闹钟”“删除闹钟”“查看闹钟列表”四个操作。表面上是同一个工具的不同参数但在技能设计上我更推荐拆成四个技能。原因是模型在决定“调用什么”时绑定的是用户意图而不是函数签名。用户说“把明天早上的闹钟取消”实际上是一个“删除闹钟”的意图如果只有一个manage_alarm工具模型还需要从参数里推断出操作类型这多了一层推理就多了一分出错的可能。把技能拆细之后描述自然更聚焦参数也更简单。唯一要注意的是别拆得过度碎片化如果用户总是同时需要两个技能的结果那考虑合并成一个复合技能。平衡点是技能的输入是否足够独立描述是否能在一句话内讲清楚触发条件。3.2 写技能描述让模型一眼看懂描述写作是纯经验活。我把自己总结的模板放在这里【技能名称】英文动词_名词如 query_weather 【一层描述】一句话说明这个技能做什么 【触发场景】列举 2-4 个典型用户问题示例帮助模型快速模式匹配 【不触发场景】列举 1-2 个视觉相似但语义不同的场景明确排除 【参数要点】逐个说明参数的填写规则和边界条件实际写出来会是技能名称: skill_send_email 技能描述: 发送一封电子邮件。触发场景包括“给张总发邮件”、“把这份报告发给团队”、“给客户回复邮件”。不触发场景包括“保存草稿”、“查看收件箱”、“转发邮件”。参数要点收件人支持多个邮箱地址用分号分隔正文支持纯文本和Markdown附件路径必须是服务器本地存在的文件路径。这段描述里最有用的一句是“不触发场景”。模型天然容易把“保存草稿”和“发送邮件”混淆因为都涉及邮件但语义差异巨大。明确写出排除项之后模型的混淆率下降非常明显。这里有个实操技巧定期从 Agent 日志里捞误调用样本然后把每次误调用场景补进“不触发场景”里没过多久这个技能的边界就变得非常锐利。3.3 实现工具函数参数设计和返回结构技能描述是给模型看的工具函数是给自己看的。实现层面要注意两点参数容错和返回结构化。参数容错指对模型可能给的脏数据做清洗。模型调工具时偶尔会出现拼音地名、错别字、口语化简称这类情况。比如“北京”可能被填成“beijing”或“bj”用户说“魔都”要能映射到上海。这些逻辑都应该放在技能内部兜底处理。最忌讳的是把脏数据直接交给底层系统然后整个调用链崩掉。返回结构也极其关键。给模型的返回结果应该是一段干净、紧凑的 JSON而不是把底层 API 的完整响应原样丢回给模型。比如天气接口返回一百多个字段但模型生成回答只需要温度、天气状态和穿衣建议那我就在技能内部完成字段筛选和文本摘要只返回这几项。这样能显著缩短上下文减少模型从噪音里提取信息的难度。3.4 注册与加载打通调用链路技能实现完成后需要接入 Agent 的运行时。我的加载流程分三步启动时扫描技能注册表解析每个技能的 JSON Schema 和入口 Prompt。把技能名称、描述、参数 Schema 拼进给模型的工具列表Function Calling 格式。模型返回的 function_call 里带技能 ID 和参数运行时根据技能 ID 找到对应 handler执行后把结果塞回对话流。这里有一个容易被忽视的点技能 ID 和工具名的映射要稳定。Function Calling 接口里需要传一个name字段这个 name 必须和注册表里的技能 ID 一一对应且不能随意更改。一旦上线后改变技能 ID而对话历史里还存在旧的调用记录模型就可能产生困惑。所以我在技能 ID 上使用“语义名 版本后缀”的方式比如skill_send_email_v2但这个 name 对外保持为skill_send_email内部版本变化不影响模型可见的工具名。4. 实操过程中踩过的坑与排查技巧4.1 描述模糊导致错误调用这是一个反复出现的坑。某次我的 Agent 频繁在用户问“晚上吃什么”时调用search_nearby_restaurants但实际用户只是闲聊并不想要餐厅推荐。排查后发现技能描述里只写了“当用户提到‘吃’、‘餐厅’、‘美食’等相关词汇时使用”导致模型把“想吃妈妈做的菜”这种表达也触发了调用。解决办法是把描述改成“当用户明确表达寻找附近餐厅、查看餐厅推荐、了解餐厅预订信息时使用”并补上“不适用于讨论家常菜、烹饪方法、食物营养”的排除句。这个改动上线后误调用率降低了七成。现在我的每个技能描述都强制包含“不适用于”子句列为写描述的硬性要求。4.2 参数过深导致 JSON 解析失败有一次我写了一个技能参数里嵌套了三层结构比如{filter: {attr: {category: food}}}。模型生成的 JSON 偶尔会缺一层括号或字段名拼错导致解析抛异常Agent 流程直接中断。排查之后我把参数结构拍平所有条件都提到顶层用扁平键名替换深嵌套{ category: food, limit: 10 }模型生成扁平 JSON 的可靠度远高于深嵌套这是 Function Calling 实践中的常见结论。除非业务硬性要求否则尽量不要让参数结构超过一层。如果确实需要复杂结构可以把结构定义预先存储在服务端技能参数里只传一个config_id模型不需要理解内部结构。4.3 技能冲突多个技能抢同一个工具当不同技能底层复用同一个 API 时会出现两个技能同时被触发的情况。比如skill_query_order_status和skill_feedback_order都会调用订单查询接口。模型可能为了完成用户一个简单的“查一下订单到哪了”请求先后调用两个技能造成冗余。解决思路是在编排层做“技能屏蔽”当路由决策环节已经选中一个技能后如果检测到其他技能与它的底层工具冲突就自动降级其中一个。这个逻辑用一张冲突表维护简单直观。另一个策略是在技能描述里互相声明边界比如“如果用户只是查询物流状态使用 query_order_status不要使用 submit_order_feedback”。4.4 上下文膨胀技能库越大越傻Function Calling 的工具描述会作为上下文的一部分传给模型。技能数量从十个涨到五十个时工具定义本身可能消耗几千个 Token这不仅增加延迟更严重的是稀释了模型对系统指令和对话历史的注意力导致整体表现变差。我现在维护一个“技能分群”机制把所有技能按领域分组如生活类、工作类、娱乐类在每轮对话开始前先通过一个分类轻量模型判断当前用户意图属于哪个群然后只把这个群的技能定义送入主模型的工具列表。这个做法能把单轮调用的工具定义压缩到 3-5 个模型准确性直接提升一个台阶。4.5 回归测试技能变更后如何验证技能库最大的隐藏风险是“改一个技能炸一片功能”。因为技能之间可能存在间接依赖或描述重叠。每次改动后不能只测这一个技能要做一轮回归。我搭了一套轻量测试集包含一百多条带标准答案的测试用例每条覆盖一个典型的调用场景。代码改动后自动跑一遍统计技能选择准确率和参数填充正确率。比如有一条用例是“帮我设置明天早上八点的闹钟”标准答案是必须选择skill_create_alarm且time字段为08:00。通过这类断言能在几分钟内发现描述改动导致的误选、漏选和参数漂移。这个测试集的价值怎么强调都不为过——它是技能库长期稳定运转的保险。5. 进阶经验与扩展思考5.1 技能复用与共享跨项目拆墙当你有两个以上 Agent 项目时技能库的复用价值就体现出来了。我在两个项目间共享一套技能库通过 Git 子模块管理。技能都有独立的仓库目录包含SKILL.md、handler.py、schema.json、tests/。项目 A 引用了其中二十个技能项目 B 引用了另外十五个两边都可以单独升级某个技能不影响对方。这种做法的前提是技能实现不依赖具体项目的业务数据库。我要求所有技能通过统一的网关接口访问外部数据项目相关的配置用环境变量注入不要在技能内部硬编码。当技能库被多个项目使用时接口契约要更严格哪怕只是返回结构里增加一个字段也要走版本号变化否则很容易悄悄弄坏依赖方的解析逻辑。5.2 技能评估不能只看准确率给技能库定评估指标时除了调用准确率我还关注三个指标遗漏率该调用技能的时候模型没调占比多少。参数完整率技能被调用后必需参数填对的比例。平均修正轮次由于调错技能或参数不对而让用户重复描述的轮次。这三个指标联合评估能反映技能库对用户体验的真实影响。比如准确率很高但遗漏率也高说明技能描述过于保守模型不敢调用需要补充触发场景示例。参数完整率低往往指向参数描述不清需要把参数的格式和缺省值写得更具体。5.3 从单 Agent 到多 Agent技能成为协作的最小单位最后聊一点扩展思考。技能库天然适合作为多 Agent 协作的最小单位。当一个主 Agent 判断任务需要调用某个技能时它可以不仅仅是执行一项操作而是把整个技能上下文打包传给一个专门处理该技能的子 Agent。主 Agent 只做意图路由子 Agent 专心处理技能内部逻辑两者通过技能参数和返回结构通信。我在一个内部实验里把这种多 Agent 结构落地过主 Agent 负责对话和任务拆解子 Agent 按技能绑定比如 “订单处理 Agent” 绑定一组订单相关技能“内容生成 Agent” 绑定一组写作技能。主 Agent 的决策压力大大降低每个子 Agent 的上下文也更聚焦。这个方向后续还可以继续扩展成“技能市场”不同的开发者发布技能包Agent 运行时按需动态加载类似插件生态。虽然这还只是一个想法但从代码角度看只要把技能的注册、描述、验证、版本管理做成标准格式这条路是完全走得通的。踩过这么多坑之后我最大的体会是Agent 能力的上限并不完全取决于底层模型有多强而取决于我们如何把它能掌握的“技能”组织得有多清晰。agent-skills这套体系看起来简单真正把它做到顺手需要在描述细节、边界划分和回归测试上花不少笨功夫。如果你正在做 Agent 项目不妨先别急着堆功能耐下心来把现有能力拆成技能块配上完整的描述和测试。一个月后你再对比效果一定会觉得这笔投入太值了。