在任何Agent项目里最让人头疼的往往不是模型选型也不是流程编排而是“技能”本身——怎么定义、怎么封装、怎么让模型稳定地调用最终形成一个能真正干活的“技能库”。我自己打磨“agent-skills”这套方案时就是从这个痛点切入的。它本质上是一套面向大模型Agent的技能封装与调度范式核心是把每项可执行能力做成结构化、可描述、可校验的“技能包”让模型在推理时像查字典一样选对工具、填对参数、拿到预期结果。这篇文章我打算从设计思路、技能构造、完整实现路径到常见坑位完整复盘一遍。无论你是刚接触Agent开发还是已经被Function Calling折磨过一阵子应该都能从这里找到一些能直接落地的经验。1. 整体设计与思路拆解1.1 为什么“技能”不能靠提示词硬堆先讲个大多数人都会踩进去的坑早期做Agent我习惯把所有能力说明写进系统提示词。比如让模型“当你需要查询天气时调用get_weather接口参数传城市名”。表面看逻辑通顺实际跑起来问题一堆——提示词稍微长一点模型就开始选择性失忆接口参数稍微多一点它就瞎填加一个新技能又要重新调提示词牵一发而动全身。本质上这是把“推理”和“记忆”混在一起了。大模型的上下文窗口是有限的提示词越长对每一条指令的注意力权重就被稀释得越厉害。尤其当Agent需要同时掌握十几个甚至几十个技能时一次性塞进上下文的做法几乎必然失败。agent-skills这套方案换了个思路技能不靠“告诉”模型而靠“让模型自己去发现”。每个技能是一个独立的、自描述的单元包含功能说明、参数Schema、调用规范、返回处理逻辑。模型需要某个能力时先通过技能描述检索到对应技能再按Schema生成结构化调用请求。整个过程像人用工具箱——你不会把几十把扳手全摆在工作台上而是用到哪把拿哪把。1.2 技能库的核心边界描述够细执行够黑盒一个合格的技能单元至少要满足两个条件。第一描述必须足够细细到模型能准确判断“这个技能是不是解决当前问题的那个”。第二执行过程对模型来说足够黑盒模型只需要按规范传参不需要关心内部实现有多复杂。举个例子。假设你有一个“发送企业微信通知”的技能。面向模型的描述不应该只写“发消息”而是要写清楚发送目标类型支持哪些成员、部门、群聊、消息格式支持哪些文本、Markdown、文件、有什么频率限制、发送失败时返回什么错误码。这些细节决定了模型在真实场景中能不能做出正确选择。而执行层面技能内部可以随便折腾——调HTTP接口、读数据库、甚至拉起一个子Agent都行。模型核心只关心“Schema里要求的参数我填没填对”和“返回结果我怎么解读”。1.3 与传统Function Calling的区别理解这套方案最直观的方式是和OpenAI的Function Calling对比着看。Function Calling是很早期的工具调用范式它把函数列表一股脑塞给模型让模型从里面挑一个并生成参数。技能数量少、参数简单时好用一旦技能库上到几十个两个问题马上浮出来一是Token开销呈线性暴涨每个请求都要把全部函数定义送给模型二是模型在长列表中的选择准确率下降经常选错工具或者张冠李戴把A技能的参数填到B技能上。agent-skills在处理这两个问题上走了一条中间路线技能描述可以做得比Function定义更丰富但不需要全部塞进上下文。当模型需要能力时通过一个“技能路由器”完成检索匹配再只把候选技能的完整Schema注入当前轮次的推理。本质上这是给Agent加了一层“短暂记忆”或者叫“工作台”——工作台上永远只放当前任务需要的几把工具。提示技能路由器不一定要用模型实现。简单场景下基于关键词和向量检索的混合匹配就够了搜出Top-K个候选技能再交给模型做最终选择成本和性能都会好看很多。2. 核心细节解析与实操要点2.1 技能清单让Agent知道“有什么可用”一个真实的Agent项目技能数量从几个到几十个不等。如果全靠模型瞎猜效率太低。所以每个技能包首先要维护一份机器可读的清单把技能的标识、名称、一句话概括、触发关键词全部列出来。这份清单有两个用途一是供路由器做初次筛选二是作为模型上下文里常驻的“索引页”。我一般把技能清单做成这样- id: skill_001 name: query_weather summary: 查询城市实时天气与未来预报 keywords: [天气, 气温, 降水, weather, temperature] enabled: true endpoint: http://internal-mock-server:9000/skills/001这里有个容易忽略的细节清单条目必须写得特别克制。一句话概括加上几个触发关键词就够绝不展开细节。原因还是上下文开销——清单要是写得太长等于又把整个技能库塞回提示词里了。一句话概括的目的只有一个让路由器能够通过关键词/向量召回把范围缩到3个以内然后把这3个技能的完整Schema交给模型精确选择。2.2 技能描述写“边界”比写“功能”更重要很多人在写技能描述时只写功能不写边界这在我做项目复盘时是最典型的问题。比如描述一个“生成报表”技能你写“能够生成销售数据报表”模型确实会在用户说“出个报表”时调用它。但用户如果说“帮我看下上个月哪个品类的退货率最高”它可能也会选这个技能——实际这个需求要的是数据分析而不只是把数据摆进表格里。正确的做法是在描述里规定清楚“什么情况用”“什么情况坚决不用”。还是那个报表技能优化后的描述是这样description: 将结构化数据渲染为标准报表文件PDF/Excel并输送到指定存储位置。 when_to_use: 用户明确要求“生成报表”“导出表格”“做一份Excel/PDF文件”时使用。 when_not_to_use: 不负责数据统计分析若用户只是询问数据趋势、异常归因应优先选择数据分析类技能用户未指定输出格式时建议先询问需求再调用。这一段让模型对技能的适用边界有了硬约束。实测下来加了边界描述之后工具选错的概率基本能下降一半以上。你甚至可以理解为这是在给模型划“责任田”——边界越清晰模型越不容易跨专业乱接活。2.3 参数Schema模型理解的唯一依据技能的参数定义直接决定了模型能不能正确“填空”。这一步没有任何捷径必须按JSON Schema规范来。有几个关键原则每个参数必须有description。别觉得参数名起得够直白就不用写模型对英文命名和中文语义的对应关系理解准确率没有你想象中那么高。比如参数名是user_id描述就写清楚“接收方用户唯一标识来自通讯录列表非手机号”。必填项一定要声明required。选填项如果影响结果准确性强烈建议都设计成必填。模型很懒你越给它“可填可不填”的空间它越倾向于少填然后返回一堆“不确定”的结果。枚举值要写成选项列表。比如“时间粒度”只有hour、day、week、month四种就明确枚举出来不要写“时间粒度类型’这种开放定义。模型一旦面对开放取值能不能蒙对完全看运气。我自己踩过的真实教训是给一个“发送邮件”技能定义attachment_paths参数时没有写明白是否支持多个附件也没有规定路径的传递格式。结果模型有时候传数组、有时候传字符串、有时候直接传一个含通配符的路径。后来统一成“字符串数组元素为服务器本地绝对路径最多5个”之后一次就没再出过问题。2.4 返回结构可解析比可读更重要Agent技能调用的返回结果往往不是给人看的而是给模型“读”的。所以返回结构一定要固定、扁平、机器友好。我通常会统一成这套结构{ code: 0, message: success, data: { outcome: success, result: { ... }, meta: { cost_ms: 120, from_cache: false } } }这里想强调两点。第一业务上是否成功不要用HTTP状态码表达。HTTP 200也可能业务失败比如查无此人、接口限流模型无法从200判断下一步该怎么办。第二error信息要写成模型能理解的话。比如“ERROR_10007”这种代码模型根本不知道什么意思你还得专门写个表去解释。不如直接返回“未找到用户ID对应的记录请核对用户ID或确认用户是否已被禁用”模型直接就能据此调整策略或向用户提问。2.5 技能路由器的两级策略前面提到路由器做初筛这里展开讲下两级路线的实现思路。第一级是“轻量召回”基于关键词匹配、别名表、向量相似度混合检索技能清单筛出Top-K候选。第二级才是“模型决策”把候选技能的完整Schema和当前对话的最近几轮内容一起交给模型让模型选一个并生成参数。轻量召回阶段一定要做到毫秒级响应因为你不能为了“选个技能”就让用户多等一次模型推理。我实测中用的方案是把技能清单加载进内存用简单的BM25做关键词召回再叠加一层向量召回embedding用小型本地模型两边结果合并去重后取Top5。这个级别完全不需要上重型的检索服务。模型决策阶段反而要克制每轮只放候选技能。如果TopK5那就只把这5个技能的定义交给模型。为什么不是一次性全部回到第一点说的上下文控制——你让模型在5个选项里做判断的准确率一定高于让它从50个选项里选。另外同一轮如果用户的需求涉及多个技能比如“先查天气再订个闹钟”也要允许模型输出多组调用请求而不是只允许它挑一个。注意技能路由器的召回逻辑本质上决定了Agent能力的“天花板”。一个技能写得再好召回阶段就被过滤掉那它也永远没有机会被模型使用。所以新的技能上线后一定要反复检查和现有技能之间的关键词重叠度。3. 实操过程与核心环节实现3.1 从零搭建一个技能的最小可用闭环现在完整走一遍从定义技能到Agent成功调用的全过程。我这里用一个“仓库库存查询”的模拟技能做示例核心是让你看到每个环节的长相而不是拘泥于某个具体框架。第一步创建技能描述文件。先写清id、name、summary和keywordsid: skill_warehouse_stock name: query_warehouse_stock summary: 查询指定仓库内商品的实时库存数量与批次信息 keywords: [库存, 仓库, SKU, 存货, stock, warehouse, 批量] enabled: true第二步把功能描述和边界规则补上description: | 根据商品SKU和仓库编码查询当前仓库的实时库存总量、可用量以及最近批次的到期日期。 when_to_use: | 当用户需要确认某商品是否缺货、查询当前库存数量、询问可用销售库存时使用。 when_not_to_use: | 不负责库存的环比变化趋势分析不负责自动补货建议若用户同时问多个仓库的汇总库存应先提示当前技能仅支持单仓查询。第三步定义参数Schema。这一步需要结合真实业务字段做收敛{ type: object, properties: { sku_code: { type: string, description: 商品唯一编码必填。通常以字母开头如ABC123。 }, warehouse_code: { type: string, description: 仓库编码枚举值WH001华东主仓、WH002华南主仓、WH003华北临时仓。 }, require_batch_info: { type: boolean, description: 是否需要返回批次明细默认false。仅当用户明确需要查看批次/效期时设为true。 } }, required: [sku_code, warehouse_code] }第四步实现技能的执行函数。内部逻辑完全自主可以调真实业务接口也可以先打桩返回模拟数据。关键是返回结构必须遵循统一规范def execute(sku_code: str, warehouse_code: str, require_batch_info: bool False) - dict: # 这里是业务处理逻辑通常请求内部服务或数据库 # 为了演示直接构造模拟数据返回 data { outcome: success, result: { sku_code: sku_code, warehouse_code: warehouse_code, total_qty: 1520, available_qty: 1330, safety_stock: 500, batch_list: None } } if require_batch_info: data[result][batch_list] [ {batch_no: B20240601, qty: 800, expiry_date: 2025-06-01}, {batch_no: B20241001, qty: 720, expiry_date: 2025-10-01} ] data[result][meta] {cost_ms: 15, from_cache: True} return {code: 0, message: success, data: data}第五步注册技能到路由器的技能中心并把技能中心的检索逻辑调通。这一步之后一个最小闭环就已经成立了。用户提出“华东仓的ABC123现在有多少库存”系统经过召回、模型筛选、参数生成三步就能拿到结构化调用请求并执行。3.2 技能入参校验的强制方式很多人在这一步偷懒想着让模型按Schema生成JSON就完了结果模型生成的参数常常不严格合法。我的经验是不管模型输出得再规范也要在系统侧强制做一遍JSON校验和逻辑校验。校验分两层。第一层是格式校验直接用JSON Schema校验库跑一遍不符合就返回“参数不符合规范请重新生成”。第二层是业务校验比如SKU在系统里存在吗仓库是禁用状态吗有些逻辑模型是不知道的必须由技能执行方硬判断。这里有个实战技巧校验失败时不要只返回一句“PARAM_ERROR”而是把具体哪个字段错了、为什么错、应该怎么改写清楚。模型拿到这种错误信息之后自我纠错的成功率会大幅提升。返回“sku_code格式不对应为字母开头后跟数字如ABC123”这样的信息模型大概率下一轮就自己修正了。3.3 多技能协作时的调用编排单个技能做好只是第一步真实的Agent任务往往会触发多个技能。比如“帮我把华南仓所有库存低于安全库存的SKU汇总一下”就可能要先后调用“查询仓库SKU列表”和“查询单个SKU库存”两个技能。这种情况下最容易犯的错误是让模型一次性把所有调用的参数都推理出来。准确率会很低——因为后一个技能的输入参数往往依赖前一个技能的返回结果。比如你必须先拿到“库存低于安全库存”的SKU清单才能逐个去查明细。正确的编排方式是让模型分步决策每轮只关注当前需要的技能调用。第一轮模型调用“查询仓库SKU列表”技能拿到所有SKU及各SKU库存数量然后模型结合返回结果筛选出低于安全库存的SKU第二轮再调用“查询单个SKU库存”技能取明细。模型在执行完一个技能、看到返回结果之后才进行下一步决策效果远比一口气规划完更稳定。实际工程中我会在技能中心框架里加入一个“任务栈”的概念——每一轮模型推理结束后检查有没有未决的子任务如果有把已完成技能的结果摘要拼到下一轮上下文中再让模型决定下一个技能调用。这是一个笨但极其有效的方法。3.4 Token成本的关键控制策略做Agent项目成本难以避免。但agent-skills这套方案在Token控制上有天然的优势前提是你做对了两件事。第一件事技能包本身要分层。每个技能分成“light描述”和“full描述”。清单里只有light描述路由召回后只返回候选技能的full描述。用户如果连续多次触发同一技能可以设计一个轻量的“技能会话缓存”——在短时间内命中同一技能就直接沿用它的参数结构和上下文不再重新注入技能描述。第二件事给技能描述做“压缩”。不要直接把自然语言描述原样塞进模型上下文。可以把常用的技能描述预编译成半结构化的摘要把修饰语、冗余词、背景说明全部拿掉只保留“何时用、何时不用、关键参数说明”。我实际对比过同样的技能压缩后的描述Token量能降到原来的40%~60%而模型的选择准确率几乎没有下降。提示技能数量到一定规模后建议对技能的embedding做定期重建。因为业务语义会随产品演进发生变化比如你早期定义的“用户ID”可能只是登录账号到后来可能是多端关联ID了不及时更新技能描述和召回索引Agent就会在旧语义里打转。4. 常见问题与排查技巧实录4.1 模型“乱选技能”怎么定位这是Agent开发里最磨人的问题。表面上看模型选了技能A但实际上用户的需求应该用技能B。处理这类问题第一反应不该是改提示词而是先查“它是真不知道有B还是知道却选错了”的。我自己的排查顺序是先打开单轮调试记录把模型当时的输入上下文完整拎出来看。如果上下文里根本没有B技能的描述那是召回的问题去调路由器的召回阈值和关键词覆盖。如果B技能的描述就在候选列表里但模型选了A那才是技能描述写法的问题——多半是因为B的when_to_use写得太笼统或者A的when_not_to_use没有把不适用的情况拦住。另外强烈建议给技能选择留“trace_id”。就是每次模型选中技能时把候选列表、模型原始输出、最终选择的技能ID全部打印出来。这看起来多花了几个字的日志成本但在问题排查时效率能提升十倍。4.2 模型频繁“编造参数”怎么破参数编造是另一种高频故障。表现是技能选对了但传入的参数在业务系统里根本不存在比如查一个不存在的用户ID。破局的思路有两个。第一个思路是“限制开放性”。凡是用户没提到的参数一律不给默认值让模型在生成前先判断“当前对话里有没有这个信息”。没有就调用“追问用户”技能而不是猜一个值。比如用户只问“库存还有多少”没说仓库模型应该反问而不是默认选华东仓。第二个思路是“加强约束”。在参数Schema里给每个参数都加上“来源”说明——来自用户原话的哪一部分、来自哪个前置技能的返回结果。如果参数来源无法对应校验层直接打回。4.3 技能路由召回精度不足怎么办如果你发现模型经常“看到”某个技能但实际该技能和当前用户问题八竿子打不着多半是召回阶段把无关的技能兜进来了。我踩过的教训是在召回阶段不要只依赖关键词匹配要同时跑多路召回合并。多路召回指的是关键词匹配一路、向量相似度一路、别名/同义词典一路。三路结果取并集后再做一轮相关性得分排序。这样做的目的不是提高单路的精确率而是提升整体的召回率。毕竟“技能库里存在备选技能但没被召回”才是更危险的故障——它无声无息地让Agent变蠢。召回宁可多不能漏精确性的判断全部留给模型做。4.4 返回结果太长导致模型“看不懂”这个坑很隐蔽。有些技能的返回结果特别长——比如查出一千条库存明细全部塞回上下文。模型接到超长结果后往往只“看到”最前面几条或者最后几条经常基于不完整的信息给出错误总结。解决方案统称为“结果裁剪”。在技能执行函数内部对返回内容做摘要化、截断化处理量级数据先聚合明细只保留Top-N条并附上一句“共N条明细如需完整列表请单独说明”。模型要的是“决策信息”而不是“原始数据”。让模型根据摘要做判断再按需拉取明细这比把海量原始数据直接堆进上下文要高效得多。4.5 更新技能后旧调用记录失效最后一个问题发生在迭代上。你更新了某个技能的参数定义然后旧记录的缓存还在按旧格式解析。这个问题经常会引发“灵异事件”——代码没变Agent却突然表现异常。我的建议是给技能包的每次变更维护一个版本号并把版本号连同对话Session一起存储。当技能版本和Session历史记录不匹配时优先按新版本重新解析参数。另外旧技能在灰度期建议保留一个“兼容模式”至少在参数格式变化时做一个自动转换层而不是一刀切切掉旧调用。写在最后的个人体会把agent-skills从最初的一张纸梳理成如今这套可复用的方案我最大的感受是Agent真正困难的地方不在模型多聪明而在工程系统多规整。技能描述写得好不好、路由召回准不准、返回结构稳不稳这些细节才决定了Agent的最终体验是“像一个靠谱的实习生”还是“像一个随时随地放飞自我的机器人”。如果你准备在自己的项目里尝试这套思路我给的建议很简单先别急着铺十个八个技能把两三个核心技能打磨到极致——边界清晰、算法规整、调用稳定。跑通了再逐步加码。技能库这东西宁缺毋滥每个不达标的技能都会变成Agent的拖累。最后再分享一个小技巧技能描述写完别急着上线先拿过去半年里真实用户问得最多的几十个问题做一轮离线测试看每个问题能否命中正确技能。这一步不花多少时间但能帮你提前规避大量线上翻车。