做 AI Agent 这一年多,我最大的感受是:真正让一个智能体从“能聊天”变成“能干活”的,不是模型选得多大,也不是 Prompt 写得多花哨,而是你有没有一套像样的agent-skills体系。简单说,就是把你希望 Agent 完成的任务,拆成一个个边界清晰、可复用、可测试的“技能”,再让模型像工人按图纸施工一样去调用它们。
这篇文章我想聊聊我在这套思路上的完整实践:怎么理解技能、怎么设计技能、怎么编排技能,以及我踩过的几个比较典型的坑。适合正在做 LLM Agent、自动化助手、企业内部 Copilot 的开发者参考,也适合刚从 Prompt 工程往 Agent 工程转型的朋友。
1. 重新理解“技能”:Agent 不是聊天框,而是工具箱
1.1 一句话说清 agent-skills 要解决的问题
很多人在做 Agent 时会陷入一个误区:把 Agent 当成一个更强的聊天机器人,什么都靠模型临场发挥。但在真实业务里,用户要的不是“聊得开心”,而是“把事情办成”。比如让 Agent 帮我查昨天的销售数据、帮我约一个会议室、帮我把客户反馈整理成工单——这些事如果每次都靠模型现场编流程,结果一定是不稳定的。
技能(Skill)的本质,是把某一类任务的执行逻辑、判定条件和边界约束打包成一个可复用的单元,让 Agent 在这件事上不再“自由发挥”,而是走一条确认过可行性的路线。
我用一个生活化类比解释一下:你不可能要求一个刚入职的实习生靠常识帮公司搞定财务报销、差旅预订和合同审核,但你可以给他一本操作手册,告诉他什么场景翻哪一章、每一步怎么做、遇到异常找谁。agent-skills 就是这本操作手册,Agent 就是那个照着手册干活的实习生。
1.2 从 function calling 到 skill:能力封装的三个演进阶段
我在项目里经历过三个阶段,算是比较典型的演进路径:
第一阶段:直接在业务代码里写死接口调用。用户在对话框说“查一下 A 客户”,代码就用正则或关键词匹配去触发某个 API。这阶段的问题很明显——换个说法就失灵,加个需求就改代码,本质上只是披着 AI 外衣的传统指令系统。
第二阶段:用 Function Calling / Tool Use。把每个可调用的函数交给模型,让它根据用户意图自己选择并填入参数。这比硬编码灵活很多,但很快你会发现:一个函数解决的是“单次动作”,而真实任务往往是“一串动作”。比如“约会议”至少涉及查日程、创建会议、发通知、设提醒四个动作,如果只暴露四个函数让模型自由组合,它经常会在中间环节出错。
第三阶段:技能化。不是把函数直接裸露给模型,而是把“约会议”这整个工作流封装成一个技能,连同一份清晰的触发描述、输入输出契约、执行策略和失败处理一起打包。模型只需要判断“用户想约会议”,剩下的流程由技能内部处理。
这才是 agent-skills 真正带来的价值:把模型的智能用在“决策”上,把确定性的逻辑从模型那里剥离出来,交给可控的代码层执行。
1.3 通用 Prompt 的局限:为什么闲聊式指令撑不起复杂任务
你可能会想:既然模型这么聪明,我把所有步骤写在 Prompt 里不就行了吗?我也这么干过,结果在任务稍微复杂一点时就崩了。
原因有三:
1)Prompt 是建议性约束,不是确定性执行。模型可以选择性遗忘、自由发挥重组步骤,尤其是 Token 上下文越长,越容易在细节上走样。
2)Prompt 难以做状态管理。多步任务往往有中间状态,比如“会议已创建但还没通知参会人”,这种状态保存在哪里?写回 Prompt 里会让上下文越来越脏,最终影响模型判断。
3)Prompt 不能单独测试。一个几十行的大 Prompt 出了问题,你很难定位是第几步逻辑出错。而一个技能可以单独跑、单独测、单独迭代。
所以我的结论是:通用 Prompt 适合探索期,技能化适合生产期。当你的 Agent 功能还在验证需求时,用 Prompt 快速迭代没问题;一旦需求明确、要稳定上线了,就该把核心路径逐条技能化。
2. 技能的四层设计法:从自然语言到可执行资产
这一节是整篇文章的核心。我会讲我实际使用的技能设计方法,概括为“四层设计法”,每一步都有对应的产出物。
2.1 第一层:能力盘点——把“能做什么”翻译成动词
很多人设计技能时犯的第一个错,是按系统模块来列。比如“用户管理模块”“订单模块”“日程模块”,这种划分是写给程序员看的,不是写给 Agent 看的。
正确做法是按“用户目标+动作”来列。每个技能应该是一个动词短语,例如:
- “创建会议邀请”
- “查询订单物流状态”
- “导出客户对账单”
- “更新项目里程碑”
这背后的逻辑是:Agent 的意图识别本质上是在做“动词匹配”。用户说“帮我约个会”“明天下午安排个会”“约一下张经理的时间”,这些句子的共同点是动作意图“创建会议”,而不是“用户管理”。你清单上写的是动词短语,模型才更容易把自然语言映射过去;你写的是模块名,模型要费劲翻译,翻译就会出错。
我在团队里做过一次统计:把所有技能描述统一改成“动词+对象”格式后,意图路由的准确率从 76% 提升到了 89%,这个提升纯粹来自描述格式的规范化。
2.2 第二层:输入输出契约——技能边界的物理实现
技能不能像函数一样只写“传入用户 ID,返回结果”,因为它的输入来自自然语言,可能缺值、可能模糊、可能带歧义。所以输入输出契约,本质上是在你和模型之间定一个明确的交接协议。
我的做法是给每个技能定义结构化参数,用参数来约束模型。下面是我常用来定义一个“会议安排”技能输入部分的模板:
{ "params": [ {"name": "title", "type": "string", "required": true, "description": "会议标题"}, {"name": "start_time", "type": "string", "required": true, "description": "开始时间,ISO 8601格式"}, {"name": "duration_minutes", "type": "int", "required": false, "default": 30}, {"name": "participants", "type": "array", "required": true, "description": "参会人邮箱列表"}, {"name": "room_keyword", "type": "string", "required": false, "description": "会议室偏好关键词"} ] }这个契约有几个容易被忽略的点:
- 字段描述要带格式约束,比如时间必须写 ISO 格式。否则模型会给出“明天下午”这种自然语言值,需要二次解析。
- default 值很重要。模型不是每次都善解人意,用户没提时长时你不能报错,给个默认值比让 Agent 反问用户更顺滑。
- 输出也要定义,不能只返回一个“成功”。我通常会定义输出结构,包括状态、结果摘要、下一步建议。这样 Agent 拿到结果后可以继续和用户对话,而不是返回一段底层数据。
提示:参数清单不要太长,超过 8 个字段模型就容易填错。如果超过 8 个字段,说明你的技能拆得太大,应该切分。
2.3 第三层:执行策略——技能肚子里装的到底是什么
技能不是简单“挂一个函数”就完了,它内部可以有三种执行策略,我根据任务复杂度来选择:
策略一:单次工具调用。技能收到参数后,直接调用一个外部 API 或数据库查询,返回结果。适合查天气、查汇率、查订单这类“一问一答”型任务。它不复杂,但需要注意超时和失败时的默认回复。
策略二:内部多步编排。技能内部包含多个动作,但不需要模型参与,由代码确定性执行。比如“取消订单”可能需要依次调用“验证订单状态→发起取消请求→发送退款通知→记录日志”。这些步骤在一个技能内部串联,对外部模型黑盒化。它的优点是稳定,缺点是如果业务规则变了,需要改代码而不是改提示词。
策略三:子技能复用。技能内部再调用其他技能。这是 agent-skills 最有威力的形态。比如“安排一次客户拜访”这个技能,里面会复用“查找客户联系人”“查询我的日程空档”“创建日程”“发送确认邮件”四个子技能。子技能之间通过技能编排层协调,而不是让外层模型陷入细节。
我建议你从策略一开始,每个技能先做成单次调用,跑通了再升级成多步或复用。一上来就想做复杂编排,八成会在调参的路上崩溃。
2.4 第四层:可测试性——技能不能只靠“看着像能跑”
我见过很多 Agent 项目死在“上线没几天就被业务方投诉”,核心原因是技能从没被单独验证过。技能的可测试性,决定了它能不能从实验品变成生产资产。
我给每个技能配套三类测试:
- 单元测试:制造各种输入,包括正常值、缺省值、异常值,验证技能本身的参数校验和错误处理。
- 意图测试:准备一组典型用户句子,验证意图路由能否把它们准确对到技能上。我一般每个技能准备 20-40 条真实语料,覆盖说法变化。
- 端到端回归:把技能接上模型和工具后,模拟完整对话,检查链路是否正常。
刚开始做这些测试时,最烦人的是意图测试里的“相似但不同”的句子,比如“帮我查一下明天的日程”和“帮我加一条明天的日程”,一个查一个写,差一个字但技能完全不同。后来我在每个技能的描述里都加了“触发条件”和“不触发条件”两部分,这个做法让意图测试的准确率大幅提升。
一个技能如果经历了这三层测试,它的稳定性会远远超过同等复杂度的纯 Prompt 方案。这也是为什么我强调:与其维护几个庞大提示词,不如维护几十个小而精的技能。
3. 技能编排与路由:单技能是点,多技能才是网
当技能数量超过十几个之后,真正的问题不再是单个技能怎么实现,而是系统怎么决定在哪个场景用哪个技能、技能之间怎么协作。
3.1 意图路由:用户输入怎么找到对的技能
路由是 agent-skills 体系里承上启下的关键层。它的任务很简单:根据用户当前的输入和上下文,选出应该激活的技能。
我这里说的路由有两层含义。第一层是技能选择,第二层是参数填充。模型在这里的工作是“分类+信息抽取”,这是 LLM 最拿手的任务,比让它自由发挥安全得多。
我常用的结构化路由输出格式如下:
{ "skill": "meeting_scheduler", "confidence": 0.95, "params": { "title": "产品周会", "start_time": "2025-06-15T10:00:00+08:00", "participants": ["dev@example.com", "pm@example.com"] } }路由设计的几个心得:
每个技能必须有一个“触发描述”明确说明什么时候用它。这个描述的质量直接决定路由准确率。我写触发描述的模板是:本技能处理什么类型的请求、常见的表达方式有哪些、什么情况下不要触发。
兜底技能必须有。当所有技能置信度都低于阈值时,不能直接报错,要进入一个通用的“澄清/转人工”技能,继续追问或说明能力边界。没有兜底技能,用户觉得 Agent 一下子就变蠢了。
路由的结果最好带置信度。我设了一个阈值(通常 0.7 以上直接执行,0.4-0.7 进入追问,0.4 以下走兜底),这样可以在“优先自动化”和“优先不犯错”之间调到合适的平衡点。
3.2 依赖与冲突:技能不是孤岛
技能数量多了以后,会出现两类问题:依赖关系和意图冲突。
依赖关系是指一个技能的结果是另一个技能的输入。比如“帮我安排一场跟李总的会议”,先要“查找李总的联系方式”,接着“查询李总可用的时间”,然后“创建会议”,最后“给李总发邀请”。这些技能之间有明确的先后顺序。我一般用一个轻量级编排器把它们串起来,而不是让模型自己“凭感觉”拼顺序。编排器内部维护一个小的任务队列,每个任务执行完就把输出映射成下一个技能的参数输入。
意图冲突更隐蔽。举个例子,用户说“帮我看看明天有没有空”,它既可能是指“查询日程”,也可能是在为“创建会议”做前置询问。我的处理方式是:优先激活信息获取类技能,把“要查什么”和“要做什么”区分开。如果语义含混,就多追问一句“你是要查空档还是约时间”,比随便押一个技能从而搞错方向的代价小得多。
3.3 一个完整场景:从“帮我安排下周的会议”到技能链执行
用具体例子来演示编排过程。假设用户输入是:“帮我安排下周三下午三点跟王经理开个项目会”。
这个输入落进路由层后,我的系统会走这么一串流程:
- 路由层识别主技能
meeting_scheduler,同时抽出参数:时间=周三 15:00、对象=王经理、标题=项目会。 - 编排器发现技能内部需要子技能:先用
contact_lookup查王经理的邮箱和可用状态,再用calendar_check验证下周三 15:00 是否空闲。 contact_lookup返回王经理邮箱,但标注“该时段王经理可能不在,需要二次确认”。- 编排器决定不直接创建会议,而是把“待确认”状态返回给 Agent,Agent 用自然语言回复用户:“下周三 15:00 王经理似乎有冲突,我再确认下他的备用时段,可以吗?”
- 用户确认后,再走
meeting_scheduler创建会议、发送邀请、设置提醒。
这整个过程里,模型真正做的只是路由判断和一小段对话管理,其余全部是技能内部的确定性流程。这正是 agent-skills 的核心收益:让确定性的逻辑回归代码,让模型只做它擅长的语义理解。
4. 实测中的三大步坑:边界模糊、上下文污染、调试困难
前面讲的都是方法论,这一节聊聊实际落地时我踩过的比较深的坑,每个坑都花了不少时间才爬出来。
4.1 坑一:技能描述写得像函数文档,路由频繁误判
刚开始我把技能描述写成“该函数用于查询订单状态,参数包含订单号,返回订单状态和物流信息”,结果路由准确率惨不忍睹。原因很好理解:模型不是程序员,它看不懂“函数用于查询”这种描述,它需要知道的是“用户在什么情况下会说这句话、你会怎么回应”。
后来我把所有技能的触发描述全部改写成“用户话术模板+反例”,效果立竿见影。比如一个技能描述我改成:
当用户提到下单、购买、买入、想要、订购 XX 商品,且目标是完成购买行为时触发该技能。 当用户只是询问商品信息、比价、查看评价时,不要触发该技能,应使用 product_inquiry 技能。这个改动很枯燥,但确实是把路由准确率从 80% 拉到 90% 以上的关键一步。我的建议是:给每个技能写 3-5 个正例和 3-5 个反例,正例告诉模型“什么时候该用我”,反例告诉模型“什么时候别用我”。这个钱省不得。
4.2 坑二:多技能复用时的上下文污染
技能化之后,子技能之间经常需要共享信息,比如联系人 ID、订单号、会议室编号。一开始图省事,把这些信息全塞进一个全局上下文变量里,结果出现了一个非常诡异的问题:用户说“帮我查一下张经理的日程”,系统返回的却是李经理的日程。
排查之后发现原因在于:前一个技能执行时把“当前操作对象”设定成了李经理,这个状态留在了共享上下文里,下一个技能读取时没做校验,直接把李经理当成了用户当前要求的人。这属于典型的上下文污染。
我的解决方案是:给每个技能定义明确的输入作用域,技能只能读取自己声明过的输入参数,不能访问其他技能的中间状态。执行完一个技能后,立即清理临时状态,只保留编排器约定的输出映射。简单的隔离规则,却彻底解决了串数据的问题。
注意:上下文隔离是技能化设计的底线原则。你可以为了性能做部分共享,但必须显式声明“谁可以写、谁可以读”,否则迟早会出跨任务的脏数据事故。
4.3 坑三:技能调试比函数调试难一个量级
传统函数调试,输入输出都是确定的,打断点看变量就行。技能调试不一样:同样的用户输入,两次运行可能走不同的分支,因为模型天生有随机性;而且技能跨模块协作时,问题可能出在路由层、参数提取层、执行层或者外部 API,定位链路很长。
我现在的调试方法有三个:
- 详细 trace 日志。每次请求打全链路日志,记录路由结果、置信度、提取的参数、执行步骤、每一步的耗时和 token 数。没有日志,技能出错就是黑盒猜谜。
- 单技能沙箱。每个技能都支持脱离完整系统单独测试。我用一个内网页面,输入技能名和参数,直接跑技能并查看输出,方便快速确认问题出在技能内部还是出在路由层。
- 固定采样比对。遇到模型行为不稳定时,我会固定几十条测试样本,反复跑十次来统计成功率,而不是相信某一次运行的结果。这样能分辨出是偶发波动还是系统性缺陷。
吐槽一句,技能调试的难点其实不在技术,而在心智模型:你不能再像调试普通代码一样“预期一个结果”,而是要建立“概率性思维”,去接受和统计不确定性,然后在设计上把不确定性控制到最低。这也是 agent-skills 方案相比纯 Prompt 方案的一个重要优势——它把不确定性收敛到了路由层和少量对话管理处,而不是散落在整个执行链路里。
最后再分享一个小技巧:技能化不是一蹴而就的。我习惯先跑通一个最小闭环,再做技能的“切片化”和“沉淀化”。也就是先用一段能用的 Prompt 跑通业务,再分析用户真实请求里的高频动作,把这些动作切成技能,最后把稳定的触发规则写进技能描述里。这个过程听起来慢,但每沉淀一个技能,后续的迭代速度就会快一截。我做完第一版用了两周,但之后每加一个新功能基本只需要半天到一天,整体收益远大于前期的投入。