我今年上半年的大部分业余时间都花在了折腾 Agent 这类应用上前后换了不少思路。最大的感受不是模型能力不够而是“能力复用”这件事没人帮我解决同样一个功能今天在这个项目里写一遍提示词明天换个项目又得重写一遍工具函数散落在各个工程里没有说明没有入口模型根本不知道该在什么时候用。后来我把注意力放到了 Skills 上——它不是什么玄乎的新框架而是一套把“教模型做一件完整的事”打包成标准结构的实践方法。简单说一个 Skill 就是一个带说明文档、脚本和示例资源的文件夹Agent 遇到对应任务时会自动发现、读取并调用。这篇文章就是我从零开始把 Skills 用起来之后沉淀下来的理解、操作步骤和避坑记录适合正在做 Agent 应用、想给大模型工作流沉淀能力库的开发者参考。我不会讲太抽象的理论尽量用我自己实际做过的例子说话。1. 先说说我为什么要盯上 Skills 这个东西1.1 从重复劳动说起提示词和代码为什么治标不治本先说个真实场景。我之前给某内部系统做了一套日志分析助手需求很简单把程序输出的原始日志喂给模型让它找出异常、分类、给出建议。第一次做的时候很顺利提示词写清楚配合几个解析函数基本效果就有了。但问题出在“下一次”第二个项目要解析的是 Nginx 访问日志逻辑稍微不一样于是我把之前的提示词复制过来改改例子又加了一轮新的工具函数。第三个项目要用模型做合同要素抽取提示词结构又变了一次。半年下来我的代码仓库里躺着六七套互不相通的提示词工程每套都有自己的一组 Python 函数变量命名都不统一。我自己反思了一下核心问题不是“提示词写得不好”而是没有把能力当成独立模块来对待。提示词黏在业务代码里工具函数散落在各个目录里模型面对一个任务时没有一个“能力索引”告诉它你有哪些技能可以用、每个技能处理什么输入、输出什么结果。于是每次新开发都像从零开始。Function Calling 算是一次进步它把函数和描述一起暴露给模型模型可以根据意图选择调用。但实际用下来你会发现函数列表一旦变长模型的选择准确率反而下降而且这些函数只活在当前代码运行时的上下文里换一个 Agent 平台就全部失效。1.2 Skills 到底解决的是什么问题Skills 的思路和 Function Calling 最大的不同是它把“能力描述”和“能力实现”彻底绑定在一起然后作为一个独立实体存放。一个 Skill 不再是一个孤零零的函数签名而是一个文件夹里面至少包含一份写给人和模型看的说明文档以及可执行的脚本或配置。这个设计把问题从“模型能不能调用到函数”变成了“模型能不能发现这个技能”。前者依赖运行时的上下文塞满各种函数定义后者依赖一个统一标准只要文件夹里结构正确、说明描述清楚Agent 平台会自动把技能注册进可选能力池由模型在合适的时机主动选择。我还记得第一次用对这种方法时的感觉终于不用在提示词里堆“请根据以下规则输出 JSON”了。模型读到 SKILL.md 之后会自己决定用什么脚本、按什么格式输出我只需要在业务层做最终结果校验。从工程角度说Skills 还带来一个额外好处版本管理和共享变得自然。一个技能就是一个文件夹我可以单独给某个技能打标签、单独测试、单独分发给其他同事使用不用把整个项目的代码都复制一遍。这比在巨大提示词文件里做修改要安全得多。1.3 什么场景适合上 Skills什么场景先别碰我自己的判断标准大概是这样的适合的场景一是重复出现的文本处理类任务比如日志分析、摘要、格式转换、信息抽取二是需要结合本地脚本才能完成的流程比如读取某个文件、调用某个 API、执行某个数据清洗步骤三是希望多个 Agent 共享能力的团队内部工具库。这些场景的共同特点是“输入有一定规律但又不完全固定”恰好是模型擅长处理、而脚本难以穷举规则的结合点。不适合的场景一是一次性任务花十分钟能做完的事不值得你花一小时去封装技能二是高度依赖人的主观判断的开放式创作比如“帮我写一首诗”这种场景不太稳定封装了反而限制模型的发挥三是强交互、多轮对话才能逐步明确需求的事情技能能处理的是“明确任务”而不是“探索需求”。我见过有人为了让一个技能看起来“完整”硬是把一个本来可以直接用提示词解决的问题拆成三四个脚本加一份复杂说明结果模型加载技能的时候犹豫不决效果反而变差。Skills 是给常用动作做的工具箱不是给所有动作做的收纳盒。2. 一个 Skill 的文件结构我踩了两轮才踩明白2.1 Skill 不是一个功能是一个可以自我说明的文件夹我第一次上手时犯的错误是以为 Skill 就等于一段配置或者一个回调函数。后来参考了若干主流 Agent 框架的约定才搞清楚关于什么是“标准”不同平台略有差异但核心思想高度统一——一个 Skill 在磁盘上表现为一个独立文件夹里面有固定的文件布局和元信息。典型的目录结构长这样my-skill/ ├─ SKILL.md ├─ scripts/ │ ├─ process.py │ └─ requirements.txt ├─ assets/ │ └─ template.json └─ tests/ └─ sample_input.txtSKILL.md 是这个技能的入口是所有元信息所在scripts 放实际跑逻辑的脚本assets 放只读数据模板、样例、参考文本之类tests 放验收输入和预期结果。有些平台还会要求 assets 里的文件必须在 SKILL.md 中显式引用不能让 Agent 自己去猜这个文件夹里有什么。我自己经历了从“写一个函数”到“建一个技能文件夹”的转变之后最大的感悟是文件夹本身就是一种隐式的能力发现机制。Agent 扫描到 skills 目录后只需要打开 SKILL.md就能判断要不要用、怎么用、需要什么参数。这个机制不依赖代码运行时上下文也不依赖人肉去改模型系统提示词。在这个结构里我最看重的其实是 assets 和 tests。assets 不仅提供静态资源还能在描述文档里被用来做“one-shot 示例展示”tests 则是回归的安全网。我后期维护技能的时候经常是先跑一遍 tests然后根据失败结果判断是脚本坏了还是描述写得不清晰而不是摸着脑袋瞎改。2.2 描述文件里每一段都有用途SKILL.md 是整个技能能否有效工作的关键。很多人把它当成 README 来写写得又全又长结果模型不知道什么条件下该用。我后来总结出一套写法每一段都有自己的明确用途名称和一句话简介给技能一个一望即知的名称简介控制在 30 字内说明核心功能。使用条件When to Use明确列出触发该技能的场景和不触发该技能的场景。这是最重要的部分模型就是靠它决定什么时候加载。输入参数Inputs以表格或列表形式声明参数名、类型、含义、是否必填最好带上示例。输出格式Output约定严格的输出结构尤其是 JSON 的 schema 或 Markdown 模板避免模型自由发挥。使用步骤How to Use简要描述内部工作流程比如先读取文件、再执行脚本、再做后处理。边界与限制What to Avoid写清楚这个技能不该做什么防止模型在不合适的场景强行使用。示例Examples一到两个完整的最小示例展示从输入到输出的全过程。版本和依赖Meta记录技能版本、依赖环境、作者或归属。这样排版的好处是模型通常只需要读“使用条件输入输出”就能做决策剩余部分只会在实际调用时被阅读。从工程角度说这相当于给模型做了一个结构化索引而不是让它在长篇大论里找重点。我还习惯把“该技能可能失败的场景”写进 What to Avoid 里。比如“日志文件中若超过 10 万行请先和用户确认分段执行”这样能减少模型盲目处理超大数据导致脚本卡死的概率。这一点是我加了之后才意识到有多重要。2.3 脚本和资产到底应该放到什么粒度脚本的粒度问题经常被忽略。初期我习惯把整个流程写进一个又长又复杂的 Python 脚本导致任何一点变化都要回到脚本里改。后来我改成“说明文档做编排脚本做单步操作”的方式SKILL.md 里描述流程顺序scripts 下放多个职责单一的小脚本甚至可以互相调用。比如日志分析这个技能我拆成了 parse_log.py负责把原始文本切成结构化的条目、classify_error.py负责根据规则或模型输出判断异常类型、format_report.py负责生成最终 Markdown 报告。每一步单独调用任何一步出问题都能定位。代价是脚本数量变多但换来的是可测试性和可替换性。资产的粒度也是一样不要什么都往 assets 里塞。只放那些技能运行必需的静态文件。我见过有人把 30MB 的参考文档塞进技能目录结果每次加载都要读一遍慢得让人抓狂。assets 应该放的是小而关键的模板或白名单大型语料应该另行管理在 SKILL.md 里说明路径即可。脚本资源放好之后注意一个问题脚本的可执行权限。在一些 Linux 环境下Agent 平台会直接以子进程方式执行脚本如果没给执行权限就会报 Permission Denied。我在交付一个技能模板给同事时遇到过这种情况后来养成了在每个脚本提交前统一检查权限的习惯。3. 手把手创建第一个 Skill日志异常提取与结构化3.1 先确定边界不要一上来就写文件很多人创建技能的第一个动作就是新建文件夹觉得边写边想效率高。我实际试下来应该先做的是定义输入输出的边界。拿日志异常提取这个技能举例我花了一个小时想清楚的问题包括输入是纯文本日志还是文件路径日志格式是什么风格自有格式、通用格式、还是混搭输出是要严格 JSON 还是要 Markdown 报告异常等级如何划分遇到无法识别的行时是跳过还是加到“待人工确认”清单里。这些边界如果在写完技能后再考虑往往要在 SKILL.md 和脚本来回改。我先在文本编辑器里草拟了一份“输入输出规格”类似接口设计文档然后才动手建目录。这个过程可以帮我发现不少模糊点。我当时在规格里定了三件事输入是原始文本内容或文件路径二者之一输出固定为 JSON包含 total_lines、errors、warnings、unknown_entries 四个字段异常等级只分 error 和 warning 两种其余的归入 unknown。这样定义清楚后后续所有描述和脚本都围绕这份规格展开避免“这个也行那个也行”的含糊。定义完边界才开始写 SKILL.md。这种“先设计后编码”的顺序看起来老套但在技能开发里特别值钱因为 SKILL.md 本身就要做到让人和模型都能一眼看懂。3.2 写描述文件的实操细节SKILL.md 的开头几句话决定了模型会不会选择这个技能所以我把使用条件写得像“路由规则”一样具体。下面是我实际使用的一个简化版本去掉了所有平台相关的字段# Log Anomaly Extractor 一句话简介从应用日志中提取异常条目并按严重级别输出结构化 JSON。 ## When to Use 当用户提供一段原始日志文本或日志文件路径且期望知道“里面有什么异常、有哪些严重错误”时使用本技能。 不要在处理结构化数据时使用本技能如 CSV 转其他格式也不要用于实时日志监控。 ## Inputs - text: 原始日志内容字符串。如果用户给的是文件路径请在调用脚本前读取文件内容再传入。 - level_threshold: 可选严重级别阈值默认 warning。 ## Output 返回一个 JSON结构为 { total_lines: 123, errors: [..., ...], warnings: [..., ...], unknown_entries: [...] }这里我故意把“不要用于实时日志监控”写进去了。因为如果不写模型很可能用户说一句“帮我看看日志有没有问题”就会调用这个技能但实际上用户只是想要人肉看一眼文本内容。负向约束能显著提高模型选择技能的准确率。格式上我一直坚持用 Markdown 表格和列表来描述参数而不是在一大段话里自然语言描述。模型对结构化的参数列表解析效果更稳而且后期人维护也容易。还有一个细节SKILL.md 里如果涉及调用脚本我会明确写上脚本的运行环境。比如“需要 Python 3.10 以上依赖见 scripts/requirements.txt”。这看起来是写给人看的但实际上模型也会读到并在调用前自检环境减少运行时错误。3.3 写配套脚本和参数约定写完 SKILL.md再写实际干活的脚本。我的分工是脚本不负责做智能判断只负责确定性处理和格式化智能判断比如某个日志片段算不算真正的异常由模型在读取脚本输出后完成。以下是我日志分析脚本的核心思路Python 代码只贴关键部分import json import re from typing import List def extract_log_entries(text: str) - List[dict]: lines text.splitlines() entries [] for line in lines: m re.match(r^(\S \S) (\w) (.*)$, line) if m: timestamp, level, message m.groups() entries.append({timestamp: timestamp, level: level, message: message}) else: entries.append({timestamp: None, level: unknown, message: line}) return entries def analyze(entries: List[dict], threshold: str warning): errors [] warnings [] unknown [] for e in entries: level e[level].lower() if level in (error, critical, fatal): errors.append(e) elif level warning: warnings.append(e) else: unknown.append(e) return { total_lines: len(entries), errors: errors[:20], warnings: warnings[:20], unknown_entries: unknown[:10], } if __name__ __main__: # 从 stdin 读取原始日志文本按固定格式输出 JSON import sys data sys.stdin.read() res analyze(extract_log_entries(data)) print(json.dumps(res, ensure_asciiFalse, indent2))脚本里我把“各类别最多输出多少条”写死成了上限值。这个细节很关键不然模型拿到的结果可能因为日志太长而爆掉上下文。脚本可以做截断但必须在输出里说明“只返回前几条”模型知道信息有截断后可以主动询问用户是否需要更多细节。参数约定方面我在 SKILL.md 里声明了 level_threshold 参数脚本里则用默认值兜底。这样就可以做到“模型按说明传参脚本按参数执行参数缺失时也不会崩溃”。脚本入口统一走 stdin 读入、stdout 输出 JSON方便 Agent 平台捕获标准输出。3.4 验证和迭代怎么算好用技能写得差不多之后验证环节不能省。我先准备了三份测试日志一份是常规格式但级别齐全的样例一份是混入乱码和异常缩短行的噪声样例一份是空文件和超长行边界样例。第一次测试就发现了一个问题脚本对“日志行中包含多个空格”的解析不准确导致大量行被归类到 unknown。因为我用了一个贪心的正则它把多余的空格吞进了 message 字段而某些“时间戳格式不标准”的日志行没能匹配。解决办法是把时间戳正则放宽并加上一行规范化处理。这类问题靠人眼很难提前发现只有靠边界样例才能暴露。我建议所有技能开发都保留 tests 目录和样例输入不管是自己迭代还是将来交给别人都会省很多时间。当技能通过样例验证后我会在真实日志文件上再跑一轮观察模型是否主动调用了这个技能、调用后输出是否稳定。如果模型在一个明显匹配的任务场景下都不调用通常说明 SKILL.md 里的描述和常见提问方式之间的关联不够紧密。我会把那类提问方式本身写进 When to Use 的举例里比如“用户可能说‘帮我扫一眼这个日志有没有报错’这时候也应该触发”。4. 用 Skills 这半年我踩过的坑和排查方法4.1 描述文件写得像论文Agent 根本不加载我见过最普遍的问题SKILL.md 写得极其完整职责范围阐述得很充分但模型在对话中就是不调用它。排除了平台故障之后问题几乎都出在描述文件对“触发关键词”的匹配效果上。模型判断是否调用技能主要是把用户意图和 SKILL.md 的描述文本做语义匹配。如果你的描述用的是抽象的、偏后台术语的话用户用口语提问时匹配度就很低。比如我最初写的是“本技能适用于日志体系中的异常元数据提取”用户实际说的是“帮我看看这个日报告里面那段红色的是什么”能匹配上才怪。后来我改成“当用户希望快速了解一段日志或日志文件中包含哪些异常时使用本技能”并且明确写上常见的用户表达方式加载率就上来了。我还养成了一个习惯每次从同事或用户那里听到一种新的口语化提问就把它补进 When to Use 的例子里。这就是一个“描述文件持续优化”的过程不是写完就结束。排查建议如果你发现模型始终不调用某个技能先在另一个干净的对话里用触发场景提问观察有没有加载日志或提示输出如果没有把描述文件里的句子逐句换成口语化表达再试。4.2 参数类型不一致导致脚本原地爆炸这个问题在接入外部脚本时特别常见。SKILL.md 里声明输入是整数类型但 Agent 平台从用户语句里抽取出来的参数永远是字符串。模型照着说明传一个字符串过来的例子我让某个技能接收 max_items5脚本里直接把它拿去 range() 用了结果 TypeError 导致整个流程中断。排查方法是在脚本入口统一做类型转换和默认值兜底。我后来给所有脚本写上这样一段防御逻辑import argparse parser argparse.ArgumentParser() parser.add_argument(--max-items, typeint, default10) parser.add_argument(--level-threshold, typestr, defaultwarning) args parser.parse_args()这样即便 SKILL.md 里写得不清楚模型传了字符串也能正确转换。更保险的做法是在 SKILL.md 的参数表格里给每个参数加一列“类型”并且示例里面把“字符串形式的数字”也列出来让模型不至于理解偏差。还建议在所有脚本入口增加参数校验和基准测试。哪怕内容只是打印“参数缺失请检查”也比模型拿到一堆乱码后自己脑补强。4.3 安全边界你以为 Agent 只读它可能真会执行Agent 调用技能时本质上是在你的机器或服务器上执行代码。这是 Skills 模式最大的安全风险点。当我设计一个需要读取日志文件的技能时脚本里如果带上了“删除临时文件”的逻辑模型可能会在用户指令的影响下拿着一个奇怪路径去调用导致不可预期行为。我的安全底线有几个技能目录尽量运行在沙箱或受限目录下不写任何包含删除目录、全局替换、curl 外部地址等高危操作的脚本如果确实需要写入文件写入路径硬编码为固定相对路径不接收用户传入的任意绝对路径脚本里禁止执行 shell 拼接命令只用静态命令加参数列表。我给自己定了一个简单的分级只读技能、写临时文件技能、需要网络请求技能三级的审核口径完全不同。只读技能基本可以直接放心跑后两级必须人工 review 一遍并且限制运行环境权限。经验之谈不要因为某个功能“只在内网临时用”就放松检查。内网环境下模型同样可能被诱导执行恶意指令安全实践不能省略。4.4 Skill 多了以后的版本与依赖管理当技能从两三个增加到二十多个之后新的问题出现了同名冲突、依赖不一致、旧版技能行为和新需求不匹配。我曾在一个通用技能目录下放了两个都叫“summary”的文件夹结果模型随机选择输出风格完全不一样。解法是给每个技能加上命名空间或前缀比如 teamA-log-analyzer 和 teamB-report-summary并从一开始就在 SKILL.md 头部写版本号和变更记录。依赖管理方面每个独立技能都要带自己的 requirements.txt不要依赖全局 Python 环境里的某个偶然安装过的库。我后来还专门写了一个一次性的检查脚本扫描所有技能目录找出缺少 SKILL.md、SKILL.md 里缺少版本号、scripts 下有文件但没有被文档引用的目录。这个动作每两周跑一次能及时清理掉“僵尸技能”。关于共享如果团队多个人同时维护技能最好把它放到代码仓库里而不是各自本地复制。我在本地维护时被坑过一次同事改完技能描述发给我我没及时同步结果两个人用同一套技能但是行为不一致。后来改成仓库统一管理才把这个问题解决。5. 是不是所有 Agent 应用都应该用 Skills我的结论5.1 什么人适合马上开始用如果你满足下面任意一条我认为可以直接上手尝试一是你已经写过两套以上类似的提示词工程或工具链并且开始觉得重复度太高二是你的 Agent 应用需要处理多种不同类型的输入而且每种类型都有明确的处理流程三是你需要和团队成员共享能力而不是一个人默默在本地维护提示词文本。Skills 带来的最大收益是“能力沉淀”。每新增一个技能就相当于给 Agent 的世界里加了一个新功能按钮。三个月后再接到同类需求不再是重写提示词而是直接引用已有技能省下来的时间非常可观。我自己的实践体会是会做合理的负面排除比一味堆技能更重要。技能库里出现三五条互斥的日志类技能之后模型可能不知道选哪个。所以要定期整理把行为相似、触发场景重叠的技能合并不能只加不减。5.2 不要把 Skills 神话Skills 不是银弹。它仍然依赖底层的模型能力、依赖你写的描述质量、也依赖运行时环境的稳定。有的场景下一个精心设计的系统提示词比十个技能都好用有的场景下普通函数调用比技能文件夹轻量得多。我的建议是把它当作一套工程规范而不是架构必需品。如果你遇到的输入非常固定、流程完全可枚举用传统代码处理反而更快更稳只有当你需要模型参与“理解与判断”时Skills 的封装方式才明显占优势。说白了Skills 擅长处理的是“规则边缘的灵活地带”。不要为了技术架构的完整性而强行引入。你只需要解决眼下最痛的问题哪个流程反复在做、哪个能力跨项目复用就把哪个封装成技能。技能数量保持在一个可控范围维护成本才不会失控。5.3 最后分享一个我自己的使用小技巧如果你也想开始实践我建议你从今天就在新技能里固定写清楚版本号和运行环境哪怕这个技能只给自己用。理由是你永远不知道三个月后会不会因为一次系统升级让某个脚本的依赖坏掉。到时候你翻 SKILL.md 看到写着版本和 requirements排查会顺畅得多。我自己的习惯是每个 SKILL.md 文件末尾放一块“Meta”信息技能版本、依赖环境、是否存在外部 API 调用、是否需要网络。每次升级脚本都在文件头更新版本说明保持文档和代码同步。现在回到最开始的那个问题为什么我会盯上 Skills。因为它让我从“每次教模型怎么做”变成了“让模型自己找到怎么做”。这种转变不是靠一个更聪明的模型实现的而是靠把能力组织成可以被发现、被复用、被维护的独立单元。这种组织方式带来的复利效果只有在积累到一定数量之后才会显现。