
提到智能体的能力组织我这两年最绕不开的词就是agent-skills。去年接一个智能客服升级项目时团队一开始的思路很朴素把要对接的系统接口全部写成Python函数在提示词里列一张工具清单模型需要哪个就调哪个。前两周确实跑得通因为工具只有七八个。等业务方把需求从“查订单”铺到“查物流、算优惠、发起退款、生成对账单”提示词变得又长又乱选型准确率肉眼可见地往下掉同一个问题有时候跳这个技能、有时候跳那个技能参数也是五花八门。后来我把整堆能力重构成按“技能”组织的模块把自然语言入口、参数契约、执行函数和错误回读做成体系系统才算真正稳定下来。这里的agent-skills不是指某个现成开源库而是我沉淀下来的一套智能体技能编排经验。我会把这套模式完整拆一遍技能怎么定义、目录怎么设计、运行时链路怎么走再加上落地中的几个大坑和修复方案。如果你正在做Agent应用或者正准备给智能体接入大量业务接口这篇内容应该能帮你少走不少弯路。1. 从“写死函数”到“可编排技能”agent-skills的抽象基点1.1 硬编码工具函数的两个死穴第一个死穴是“提示词越长选择越不稳定”。我手头留存过一组实测数据工具数量在7到8个的时候模型选择正确率在95%左右增加到20个上下正确率掉到85%以下一旦超过30个就只能维持在70%上下。原因不复杂——模型对函数名的语义理解非常有限函数注释写法一乱注意力很快被分散一个口语化问题丢进去它在十几个候选里翻来覆去最后经常选一个“看起来有点像”却完全不对的函数。第二个死穴是“新增或变更一个工具往往要动全局提示词”。我曾经为了加一个查询历史会话的函数改了一次系统提示词结果“查订单”类问题开始偶尔被分到“查客服记录”上因为两个函数描述里都写了“查询”模型分不清到底是哪个。这种两处耦合的问题会随着工具数量增加不断放大到最后改一个函数得把整套提示词回归一遍。1.2 技能视角自然语言入口、参数契约、执行逻辑与反馈闭环所以agent-skills的抽象基点是把“技能”当作比“函数”更高一级的能力单元。一个技能并不是简单包一层函数而是同时具备四样东西可被模型读取的自然语言描述、可校验的参数契约、确定性的本地执行逻辑、以及结构化的结果与错误反馈。这个定义和“工具”“插件”有什么区别工具通常是无状态的单次调用模型填参数、它干活干完就结束插件更偏集成侧概念强调把一组外部能力打包进来而技能强调的是“让模型能读懂的能力边界”。模型不需要知道技能内部怎么实现只需要看懂它的描述、参数格式以及什么东西不在它的范围内。举个生活化的比喻普通函数像食堂后厨的散点菜模型要伸长了手去点技能则是一道带完整说明的标准化套餐菜单上写清楚了主料、配料、辣度选择和忌口后厨也知道出锅后怎么摆盘、怎么反馈。到了Agent场景里这种标准化正是稳定性的来源。2. 技能目录怎么设计一个能直接跑起来的工程结构2.1 技能包的最小文件集在我实际搭建时一个技能包通常包含这几个文件skills/ ├── order/ │ ├── SKILL.md # 给模型读的能力描述 │ ├── schema.yaml # 参数与返回值契约 │ ├── main.py # 实际执行逻辑 │ └── tests/ │ ├── case_query.json │ └── case_refund.json ├── logistics/ │ ├── SKILL.md │ ├── schema.yaml │ └── main.py └── registry.py # 技能注册与扫描入口每个文件各司其职文件作用给谁看SKILL.md说明技能的触发条件、能力边界、正反例模型和评估脚本schema.yaml定义参数类型、必填项、返回值结构模型填参时、调度层校验时main.py完成真实业务逻辑调用外部接口本地执行器tests/记录真实用户问法及期望结果回归评估工具registry.py的作用是启动时扫描skills目录把每个技能包注册到内存里。我还会在注册时做一次三向一致性校验SKILL.md里声明的参数必须在schema.yaml里存在main.py的函数签名也必须能覆盖schema.yaml中的必填字段。任何一项对不上这个技能直接拒绝注册宁可不加载也不能上线后让模型填一堆它根本不认识的参数。2.2 SKILL.md描述规范正向触发词与负例边界缺一不可描述写得好不好直接决定模型选不选得对。很多项目里SKILL.md只有一句话比如name: order_query description: 查询订单信息。这种描述基本等于没写。订单状态、订单明细、历史订单、售后单都算“订单信息”模型根本分不清用户到底要哪个只会靠猜。我调过多个技能之后的结论是一份能用的SKILL.md至少要包含正向触发词和负例边界。正向触发词告诉模型“这类问法归我管”比如“查一下订单号xxxx的状态”“我的订单里有几件商品”负例边界则直接声明“哪些情况不要找我”比如物流轨迹要去找logistics_tracking退款进度要去找refund_status。一个相对完整的描述长这样name: order_query description: 查询订单的基础状态与明细包括订单编号、下单时间、商品清单、当前状态。 triggers: - 我订单到哪一步了 - 查一下订单号xxxx的状态 - 我的订单里有几件商品 out_of_scope: - 物流轨迹请调用logistics_tracking - 退款进度请调用refund_status parameters: order_id: type: string description: 订单编号优先从用户消息中抽取缺失时回问注意最后那几条out_of_scope它们的作用不是给模型看热闹而是当模型误选这个技能时能顺着提示去改选另一个技能。这个设计在后面的“描述冲突”里会帮上大忙。3. 从用户指令到技能执行一条完整的运行时链路3.1 先召回再精排别让模型在海量技能里大海捞针技能数量少的时候把全部技能的描述塞给模型让它选问题不大。但是当技能超过二十个全量暴露就会带来两个麻烦一是上下文被占掉一大截二是模型选择压力指数上升。所以我在运行时链路里加了一步“候选召回”。召回阶段做的事情很简单先把所有技能的描述和触发词拼接成文本块给用户消息做一次向量化然后按相似度排序取top 8。代码大概长这样async def retrieve_candidates(user_message, registry, top_k8): texts [] for s in registry.skills: texts.append(f{s.description}\n{ .join(s.triggers)}) embedding await embed(user_message) scores cosine_similarity(embedding, texts) return registry.top_k(scores, ktop_k)召回不追求绝对精确它的目标只是把30个技能压缩成6到8个候选。真正做最终选择的还是模型但这时候模型是在一个小集合里精挑细选而不是面对一长串名单硬猜。这一步做完选型准确率的提升是非常明显的。3.2 参数填充、本地执行与错误回读模型不在食物链顶端候选集确定之后调度层会做四件事让模型从候选技能中选一个、按照schema填参数、本地执行技能、最后把结果或错误信息结构化回传给模型。伪代码大概是async def dispatch(selected_skill, user_message, context): params await llm.extract_params(selected_skill.schema, user_message, context) params validate_with_schema(selected_skill.schema, params) try: result await selected_skill.execute(**params) return {ok: True, result: result} except SkillRuntimeError as e: feedback { ok: False, skill_id: selected_skill.id, error_type: e.type, failed_param: e.failed_param, hint: e.hint, } retry_params await llm.retry_with_feedback(feedback, selected_skill.schema, user_message) result await selected_skill.execute(**retry_params) return {ok: True, result: result}这里有一个我反复强调的点错误回读必须结构化。模型调用技能失败时它接收到的反馈要明确告诉它哪一步失败了、失败的原因是参数错误还是接口超时、哪个参数需要修正。如果没有这些信息模型面对“调用失败”四个字只会做同一件事——把同样的参数原样重试一遍进入死循环。返回值同样要带结构化的ok标记。这样上层编排逻辑就可以根据ok字段决定下一步是继续串联技能、向用户展示结果还是转人工处理不需要到处try-except接异常。4. 落地过程中踩过的三个大坑冲突、漂移与组合失效4.1 描述冲突两个技能同时在说“我能查询订单”第一个坑是技能描述互相打架。当时订单查询和物流查询两个技能的描述都写了“查询订单”“物流”这类关键词结果用户问“我的快递到哪了”两个技能都被召回到候选前列模型在两者之间左右摇摆选哪个完全看心情。手工统计了几天选型准确率只剩八成左右。修复方案分两步。第一步在物流技能的SKILL.md里加正向触发词“快递”“包裹”“到哪”“运单号”第二步在订单技能里显式声明out_of_scope“包裹物流轨迹不在本技能范围请调用logistics_tracking”。这样模型就算一时被误导也能根据边界声明改选正确技能。之后我把这类问题固化成自查项新技能上线前把它和现有技能描述放到一起做相似度计算如果出现两个技能在同一个用户问法下都进了召回前三就要手动处理描述重叠。4.2 技能爆炸导致的选择漂移从全量选择改成场景分组第二个坑是技能数量积累到一定程度后模型选择开始“漂移”。我观察到的临界点大概在20个技能左右选型准确率明显下降。原因在于候选技能一多描述之间的细微差异被拉平模型很容易对用户意图过度推断明明不匹配也硬要挑一个出来。我的做法是先做场景路由。把技能按场景分组比如商城场景、售后场景、数据报表场景每个组内的技能数控制在6到8个之间。调度时先根据会话上下文判断用户处在哪个场景只加载这个场景的技能组再走召回和精排。分组之后选型准确率从82%回到94%左右。这背后其实是一个“少即是多”的原则与其让模型从30个技能里硬凑一个不如让它诚实地说“当前没有可用技能”也不要给一个莫名其妙的错误调用。技能覆盖度要够但单次决策的候选集必须小。4.3 组合失效技能A的输出喂不进技能B的输入第三个坑藏得更深单个技能测试都是好的一旦让Agent把多个技能串联起来就到处报错。典型例子是订单明细技能返回的字段叫order_id而生成对账单技能需要的参数叫billing_code两个技能单独跑都没问题串联时模型拿着order_id往billing_code里填接口直接返回400。根因是技能之间没有统一的数据契约。修复思路也很直接在技能输出端加一个标准化管道统一字段命名规范或者用一个轻量的字段映射中间件做转换FIELD_MAP {order_id: billing_code} normalized_payload {FIELD_MAP.get(k, k): v for k, v in payload.items()}同时要求每个技能在schema.yaml里声明output_schema这样编排层在串联两个技能之前就能先检查前一个技能的输出结构能不能满足后一个技能的输入要求不满足就提前介入而不是等运行时爆错误。我把三个坑总结成一张表方便对照症状根因修复方向两个候选技能同时被选中选择不稳定描述关键词与边界重叠正向触发词out_of_scope声明技能数量增多后选准率下降全量候选导致选择压力过大场景分组预路由压缩候选集多技能串联时报字段错误技能间数据契约不统一标准字段映射中间件output_schema声明5. 从“能用”到“好用”评估、版本与能力地图5.1 技能质量评估看十次指令的选型准确率技能上线不是终点后面还得持续维护。我在每个技能包的tests目录里放了一组真实用户问法每条都标注了期望选中的技能、期望参数和期望结果。评估脚本会定时跑全量case统计三个核心指标选型准确率即该选中某个技能时模型是否真的选中了它参数正确率即抽出来的参数是不是符合schema调用成功率即真实执行是否成功落地。CI里我会设定一个阈值选型准确率低于98%就直接报警。这个阈值听起来高但技能包设计合理时完全可以达到如果达不到说明不是模型不行而是某个技能的描述边界没划清楚。描述微调之后必须重跑整组case防止按下葫芦浮起瓢。5.2 技能版本与兼容契约升级不应该破坏现网Agent技能也会升级但升级方式必须可控。我定的规则是新增一个可选参数属于小版本修改参数名、删除字段或改变返回值结构属于破坏性变更必须升大版本。Agent在调用技能时显式声明api_version调度层按版本号路由到对应实现这样老流程不会被新技能悄悄带跑。实际开发中我习惯在变更技能前先找出所有依赖它的Agent用例跑一遍回归再决定是否发布。技能包的变更日志也值得写虽然看起来麻烦但等到技能数量上百之后翻日志定位回归原因时会省下大量时间。5.3 能力地图一张表格看全所有技能的边界与覆盖率技能多了以后光靠人脑记忆完全不现实我会维护一张能力地图核心信息就几列技能ID、描述摘要、正向触发词、参数依赖、版本号。画出来大概就是这个效果技能ID描述摘要触发入口参数依赖版本order_query查询订单状态与明细我订单到哪了、查单号order_id1.2.0logistics_tracking查询快递物流轨迹快递到哪了、运单号tracking_no1.0.1refund_status查询退款进度退款到哪了、退款状态order_id1.1.0地图的价值不只是展示它能让你一眼看出两件事一是重建技能是否重叠比如两个技能都覆盖“退款”相关问法说明边界没划清楚二是能力覆盖是否有空洞用户高频问题找不到对应入口业务侧就知道该补哪个技能了。最后分享一点个人体会。设计agent-skills的过程中我花在写代码上的时间其实不到三分之一剩下都在写SKILL.md、调描述、跑评估用例。技能描述写得好不好直接决定模型上线后稳不稳。技能数量也绝不是越多越好——覆盖度要够、重叠度要低、单次决策的候选集要小。这套模式后来被团队推广到好几个Agent项目里每次新接一个业务接口大家的第一反应也从“改提示词”变成了“加一个技能包”。如果你正在做多工具Agent建议先从给每个工具补边界声明开始实际跑一阵你会明显感受到选型稳定性的提升。