Agent Skills实战指南:从写死Prompt到技能化封装
发布时间:2026/10/13 16:21:24 锦皓数字建站

我早年做Agent应用最痛苦的不是模型能力不够而是同样的操作换个场景就得把一大段System Prompt改来改去。后来团队里一个老哥引入了Agent Skills技能化封装这套玩法把一个个能力拆成独立的、带说明、带校验、带测试的技能包维护成本和稳定性直接上了一个台阶。这篇文章我就把这段实战里沉淀下来的设计思路、代码结构、踩坑记录和协作规范全部摊开讲适合正在用Agent做复杂任务的开发者也适合想从写死Prompts升级到沉淀技能资产的团队参考。1. Agent缺的不是工具而是可被管理的技能1.1 为什么长Prompt方案会走到天花板早期做智能体大家最习惯的做法是在系统提示词里塞一大段功能描述你是客服助手、你会这样回复、你调用这些接口、你注意这些边界……刚开始还好等能力变多就崩了。我做过一个企业内部的知识管理Agent一开始只有搜索文档和总结摘要两个能力。System Prompt大概两千字模型表现很好。后来陆续加了提取项目周报生成会议行动项关联相似问答导出客户反馈……系统提示词膨胀到一万五千字左右。问题接踵而至上下文被提示词占满真正放对话历史和工具结果的窗口被挤压模型开始忘事。指令之间互相污染。比如格式使用Markdown这条全局规则会让导出客户反馈到Excel的调用也硬生生套上Markdown表格解析器直接报错。每次改动都是一次全量回归。改一个技能的行为要重新验证其他所有技能没有被影响光是回归测试的时间就让人崩溃。长Prompt本质上是一个把所有规则写死在一个玻璃罐里的方案。它没有边界没有模块没有版本自然也就没有管理。1.2 Skills真正解决的三个层次的问题Agent Skills下文简称Skill的思路其实很简单把Agent做某一件事所需的说明、参数定义、执行逻辑、校验规则全部打包成一个独立目录让Agent在需要时按名字、按描述、按依赖关系去加载。我在实际项目里体会到它解决的是三个层次的问题第一层上下文隔离。每个Skill只在被调用时才注入相关说明调用完就释放。系统提示词可以保持精简模型永远专注于当前任务不至于被一堆无关规则干扰。第二层行为标准化。一个Skill不再依赖提示词里你最好这样做的模糊约束而是通过结构化的输入输出定义、校验脚本、示例数据让Agent的行为稳定在一个可接受的范围内。团队的另一个同事接手时不需要猜这个技能到底允不允许自由发挥打开目录就知道边界在哪。第三层能力资产化。Skill是文件系统里的实体天然适合做版本管理、代码评审、单元测试和灰度发布。技能从一个只能靠口头传递的经验变成了可以被组织沉淀和审计的资产。1.3 什么时候不必上Skills这里说句实话不是所有Agent场景都需要Skills。如果你只是做一个Demo对话里加个搜索工具两三百字的提示词能跑通那就别过度设计。Skills的引入有成本——目录结构、加载机制、测试用例、调试链路每一项都要花时间。我个人的判断标准是三条这个能力是否会被多个Agent复用这个能力的逻辑是否会持续迭代比如格式要求总变、边界经常调这个能力是否涉及外部副作用发邮件、写库、调接口需要严格约束。三个条件至少命中两个我才建议把它做成Skill。否则宁可用普通工具函数也别为了形式主义搞一堆文件夹。2. 一个可复用的Skill应该拆成什么文件2.1 我推荐的Skill目录结构与理由先给大家看一个我在某开源Agent框架里常用的Skill目录结构这里为了叙述方便我给框架起个代称叫AgentKitskills/ └── extract-action-items/ # Skill唯一标识建议用短横线命名 ├── SKILL.md # 给Agent看的功能说明书 ├── schema.json # 输入输出的JSON Schema ├── scripts/ │ ├── extract.py # 核心执行逻辑可运行、可测试 │ └── validate.py # 结果校验脚本 ├── examples/ │ ├── input_1.txt │ ├── output_1.json │ └── test_run.py # 单元测试入口 ├── references/ # 领域知识、规则文档仅在需要时被加载 │ └── meeting-format.md └── meta.yaml # 版本、作者、依赖、权限声明这个结构不是我拍脑袋定的每一样都有它的用途SKILL.md决定Agent什么时候该用这个技能、怎么用这是提示词层面的引导schema.json决定数据长什么样这是结构化层面的约束scripts/里是真正可执行的代码而不是空口指令examples/用来做回归测试防止改一个正则下一周所有输出全崩references/放长文本知识避免每次都把所有细节灌进上下文meta.yaml则让这个Skill可以被索引、被依赖解析、被权限过滤。2.2 SKILL.md给Agent看的说明书SKILL.md是这个技能包的门面和入口也是大多数人在实践中写不好的地方。我见过很多人把SKILL.md写得像一本操作手册动辄三五千字从背景讲到原理再讲到实现细节。这其实是把给模型看的说明和给人看的文档混为一谈了。我实践下来觉得好的SKILL.md应该满足三个原则开头三句话必须能触发匹配。第一段直接用当用户想要……句式列出使用场景让Agent在扫描技能库时迅速命中。主体只讲使用方法不写历史渊源。比如输入格式是什么、输出要求是什么、示例有哪些、失败时怎么办全部用条目化、代码块、表格来呈现。必须写明什么时候不要用。这一点最容易被忽略却最能防止误触发。我见过一个技能因为描述里写了个会议结果用户说我今天不想开会Agent就自动提取起行动项来了。有了不要用的条款类似情况会少很多。下面是一个SKILL.md片段节选Skill:extract-action-itemsWhat it does: 从会议记录或任务讨论文本中提取明确的Action Items行动项输出负责人、截止时间、任务描述。Use when:用户提供会议纪要、聊天记录要求提取任务/行动项/待办。用户粘贴一大段对话要求整理出下一步要做的事。Do NOT use when:用户只是总结会议内容不需要提取具体任务。输入内容中没有明确的待办含义模型应先判断再决定是否调用。Input:textstring必填原始文本内容todaystring可选参考日期用于计算相对时间格式 YYYY-MM-DDOutput:JSON对象字段如下action_items数组action_items[].taskstring任务描述action_items[].ownerstring负责人从原文中推断无法推断则 nullaction_items[].due_datestringYYYY-MM-DD可为 nullExamples:输入今天张三说周五前要完成接口联调李四负责收集测试数据。 输出{action_items: [{task: 完成接口联调, owner: 张三, due_date: null}, {task: 收集测试数据, owner: 李四, due_date: null}]}你发现没有这份说明书里没有一个多余的形容词全部是可验证的事实。这就是我要说的教程式文档是写给人的SKILL.md是写给模型的写完之后最好自己让不同模型跑一遍看看它们能不能准确理解边界。2.3 scripts目录可执行的逻辑与校验如果说SKILL.md是告诉Agent该怎么做那scripts/就是真的去做并且做完后检查自己做得对不对。以extract_action_items.py为例我会让它接收一个JSON包含text和today字段运行某种轻量级的规则正则可选模型调用然后输出一个JSON。这一层实际在做什么呢第一负责格式强制。模型直接输出的内容经常是不可靠的比如日期格式五花八门下周五明天2024/6/1六月一号。我会在脚本里做归一化用dateparser解析相对时间统一转成YYYY-MM-DD。有了这一步下游集成再也不用写一堆兼容逻辑。第二负责后置校验。脚本的末尾会检查输出里的每个task是否非空、每个due_date是否符合格式不满足就返回结构化错误。第三给模型一个校验失败重试的钩子。Agent在执行流程里看到脚本返回的error就可以带着错误信息重新调整自己的输出而不是静默失败。这类代码说到底不是复杂的系统但它的存在让Skill从提示词层面变成了工程层面可测试、可回滚、可观测。2.4 为什么要把知识和代码分离很多Skill的原始需求来自文档公司报销制度、产品规格说明、部署手册……有人图省事直接把文档内容全文粘贴进SKILL.md。这个做法在短期看着能用长期一定会出问题。文档动辄上万字塞进上下文后模型确实知道了但也付出了巨大的注意力代价——检索到的关键信息会被淹没在大量无关段落里。我后来形成了一套约定只有决策规则和操作边界进SKILL.md比如金额超过5000元需要总监审批。原始文档放references/目录文件头里写明摘要。如果某些章节经常被引用用split脚本按主题拆成多个小文件Skill被触发时只按需加载对应文件。这有点像是给Agent做了一套参考书管理SKILL.md是目录和摘要references/是可查阅的原文。需要更多细节时Agent自己决定去翻参考书。这个设计在上下文预算紧张的Agent系统中尤其重要。3. 从零手写一个会议纪要与行动项提取Skill3.1 先定义输入输出再写提示词直接进入正题——我带大家完整走一遍如何从零写一个真正能用的Skill名字就叫extract-action-items。很多新人一上来就写SKILL.md这个顺序是错的。我个人的习惯是先在纸上或者一个临时文档里把输入输出定义清楚再去想提示词。输入输出定义是本阶段最重要的事我的实操方式如下第1步确定触发场景。这个Skill解决什么问题我设定的是用户可能会贴会议纪要、微信聊天记录、工作邮件然后说帮我提取行动项。第2步画出理想输出样例。我先自己想好一次成功调用的输出长什么样。对于提取行动项理想的输出是一组JSON数组每个元素包含任务描述、负责人、截止时间。出于通用性的考虑我会让owner和due_date允许为null因为很多记录里确实没有明确信息。第3步写下输入约束。输入除了text外还可以接受一个today参数用来解析明天下周这类相对时间。第4步反向找边界。什么情况这个Skill应该拒绝比如用户只给了一句张总让大家加油干没有任务、没有负责人、没有时间这时候宁可不识别也不要硬编造一个加油干的行动项。等你把这些都写在文档里你就会发现SKILL.md的初稿其实已经成型了根本不用硬编。3.2 把模糊要求固化成可被校验的JSON Schema接下来把输入输出定义写成schema.json这一步的价值是让Agent的输出可以被程序校验而不是看起来差不多就行。下面是一个精简版{ type: object, properties: { action_items: { type: array, items: { type: object, properties: { task: { type: string, minLength: 1 }, owner: { type: [string, null] }, due_date: { type: [string, null], format: date } }, required: [task, owner, due_date] } } }, required: [action_items] }我在这里着重强调一个日常容易被忽略的细节due_date的格式不要只写string要在说明里明确到YYYY-MM-DD。因为模型输出2024年6月1日6/1/2024下周都符合string类型但下游系统根本没法统一解析。加上format: date后配合脚本校验能把大部分不规范的输出拦在前面。我还建议在schema旁边放一个小的校验脚本# scripts/validate.py import json, sys, re def validate(raw: str): data json.loads(raw) if isinstance(data, dict) and action_items in data: items data[action_items] else: items data for item in items: assert task in item and len(item[task]) 0, task missing if item.get(due_date): assert re.match(r^\d{4}-\d{2}-\d{2}$, item[due_date]), bad date return True if __name__ __main__: validate(sys.stdin.read())这段代码看起来简单但是给Agent提供了真正的失败反馈。模型在运行时如果产出了不规范的日期Agent就能读取校验报错然后重新修正输出。没有这层闭环你看到的错误就只是一个模型输出不标准的玄学问题。3.3 单元测试不是可选而是必须我前端时间接手团队里一个旧Skill它没有examples/目录。我改了里面一个正则表达式以为只影响了金额提取结果下游所有日期提取输出全面崩盘。没有测试我只能靠肉眼一条条核对样本一下午就没了。经此一役我要求所有Skill必须带单元测试。一个最朴素的测试框架长这样# examples/test_run.py import json from scripts.extract import extract cases [ { input: {today: 2024-06-01, text: 今天张三说周五前要完成接口联调李四负责收集测试数据。}, expected: { action_items: [ {task: 完成接口联调, owner: 张三, due_date: 2024-06-07}, {task: 收集测试数据, owner: 李四, due_date: None} ] } }, { input: {today: 2024-06-01, text: 下周一把部署文档发给客户王五跟进。}, expected: { action_items: [ {task: 把部署文档发给客户, owner: 王五, due_date: 2024-06-03} ] } }, { input: {today: 2024-06-01, text: 这次会议没有明确任务只是同步进度。}, expected: {action_items: []} } ] for i, case in enumerate(cases): result extract(case[input][text], case[input][today]) assert result case[expected], fcase {i} failed:\n{result} print(all tests passed)你注意到第三组用例的价值了吗什么都没有也是一种合法输出。我的经验是最容易让Skill泛滥的误区就是模型老想凹点什么出来把一句客套话也硬解释成行动项。测试用例里显式写上应该输出空数组的样例比在提示词里骂不要瞎编一百遍都管用。3.4 运行机制Agent如何决定调用这个SkillSkill写好了下一个问题是Agent在什么环节会看到它、为什么会决定加载它、加载之后又怎么执行在AgentKit这类框架里通常有三条路径路径A声明式工具注册。Skill在meta.yaml里声明自己提供哪些工具函数Agent初始化时扫描skills/目录把每个Skill暴露的scripts/*.py注册成可调用工具。调用时由Agent根据函数名和描述决定要不要用。路径B动态意图匹配。Agent收到用户消息后先做一轮轻量级意图匹配把用户消息和每个Skill的SKILL.md的开头触发条件做语义相似度比对。超过阈值就把对应Skill注入上下文再由Agent决定具体怎么用。路径C事件驱动调用。某些场景下Skill不是由模型主动选择的而是由流程引擎直接调度的比如收到邮件Hook之后自动触发整理邮件待办的Skill。我个人认为在生产环境里最稳的是合并方案日常优先走B让模型自己找技能但涉及外部副作用发送、写库的技能必须要走A和C用白名单严格控制调用入口。模型主动去调用发邮件类技能在正式系统里有点太野了这个风险值得防御。4. 多Agent场景下Skills的加载、路由与上下文取舍4.1 是全部加载还是按需加载在单Agent场景Skills设计主要考虑这个Agent会不会用。到了多Agent场景问题就变成几十个Agent、成百上千个Skills是否全部都放在候选池里在项目早期我是直接把所有Skills的SKILL.md喂给一个集中式的路由模型让它从中选择合适技能。后果是上下文涨到好几万token路由模型自己都开始看不过来。相似技能的干扰明显增加比如提取行动项和提取风险项经常被混淆。权限控制极难做所有Agent都能看到所有技能包括一些内部管理类技能。后来我们把架构改成了按Agent角色分配技能池。每个Agent在meta.yaml里声明自己需要的技能域比如行政助手Agent只能看到会议记录类、报销类、日程类技能。这样一来路由模型的候选集从几百变成几十准确率显著提升上下文占用也降下来了。4.2 让Skill自己声明依赖避免上下文爆炸第二个教训关于依赖。有一个很复杂的生成项目周报Skill它本身需要先调用获取Git提交记录技能再调用提取工作项技能最后用格式化文本技能做收尾。最初我们在Agent启动时把这个Skill的所有依赖技能全部加载进上下文结果一个周报任务占掉了一半的可用窗口。问题出在依赖声明的粒度上。我们后来在meta.yaml里专门加了一个requires字段name: generate-weekly-report version: 1.3.0 requires: - skill: fetch-git-log load: runtime - skill: extract-work-items load: runtime - skill: format-report load: request区别在哪里runtime表示Agent先加载这个技能的函数名和最小描述两三百token实际执行时才去读完整SKILL.mdrequest表示只有当Agent明确要使用它、且用户确认了这个操作之后才会注入详细说明。这个改动非常有效。一个复杂Skill的完整描述从几千token降到了几百token只有真正执行到某一步时才临时加载所需细节。这跟我们平时看依赖库的机制很像不到调用不加载全部文档。4.3 权限粒度谁能让Skill真正执行有句话说得好评估一个Agent系统不是看它能调用多少工具而是看它在不该调用工具时忍不忍得住。常见的权限控制维度我总结下来有四层技能可见性Visibility某个Agent能不能在技能列表里看到这个Skill。通常用来避免无关误用。调用权限Execution能不能真正执行Skill里的脚本。牵扯外部操作的Skill这里建议加人工审批钩子。数据域Data ScopeSkill能访问哪些数据源、哪些字段。同一个查询客户信息Skill客服Agent只能看到订单和售后运营Agent才能看全量画像。参数边界Parameter Policy就算是执行同一个Skill也限制某些参数值。比如发送通知Skill只允许发到内部渠道不允许向外部号码发短信。我见过一些团队所有Skill默认全开结果某个Agent在测试时候自动调用了批量删除归档技能。幸好当时下游有二次确认不然后果很麻烦。所以我的建议是Skill默认不可见、不可执行需要谁用给谁开白名单宁可配置麻烦一点也不能把执行权限默认放开。4.4 恶意或脏Skill的防御我踩过的一次教训作为技术作者我还是得提一个很多教程不会讲的安全问题——Skill本身也可能是攻击面。有一次我们在某个Agent平台导入了一个从开放仓库下载的天气查询Skill从SKILL.md看完全正常。结果运行监控发现这个Skill的scripts/weather.py里有一小段混淆代码每隔一段时间会往某个外部地址发送一段内部环境变量。你根本防不住所有开源Skill都干净尤其是那种会自动更新脚本的自动升级型Skill。从那次之后我们定了几条铁律所有外部引入的Skill必须在隔离容器里先跑一遍静态扫描检查有没有网络请求、shell调用、文件系统高权限操作。禁止Skill自带自动更新逻辑。要升级就走团队内部的版本发布流程。技能脚本里对内部服务的地址访问全部走环境变量注入的只读配置不允许Skill自己读取任意环境变量。运行时的出网权限默认关闭需要出网的Skill单独申请并且记录每次出网的调用链。这听着很重但一旦你的Agent系统开始接入第三方技能你就是在和生产环境里运行不可信代码。能用白名单解决的问题就不要依赖模型的安全判断。5. 团队协作里那些困扰人的Last Mile问题5.1 版本不一致每个人本地Skill各怀鬼胎几个人的小团队里最容易出现的问题就是你改了我没用我改了你没同步。我们当时的协作模式是这样的每个人在自己的Agent项目里都有一份skills/目录为了方便直接拷贝到手机里改或者在另一台电脑上调试。结果A同学把extract-action-items的日期解析从dateparser换成了自写正则B同学还在用旧的。两个人跑同一个测试输入出来完全不同的结果。排查了大半天才发现是版本不一致。后来我们把Skills从单项目里抽出来单独建了一个skills仓库用包管理工具发布版本号。各项目通过lockfile锁定版本升级时统一走依赖更新命令不允许手动拷贝文件。跟管理第三方npm包的方式一样这套机制稳定解决了同步问题。5.2 命名混乱技能库变成垃圾堆另一个值得一提的教训是命名规范。技能库一开始很清爽后来大家图省事什么名字都往里面丢meeting、meeting-better、meeting-final-v2、提取会议行动项、GetActionItemsFromMeeting……这种混乱局面带来的直接后果是Agent路由模型在匹配时经常分不清该用哪一个甚至同时命中多个引发奇怪的执行链。我们后来确定了三条规定技能名一律使用动词-名词的短横线格式比如extract-action-items、generate-weekly-report、send-reminder。一个能力只保留一个技能要改进就在同一个目录里改版本不允许新建xxx-v2来绕过版本管理。SKILL.md的Use when描述必须和技能名强相关如果技能名叫send-reminder那就不要在里面顺带做整理日历的功能。技能库其实很像函数库命名和职责单一性是第一位的。麻烦往往不是技能太少而是相似技能太多。5.3 从离线文档到内部市场的演化随着技能数量过百我们已经没法靠一个README来宣传有哪些技能可用了。大家经常想找某个能力发现有十个类似的还得逐一打开看。后来我们搭了一个极简的内部技能市场每个Skill在meta.yaml里登记摘要、标签、维护人、版本、最近使用次数和成功率Agent开发者在选技能时能直接按标签检索、看成功率和最近更新时间。数据非常朴素但解决了一个真实痛点把团队经验透明化而不是每次都要去问人。有了市场之后还顺手验证了一个观点使用频率和成功率高的Skill往往都是那些输入输出定义清晰、测试用例完整的。反例是那些SKILL.md写得天花乱坠、代码没测试的上线不久就被反馈淹没。数据不会骗人技能质量和工程质量高度相关。6. 回看这份设计我会在下一个项目里改掉什么写到这里把整个Skills从零到规模化落地的过程回忆了一遍我还是想诚实地指出自己过去做得不够好的地方也给大家提几个改进方向。第一我早期太关注Skill本身写得好不好忽略了Agent调用技能的观测性。当时只在日志里记录调用了哪个Skill、耗时多久。后来加了调用链ID、输入输出摘要、校验失败率排查问题的速度快了一个量级。新的项目里我会把观测埋点当成Skill模板的一部分而不是后补。第二静态的SKILL.md可以做得更动态。我现在看到不少新实践是把SKILL.md里的规则分成全局静态约束和运行时策略变量两部分。比如审批流程这个Skill审批阈值写成一个可配置变量运维上直接改配置就能调不用重发Skill版本。比起死板地改代码这个思路在多变业务场景里更实用。第三所有Skill都要有一个明确的退出条件。不只是任务完成还包括用户连续拒绝三次置信度过低严重违反约束时如何安全停止继续调用。我们在某个自动处理工单的Agent上因为没有设计好退出条件模型陷入提取→尝试执行→失败→再提取的循环白白浪费了很多调用额度。这也是我在下一个项目里准备最先补上的机制。如果你正在规划自己的Agent技能体系我的建议是先让一个小而精的技能跑通全流程再逐步扩展到几十个技能。技能资产的真正价值不在于数量多而在于每一次运行都稳定、每一次变更都可追踪、每一次失败都能复现。等你做到这三点你会明显感觉到Agent从玩具变成了生产力工具。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。