去年年底我在做一个内部知识库问答项目时,遇到了一个特别拧巴的问题:大模型聊天很流畅,但一让它"干点正事"——比如查一下某个报表的字段定义、自动把客服工单归类、跨系统同步一条数据——就各种掉链子。要么是提示词写了八百字它还是分不清先做什么后做什么,要么是每次调接口的参数格式都不一样,根本没法稳定复用。那段时间我反复改prompt、调few-shot,折腾到怀疑人生。
后来我换了思路:别再试图把事无巨细的指令全部塞给模型,把能力拆成一个一个独立的、可复用的"技能"(Skills),让模型像工具箱一样按需调用。这个方向当时在圈子里还比较新,中文资料也少,我基本是边踩坑边总结。现在这套思路已经跑通了我好几个项目,今天把这大半年的实践完整梳理一遍。不管你是做RAG应用、自动化工作流还是智能客服,只要想让大模型从"会聊天"进化到"能干活",这篇文章都值得你花十分钟读完。
1. 从"能聊天"到"能干活":Agent Skills到底解决了什么问题
先说说最根本的问题:为什么传统的prompt工程搞不定"让模型干活"这件事?
1.1 一个prompt塞太多职责,模型必然顾此失彼
我之前犯过一个典型错误:一个系统提示词里同时塞了意图识别、槽位抽取、调接口、格式化输出、兜底闲聊五件事。结果是模型在简单场景下表现尚可,一旦用户表达稍微绕一点,它就不知道该优先执行哪条指令了。
这其实是LLM的注意力机制决定的。上下文越长、指令越密集,模型对每条指令的"权重分配"就越分散,尤其是中后段的内容经常被"稀释"。你写prompt时觉得逻辑很清楚,但模型不是按你的逻辑执行的,它是按token的概率分布走的。当指令互相嵌套、职责互相重叠时,出错率是呈指数上升的。
1.2 Skills的本质:把复杂任务拆成模块,每个模块只做一件事
所谓Agent Skill,说白了就是一组"能力单元"的定义:它包含这个能力适用的场景、执行所需的工具或接口、输入输出的格式约定,以及一条针对这个具体任务优化过的执行逻辑(可能是一段prompt,也可能是一段代码加prompt的组合)。
举个例子,同样是"查天气"这个需求:
- 传统做法:在系统prompt里写"当用户询问天气时,调用工具查询,返回格式为xxx"
- Skills做法:单独定义一个
weather_query技能,里面规定了触发条件、API地址、参数schema、返回结果模板。模型平时根本不需要"知道"这个技能的存在,只有用户意图命中时它才被加载进上下文
这种按需加载的思路,直接解决了上下文污染的问题。模型每次只需要关注当前这一个任务,自然表现得比"一心多用"时要稳定得多。
1.3 为什么现在才火起来
其实这个理念并不新,早期RPA、工作流引擎都是这么干的。但现在火起来的核心原因是:LLM的意图识别能力终于够用了。以前你需要写一堆规则来判断"用户到底想干嘛",现在模型自己就能判断该调用哪个技能,甚至能判断技能调用的顺序——这就把"技能编排"这件事的复杂度降了一个数量级。
2. 拆解一个Skill的最小单元:输入、工具、决策回路
一个可用的Agent Skill,绝不是一段prompt那么简单。我把它拆成四个部分,你可以把它理解为乐高积木上必须有的四个卡扣。
2.1 触发条件:判断"什么时候该用我"
这是最容易忽视但最重要的部分。触发条件决定了技能是否被激活,写不好就会出现"该出手时不出手,不该出手时瞎出手"的尴尬局面。
我一般用两种方式组合:
- 语义意图描述:用一两句话描述该技能适用的用户意图场景
- 关键词/正则规则:适合一些指令性强的场景,比如"创建一个日程"这种句式比较固定的请求
注意:不要只依赖关键词匹配。用户说"帮我定个明天下午三点的会议",里面根本没有"创建"或"日程"这类词。语义意图描述在应对同义表达时远比关键词可靠。
2.2 参数Schema:把自然语言翻译成结构化输入
模型理解用户意图后,需要把自然语言实体抽取成工具能识别的结构化参数。这里面最关键的是一份设计合理的JSON Schema,它决定了模型抽取的准确率。
我的经验是,schema字段命名越贴近自然语言越好。比如用户说"明天下午三点",如果你在schema里写start_time,模型抽取出"明天下午三点"之后就不知道怎么转了。反之如果你写start_time_description并注明"自然语言描述即可",模型就能直接填值,转换逻辑交给代码去处理。这背后的原因是:让LLM做格式转换容易出错,但做"语义搬运"非常擅长。
2.3 执行器:真正干活的代码
执行器是技能的"手",可以是一个HTTP请求、一个Python函数、一条SQL查询,也可以是另一个子Agent。它的接口设计原则只有一个:输入输出都应该是纯数据,不要带任何prompt逻辑。
我踩过的一个坑是把prompt模板直接套在HTTP请求的body里,结果下游接口报错时,排查了半天才发现是多了一个换行符。执行器越纯粹,调试越容易。
2.4 决策回路:用还是不用、错了我怎么办
这一层决定了技能的鲁棒性。我总结了三层决策逻辑:
| 决策层级 | 触发时机 | 处理策略 |
|---|---|---|
| 前置校验 | 参数缺失/无效时 | 向用户追问,而不是硬着头皮执行 |
| 执行异常 | 接口报错/超时时 | 封装错误信息返回给模型,让它决定是换参数重试还是道歉兜底 |
| 结果复查 | 返回结果结构异常时 | 触发一次轻量校验,不通过则进入人工干预流程 |
这套决策回路的核心思想是:Agent不能把"调用失败"直接抛给用户,它必须消化错误、自主决策下一步动作。
3. 实战:从零封装一个可复用的Agent Skill
理论知识说再多不如动手写一个。我以"会议室预订"这个最常见的办公场景为例,带你完整走一遍封装流程。
3.1 第一步:定义技能描述(Skill Description)
这是技能被模型"看见"的第一眼,一定要写得准确。
{ "name": "meeting_room_booking", "description": "当用户需要预订会议室、查找可用会议室、修改或取消会议预订时,使用此技能。适用于所有涉及会议室资源的查询和预订操作。", "parameters": { "type": "object", "properties": { "action": { "type": "string", "enum": ["query", "book", "cancel", "modify"], "description": "用户想要执行的具体操作类型" }, "room_capacity": { "type": "integer", "description": "需要容纳的人数,用于筛选可用会议室" }, "start_time": { "type": "string", "description": "自然语言描述的会议开始时间,如'明天下午三点'" }, "duration_minutes": { "type": "integer", "default": 60, "description": "会议时长(分钟),默认60分钟" } }, "required": ["action", "start_time"] } }注意几个细节:action字段用的是枚举值,限制了模型的选择空间,大幅降低了"模型自创操作类型"的概率;时间字段允许自然语言输入,让模型只做语义搬运,不做格式转换。
3.2 第二步:实现执行器
# 伪代码:会议室预订执行器 def execute_meeting_room_booking(params): action = params["action"] try: if action == "query": return query_available_rooms( capacity=params.get("room_capacity", 4), start_time=to_timestamp(params["start_time"]) ) elif action == "book": return book_room( room_id=params["room_id"], start_time=to_timestamp(params["start_time"]), duration=params.get("duration_minutes", 60) ) elif action == "cancel": return cancel_booking(booking_id=params["booking_id"]) elif action == "modify": return modify_booking(**params) except RoomNotFoundError as e: return {"error": "not_available", "message": "该时间段没有可用会议室"}执行器里我把业务异常统一捕获,转成结构化的错误码返回。这样模型拿到的不是一段堆栈信息,而是一个明确的信号:该换参数重试,还是该放弃。这个设计看似简单,但实际上决定了Agent的"临场应变能力"。
3.3 第三步:接回Agent主循环
当Agent检测到意图命中meeting_room_booking后,整个交互流程是这样的:
- 模型解析自然语言,抽取
action、start_time等参数 - 参数校验失败则生成追问话术,例如"请问您需要预订几点的会议室?"
- 参数完整则调用执行器
- 执行器返回
RoomNotFoundError,模型根据错误信息改说"该时段已满,我帮您查了附近15分钟的其他空档,是否需要预订?" - 模型用自然语言把结构化结果转化为人话回复用户
这个流程的核心是:模型只负责"理解和表达"两件事,中间的"执行和判断"全部交给代码。
3.4 第四步:写一条好的运行提示词(System Prompt for the Skill)
技能激活后,我们还会给模型一条"运行说明",控制它在当前任务中的行为风格:
- 明确它只能操作与会议室相关的功能
- 规定一次只能执行一个动作,追问确认后才能执行下一个(防止模型一次把"预订+取消"全干了)
- 规定所有输出必须基于执行器返回的真实结果,严禁编造预订成功
这条"严禁编造结果"写不写,决定了你的Agent会不会一本正经地胡说八道。我之前没写的时候,模型居然在接口超时时自己编了一个"预订成功"的假确认,被用户截图投诉过。
4. 让Skill真正"好用"的五个关键细节
从"能跑"到"好用",中间隔着一堆细节。这五个点是我在多个项目中反复验证过的。
4.1 命名要语义化,别用缩写
技能名称别叫rm_sk_usr_boq这种,模型根本猜不出含义。直接叫meeting_room_booking、customer_order_lookup,让技能名字本身就是一种意图提示。模型是从名称来联想内容和触发条件的,一个好名字能显著提升触发准确率。
4.2 为每个技能准备2-3个示例对话
在技能定义里附带few-shot示例,这是成本最低的调优手段。我一般会附两个:
- 一个是标准场景的完整对话,让模型看清参数抽取和回复格式
- 一个是边界场景的对话,比如用户说"随便找个能坐8个人的地方",模型应该怎么处理缺失的时间参数
4.3 参数校验放代码里,别指望模型
模型抽参数会漏、会错、会发挥。所以参数校验的逻辑一定要放在执行器入口处,不要放在prompt里。你就算把"必须提供start_time"写三遍,模型该漏还是漏。但你在代码里做一次params.get("start_time")判空,100%可靠。
4.4 技能之间要保持职责正交
什么叫正交?就是两个技能解决的场景不重叠。比如"查会议室"和"查订餐",虽然都和会议相关,但职责完全不同,绝不合成一个技能。一旦职责重叠,模型就容易选错,而且一旦选错,另一个技能往往还会给出一个"看似正确、实则荒谬"的结果。
4.5 输出格式交给模板,不要交给模型自由发挥
对于执行器返回的结果,不要让模型自己决定怎么排版。我通常的做法是:执行器返回结构化数据后,由代码格式化一份固定模板,再让模型基于模板做"润色改写"。这样既保证了关键信息(时间、地点、预订号)不错位,又保留了自然语言的亲和力。
5. 测试与评估:如何判断你的Agent Skills靠不靠谱
我在这个项目上吃过最大的亏就是"感觉能用了就直接上线",结果被用户花式提问打回原形。后来我搭了一套相对规范的测试流程,分享给你。
5.1 建一个覆盖主要意图的测试集
准备至少50条针对当前技能的用户话语,要刻意覆盖:
- 标准说法:正常的完整表达
- 口语化说法:如"帮我找个地儿开会"
- 缺参数说法:如"订个会议室"(缺少时间)
- 否定/取消说法:如"我之前订的那个不要了"
- 模糊说法:如"下午那个会能换到明天吗"(可能是修改也可能是取消重建)
5.2 三个核心评估维度
| 维度 | 统计口径 | 达标线 |
|---|---|---|
| 触发准确率 | 该触发时触发,不该触发时不触发 | ≥95% |
| 参数抽取完整率 | 用户提供了的关键参数是否全部被识别 | ≥90% |
| 任务完成率 | 从调用到最终用户确认的全链路成功率 | ≥85% |
这三个指标得分开看,触发准不一定参数抽得准,参数抽准了也不一定任务能跑通。任何一个环节掉链子,用户体感都是"这玩意儿没用"。
5.3 回归测试是个体力活,但必须做
每次修改技能定义后,把之前的测试集完整跑一遍。这不光是为了防回归,还有一个重要原因:LLM的升级可能悄悄改变某些行为。供应商升级模型版本后,你之前调好的提示词可能就失效了,只有回归测试能提前发现这种问题。
6. 踩坑实录:三个最容易翻车的场景与修复方案
最后分享三个我实际踩过的坑,都是那种"不到生产环境根本发现不了"的典型问题。
6.1 坑一:模型自作主张"串联"了不相关的技能
有一次用户说"帮我把会议改成明天,顺便查下明天天气"。模型居然一口气调用了修改会议和查天气两个技能,然后编了一个"已修改成功"的结果——实际上第二个技能因为参数冲突根本没执行成功。
修复方案:在主循环里加了"一次会话最多执行一个技能,如果检测到多意图请求,逐条确认再执行"的约束。坏处是交互变啰嗦了,好处是再也没有出现过半真半假的伪造结果。
6.2 坑二:接口返回结构折腾得模型"看不懂"
有一个技能调的是第三方系统的接口,返回的字段叫sta、rm.size这种。模型根本猜不出sta是"状态"的意思,经常把返回结果解读错。
修复方案:在执行器层加了一层"翻译器",把第三方晦涩的字段名映射成模型能看懂的语义化字段。这相当于在模型和外部系统之间加了一道桥,两个世界的"语言"各归各的,互不干扰。
6.3 坑三:认知负担过载,技能数量超过15个之后触发准确率暴跌
当技能数量增多后,模型在判断该用哪个技能时,需要"翻遍"所有技能描述。技能越多,选择困难越严重,错误触发率急剧上升。
修复方案:分层处理。把技能按领域分组(会议室类、订单类、报销类……),先用一个粗粒度的"意图路由"定位到领域,再把该领域的技能列表喂给模型做细粒度选择。相当于先判断"用户想聊工作还是聊生活",再判断具体是工作里的哪件事。
我在实际使用中的体会是,这套分层路由至少帮我挽回了20%左右的错误触发率,而且因为每层需要看的描述变少了,响应速度也快了不少。
做Agent Skills这件事,我最大的感悟是:别把它当成一个prompt技巧,它本质上是一种系统设计思维。你在规划技能边界时思考的"什么该让模型决定、什么该让代码决定",其实就是所有可靠Agent系统都绕不开的核心命题。这套方法现在还在快速演进,但底层的边界划分、职责单一、决策回路这几个原则,我判断短期内不会过时。如果你也正在被"大模型不干活"的问题困扰,建议从最小的一个场景开始,把第一个Skill认真封装好,跑通之后再慢慢扩展——这个起步路径,是我验证过最稳的一条。