
最近我把手头的Agent项目从“一个啥都能聊的大模型壳子”重构成了“一堆小技能组合”的结构效果比预期好不少。核心思路就是标题里写的这个方向——agent-skills也就是把Agent反复要做的那些确定性任务沉淀成一个个可复用的技能包每个技能包由一个说明文件加若干可执行文件组成Agent在需要的时候自动加载说明、按需执行脚本。这套模式解决了我最头疼的两个问题一是同样的事情每次用对话描述模型发挥不稳定输出格式飘忽不定二是能力没法沉淀这次调通了下次还要从头教。技能包化之后执行逻辑写死在脚本里Agent只负责理解意图、选择技能、组织参数结果的稳定性和一致性一下就上来了。这篇文章我会把技能包的结构设计、SKILL.md的编写规范、实际构建一个数据流转技能的全过程以及我在落地过程中踩过的坑一次性讲清楚。如果你是正在做Agent应用开发、或者想让自己手里的AI工具从“能聊”进化到“能干”的开发者这篇文章应该能帮你少走不少弯路。1. 为什么需要技能化Agent设计思路的一次重构1.1 通用对话模型做固定任务时到底差在哪先说一个现场。我之前用通用模型直接处理数据清洗给它一段描述“帮我把这个JSON里的用户字段提取出来转成CSV”。第一遍它做得不错第二遍换了个文件嵌套结构稍微深一点它就开始自己发挥字段名猜的、空值处理的逻辑也变了输出结果一会儿带引号一会儿不带后处理程序直接崩。这不是模型笨而是通用模型擅长的是语言理解和生成对固定流程的执行天生不稳定。打个比方你让一个新员工“按流程处理数据”他能听懂但每次处理的方式细节都可能不一样而一个老师傅会直接取出专用工具按操作手册一步步来结果自然一致。技能化的核心就是把“老师傅的工具箱”这个概念落地。每个技能包里包含一本操作手册SKILL.md和一把专用工具脚本Agent需要时先读手册再调用工具。流程被固化在代码里模型不需要从零推断每一步该怎么做只需要做它擅长的判断——用户的需求匹配哪个技能。从我实际的体感来说技能化之后同样一个数据清洗任务过去可能需要反复纠正模型的输出格式现在只要技能描述写得清晰一个来回就能拿到可直接落库的结果。1.2 技能包和工具调用、MCP这些方案有什么不同很多做Agent的朋友会问现在不是有function calling吗不是有MCP吗为什么还要搞一套技能包的结构我在实践中的理解是这样传统工具调用是把一批函数注册到系统层每个函数都要在上下文中留有定义工具数量一多每次请求光工具描述就吃掉大量token而且工具之间的边界、优先级经常互相干扰。MCP解决的是工具和外部系统的标准化连接问题让Agent能统一访问数据源和外部服务但它天然偏向“客户端-服务端”架构跑起来需要服务、需要鉴权对于本地文件和一次性脚本这种轻量场景反而有点重。Agent Skills这套思路补的恰恰是中间这块它不要求常驻服务本质就是文件系统里的一组约定好的目录和文件。技能的发现靠的是描述文件的语义匹配技能的加载是靠Agent判断“当前任务是否命中”实际执行可能是一个Python脚本、一个Shell命令甚至是一段纯规则文本。好处很明显轻量一个技能就是一个文件夹拷走就能用上下文友好Agent只加载命中的技能描述不相关的技能完全不占上下文可版本化技能包和代码一起进Git改了什么一目了然可组合多个技能之间互不感知由Agent在对话层编排。当然实际项目里这些方案不是互斥的我现在就是本地技能包做确定性的数据处理MCP接外部数据源各管一段。2. 技能包的结构设计与编写规范2.1 SKILL.md技能的名片与说明书一个标准技能包最少包含两部分SKILL.md描述文件以及若干可执行文件。SKILL.md是整个技能的入口Agent通过它来理解技能到底是干什么的、什么时候该用、怎么用。这个文件写得好不好直接决定技能被准确调用的概率。先看一个最基础的SKILL.md长什么样--- name: json-to-csv description: 将嵌套JSON数据按照用户指定的字段映射转换为CSV文件适用于数据清洗、报表导出场景。当用户提供JSON文件路径并希望生成CSV时使用。 --- # JSON 转 CSV 技能 ## 功能 - 读取输入的JSON文件支持嵌套对象与数组展开。 - 按用户提供的字段映射关系输出CSV未指定时自动提取所有叶子字段。 - 保留空值并在CSV中以空字符串表示。 ## 执行步骤 1. 确认用户提供的JSON文件路径存在且为合法JSON格式。 2. 确认输出路径默认输出到输入文件同目录文件名加 _converted 后缀。 3. 运行 python3 scripts/convert.py --input json路径 --output csv路径 [--fields f1,f2]。 4. 将生成的CSV路径返回给用户。 ## 注意事项 - 仅处理JSON格式非JSON文件直接返回错误提示。 - 不修改原文件只生成新文件。 - 超大文件100MB需要提示用户分批处理。这个文件里最关键的不是正文的功能说明而是frontmatter里的description字段。它是Agent判断“当前用户请求该不该调用这个技能”的唯一依据一定要写清楚三个要素输入是什么、输出是什么、在什么场景下用。写法上要注意避免太抽象的空话比如“处理数据”这种描述命中率极低要写“将嵌套JSON按字段映射转换为CSV适用于报表导出与数据整理”。正文部分则是给Agent看的说明书。要写清楚触发条件、执行步骤、边界和注意事项。我的经验是执行步骤一定要编号Agent对编号步骤的执行顺序更敏感注意事项里要写明技能不能做什么避免Agent在边界外乱用。2.2 技能目录组织的几种常见模式技能变多之后目录组织就是一门学问了。我实践中比较常用的模式有三种第一种是单文件技能整个技能只有一个SKILL.md不涉及任何脚本。这种适合纯规则判断类场景比如“列出某个时间段内的所有日期”“判断一个字符串是否符合邮箱格式”。Agent读一遍描述直接靠自身能力就能完成不需要额外工具。第二种是标准技能包目录结构长这样skills/ └── json-to-csv/ ├── SKILL.md ├── scripts/ │ └── convert.py └── assets/ └── example.jsonscripts目录用来放可执行脚本assets目录放示例数据或模板文件。这种结构适合大多数涉及文件处理、数据转换、网络请求的技能。第三种是多技能领域目录当你一个领域下有多个关联技能时用领域名做一级目录分组skills/ ├──>#!/usr/bin/env python3 JSON to CSV converter for agent-skills. import argparse import csv import json import sys from pathlib import Path def flatten(obj, prefix): Flatten nested dict/list structures into dot-separated keys. items [] if isinstance(obj, dict): for key, value in obj.items(): full_key f{prefix}.{key} if prefix else key items.extend(flatten(value, full_key).items()) return dict(items) if isinstance(obj, list): for index, value in enumerate(obj): full_key f{prefix}.{index} items.extend(flatten(value, full_key).items()) return dict(items) return {prefix: obj} def main(): parser argparse.ArgumentParser(descriptionConvert JSON to CSV) parser.add_argument(--input, requiredTrue, helpInput JSON file path) parser.add_argument(--output, requiredTrue, helpOutput CSV file path) parser.add_argument(--fields, helpComma-separated field mapping, e.g. name,age,city) args parser.parse_args() input_path Path(args.input) if not input_path.exists(): print(fError: input file {input_path} does not exist, filesys.stderr) sys.exit(1) try: with open(input_path, r, encodingutf-8) as f: data json.load(f) except json.JSONDecodeError as e: print(fError: invalid JSON format - {e}, filesys.stderr) sys.exit(1) if isinstance(data, dict): rows [flatten(data)] elif isinstance(data, list) and len(data) 0: rows [flatten(item) for item in data] else: print(Error: input JSON must be a non-empty object or array, filesys.stderr) sys.exit(1) all_fields [] if args.fields: all_fields [f.strip() for f in args.fields.split(,)] else: for row in rows: for key in row.keys(): if key not in all_fields: all_fields.append(key) output_path Path(args.output) with open(output_path, w, encodingutf-8, newline) as f: writer csv.DictWriter(f, fieldnamesall_fields) writer.writeheader() for row in rows: writer.writerow({key: row.get(key, ) for key in all_fields}) print(fSuccessfully converted {len(rows)} rows to {output_path}) if __name__ __main__: main()脚本逻辑不复杂我挑几个关键点解释一下。flatten函数是处理嵌套结构的核心它把嵌套的字典和列表统一展开成“点分键”的扁平结构比如用户信息里的地址子对象{city: {name: 北京}}展开后就变成city.name北京这样就能平铺进CSV表格了。这个设计的取舍是牺牲了层级还原能力换来了粗暴简单和完全确定性对Agent技能来说确定性比灵活性重要得多。在字段处理上args.fields提供了用户指定字段映射的入口不指定时自动收集所有出现过的叶子字段但这里有个细节自动收集时我用了一个列表去重逻辑保证字段顺序是首次出现顺序而不是set的乱序这样输出的表头每次都是一致的方便后续程序消费。空值处理上writer.writerow({key: row.get(key, ) ...})保证了某个对象里缺少某字段时CSV里对应位置输出空字符串而不是报KeyError。3.3 技能调试与回归验证技能脚本写完不是直接扔给Agent用的我习惯先手工测试再接入Agent。测试分三步走第一步用最小样例测通主流程。我准备了一个只含两条数据的JSON文件[ {name: 张三, age: 28, city: {name: 北京}}, {name: 李四, age: 35, city: {name: 上海}} ]然后手动执行命令python3 scripts/convert.py --input test.json --output test.csv得到test.csvname,age,city.name 张三,28,北京 李四,35,上海字段被成功展平嵌套的city.name也正确映射符合预期。第二步测异常输入。我分别测了三类情况不存在的文件路径、非法JSON内容、空数组。脚本都正确输出了stderr错误信息并返回了非零状态码这很重要因为Agent在bash里执行命令时依赖退出码判断成功失败如果脚本出错还返回0Agent会以为任务成功了然后拿着错误结果继续往下跑。第三步把技能真正交给Agent测试。我给出指令“把这个目录下的user_data.json转成CSV字段只要name和age”。Agent读到了SKILL.md的描述正确理解了意图主动拼出了包含--fields name,age的命令并执行。这里还有个意外收获因为SKILL.md里写了“不修改原文件只生成新文件”Agent在执行后主动向用户说明了输出文件的路径和与原文件的区别这个体验比之前直接对话处理稳定太多。4. 技能编排、上下文管理与协作经验4.1 多技能协同的约定技能之间不互相依赖随着技能数量增加我开始遇到“多个技能要配合完成任务”的场景。比如电商数据分析需求的完整链路是先从网页抓取商品信息web-fetch技能然后清洗成结构化数据json-to-csv技能最后生成统计报告report-generator技能。如果让技能之间直接互相调用就会形成蜘蛛网般的依赖关系维护成本直线上升。我采用的约定是技能之间保持完全独立多技能的协作统一由Agent在对话层编排。也就是Agent先调用web-fetch拿到原始数据判断数据格式后再决定是否调用json-to-csv最终调用report-generator生成报告。每个技能都对自己的输入输出负责不关心上游下游是谁。这个约定让每个技能都可以独立测试、独立升级。我升级json-to-csv时完全不用担心影响其他技能只要它自己对JSON转CSV的输出负责就够了。另一个细节是技能的命名。技能包目录名和SKILL.md里的name字段要一致而且名字要能直观反映能力避免出现含义模糊的代号。我就干过给技能起名叫“utils”的蠢事结果Agent经常搞不清这个技能到底是干什么的命中率极差。后来全部改成“json-to-csv”“excel-to-json”“html-extract”这种见名知义的命名情况立刻好转。4.2 控制Agent上下文开销的几个土办法技能多了以后另一个现实问题就是上下文长度。虽然现代模型上下文窗口很大但塞满无效信息依然会影响输出质量还会增加成本。所以在技能包的设计上我总结了几条省上下文的经验第一SKILL.md的描述文件要严格控制在一屏以内大概600字左右。过长的描述文件会让Agent在判断是否调用时产生犹豫而且会挤占后续对话的上下文空间。详细的技术文档可以放在docs子目录里Agent需要时再读取。第二不要在SKILL.md里重复通用知识。比如技术栈常用的操作说明、基础命令语法这些Agent本来就会写进去纯属浪费。SKILL.md只写和这个技能相关的、Agent可能不知道的信息。第三脚本输出要精简。脚本的执行结果不要打印大段日志一行成功提示加输出路径就够了详细信息写入日志文件。Agent能看到的是stdout和stderr输出越简洁它在后续推理时消耗的token越少。第四对于会生成大段结构化结果的技能可以考虑先输出到文件再让Agent只读取文件头部做确认。比如json-to-csv技能执行完直接返回“已生成xxx.csv共120行”而不是把120行数据全部打印出来。这条经验在大文件处理时尤其好用能避免Agent被输出淹没。5. 常见问题与排障速查5.1 技能不生效、被误调用的排查思路技能体系跑起来之后我遇到最多的问题就是技能不生效或乱调用。这里把典型的症状和排查方向整理成一个速查表症状可能原因排查与解法Agent完全不调用该技能description写得过于模糊Agent没有识别到匹配重写description明确输入输出和触发场景用具体动词Agent调用了但执行报错技能脚本路径或依赖有问题先手动执行脚本验证确认Python环境和依赖正常Agent调用了错误的技能多个技能描述有重叠Agent无法区分明确各技能的边界在description里加上“不适用于”场景脚本执行成功但结果不对脚本对输入格式的假设不成立用真实数据回归测试检查键名提取和字段映射逻辑技能加载很慢或很大技能包塞了过多不必要的大文件将大文件移到外部存储技能包只保留最小必要文件其中description重写是最高频的解法。我之前有个技能叫“data-process”description写的是“处理各种常见数据格式转换”结果Agent几乎不知道该在什么时候用它因为“各种常见数据格式”太宽泛了。后来我改成“将JSON或CSV数据转换为Markdown表格适用于在对话中展示结构化数据”命中率立马提升。5.2 安全护栏技能包的权限边界与审计技能包带来的安全风险容易被低估。一个不谨慎的技能脚本轻则误删文件重则泄露数据所以我在项目里建立了三条安全护栏第一条是执行环境沙箱化。所有技能脚本在受限用户下执行禁止root权限禁止写入系统目录和项目目录以外的位置。实际操作中我在服务器上单独建了agent-run用户技能脚本的读写范围限定在指定工作目录内。第二条是技能包来源可控。团队内部使用的技能包必须提交到仓库经过代码评审才能合并禁止个人直接从网上下载不明来源的技能包放到生产环境。如果确实需要引入公共技能包我会先逐行审查脚本内容确认没有危险操作后才放行。第三条是操作可审计。技能包里的关键脚本尤其是涉及文件删除、网络请求、数据导出的必须在执行时写审计日志记录操作时间、执行用户、输入输出参数。这样一旦出了问题能快速定位是哪个技能、哪次调用造成的。定这些规则的过程中我最大的感受是技能化的好处是它把Agent的能力边界变得清晰可见了你能直接看到Agent拥有哪些能力、每个能力做什么但这也意味着每个脚本都要以“这是可以在任意时刻被执行的可信代码”的标准来审查。宁可多一点流程也不能贪方便留下后患。结合我自己的经验技能化这条路的长期价值不只是执行稳定性更重要的是它让Agent的能力演进变成了一件可迭代的事。这个月加一个技能下个月优化另一个技能每次改动都是增量式的不会牵一发动全身。如果你正在做Agent相关的东西不妨从手头一个最常做的固定任务开始把它固化成一个技能包你会很快体会到这种模式的爽感。