上周有人问我:你们给Agent加技能,到底是加了个什么东西?我一时不知道从哪讲起,后来打了个比方——你把它理解成一本“带示例的岗位操作手册”就行了。模型本身不认你内部的目录结构,也不知道你团队的归档规则,但如果你给它一份写得足够清晰的技能说明,它就能按你的预期调用本地脚本、读取文件、生成摘要、把结果放回该放的位置。这套机制现在很多大模型应用框架里都在做,大家习惯叫它Skills。
这篇想聊的就是Skills本身:它解决什么问题,一个可复用的技能包应该长什么样,以及我在实际项目中从零搭一个技能的具体过程和踩过的坑。比较适合刚接触Agent开发、想把自己的内部工具暴露给模型用的人,也适合已经在用但总觉得“模型不按套路调用”的同学。
1. Skills到底在解决什么问题
1.1 模型天生是“文案专家”,不是“系统集成商”
先回到一个基础认知:大模型本身只会读文本、写文本。你让它读一个本地目录里的会议纪要,它根本接触不到文件系统;你让它调用你们内部的归档接口,它也不知道接口地址和参数格式。传统做法是靠Function Calling,也就是把一个个函数注册给模型,模型根据用户问题决定调哪个、填什么参数。这条路没问题,但用久了会发现一个尴尬:单次函数调用只能完成“一个动作”,而真实任务往往是多动作的。
比如“把今天讨论出的结论整理成摘要,按项目名归档到对应文件夹,再通知相关同事”,这至少涉及读文件、写摘要、移动文件、触发通知四个动作。如果全拆成底层函数,模型不仅要在一次对话里连续发起多次调用,还要自己记住中间产物放在哪,稍有偏差就断链。Skills的思路不一样:它把“摘要归档”这个完整动作打包成一个能力单元,模型只需要识别“用户在让我做一次摘要归档”,然后把文件路径等信息填进去,剩下的流程在技能内部自动跑完。
我自己的理解是:函数是手,技能是“一套组合动作的记忆”。模型不需要关心动作细节,它只需要判断“什么时候该用这套动作”。这一点决定了Skills和普通工具调用的核心差异。
1.2 Skills与插件、工作流的边界
很多人会把Skills、插件、Workflow混在一起,但它们在系统里的层级其实是不同的。
- Function Calling:最低层,一次调用一个函数,模型实时决策。
- Skills:中间层,封装“指令+参数约束+内部实现”,模型按描述决策是否使用,使用后内部可包含多次工具调用。
- 插件/工作流:更高层或更刚性,通常是预先编排好的固定流程,人为决定何时触发,模型只参与其中部分环节。
用一个生活化类比:函数是单个琴键,技能是“一段练熟的和弦”,工作流是“整首曲子的谱子”。琴键可以单个按,但真正弹曲子需要和弦,而整首曲子有固定顺序。Skills就是为了让模型更高效地“弹和弦”,而不是每次从单个琴键练起。
这个边界直接决定了你在设计系统时把逻辑放在哪层。如果这个动作的输入输出比较固定、有明确触发条件,做成Skills最合适;如果涉及多个系统、跨团队审批、强流程管控,那还是老老实实放在工作流引擎里,别硬塞给模型。
2. 一个能落地的Skills包长什么样
2.1 目录与清单:先把规范定在前面
目前主流做法里,技能包通常是一个自包含的目录,里面有一份人机共读的清单文件,再加上脚本、示例、依赖清单。以我常用的结构为例:
skills/ doc-summary/ SKILL.md scripts/ summarize.py archive.py examples/ meeting_notes.txt report.txt requirements.txt这里最核心的是SKILL.md,它相当于这个技能的“门牌”。模型不会去看你的Python脚本,它只通过这份清单来判断“这个技能是干什么的、什么时候用、参数怎么填”。
写SKILL.md有个容易被忽略的地方:它服务的读者有两个,一个是开发同学(需要看懂实现),一个是模型(需要理解触发条件)。所以语言要精确,不要写一堆“本技能旨在……”之类的废话结构,直接写“当用户要求做X时使用”,这种指令式描述命中率最高。
2.2 元数据写得好不好,直接决定命中率
很多教程会告诉你描述要写清楚,但很少说清楚到什么程度。我拿正反两版对比一下。
反例:
技能名称:文档摘要 描述:用于摘要文档。 参数:filePath(文件路径)这种描述模型基本不知道什么时候该调用。用户说“把这周例会的重点整理一下”,模型很难联想到“文档摘要”这个词。
正例:
技能名称:会议纪要摘要归档 描述:当用户要求将会议纪要、讨论记录或报告文件转化为结构化摘要,并按日期/项目归档时使用。 输入:需要处理的文件路径。 输出:摘要文本、提取出的标签列表、归档后的完整路径。 注意:只处理文本格式文件;如果文件不存在或内容为空,必须拒绝执行并说明原因,不要自行猜测路径。前后差别一目了然。正例里包含了触发场景、输入输出边界、拒绝条件。模型在对话里做语义匹配时,这些细节就是它的“路标”。
另外参数Schema也要尽量收窄。以我实际经验,给每个参数加描述、加默认值、加取值范围,比啥都不写强很多。比如outputDir不要开放成自由文本,而是给几个枚举值:outputDir: "meeting_notes/2025/",否则模型真的会给你编一个不存在的路径出来。
2.3 行为逻辑与输出约定:别让模型去猜结果
技能内部的实现可以是Python脚本、Shell命令,甚至是一段调用外部API的逻辑,但有一个原则必须守:技能的返回结果要尽量结构化,并且对模型友好。
我习惯让脚本最终输出一段JSON,至少包含状态和关键结果:
{ "status": "done", "summary": "会议讨论了Q3排期,确定8月15日完成开发联调", "tags": ["排期", "联调"], "archived_to": "meeting_notes/2025/2025-07-28.md", "error": null }为什么一定要结构化?因为模型在拿到这个结果后,可能还要继续回答用户的问题,比如用户追问“归档到哪里了”,模型如果能直接从summary和archived_to字段里读到答案,就能流畅地继续对话。如果你让技能返回一大段散文文本,模型还要自己抽取关键信息,容易抽错也容易带偏。
我在设计输出格式时会把“模型接下来要做什么”也考虑进去。结果里包含用户最关心的几个点,而不是把调试日志一起丢出来。调试信息写进单独日志文件,千万别混在返回给模型的内容里。
3. 完整实操:给内部文档做一个“摘要归档”技能
3.1 场景先拆解,技能后设计
我们团队把Agent接到内部知识库目录上,最烦的是每周都有大量会议纪要散落在各处,归档靠手动,常常漏掉。于是我们想做一个技能:把上传过来的会议记录文件整理成摘要,按月份归档,并返回归档地址。
动手之前先拆任务:读取文件、生成结构化摘要、提取标签、移动文件到月份目录。这四步里,生成摘要和识别标签是模型擅长的事,其余文件操作是脚本擅长的事。所以技能的边界可以这样划:模型负责理解内容,脚本负责执行文件操作。
这一步很多人容易忽略,一上来就写SKILL.md,结果技能内部逻辑和模型能力边界糊在一起。建议先画一张“任务拆解表”,分清哪些是模型能力,哪些是代码能力,哪些是外部系统能力,然后再决定技能内部怎么写。
3.2 清单文件和参数设置
我写的SKILL.md核心部分如下(已脱敏):
# 会议纪要摘要归档 ## 描述 当用户要求把会议纪要、讨论记录、访谈记录等文件转化为结构化摘要并归档时使用。 如果用户只说“帮我整理一下这个文件”,但未明确归档需求,也视为本技能触发条件。 ## 参数 - filePath: 必填,待处理文件的绝对路径。 - outputDir: 必填,归档目录,仅允许以下枚举值: - meeting_notes/2025/ - meeting_notes/2024/ - language: 可选,摘要输出语言,默认中文。 ## 行为 1. 检查文件是否存在,不存在则返回错误。 2. 读取文件内容,截断超过2万字符的部分。 3. 生成结构化摘要,提取3~5个关键词。 4. 将原文件复制到 outputDir 下,文件名加当天日期。 5. 返回JSON结果。 ## 注意 - 禁止修改原文件。 - 禁止访问输出目录之外的任何路径。 - 如果提取到的文本长度小于100字符,视为无效文件,直接返回"待人工处理"。参数这块我特别想强调:outputDir用枚举限制,是因为模型对路径的理解完全是“文本层面”的,它觉得随便填一个字符串没问题,但脚本执行时路径不对就出事故。宁可让模型多问一句“归档到哪个月份”,也别让它自由发挥路径。
文件操作统一用相对安全的复制而非移动,原文件保留,脚本只操作副本。这样即便出错了,原文件还在,能兜底。
3.3 实测表现与参数调优
技能写完后,我拿三种典型样本做了测试:短会议纪要、长报告(几十页)、扫描件转出来的乱码文本。测试结果可以分享几个比较典型的问题:
- 短纪要基本一次通过,命中率高,参数解析也准。
- 长报告在读取阶段偶尔超时,因为字符数太大,模型生成摘要时间过长。解决办法是在脚本里做了截断,超过2万字符只取头尾各1万字符,并在摘要里标注“原文过长,已截断”。之后稳定很多。
- 扫描件乱码是最有意思的,模型不会识别乱码文本,但它会把乱码内容原样当成“内容摘要”输出。后来我在SKILL.md里加了那条“文本长度小于100字符视为无效”,才把这个坑堵住。
这其实说明一个道理:技能调试不只是在代码层面调,还要在“触发边界”上调。你得知道模型在哪些场景下会误判,然后把边界写进清单文件里,让模型自己学会拒绝。别指望模型天然具备这种判断力,它是靠你的描述才学会拒绝的。
测试时推荐给技能启用完整日志,记录“模型选择了哪个技能、填了什么参数、技能返回了什么”。这套日志是排查问题的第一现场,很多问题一眼就能从日志里看出来。
4. 真实使用中踩过的坑与排查方法
4.1 模型死活不调用技能,怎么查
这是最常见的问题:技能写好、注册好了,用户输入也很明确,但模型就是不用,反而自己用通用能力瞎答一遍。
我的排查顺序一般是这样:
- 先看模型日志里的思考过程,确认它是不是压根没看到这个技能。
- 检查系统提示词里对技能列表的描述,有些框架会要求技能数量少、描述简短,如果技能列表太长,模型可能会漏掉排在后半段的技能。
- 再看参数必填项。模型有“不确定就放弃”的倾向,如果必填参数太多、又没有默认值,它可能选择不用技能也不犯错。解决办法是尽量少设必填,能给默认值就给默认值,给不了就在清单里给一个调用示例。
我用过一个真实的例子:一开始summary技能的shortVersion字段设为必填,结果模型在遇到信息不全的文档时总是拒绝调用,后来改成可选,命中率明显上升。
如果都不行,大概率是描述和用户意图匹配度太低。这时候把用户问题原话贴出来,对照描述逐词看,该加同义词就加同义词。
4.2 技能之间互相串台,边界怎么守
技能一多,另一个麻烦就来了:两个技能的描述重叠,模型选错。比如我有“文档摘要”和“文档格式化”,当用户说“把这份报告整理一下”,模型可能随机选一个。
解决办法我在自己项目里试下来比较有效的是:在SKILL.md开头加“排他说明”。
## 排他性 如果用户仅要求美化排版、调整格式而非生成摘要,请使用“文档格式化”技能,不要使用本技能。另一层是代码里的防御。脚本内部不只依赖模型的参数,还要自己校验:文件扩展名对不对、目录是否在白名单里、内容格式是否符合预期。校验不通过就返回明确错误,并提示模型检查参数。
这里提醒一句:技能里如果会执行脚本,一定不要让模型直接传shell命令进来。正确的做法是脚本自己固定好逻辑,模型只填受控参数。我见过有人把“参数”设计成整条命令行,结果模型生成了一段意想不到的代码直接执行,这属于严重的安全隐患。我的原则是:技能里的脚本可以读参数,但绝不解释执行模型给的自由文本。
4.3 问题排查速查表
把这段时间积累的典型问题整理成一张表,方便对照:
| 现象 | 可能原因 | 检查点 | 解决思路 |
|---|---|---|---|
| 模型不调用技能 | 描述与意图匹配度低 | 日志中模型思考过程 | 改写描述,增加同义触发词 |
| 参数解析错误 | Schema缺失约束 | 参数日志 | 加枚举、加默认值、加说明 |
| 技能执行报错 | 路径非法或文件不存在 | 脚本校验日志 | 脚本入口加防御性校验 |
| 输出内容牛头不对马嘴 | 技能内部结果未结构化 | 返回值日志 | 统一JSON结构,分离调试信息 |
| 多个技能互相抢任务 | 描述边界重叠 | 对比技能描述 | 增加排他说明和触发条件 |
这张表我贴在项目文档首页当索引,新同学接手时先看这张表,能省掉很多摸索时间。
5. 后续还能怎么扩展
5.1 从单技能到技能组合
单个技能跑通之后,下一步可以尝试技能之间的组合。比如“摘要归档”技能执行完,返回了一个归档路径,紧接着可以让“通知发送”技能读取这个路径,把摘要发给相关同事。两个技能彼此独立,通过结构化中间结果衔接,这样每个技能的可复用性都很高。
我做组合时有一条经验:不要让一个技能内部直接调用另一个技能,最好让模型在一轮对话里先调用A、拿到结果、再决定调用B。硬编码串联会牺牲灵活性,一旦流程变化就要改代码;让模型串联,改描述就能调整顺序。
5.2 团队共享与版本管理
技能包本质上是代码资产,应该纳入版本管理。我们团队的做法是:每个技能一个文件夹,SKILL.md和脚本一起提交,变更走评审。评审重点看三点:描述是否清晰、参数是否受限、脚本是否安全。
版本号也建议带上。技能会随着反馈不断调整,如果没有版本管理,你今天改的描述明天可能被另一个同学又改回去,然后开始互相怀疑。我习惯在SKILL.md里加一个version字段,变更时递增,这样至少能追溯是哪一版开始出现行为变化的。
团队新同学进来,不需要从零开始理解Agent的内部逻辑,直接翻技能库,看清单文件就能明白哪些事情可以交给Agent做、怎么调用。这比任何培训文档都管用。
结尾
做Skills这段时间,我个人最大的体会是:它本质上是在“模型的能力边界”和“系统的确定性需求”之间搭桥。桥搭得好不好,不在于脚本多复杂,而在于你对模型的描述够不够精准。我把技能清单当成接口文档来写,收益特别明显。
最后再分享一个小技巧:每次技能出问题,别急着改代码,先翻日志,模型其实会在日志里留下大量线索,只是大多数时候没人看。另一个建议是第一次做技能别贪多,踏踏实实把一件小事的边界写清楚,跑通一次之后再往复杂了做,你会少踩很多坑。