上个月我做内部客服与销售场景的智能体联调,碰见的头号问题不是模型答不上来,而是它明明知道该干什么,动作却总不到位:说好了要调用CRM拉客户记录,它随手编了一个工单号;说好了按销售SOP发跟进邮件,它把邮件写得既不符合公司口径也漏了关键附件。我一度以为是提示词写得不够长,后来反复试了几轮才发现,问题根本不在“说得多不多”,而在“能力被组织成了什么形态”。这篇想聊的 agent-skills,就是我后来把散装提示词收拾成结构化技能库的一套实践总结:技能该怎么定义、怎么写、怎么注册、怎么路由,以及上线后必然会碰见的三个坑和完整排查过程。
如果你正在搭工具型Agent、多智能体协作系统,或者给Agent做产品落地,这篇文章里的大部分内容可以拿来直接用。就算你只是写过一个带function calling的Demo,看完也会明白为什么有些Agent项目“演示很惊艳、上线就翻车”——绝大多数翻车点并不在模型推理,而在技能层的边界没划干净。
1. agent-skills到底在解决什么问题:从“会聊天”到“能干活”的最后一公里
1.1 大模型原生能力与“可执行能力”之间隔着一条沟
先说一个很多人没意识到的前提:模型本身有很强的语言理解和生成能力,但“能力”这个词在Agent语境里必须被重新定义。对客服机器人来说,能回答“我们的退货政策是什么”和能执行“查订单、判断是否超期、生成退货单、发送确认短信”是两件完全不同的事。前者靠模型参数就能完成,后者需要一套代码、接口、权限、状态管理共同配合。模型只是这套执行链路的语言中枢,真正干活的是技能。
我当时把团队里所有Agent相关的代码和提示词清点了一遍,发现大家写Agent的方式高度雷同:在系统提示词里长篇大论描述角色和任务,然后在用户对话里塞几个工具返回的结果,指望模型自己看着办。这种做法的最大问题是——没有边界。模型每次都在“自由发挥”中试图猜测该调用什么、不该调用什么,一旦上下文里出现相似信息,它就会产生非常自然的误判。而 agent-skills 要做的,就是把“边界”变成显式结构:哪项能力是一门独立技能,技能接收什么参数、触发条件是什么、内部按什么流程执行、输出格式长什么样,全部预先定义清楚。
1.2 为什么碎片提示词不够用,必须技能化
我在项目里经常用一个类比:让一个实习生直接看公司规章制度干活,和你先给他一份权限清单、每项任务配上标准操作手册,效果是完全不同的。碎片提示词相当于前者,技能化相当于后者。Agent在真实业务里要处理的请求是高度重复的,高频动作就那么几十个:查订单、改地址、生成报价、跟进客户、提交工单、审批报销。每个动作都必须具备稳定的输入输出契约,否则根本没法做测试、没法做回归、没法多人协作。
更有意思的是,技能化的收益会随Agent数量增加而放大。我见过不少团队同时维护三四个Agent,每个Agent的提示词里都写了一坨“你会调用订单接口”之类的描述,结果接口一改,所有Agent集体失灵,排查起来只能逐个人工翻提示词。把技能抽出来做成独立模块后,接口变更只需要更新对应的技能实现,所有Agent通过技能注册表自动获取新行为。这其实就是agent-skills这套思路在工程上最核心的价值:让能力可以被单独维护、单独测试、单独升级。
1.3 agent-skills的最小可执行单元:一个技能该长什么样
我在自己的项目里划定过最小单元的边界,供参考:一个技能至少要包含五样东西,少了任何一样都不算“可执行技能”。第一是技能名称,必须短且无歧义;第二是描述,说明这个技能在什么条件下被触发、核心职责是什么;第三是输入参数声明,逐个列清楚字段名、类型、是否必填、含义;第四是执行逻辑,可以是提示词加代码的组合,也可以是一段固定的处理流程;第五是输出格式,明确定义返回给Agent的内容长什么样。
这五样东西看起来简单,真正做下去就会发现难在平衡:写得太粗,模型不知道该在什么时候调用;写得太细,维护成本暴涨,而且描述里任何一个词都可能微妙地改变模型的选择。我在下面几节会分别展开讲这套定义怎么落地,重点是给出可以直接抄的schema和踩出来的经验。
2. 一套能落地的技能schema:目录结构、字段定义与参数约束
2.1 技能仓库的基本目录结构
我先说我在项目中实际采用的目录结构,它不一定适合所有团队,但对中大型Agent项目很有参考价值。每个技能独占一个目录,名称就是技能ID,目录内包含一个描述文件、一个执行脚本、一个样例集目录。
skills/ check_order/ skill.yaml run.py examples/ case_001.json case_002.json send_followup_email/ skill.yaml run.py examples/ case_001.json每个技能一个目录的好处是隔离性极强,多人协作时不容易互相污染。描述文件专门给模型看,执行脚本专门给代码跑,样例集则同时服务两件事:一是给模型提供few-shot示例,二是作为离线评测的回归样本。把这三者分开,能避免一个常见问题——把大段示例塞进描述文件里,导致模型在触发判断时被示例中无关紧要的措辞带偏。
2.2 skill.yaml字段设计:哪些字段直接决定模型行为
skill.yaml是我最看重的文件,它直接决定了Agent能不能在正确时机调用正确技能。我写过几个版本,最终沉淀下来一套比较稳定的字段结构,下面用销售跟进邮件的技能做示例。
name: send_followup_email description: 当用户要求向客户发送跟进邮件,且已经具备客户姓名或联系方式时,使用本技能生成并发送邮件。不适用于首次开发新客户。 input_schema: - name: customer_name type: string required: true description: 客户联系人的姓名,必须来自客户资料或对话上下文,禁止编造 - name: last_contact_summary type: string required: false description: 上次沟通的关键信息摘要,用于生成个性化内容 - name: email_language type: enum values: [zh, en] required: false default: zh description: 邮件语言,默认中文 output_format: type: email_draft fields: - subject - body - attachment_ids allowed_callers: [sales_agent, support_agent]这里有几个字段需要特别说明。description字段是整个技能里优先级最高、最容易影响成败的配置,它必须写明触发条件,还必须写明不触发条件。我在“不适用于首次开发新客户”这句话上就吃过亏,最开始没写,结果销售Agent在客户完全没接触过产品时也调用了跟进邮件技能,生成的邮件让人看得一头雾水。
input_schema里每个字段都要带上required标记和描述。我见过很多团队的技能参数只写类型不写含义,后果是模型经常把相似字段的值填错位置,比如把客户ID填到订单号里。最有效的缓解手段就是在参数描述里写清楚“数据来源”和“禁止编造”,这两个约束对模型的引导效果非常显著。
output_format很多人会忽略,但它在多技能串联时恰恰是保命配置。任何一个技能的输出都不应该是一坨自由文本,而应当是一份结构化的中间数据。后面章节我会单独讲一个因为输出格式不统一导致的串联事故。
2.3 参数约束与“参数幻觉”的第一道防线
技能参数最危险的问题不是模型填错,而是它一本正经地编造参数。我见过Agent在没有任何依据的情况下,给用户生成了一个完全虚构的订单号并查询成功——当然查询结果是空,但整个对话看起来非常可信。这种事发生一两次,用户对系统的信任就直接归零。
处理参数幻觉不能只靠模型自觉,要在schema层和代码层同时设卡。schema层把每个参数标注为“必须来自对话上下文、客户资料或系统数据”,代码层则要在技能执行前做前置校验:如果上游Agent传入的参数没有来源标记,直接拒绝执行并回报缺失参数。我在校验逻辑里还加了一条规则:凡是敏感操作类技能,关键ID字段必须能通过格式约束校验,比如订单号不符合系统的编号规则,整单直接打回。这套机制上线后,参数编造率从肉眼可见的高频降到偶发,再配合后面的路由与评测机制,才算是把问题控制住了。
3. 从零搭一门技能:把“跟进邮件”做成可复用的标准化能力
3.1 先把需求拆到不能再拆,再考虑写代码
拿跟进邮件这个例子来完整过一遍流程。最初销售团队提的需求是“让Agent帮我们写跟进邮件”,这个需求太模糊,直接落到技能上会变成一团浆糊。我先把需求拆成几个子问题:什么情况下算“跟进”?跟进邮件要包含哪些必要元素?发送前需要谁确认?数据从哪个系统取?
逐一回答之后,我划定出技能的行为边界:输入是客户姓名和最近沟通摘要,输出是一封符合公司模板的邮件草稿,发送动作必须由人工确认后触发。注意,在初版技能里我没有做真正的自动发送,只做到草稿生成。这个决定帮我避开了大量权限、合规和误发送问题,等邮件草稿的生成质量被业务方认可后,再考虑接发送接口。给技能划定“不做清单”,和定义“做什么”同样重要。
3.2 描述、示例与内部提示词的分工
技能内部实现我采用了一段固定提示词加代码模板的组合。固定提示词负责语言生成,代码模板负责结构保障,两者各管各的:提示词不负责决定邮件整体结构,它只负责根据客户信息和沟通摘要填充结构和润色文字;代码负责拼装subject、body、attachment_ids这些字段,保证输出永远符合output_format。
示例的选择也很有讲究。我在examples里放了三个案例:一个客户回复过邮件、一个客户完全不回复、一个客户明确表示不感兴趣。这三个case覆盖了三种截然不同的语气策略,模型根据输入特征选择匹配的案例作为参照。实测下来,有样例和没样例的质量差距非常明显,尤其在不回复客户的措辞上,没样例时模型总是写得过于客气,有样例后明显更成熟。
写内部提示词时有一条经验值得记下来:不要在提示词里出现“你是一个邮件专家”这种人格设定,而应该直接写清任务目标、约束条件、输出规范。人格设定会让模型在生成时过度发挥,产生大量装饰性词汇,而任务式提示词更容易保持稳定的输出结构。
3.3 注册与调用链路:让Agent能发现技能
技能写好后,需要在Agent侧做注册。我用的方式是维护一个全局技能索引,索引内容就是每个技能skill.yaml里的name、description、input_schema的摘要信息。Agent每轮对话开始前,系统会把技能索引塞进上下文,模型根据用户请求和索引信息判断该调哪个技能。索引不能完整展示所有技能的详细信息,否则上下文太长,所以我在索引里做了两级结构:一级是所有技能的一行摘要;二级是模型初步选定后,再加载对应技能的完整schema。
这个两级加载策略是我比较得意的一个局部优化。最开始我把每个技能的完整描述都塞进去,上下文疯狂变长,模型反而变得犹豫不决,经常在两个相似技能之间反复横跳。改成先看摘要、再加载详情之后,技能选择的准确率和响应速度同时都变好了。
3.4 跑通之后先别急着上线:验收清单
技能跑通第一版后,我列了一张验收清单,每项都不过关就打回重做。清单内容包括:技能在至少三种不同措辞的请求下都能被正确触发;在明显不属于该技能的请求下不会被误触发;必填参数缺失时能明确告知用户缺什么;输出格式在连续十次调用中保持一致;异常情况(如客户信息查询不到)有明确的返回路径而不是强行编造。
这张清单救过我很多次。最典型的案例是误触发问题:技能刚写好的时候,用户只要提到“邮件”两个字,Agent就会调用它,哪怕用户其实是在问“我昨天发的邮件为什么被退信了”。我在验收时发现这个问题,在description里补充了“仅适用于撰写并发送新邮件,不适用于查询邮件状态”,误触发率立刻大幅下降。验收测的从来不是能力上线,而是边界是否生效。
4. 技能路由与冲突消解:Agent拿什么判断该调哪门技能
4.1 两种主流路由策略:向量召回与规则预筛
技能数量一多,怎么选就变成了核心问题。我见到的主流做法有两种:向量召回和规则预筛,实际项目中我两者都用了。向量召回是把技能描述和用户请求都做embedding,算相似度挑Top K;规则预筛则是在模型判断之前,先用一套显式规则把明显不匹配的技能过滤掉。向量召回的优点是灵活,能容忍用户口语化表达;缺点是语义相近的技能会同时召回,模型需要在相似项里做选择,准确率下降。
规则预筛的好处是硬约束,杜绝低级误触发,比如用户提到“退款”时绝不调用“开发新客户”技能。但纯规则覆盖不了所有真实表达,所以我把两者组合成pipeline:规则预筛负责缩小候选集,向量召回负责把候选集排序,最终把候选集连同用户请求一起交给模型做选择。这套组合在我项目里的技能数量超过30个之后,仍然保持了可接受的选择准确率。
4.2 技能冲突:语义相似是最大的坑
当技能数量上升后,最常出现的头疼问题是两个技能听起来都像正确答案。比如“生成跟进邮件”和“修改跟进邮件模板”,用户说“帮我把上周那个邮件改改”,模型就很容易在两者之间犯迷糊。我在项目里处理这类冲突时采用了一个简单有效的原则:职责边界按数据域划分,而不是按动作划分。生成邮件管“写新内容”,修改模板管“改动存量模板”,两者操作的数据对象完全不同,只要描述里把各自操作的数据域说清楚,冲突就能化解掉大半。
实在有无法通过描述化解的强冲突,我会选择直接合并技能,而不是硬靠路由区分。合并后技能内部做分支判断,虽然单技能变复杂了,但路由层的压力显著下降。我的经验是:路由层出现的冲突,往往暴露的是技能划分本身有问题,与其在路由上打补丁,不如回去重构技能划分。
4.3 置信度阈值:什么时候该主动问用户
路由判断不可能100%准确,所以必须给模型一个“不确定时怎么办”的出口。我在技能选择环节加了一个置信度机制:当最佳候选的匹配得分和次佳候选非常接近时,不直接执行,而是生成一个确认问题问用户。看起来多了一次对话,实际上避免的是大量错误执行带来的返工成本。
这个阈值不能拍脑袋定,我是用历史样本算出来的。取一批已经标记了正确技能的用户请求,跑一遍路由打分,画出得分差的分布,选分界点作为阈值。后续运行过程中再根据线上准确率数据微调。这类机制的成本很低,但对体验提升明显,用户宁可被多问一次,也不愿意看到Agent自作主张做错事。
5. 真实排查链路复盘:参数幻觉、状态污染与外呼依赖失败的完整处理过程
5.1 第一个坑:参数幻觉,日志里看到系统自己编了个订单号
这是一个让我印象很深的case。用户问“帮我查一下订单XS20240301到哪了”,Agent正常抽取了订单号,返回了物流信息,一切看起来正常。但后来我们通过日志回放发现,订单号解析正确是因为用户凑巧说全了;在另一个测试用例里,用户只说了“我上周买的那个东西”,Agent直接自己编造了一个形如XS20240311的订单号,并“成功”查到了结果。
排查链路是这样的:先把模型输入输出和技能输入输出全量打了日志,定位到参数是从模型生成里抽取的,而不是从对话状态里匹配的;然后复现测试,发现模型在参数缺失时倾向于“补全”而不是“询问”;最后修复方案分两层——代码层在技能执行前做订单号格式+数据库存在性校验,不通过就返回明确的缺参提示,模型层在技能描述里追加一句“订单号必须由用户提供或从订单系统查询获得,禁止生成”。修复后两周内同类case没有再出现。核心结论是:不要指望模型诚实承认自己不知道,要在技能执行链路上把它卡住。
5.2 第二个坑:技能串联时的中间状态污染
我的系统里有一条典型链路:用户要求“给这周的三个意向客户各写一封跟进邮件”。系统先把客户列表查出来,再逐个调用跟进邮件技能。问题出在第二个客户开始,邮件正文里偶尔会出现第一个客户的姓名和公司名。查日志发现,原因是两个技能共用了同一个上下文缓存,第一封邮件的完整输出残留被第二封邮件技能当作参考信息读取了。
修复方案说起来不复杂:每个技能调用时显式清空与当前任务无关的缓存字段,并把output_format渲染后的结果与内部推导过程严格隔离。我给技能执行加了一个“干净上下文”的约束,内部处理时用的临时变量在技能结束时统一回收,只有声明在output_format里的字段能传递到下一个环节。这条经验后来也写进了团队的技能开发规范:技能输入输出之间只允许通过显式字段传值,不允许依赖任何隐式状态。
5.3 第三个坑:外部依赖不可用时的优雅降级
技能依赖的很多外部接口不是每一次都稳定。有一回订单查询服务临时抖动,技能直接抛异常,Agent随之给用户回了一句“系统错误,请稍后重试”。用户体验很差,但更糟糕的是日志里连具体是哪个环节出错都看不清。我在排查中发现,技能异常处理完全没有分层:网络超时、数据不存在、权限不足全都混成一种错误返回给上层。
我把错误处理重新做了结构化分类。第一类是可重试错误,比如网络超时,加入自动重试,最多三次。第二类是业务错误,比如订单不存在,直接返回“未查询到对应订单”并建议用户核对信息。第三类是权限错误,返回“当前账号无权限查看该数据”。在技能描述里也同步说明了这些返回路径,让模型知道当技能返回特定错误码时,应该给用户怎样的回应。这个梳理做完后,技能面对异常的应对质量明显提升,日志可读性也大幅改善。
5.4 排查方法论沉淀:从结果倒推链路节点
三个坑排查完之后,我给团队沉淀了一套排查方法论。核心是:定位一次Agent异常行为,先画执行链路,标识每一个可能篡改数据的节点,再通过日志逐节点回放。技能层的错误绝大多数不是模型单一原因,而是链路中某个节点悄悄改变了数据形态。日志必须覆盖四个层面:模型原始输出、技能输入参数、技能执行中间态、技能最终输出。这四个层面缺一个,排查效率都会大打折扣。
我现在在项目里强制要求:任何技能上线前必须打印这四个层面的日志。加了这条硬性要求后,新问题的平均定位时间从以小时计降到了以分钟计。这也是我把这一节写这么长的原因——排查能力不是靠天赋,是靠可观测性设计。
6. 技能上线后的质量运营:评测集、行为回放与野生技能治理
6.1 离线评测集:先让技能过一遍“模拟考”
技能是代码加模型行为的混合体,不能只看代码测试通过就放行。我给每个技能建了一套离线评测集,样例主要来自两个渠道:一是上线前的设计案例,二是线上真实请求的回放。评测时把每个样例输入给技能,检查三件事:触发是否准确、输出格式是否合规、内容质量是否达到标注标准。
内容质量我采用“标注示例+人工打分”的方式。每个样例都有一个人工写好的参考答案,技能生成结果与参考答案做相似度评估,再由业务同学随机抽检打分。这个流程看起来重,但对稳定性要求高的技能非常值得。我见过不少团队忽略了这一层,技能每次改完都凭感觉上线,结果行为时好时坏,业务方对Agent的信任会快速流失。
6.2 线上回放与标签体系:让每次调用都留下账本
线下评测覆盖不了长尾场景,更真实的反馈来自线上。我在技能调用链路里加了一套回放系统:技能每次执行的所有关键数据全部落库,然后通过一个管理页面可以按时间、用户、技能维度回看每次调用的完整链路。有了这套账本后,我们每周做一次线上行为抽检,把“触发正确”“触发错误”“漏触发”“输出质量低”这些标签打到具体case上。
标签体系最有价值的不是统计数字本身,而是它能持续告诉我们技能描述哪些地方产生了理解偏差。比如某周大量出现“漏触发”标签,我一看案例全跟“用户用缩写提到业务名词”相关,就会在对应技能的description里补充缩写说明。这个反馈闭环让技能质量可以持续改善,而不是上线后就听天由命。
6.3 野生技能治理:谁能新增技能,必须有流程
最后提醒一个组织层面的坑。技能体系成型后,大家会越来越愿意往里面加新技能,这是好事,但没有治理就会乱套。我在项目里见过最夸张的情况是,团队里三个人各自建了一个功能几乎一样的技能,只是名称和描述风格不同,Agent的选择准确率直线下降。后来我做了一套治理规则:新增技能必须先在技能索引里查重,确认没有功能重叠;必须写明与其他技能的边界差异;必须附带五个测试样例才能提审。
这套规则后来被人开玩笑叫作“野生技能驯化流程”。虽然让新增技能的流程变重了,但换来的是技能索引的长期整洁。我个人的体会是,Agent项目做到后期,拼的不是写出一个惊艳的技能,而是能否让几十个技能长期稳定地协同工作。技能数量上了规模之后,治理成本甚至会超过开发成本,早一点建立流程,后期会省下很多精力。
如果你现在正被Agent“行为不稳定”折磨,我的建议是先别急着花更多钱调更大模型,先回头看看技能层是不是该好好整理一遍了。把能力的边界划清楚,把参数的来路卡严实,把每一个失败路径都留下日志——这一套做完之后,你会发现模型还是那个模型,但Agent可靠多了。