Agent 圈子里聊得最多的一个词,最近一定是 agent-skills。我自己在项目里反复打磨这个方向小半年了,从最开始把技能写成一大坨 prompt 塞进系统消息,到后来拆成独立、可复用、可测试的技能包,中间踩了不少坑,也总结了一套相对顺手的做法。这篇就围绕 agent-skills 这个概念,把我在实操中积累的设计思路、落地步骤和排查经验完整梳理一遍,希望能给正在做 Agent 应用的同学一些参考。
1. Agent Skills 到底是什么,为什么突然这么火
1.1 一个"技能"和一段"提示词"的本质区别
先说清楚我理解的 agent-skills 是什么。简单讲,它是给 Agent 预定义的一组可调用、可编排、可复用的能力单元。每个技能单元通常包含触发条件、执行逻辑、输入输出协议、工具调用规则,以及最小化的上下文说明。
很多人觉得这不就是 prompt 里的工具描述吗?还真不是。工具描述只是技能的一个对外接口,而一个完整的技能包要解决的是"Agent 知道什么时候该用、怎么用、用完怎么收尾"这一整条链路的问题。举一个例子,我之前做一个内部的知识库问答 Agent,早期版本把所有处理逻辑写在一个巨型 system prompt 里,问它"帮我查一下上季度的报表明细",它能回答,但换一种问法"Q3 的销售数据汇总下",表现就开始不稳定,经常答非所问或者自己瞎编格式。
后来我把"报表查询"拆成一个独立技能,里面明确规定:触发场景是用户提到季度、报表、销售数据等关键词;执行流程是先确认时间范围,再走 API 查询,最后按固定模板生成结果;异常情况是数据缺失时怎么反馈。改成这套之后,同样的提问,识别准确率和输出稳定性提升非常明显。
1.2 为什么要把技能从 Prompt 里拆出来
把技能从 prompt 里拆出来,核心动机有三个:稳定、复用、演进。
稳定方面,prompt 越写越长,模型对指令的遵循率会下降,尤其是多个任务混在一起的时候,互相干扰特别严重。技能拆出来后,每个技能的指令上下文可以单独拼装,需要时才注入,模型每轮实际看到的指令量是可控的。
复用方面,同样的技能可以在不同 Agent 之间共享。比如我写过一个人力资源场景的"请假审批技能",后来做另一个行政类的 Agent,直接复制过去改个 API 地址就能用,不用重新调 prompt。
演进方面更有意思。Prompt 是"活文档",改一次就可能影响全局,你很难做版本管理。技能包可以像代码一样走 Git、走测试、走评审,每次改动影响面可控,出了问题能快速回滚。
这些特性决定了 agent-skills 不是一个锦上添花的概念,而是 Agent 走向工程化的必经之路。我见过太多项目死在"Agent 能力看着都有,但一交到用户手上就失控"这个阶段,本质上就是没有把能力做结构化拆分。
2. 技能体系的整体设计与拆解思路
2.1 先确定能力边界,再动手拆技能
我在设计技能体系时,第一件事不是写代码,也不是写 prompt,而是先列能力清单。做法是把业务上一段时间内用户真正会问到、用到的高频需求全部罗列出来,然后逐个判断:这个需求是"一次性的对话能力"还是"可以沉淀成反复使用的技能"。
判断标准有三条:一是使用频率高不高;二是流程是否足够标准化;三是是否涉及外部系统调用。三个条件至少满足两个,才值得拆成一个独立技能。举例来说,"帮用户查快递"这种需求,高频、流程固定、要调外部接口,适合做技能;但是"陪用户闲聊"这种,虽然高频,但流程完全没有标准可言,做成技能反而是束缚,应该保留在通用对话能力里。
这条标准帮我砍掉了不少伪需求。最初我列的技能清单有二十多项,按这个标准筛完只剩九项,后续开发和维护成本大幅下降。
2.2 技能包的目录结构与分层设计
技能包我会按功能域分层组织,而不是平铺一堆技能文件。这样做的原因是 Agent 在运行时需要快速定位技能,清晰的分层目录可以显著减少检索和匹配的耗时。
我常用的目录结构大致是这个形态:
skills/ common/ # 通用基础技能 web_search/ # 网络搜索 pdf_parser/ # 文档解析 datetime_utils/ # 日期处理 business/ # 业务领域技能 order_query/ # 订单查询 refund_process/ # 退款处理 report_generate/ # 报表生成 expert/ # 专家级技能 risk_analyze/ # 风险分析 legal_review/ # 法务审核每个技能目录内部再放三个核心文件:skill.yaml(技能元信息)、instructions.md(执行指令)、参考代码或 API 配置。skill.yaml 里记录技能名称、描述、适用场景、触发关键词、输入输出协议,这部分主要给 Agent 的"技能调度层"读取。instructions.md 是给模型看的自然语言指令,写清楚执行步骤和边界规则。API 配置则定义实际调用外部系统时的接口细节。
这种分层设计的价值在后期维护时体现得最明显。通用技能几乎不用动,业务技能随着需求迭代频繁更新,专家技能看规则变化。分层之后,每次改动只影响自己那一层,回归测试范围很小。
2.3 技能调度的核心机制:该用哪个技能,谁来决定
技能多了以后,最考验设计的地方其实是调度环节。Agent 面对用户的一句话,怎么知道自己该激活哪个技能?我试过三种方案,效果差异很大。
第一种是把所有技能描述直接塞给模型,让模型自己选。这种方法实现简单,但技能数量一多,描述互相干扰,选择准确率明显下滑。实测八个技能以内还行,超过十二个就开始频繁选错。
第二种是加一层分类路由。先用一个轻量分类模型判断用户意图属于哪个功能域,再只把该域内的技能描述注入给主模型。这种方式准确率提升不少,但增加了链路复杂度和延迟。
第三种是我目前采用的混合方式:常用技能走规则关键词匹配快速激活,模糊场景交给模型判断,最后加一道确认机制。比如用户说"查一下我的订单",先做关键词匹配,命中"订单查询"技能就直接进入对应流程;如果用户表达很模糊,比如"看看我上次买的东西到哪了",就交给模型在具体场景的候选技能里做选择。确认机制是指如果技能匹配置信度不高,Agent 先向用户复述一遍理解结果,得到确认后再执行。这个设计能明显降低误操作率。
这三层配合下来,准确率和响应速度达成一个比较理想的平衡。我后来的经历证明,一上来就追求纯模型路由并不划算,规则兜底加模型判断的组合才是工程上的稳妥选择。
3. 核心细节解析与实操要点
3.1 技能描述怎么写,模型才看得懂、选得准
这是 agent-skills 里最容易被低估的环节。技能描述写得不好,再强的调度机制也救不回来。
先说一个常见的反面例子:很多人写技能描述就一句话,"查询用户的订单信息"。这种描述太泛,模型不知道"订单"的具体边界是什么,也不知道什么时候该用不该用。我踩过这个坑之后,总结出一套相对成熟的描述写法。
技能描述必须包含三块内容。第一块是触发场景,要写清楚哪些用户表达应该落到这个技能上,最好附两三个典型问句示例。第二块是能力范围,明确这个技能能做什么、不能做什么,比如"只支持查询近一年内的订单"这种边界一定要写死。第三块是行为约定,比如用户询问多笔订单时是逐条展示还是汇总展示,这些细节直接影响输出质量。
我整理过一个技能描述模板,大致长这样:
name: order_query description: > 当用户查询订单状态、物流进度、购买记录时使用。 典型提问示例:"我的订单到哪了""帮我看看最近买的手机发货没""查一下上周的订单情况"。 本技能仅支持查询当前账号近一年内的订单,不支持修改订单、申请退款等操作。 若用户同时询问多笔订单,默认按时间倒序展示最近三笔,并提示可展开查看全部。把描述写到这个粒度,模型在选择技能时基本不会跑偏。有一个细节值得注意:描述不要写得太抽象,模型对"典型示例"的感知远远强于对"功能特点"的感知。所以我在每个技能描述里都会塞一到三个具体的用户问句示例,效果非常直接。
3.2 技能内部执行指令的编写规范
instructions.md 是技能执行时的操作手册,直接决定 Agent 干活的靠谱程度。这部分我踩过的坑最多,主要的经验可以归纳成四条。
第一条,执行步骤要清单化,不要写成散文。模型对"第一步做什么、第二步做什么"的序列指令遵循率很高,但对"根据实际情况灵活处理"这种开放式指令会把控不住。一个技能的执行步骤控制在五到八步比较合适,超过十步就考虑拆成子技能。
第二条,每个技能要点明"前置条件检查"。比如查询订单前必须确认用户已经登录、会话里已经拿到用户 ID,如果没有就走统一的鉴权流程。不写这一步,Agent 很可能拿着空参数就发起查询,然后报一堆看不懂的错误。
第三条,输出格式要提前锁定。我会在指令里直接定义输出模板,比如"结果必须用表格呈现,列名固定为:订单号、商品名称、下单时间、物流状态",然后要求模型严格按照模板输出。模板锁死以后,下游的页面渲染和用户阅读体验都稳定了。
第四条,必须写清楚"失败处理路径"。技能执行失败是常态,关键是失败后怎么办。我的约定是:API 调用失败时,先做一次重试;重试仍失败,则向用户输出一句标准话术"暂时查不到您的订单信息,请稍后再试",同时把错误日志记录下来。很多 Agent 产品给用户的体验差,就是死在技能失败后模型开始自由发挥,编出一些不存在的错误原因。
3.3 技能复用与联动:技能拆小了怎么拼起来
技能拆得越细,复用性越好,但同时也带来一个新问题——单个技能解决不了复杂任务,需要多个技能联动。这个矛盾我花了不少时间才理顺。
我的做法是引入一个"编排层",允许技能通过显式声明来互相调用。比如"生成季度销售报表"这个复合技能,内部定义了一条执行链路:先调用"数据查询技能"拉取原始数据,再调用"数据清洗技能"处理异常值,最后调用"报表渲染技能"生成最终文档。每个子技能保持独立,随时可以被其他复合技能复用。
实现这种联动的方式不复杂,本质上是在技能指令里写明"本技能执行过程中会调用以下子技能"以及"子技能之间的数据传输格式"。但有一个关键点,数据格式必须统一。我踩过的一个典型坑是:数据查询技能输出的是一个 JSON 数组,而数据清洗技能期望的入参是 CSV 文本,中间没有做转换,导致整条链路在联动时反复报错。后来我在技能目录里单独加了一个 data_protocols 文件,集中定义所有技能间传递数据的标准格式,这个问题就彻底解决了。
3.4 技能安全边界:权限、敏感操作与回归控制
技能一旦接上真实业务系统,安全问题就不容回避。我在这方面坚持几条铁律,宁可功能少一点,不能出安全事故。
第一条铁律是"最小权限原则"。每个技能只配它确实需要的那点权限,绝不给 Agent 一个拥有全部操作权限的通用接口。给"订单查询技能"就只配只读权限,给"退款处理技能"才单独配写权限,并且要额外加一层二次授权流程。
第二条是"敏感操作必须走确认流程"。涉及资金、隐私、删除类操作的技能,我在执行链路上固定插一个确认节点,Agent 不能直接执行,必须先生成一个操作摘要让用户确认。这个机制很土但非常有效,它能拦住大模型随机抽风导致的误操作。
第三条是建立技能操作的审计日志。每次技能被调用,就记录触发时间、输入参数、调用结果、消耗 token 数。这些日志一方面是排查线上问题的线索,另一方面也可以用来分析用户真实需求,反哺技能迭代。我后来做技能效果评估时,靠的全是这些积累下来的日志数据,而不是凭感觉。
4. 实操过程:从零实现一个完整技能包
4.1 场景选择与需求定义
用哪个技能当例子讲实操比较合适?我选"请假审批"这个吧,因为它覆盖了一个典型技能的完整链路:关键词触发、表单信息收集、外部系统调用、规则判断、结果反馈,整个流程足够代表性,又不会因为业务复杂度太高把读者绕晕。
这个技能的需求定义是这样的:用户向 Agent 发起请假请求,Agent 需要收集请假类型、开始时间、结束时间、请假事由,然后调用 OA 系统的接口提交申请,根据接口返回结果组织反馈话术。如果用户给的信息不完整,Agent 需要主动追问,而不是带着残缺参数硬调接口。
这个定义实际上就是技能的需求说明书。我会强调团队成员在这个环节多花时间对齐,因为后面所有编码、调优都是围绕这个定义展开的,定义不清后面全是返工。
4.2 编写技能元信息与执行指令
定义清楚后,第一步是写 skill.yaml。请假审批技能的元信息大概长这样:
name: leave_apply version: "1.2.0" domain: hr triggers: - 请假 - 申请休假 - 年假 - 调休 capabilities: - 收集请假信息并提交OA审批 constraints: - 仅支持当前登录用户本人发起 - 不支持的请假类型需明确告知用户 input_schema: leave_type: type: enum values: [年假, 事假, 病假, 调休] start_time: type: date end_time: type: date reason: type: string max_length: 200 output_schema: status: type: enum values: [pending, approved, rejected] leave_id: type: string这个文件的核心作用是给调度层和模型做"技能画像"。其中 triggers 字段特别重要,它决定了这个技能能不能被快速命中;input_schema 和 output_schema 则给模型提供了处理入参和出参的完整约束。
接着写 instructions.md。这个文件才是真正指导模型干活的核心。我在里面明确写了执行流程:第一步,核对用户是否已登录;第二步,逐项确认用户提供的请假信息,缺哪项补问哪项,用户提供的信息直接填入参数;第三步,把参数转成 OA 接口要求的 payload 并发起请求;第四步,根据接口返回结果组织用户回复。同时在里面加了两个重要的规则提示:请假时间跨度不能超过可选范围,否则要提示用户分次申请;发起提交前必须把最终确认信息回显给用户,得到回复"确认"后再真正调接口。
4.3 接入外部接口与数据校验
写到这里,就进入真正动手接接口的环节了。我没有把接口调用逻辑写成一段固定代码塞给 Agent 执行,而是把它封装成一个可被模型调用的工具函数,这样模型只负责填充参数、发起调用、接收结果,具体 HTTP 细节由工具层处理。
def create_leave_request(leave_type, start_time, end_time, reason, user_token): payload = { "type": leave_type, "start": start_time, "end": end_time, "reason": reason, "applicant": get_user_id(user_token), } # 这里做一层防呆校验 if not validate_dates(start_time, end_time): return {"code": 4001, "message": "日期格式不正确或开始晚于结束"} if get_remaining_leave_days(user_token, leave_type) <= 0: return {"code": 4002, "message": "可用假期余额不足"} resp = requests.post(f"{OA_BASE_URL}/api/v1/leave", json=payload, headers=auth_headers(user_token), timeout=10) if resp.status_code != 200: return {"code": 4003, "message": "OA系统暂时不可用,请稍后重试"} return {"code": 0, "data": resp.json()["data"], "message": "success"}值得说一下这个防呆校验的作用。模型在填充参数时什么离谱的情况都可能发生,比如给一个"2024-02-30"这种不存在的日期,或者结束时间早于开始时间,如果直接把这些参数打到上游 OA 系统,迟早被人投诉。工具层做一层基础校验,等于给不可控的模型行为加了一道保险。
接口报错时的处理逻辑也写在工具函数里了。我规定所有错误必须以标准结构返回,错误码分三类:参数类错误返回 4001,业务规则类错误返回 4002,系统异常类错误返回 4003。模型拿到这个结构化错误后,可以根据指令里的失败处理路径组织话术,而不是自己瞎解释"服务器可能有问题"。
4.4 调试与联调的关键技巧
技能写完,真正的战斗才开始。我把这段调试经验单独拉出来说,是因为太多人在这里栽跟头。
第一件要做的事是"单技能单测"。我整理了一份包含十多个测试用例的清单,覆盖正常请求、缺少参数、非法日期、余额不足、接口超时、用户未登录等场景。每跑一个用例,都看模型在这一轮里是否选择了正确技能、是否正确追问缺失参数、是否在特殊场景下给出符合预期的话术。这里推荐一个做法:不要直接看最终回复,而是把模型每一步的中间推理过程和工具调用日志拉出来看,才能定位问题到底出在技能选择、参数填充还是输出编排。
第二件要做的事是"跨技能串联测试"。比如用户说"下周我想休个年假,帮我走个流程",这句话其实先触发了日历查询技能确认日期是否可行,再触发请假审批技能提交申请。这种串联场景我最开始没测,结果上线第一天就出问题:模型在前一个技能结束时把上下文搞丢了,后面那个技能拿不到完整的请假信息。后来我在技能指令里强制约定,上一个技能得到的关键信息必须保留在会话上下文中并明确标记字段名,串联问题才算根治。
第三件是建立回归测试机制。技能迭代时,老功能不能退化。我每次改完一个技能,就把之前积累的测试用例全套跑一遍,看有没有哪个用例的表现在改完之后变差了。人工跑这套测试很累,我后来写了一个简单的自动化脚本,把测试用例作为输入循环调用 Agent 并对比输出关键字段,效率提升明显。
5. 常见问题与排查技巧实录
5.1 技能总是不被触发?先查描述里的触发信号
最常遇到的问题就是技能写好了,但用户怎么问都不触发。我排查这种问题时有一套稳定的排查顺序。
第一步查 skill.yaml 里的 triggers 是否覆盖了真实用户的表达习惯。我吃过一个亏:写的触发词是"请假""休假""年假",但实际用户最常说的一句话是"我想歇几天"。后来我把用户访谈里收集到的真实问法整理成一份话术清单,然后逐条过了一遍 triggers,把覆盖不到的统统补上。
第二步查技能描述里是否写清了"什么时候不能用"。模型在选择技能时,负向约束和正向约束同样重要。如果描述里只写了"用户问请假相关问题时使用",没说"用户只是闲聊提了一句上周请假出去玩不算申请场景",那模型就可能在非触发场景被误导,给用户推送一个"是否要提交请假申请"的卡片,体验极差。
第三步查技能是不是被其他技能"抢走"了。多个技能描述如果有重叠,模型很容易选错。我的解决方法是给每个技能定义足够差异化的触发场景,并且在技能描述里显式声明"如果用户意图属于XX技能的范畴,不要使用本技能"。
5.2 输出内容不稳定?用模板和校验双重锁死
模型输出不稳定是 Agent 应用的老大难问题了。我的处理思路是:凡是可以模板化的输出,一律模板化;凡是模板化不了的,就在指令里给出正反例。
以请假审批技能为例,我要求成功提交申请后的回复必须包含申请单号,于是我在输出模板里明确写了一句"回复用户时必须包含 OA 系统返回的申请单号,单号字段名为 leave_id"。实测下来,加上这个约束之后,包含正确单号的回复率直接从八成提升到几乎百分百。原因很简单,模型对"必须包含某个具体字段"这种硬约束的遵循能力,远强于对"回复要全面一些"这种模糊期望的遵循能力。
另一个有效手段是给输出加一层"后置校验"。我在工具层写完提交逻辑后,会检查模型生成的最终回复里是否包含必要字段,如果不包含就自动触发一次重写,把缺失字段的信息重新传给模型并要求补上。这种做法相当于在一个可控的点上把模型犯的错兜住。
5.3 技能之间状态混乱?统一上下文协议
多个技能联动时,最常见的故障是信息在技能切换间丢失。用户跟 Agent 说"帮我查下最近的订单,顺便看下能不能退",这里先走了订单查询技能,再切退款处理技能,退款技能需要知道上一个技能查出来的订单号,但如果上下文没有按约定传递,退款技能就会拿不到参数。
这个问题我在前面提了一句解决办法,这里详细展开。我在项目里定了一个统一的上下文协议,规定所有技能输入参数中的关键实体,在技能执行完成后必须回写到会话上下文的固定字段中,比如 order_id、user_name、leave_id。字段命名全局统一,后续任何技能都能直接读取。同时,技能指令里明确写出"执行本技能后,必须将以下字段回写到上下文:..."。执行完一轮联调后,技能间状态传递的稳定性显著上升。
这个协议看起来简单,但需要项目早期就定下来,并且代码评审时严格把关。我见过不少项目做到一半才引入这个规范,结果历史技能全部要返工,代价相当大。
5.4 技能效果怎么评估?不只看答没答对
最后聊一个容易被忽视的问题:技能上线后,怎么判断它真的有用?我见过太多人只统计一个指标——用户的问题有没有被成功回答。这个指标太粗糙了,很难指导迭代。
我自己在评估技能时至少看四个维度:一是触发准确率,该触发时是否触发,不该触发时是否瞎触发;二是过程合规率,技能执行时是否严格按照指令中的步骤来,有没有跳过前置检查或遗漏必要字段;三是输出规范率,生成的回复是否符合约定的模板和格式;四是用户反馈率,用户对回复的点赞点踩数据,以及是否出现了要求转人工的行为。
这四个维度各有侧重,触发准确率衡量调度层的效果,过程合规率衡量执行层的稳定性,输出规范率衡量产出质量,用户反馈率衡量真实体验。配上前面提到的日志系统,每周拉一张表出来看趋势,哪个技能要优化、优化哪里,一目了然。
我印象最深的一次优化:某周报表显示一个技能的触发准确率只有六成,排查日志后发现是用户表达变化太快,新增了一波"帮我看看额度还剩多少"的表达,触发词里完全没有覆盖。这种问题,光看"答没答对"是永远发现不了的,还得靠分维度指标和日志来定位。
6. 几条踩坑后的真心话
我自己做 agent-skills 这一年多下来,最大的体会是:技能体系的复杂度不会消失,只会转移。你不做技能拆分,复杂度就堆积在 prompt 里,每次修改都是一次渡劫;做了技能拆分,复杂度转移到了调度、上下文、安全和测试上,但每一块都能用工程手段去解决,这是本质区别。
如果你正准备在项目里引入 agent-skills,我的建议是从小处起步,先挑三个最高频、流程最标准的场景做成技能包,跑通整个开发、测试、上线的流程,再逐步扩展。一上来就想把十几个技能一次做齐,大概率会在调度和调试环节被拖垮。
最后再分享一个小技巧:技能描述里的典型示例问句,一定要从真实用户对话里收集,不要自己编。自己编的问句往往偏书面、偏规范,真实用户说话随心所欲,示例越贴近真实表达,模型命中技能的准确率就越高。这一条看起来不起眼,对整体效果的贡献却比很多花哨的架构设计都大。