Skills机制深度解析:从原理到实战,手把手实现周报生成Skill
你如果最近关注大模型应用开发一定避不开“Skills”这个词。我去年开始折腾Agent项目时对它的理解一度非常粗浅以为就是给模型塞一段提示词后来做多了才发现Skills机制解决的是Agent落地过程中一个很要命的问题模型很聪明但它不知道“按你们公司的规矩做事”。这篇文章我会把Skills机制从原理讲到实战最后带一个能直接跑的“周报生成Skill”项目。不写虚的全部是能落地、能复现的东西适合正在做Agent应用、或者打算把大模型接入工作流的开发者。1. Skills机制的核心原理与设计思想1.1 为什么Agent需要“技能包”大模型本身的知识储备很强能写代码、能分析文档、能回答各种问题但它在面对一个具体业务场景时往往会“有劲使不上”。为什么因为业务场景要求的不只是知识而是一套固定的做事流程、输出格式、判断标准。举个例子你让模型帮你写周报它确实能写但写出来的可能是散文风格完全没有结构甚至分不清工作和成果的区别。这时候你当然可以在Prompt里把要求全部写清楚但问题是每次对话你都要重新写一遍吗这就是Skills机制存在的意义把一类任务的执行方案固化成一个可复用的“技能包”。它本质上是给模型预装了一套“岗位说明书标准作业程序”让模型在碰到对应场景时能直接按这套预设来执行而不是每次都在对话里临时交代。我见过一个特别贴切的类比模型像一个聪明但没经验的新人你给他讲一遍方法论他能理解但下次遇到类似的活儿他还是会按自己的习惯来。Skills就像是一本新人手册把流程、格式、禁忌全部写清楚了新人拿到手册就能干出八九不离十的活。1.2 一个Skill包的内部结构从制品形态上看一个Skill包通常包含三类内容元信息、指令主体、示例数据。元信息负责告诉系统“这个技能是干什么的、什么场景下该用它”指令主体负责告诉模型“拿到这个任务后你该怎么执行”示例数据则负责给模型“打样”让它理解优质的输出长什么样。我一般会把Skill包组织成下面这种目录结构skill-weekly-report/ ├── skill.yaml # 元信息名称、描述、触发条件、版本 ├── SKILL.md # 指令主体角色设定、执行步骤、输出要求 └── examples/ ├── good_01.md # 高质量输出示例 ├── good_02.md # 另一个高质量输出示例 └── bad_01.md # 典型错误输出示例有人会问为什么不能把所有内容塞进一个文件分开存放的好处在于系统加载Skill时可以根据需要做“分层注入”有些场景只需要元信息和指令示例可以在需要时再补进去。而且从维护角度讲指令和示例分开改回归测试起来也更清晰——你改了示例不用动指令改了指令不用动示例。1.3 按需加载与自动路由Skills机制真正厉害的地方不是“有技能包”而是“按需加载”。大模型有一个现实约束上下文窗口是稀缺资源。如果你把20个技能包全部塞进上下文哪怕每个只占500个token一次对话也要吃掉1万token的空间。这不仅仅是成本问题更严重的是无关信息会干扰模型的注意力导致回答质量下降。所以成熟的Skill机制通常包含一个路由层。系统根据用户当前的输入先判断命中了哪个或哪几个Skill再把对应的技能包注入上下文。路由判断的依据第一重是元信息里的description和triggers做关键词匹配第二重可以做语义匹配比如用embedding计算用户输入和技能描述之间的相似度。我实际测试下来的感受是路由这层做得再粗糙也比“全量灌入”要好得多。哪怕是纯规则匹配只要触发词的覆盖面够全在多数场景下都够用了。后面我会专门演示一个带路由的完整实现。2. 规划一个高质量Skill的四个关键点2.1 场景收敛一个Skill只解决一类问题我见过最典型的坑是想把一个Skill做成“万能工具箱”。比如有人写了一个“数据处理助手”的Skill既能清洗表格、又能画图表、还能写SQL查询看起来很强实际用起来模型经常搞混——用户问“帮我把这列去重”模型却开始回答“我可以帮你做以下十种数据处理操作”。原因在于Skill本质上是给模型建立一种“强约束”而约束的前提是边界清晰。一个Skill只解决一类问题模型反而更容易学会。你写“SQL查询助手”就只负责把自然语言转成SQL你写“周报生成器”就只负责把工作事项结构化。场景收敛之后指令能写得非常聚焦示例也能覆盖到关键变化模型的表现才会稳定。我个人的建议是判断一个Skill是否需要拆分就看它有没有两类差异很大的输出格式要求。如果同一个技能需要输出JSON又需要输出自然语言段落大概率应该拆成两个。2.2 指令编写角色、流程、约束三层缺一不可指令主体是Skill包的核心我建议按三层结构来组织第一层是角色设定。告诉模型你要扮演什么角色这个角色具备什么背景。不要小看这一层角色设定能显著影响模型的语气和专业度。比如“你是一名具备5年经验的互联网项目管理助理”和“你是一个周报生成机器人”同样的任务输出质量完全不一样。第二层是执行流程。把任务拆解成明确的步骤让模型按顺序执行。周报生成可以拆成“先从素材中提取关键事件、再按重要度排序、再按模板填充、最后检查格式”。步骤越具体模型的执行力越强。第三层是输出约束。这一步最关键主要包括格式要求、字数范围、语言风格、禁忌事项。我通常会明确写“禁止使用夸张形容词”“不要编造未提供的事实”“如果你认为素材不足请先向用户提问而不是强行生成”。这些约束能有效压制模型跑偏的可能性。2.3 示例要给“好例子”也要给“坏例子”很多人在做Skill的时候只给正面示例觉得让模型看到“标准答案”就够了。但我做了大量测试后发现负面示例的作用往往更大。因为大模型在生成时非常擅长捕捉“期望”的输出但“什么是好”可以有多种解释而“什么是绝不能出现的”通常更具体、更容易被模型记住。我在做SQL生成Skill的时候加了一个坏例子用户问“查询最近30天的订单”模型生成了WHERE order_date BETWEEN NOW() - INTERVAL 30 DAY AND NOW()这个写法本身没错但在订单表只有当天数据入库的库里这种查询会漏数据。把这个坏例子放进去并注明原因后模型后续输出明显更谨慎了。示例数量我建议控制在2到4个优质示例太多会挤压上下文太少则覆盖面不够。另外示例必须带上简短说明告诉模型“这个例子为什么好”“那个例子为什么不行”否则示例就只是干巴巴的文本模型未必能提炼出规律。2.4 元信息是路由命中的关键元信息是整个Skill包的“入口”它直接影响系统能不能在正确的时机触发这个Skill。很多人写完指令主体后随便写一个description结果用户实际使用时Skill经常不生效。问题八成出在元信息写得不够精准。我常用的写法是description里面包含“任务类型描述典型触发词适用场景”。举一个实际的例子如果我只写“生成周报”模型在用户说“帮我总结一下这周干的活”时可能就匹配不上。但如果description写的是“将用户提供的工作事项列表整理为标准周报。当用户提到周报、工作总结、每周汇报、本周工作等场景时使用”命中率会明显提高。triggers列表也要尽量覆盖同义表达和简写形式中文场景下尤其要留意口语化说法。我见过一个很搞笑的案例一个“日程安排”的Skilltriggers里只写了“日程”“安排”“排期”用户说“帮我把明天的时间排一下”结果没命中。等到把“排时间”“约会议”“计划明天”这些口语表达补进去之后命中率一下子就上来了。3. 从零实现一个“周报生成”Skill这一节我会完整演示一个可直接运行的“周报生成Skill”项目。整体分四步设计目录结构、编写Skill内容、实现路由加载、验证效果。3.1 项目目录规划与依赖先规划一个干净的目录结构。我在实战中习惯把“技能包内容”和“调度代码”分开这样技能包随时可以热更新不会影响代码主体。weekly-report-skill/ ├── skills/ │ └── weekly_report/ │ ├── skill.yaml │ ├── SKILL.md │ └── examples/ │ ├── good_01.md │ ├── good_02.md │ └── bad_01.md ├── loader.py ├── main.py └── requirements.txt依赖非常少只需要openai作为LLM调用客户端、pyyaml用来解析skill.yaml。如果你想做语义路由可以再补一个sentence-transformers但基础版我们先用关键词匹配实现。3.2 编写skill.yaml与SKILL.md先看skill.yaml这是整个技能包的入口负责给路由层提供信息。name: weekly_report description: 将用户提供的工作事项列表整理为标准周报。 当用户提到“写周报”“周工作总结”“每周汇报”“本周干了什么” “这周的工作”“帮我把工作梳理一下”等场景时使用。 version: 1.0.0 author: zhang_san triggers: - 周报 - 周总结 - 工作总结 - 每周汇报 - 本周工作 - 梳理工作这里的description我故意写得很“啰嗦”把各种说法都包进去。有人会担心这种写法不优雅但实测表明路由匹配阶段多几个同义表达命中的概率会提高很多。路由层本来就是做筛选的不是做语义精读的覆盖度优先是对的。再写SKILL.md这是指令主体。# 周报生成技能 ## 角色设定 你是一名具备5年经验的互联网行业项目管理助理擅长从零散的工作记录中提炼重点并按标准格式输出周报。 ## 执行步骤 1. 仔细阅读用户提供的本周事项列表提取关键事件。 2. 将事件按“目标-行动-结果”的结构整理突出可量化的成果。 3. 按以下模板输出周报字数控制在200字以内。 4. 如果用户提供的信息不足以支撑完整周报先列出缺失项并向用户提问不要自行编造。 ## 输出模板 ### 本周核心成果 - 成果1尽量包含量化数据 - 成果2 ### 本周关键行动 - 行动1 - 行动2 ### 问题与风险 - 需要协调的事项 ## 硬性约束 - 禁止使用“非常”“十分”“极大地”等空泛形容词改用具体数据说明。 - 禁止编造用户没有提供的事实。 - 自成段落不要使用markdown表格。 - 信息不足时优先提问而不是猜测。这一版指令我特意把硬性约束单独拎出来放在最后原因是模型在长文本里对“最后几条指令”的记忆权重会更高。这不是什么玄学而是模型注意力机制的客观特性——靠后的内容往往对生成结果影响更大。利用这个特性把最关键的约束放在末尾实测效果确实比散落在正文里要好。3.3 加载与路由调度的代码实现接下来实现两个核心模块。loader.py负责读取技能包文件main.py负责路由调度和LLM调用。import yaml from pathlib import Path class Skill: def __init__(self, name, meta, instruction, examples): self.name name self.meta meta self.instruction instruction self.examples examples def load_skill(skill_dir: Path) - Skill: meta yaml.safe_load((skill_dir / skill.yaml).read_text(encodingutf-8)) instruction (skill_dir / SKILL.md).read_text(encodingutf-8) examples_dir skill_dir / examples examples {} for f in examples_dir.glob(*.md): examples[f.stem] f.read_text(encodingutf-8) return Skill( namemeta[name], metameta, instructioninstruction, examplesexamples )这段代码做的事情很简单解析skill.yaml、读取SKILL.md、把examples目录下所有md文件读成一个字典。这里有个细节用Path.glob(*.md)而不是手动列文件名是为了以后往examples目录里加新示例文件时不用改代码。再看看main.py里怎么实现路由和调用。from pathlib import Path from loader import load_skill import re def match_skill(user_input: str, skill) - bool: # 先判断triggers关键词是否命中 for trigger in skill.meta.get(triggers, []): if trigger in user_input: return True # 再判断description中的场景词是否命中 desc skill.meta.get(description, ) for keyword in [周报, 周总结, 工作总结, 汇总工作]: if keyword in user_input: return True return False def build_prompt(user_input: str, skill) - str: prompt skill.instruction \n\n prompt ## 参考示例\n prompt skill.examples.get(good_01, ) \n prompt skill.examples.get(good_02, ) \n prompt ## 错误示例\n prompt skill.examples.get(bad_01, ) \n\n prompt f## 用户本周工作事项\n{user_input}\n return promptmatch_skill是路由层目前用最简单的方式实现先查triggers关键词再查description里预设的场景词。这只是最低保底版本如果技能包数量多起来建议把这一步升级成向量相似度匹配用embedding算用户输入和每个技能描述之间的距离取top-k个技能再注入上下文。build_prompt负责组装最终的Prompt。我刻意把示例放在指令和用户输入之间这个顺序我觉得是最顺的模型先记住流程再看几个例子建立具象认知最后处理真实输入。如果倒过来模型可能还没建立好模式就被用户输入带偏了。再补一个LLM调用的封装。from openai import OpenAI client OpenAI() def generate_weekly_report(user_input: str, skill_dir: Path Path(skills/weekly_report)): skill load_skill(skill_dir) if not match_skill(user_input, skill): print(没有命中周报技能请补充诉求或检查触发词。) return None prompt build_prompt(user_input, skill) resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: system, content: 你是一个助手}, {role: user, content: prompt}] ) return resp.choices[0].message.content这里有个关键的测试心得我最初是把整个build_prompt结果放进system消息里后来发现放进user消息效果更好。原因是有些模型对system消息的服从度不如user消息高尤其是在处理“格式套用”这种任务时把Skill指令放在user侧模型会更把它当成“必须完成的任务”而不是一段可参考的背景说明。3.4 效果验证与参数调整写好之后先用几个典型的用户输入测一下效果。我一般会准备三组测试用例一组是完全命中触发词的一组是口语化改写但意思相同的一组是完全没有关联的任务。第一组输入“帮我写本周周报主要内容上线了搜索功能”命中没问题。第二组输入“这周干了点活帮我梳理一下”这时候技能里的description场景词就起作用了“梳理”这个词一旦命中模型就能唤起周报模板。第三组输入“帮我把这份合同审一下”不应该命中如果命中了说明路由的关键词太宽泛需要收窄。出现“不命中但应该命中”的情况时我建议优先补充triggers而不是急着改description。因为triggers在路由匹配里权重最高补充同义表达是最小改动。出现“错误命中”的情况时比如用户谈合同纠纷模型却调用了周报生成那就要把contract相关的关键词加入一个“排除词表”在路由层先做一轮负向过滤。再补充一个细节实际生成如果发现格式不对比如模型非要输出markdown表格那你需要把SKILL.md里的硬性约束加强比如改成“禁止使用markdown表格。如果出现表格符号请重写为列表形式。”我测试过用“如果...请...”这种句式约束比单纯说“不要使用表格”更有效因为模型对“不要做X”的禁令有时会理解成“不要做X以外的其他格式”反而容易出错。4. 实战中遇到的常见坑与排查思路4.1 技能经常“不生效”先查三层很多朋友跑通Demo之后把Skill挂到真实的Agent里立刻就发现技能“不生效”。这类问题我总结下来99%出在三层中的某一层。第一层是路由层没命中。你自己测试时用词很标准但真实用户不会照着触发词说话。排查方法很简单把用户的原始输入打印出来手动跑一下match_skill函数看看是否返回True。如果返回False就去补triggers或description的场景词。第二层是技能包没有被正确加载。我遇到过一次很蠢的错误load_skill的时候写错了相对路径进程起来之后根本没有读到技能包文件但也没报错最后Prompt里根本没有技能指令效果当然不对。建议加载之后把skill.name打印出来确认识别到了。第三层是上层Agent框架把技能包内容截断了。很多Agent框架对system prompt有长度上限技能包太长会被静默截断。这种问题比较隐蔽唯一的排查办法是把最终发给LLM的messages完整打印出来肉眼检查一遍。最后这个建议虽然听起来很基础但我还是想说先打印整个Prompt再让模型背锅。4.2 指令冲突与上下文挤占Skill挂在复杂Agent里经常遇到的一个问题是“指令打架”。比如系统级Prompt里写了“你是全能的AI助手”而技能包里写“你是周报生成器”模型就可能困惑到底该听谁的。我的经验是Skill指令的优先级应该保持在一个合理的中间层——比系统级Prompt高但比用户当前消息低。如果系统Prompt和Skill指令冲突一定要保证技能包里的角色设定更具体因为具体指令在模型眼里往往代表“当前任务”会被优先执行。上下文挤占是另一个值得重视的坑。Skill里的示例如果写得过长或者技能包数量很多最终拼接出来的Prompt会非常臃肿。上下文一旦满了模型就开始丢信息——经常丢的是中间部分的指令。所以技能包里的指令要尽可能精炼示例越长越要克制能用3句话说明白的示例就不要写10句。建议在构建Prompt之后加一个token估算步骤超过阈值就砍示例数量或者简化指令。4.3 多个Skill互相“打架”当你开始做第二个、第三个Skill的时候一个新的麻烦就出现了用户输入同时命中了多个Skill它们各自都想“接管”对话。比如用户说“帮我把本周的工作整理成汇报材料”这个需求既可能属于“周报生成”技能也可能属于“PPT大纲生成”技能系统到底该听谁的我的处理办法是给每个技能增加一个priority字段用来声明它的紧急程度或适用范围。周报生成和王牌模板这种“高适用范围”技能设为高优先级像“合同审查”这种特定场景技能设为低优先级。当多个技能同时命中时先取高优先级的技能作为主技能再判断低优先级技能是否可以作为辅助参考注入。如果两个技能优先级相同就取description和用户输入语义相似度更高的那个。另外一个偏经验性的建议不要同时给一个Agent挂超过10个技能。技能越多路由错误率越高而且每次命中的技能组合都会影响模型的稳定性。我做过一个对比实验挂了15个技能的Agent相比只挂5个技能的版本单任务准确率反而下降了8个点左右。贪多嚼不烂。4.4 版本迭代与回归测试Skill内容的迭代是常态但很多人只改版本号不回归测试最后翻车了就懵了。我建了一个很简单的回归测试脚本把常见用例写成一个JSONL文件每次改动技能包之后跑一遍对比输出是否仍然满足硬性约束。{input: 本周完成了用户中心的登录模块开发修复了三个bug, keywords: [登录模块, 修复]} {input: 帮我把这周的工作梳理一下, keywords: [梳理]} {input: 这份合同帮我审一下重点看赔偿条款, keywords: [], expect_no_match: true}跑回归测试时不看生成文本的完整内容只看几个关键点是否包含周报模板结构、是否出现违禁词、是否编造了未提供的信息。这种“弱断言”方式比人工通读全文高效得多。另外一个建议是每个技能包的skill.yaml里加一个changelog字段记录每次改动的日期和原因。看起来多此一举但技能包多起来之后回看“为什么上周行情判定改成这样”会非常省力。5. 从Skills到Agent工程化我的一点体会最后再分享一个把Skills用好的心态。很多人一开始雄心勃勃想把所有场景一次性做成技能包结果做了一个月发现到处漏水。我的经验恰恰相反先把一两个最高频、最痛点的场景做成精品技能跑通之后再复制方法论。我现在所有Agent项目里的技能包都是从一个“最小可用版”开始迭代出来的。第一版可能只有指令没有示例效果大概70分加上正例之后能到80分再补上坏例子和硬性约束能到90分最后把路由触发词丰富到位稳定到95分以上。这四步不是一次做完的而是根据真实对话日志逐步优化的。如果你现在正准备动手做自己的第一个Skill建议从这两个方向里选一个一是“信息整理类”比如周报、纪要、日报这类任务格式固定容易出效果二是“格式转换类”比如从自然语言转SQL、从需求描述转接口文档这类任务能直观体现“技能约束”的价值。从这些高频小场景入手比一开始就想做“超级技能包”靠谱得多。动手去写第一个Skill吧跑通一次你就会理解所谓智能体工程化很多时候不是让模型更聪明而是把聪明的模型放进一个靠谱的“作业流程”里。