skills,一个简洁到近乎任性的项目标题。我盯着它看了很久,最后决定把它写成一份完整的大模型智能体实战复盘。起因是过去一年里,我一直在折腾 AI Agent 的应用开发,被“提示词越堆越长、功能却越改越不可控”这个老问题反复折磨。后来我换了一条路径:把重复性任务拆解成一个个可复用的技能(skills)包,让智能体按需加载、按步骤执行,稳定性和可维护性都上了一个台阶。这篇文章就是把这套逻辑完整讲透:技能是什么、为什么能解决提示词解决不了的问题、完整的目录结构和设计思路、从零搭建一个技能的实操过程,以及我在多次调试中踩过的坑和排错方法。不管你是刚开始做 Agent 的新手,还是已经写了无数提示词的熟手,都能找到能直接拿去用的部分。
1. 先弄清楚:这个 “skills” 到底解决什么问题
1.1 从一次让我崩溃的智能体事故说起
大概半年前,我在做一个内部的数据分析自动化项目。需求不复杂:每周拿到一份销售明细的 CSV,需要清洗、聚类、生成固定格式的周报。一开始我直接把需求写成一大段提示词塞进系统提示词里,然后让智能体去“发挥”。结果很稳定地翻车:第一次输出格式错了,第二次日期格式乱了,第三次倒是格式对了,但把数据读错了。每次我都得重新交代一遍需求,甚至把整个对话推倒重来。
我不怪模型,因为问题根源在于我把一个“应该由确定性代码完成”的任务,交给了一个擅长自由发挥的模型。每次它都在重新“猜”我的需求,而不是执行一套稳定的流程。这就是我在这个项目里最大的教训:想让智能体稳定地做一件事,不能只靠提示词,得给它一套可以反复使用、包含明确步骤和可选专用工具的“技能包”。
1.2 技能的定位:从“一段话”升级成“一个可复用的工具箱”
打个比方。提示词像是你口头交代一个实习生:“帮我把这份数据整理一下。” 他第一次会做错,第二次又换个方式做错。而技能是什么?技能是你递给他一本操作手册,外加几个专用工具:手册里写清楚“第一步做什么、第二步做什么、遇到异常怎么处理”,工具则是“你只管用,它能完成精确计算的部分”。模型负责判断“现在该上哪个技能”,然后加载技能里的规则,必要的时候调用技能里的脚本来完成确定性操作。
所以“技能”这个标题下的内容,本质上讲的是智能体能力扩展的组织方式:用一个标准化的目录结构,把某类任务所需的说明书(技能描述与步骤)、可执行程序(脚本)和参考资料(模板、样本数据)打包在一起。这是目前各类大模型 Agent 项目中最常见、也最值得投入时间的实践方向。
1.3 谁适合读这篇
说白了,这三种人最适合:
- 正在开发智能体应用、但发现提示词越来越难维护的开发者;
- 想把个人工作流(日报、周报、文件批处理、数据清洗)沉淀成自动化能力的效率爱好者;
- 第一次听说“技能”这个概念、想搞清楚目录结构和调用机制的新手。
文章后半部分我会给出完整的实操步骤和排查经验,全程用我实际用过的案例来讲,不绕弯。
2. 技能的整体设计与核心思路
2.1 技能和普通提示词的根本差异
先说一个我经常被问到的问题:“技能不就是把提示词换个地方放吗?” 不是的。两者的区别可以归纳成三点:
| 维度 | 普通提示词 | 技能 |
|---|---|---|
| 载体 | 一段自然语言文本 | 一个目录:说明文件 + 脚本 + 资源 |
| 执行方式 | 模型现场理解并自由发挥 | 模型按说明加载规则,必要时调用脚本做确定性计算 |
| 复现性 | 换个说法结果就变 | 相同输入下,关键操作由代码保证,结果稳定 |
| 可维护性 | 改一处可能影响全局 | 每个技能独立,互不干扰 |
| 扩展能力 | 模型只能“说”,不能精确操作 | 能读文件、执行命令、调用外部接口 |
我用一个生活化的类比:普通提示词是“你告诉厨师要做一道鱼香肉丝”,至于肉切多厚、油温几成、咸淡怎么调,全凭厨师当天的心情。技能则是“你同时给厨师一张菜谱和一套标准量具”,菜谱写清楚每个步骤的量化标准,量具负责精确控制。模型变成流程调度者,而不是每一步都靠感觉。
2.2 一个技能的标准目录结构
在我的实践中,一个技能通常长这样:
skill_name/ ├── SKILL.md # 技能说明文件:名字、描述、完整流程 ├── scripts/ # 可执行脚本:Python、Shell 等 │ └── main.py └── assets/ # 资源文件:模板、参考样本、静态配置 └── report_template.md三个组成部分各司其职:
- SKILL.md是灵魂。它前半部分是元信息(名字、描述),后半部分是给模型看的操作手册。
- scripts/放模型可以调用的程序。判断标准很简单:如果这个环节必须精确(比如解析 CSV、计算汇总、批量改文件名),就应该写成脚本,而不是让模型自己编代码现写。因为模型现场写代码会有概率出错,而且每次都不一样。
- assets/放只读资源。比如周报的固定排版模板、某个字段的合法值列表。资源文件的好处是避免把大段固定内容写进说明文件,导致上下文被撑爆。
这里有个值得强调的设计原则:说明文件里的描述要精炼,把详细步骤放在正文;脚本要保持小和单一;资源只放“确定性内容”。这个在后面实操里我会反复验证。
2.3 命名与描述:为什么这是“AI 的入口”
技能靠什么被触发?绝大多数情况下,是模型在每次对话时看到每个技能的“名字 + 描述”,然后判断当前任务是否匹配。也就是说,名字和描述就是技能的“展示位”。
我踩过最典型的坑:给技能起名叫data_handle,描述写“这个技能用于数据处理”。结果模型在 80% 的输出里都把它当成一个模糊的参考,从不主动调用。后来我改成generate_sales_weekly_report,描述写成“当用户需要根据销售明细 CSV 生成带固定模板的周报时使用。输入:CSV 文件路径;输出:按模板生成 Markdown 周报。” 触发率立刻高了很多。
所以给技能写描述时,我会遵循一个公式:
触发条件(什么任务场景下用) + 输入(需要什么参数/文件) + 输出(会产出什么结果) + 边界(不做什么)
边界尤其容易忽略。比如一个“数据清洗”技能,如果不写“只处理 UTF-8 编码的 CSV,不负责数据库读写”,模型什么活都往里塞,后面你会很难受。
2.4 我总结的四条设计原则
做技能这一年多,我把踩过的坑提炼成了四句话:
- 单一职责。一个技能只解决一类任务。哪怕两件事很像(比如“周报生成”和“月报生成”),如果模板差异大,就分开;如果只是参数不同,才考虑合并。
- 渐进式暴露。描述里只写“这个技能能做什么、什么时候用”,不要写详细步骤。详细步骤放在说明正文里,等技能被触发、上下文加载了再展开,节省模型的决策成本。
- 确定性操作交给脚本。凡是“算”的活都给代码,凡是“理解、判断、组织语言”的活留给模型。边界划清楚,稳定性翻倍。
- 结果导向。说明正文里必须明确写出“输出格式是什么、遇到数据缺失怎么处理、脚本失败怎么反馈”。把异常路径说清楚,模型才知道什么时候该求救,而不是硬编一个错误答案。
3. 从零搭一个技能:完整实操过程
3.1 先定义场景与输入输出
纸上谈兵没意思,我用前面提到的“销售周报生成”来做一个完整案例。第一步不是写代码,而是把需求锁死。
我这个技能的场景是:用户丢给我一个销售明细分表(sales_data.csv),我需要生成一份固定格式的周报,包含总销售额、订单数、Top5 商品、环比变化、异常提示。输入是表路径,输出是一份 Markdown 周报。为了减少后续扯皮,我还提前约定了几条硬规则:
- CSV 第一行必须是列名,且必须包含字段:date, product, quantity, amount;
- date 格式是 YYYY-MM-DD;
- 如果遇到缺失金额的整行,直接丢弃并记录数量;
- 只汇总最近 7 天数据(相对于数据中最新日期)。
这些规则看起来琐碎,但它们就是后面说明文件和脚本的分工依据。
3.2 编写技能说明文件(SKILL.md)
接下来写 SKILL.md。我的习惯是前面用一段简短的 YAML 元信息区,后面是正文。元信息只保留 name 和 description 两个字段,保持精简。
--- name: generate_sales_weekly_report description: >- 当用户需要根据销售明细 CSV 生成周报时使用。输入:CSV 文件路径; 输出:按固定模板生成的 Markdown 周报,包含总销售额、订单数、 Top5 商品、环比变化和异常提示。只处理 UTF-8 编码的 CSV, 不负责数据库读写,不做跨周数据分析。 ---然后正文部分,我会用清晰的小节来约束模型的执行方式。内容大概是这样:
# 执行步骤 1. 先检查输入文件是否存在且为 CSV 格式,编码必须是 UTF-8。如果不满足,直接返回错误说明,不要继续。 2. 调用 scripts/analyze.py 脚本,传入两个参数:CSV 路径和输出统计结果文件路径。 3. 脚本退出码为 0 时,读取统计结果文件,按 assets/weekly_report_template.md 的模板生成 Markdown 周报。 4. 脚本退出码非 0 时,读取标准错误输出,把报错原因整理成中文问题说明反馈给用户,不要强行生成周报。 # 硬性要求 - 所有金额按两位小数展示,单位是元。 - Top5 商品按销售额降序排列。 - 环比变化指最近 7 天窗口与上一个 7 天窗口的对比。注意,整个说明文件的核心不是“教模型怎么写周报”,而是“教模型怎么按照一套流程调用脚本、解析结果、套用模板”。真正精确的计算全部交给脚本去做。
3.3 补充脚本与资源文件
接着写 scripts/analyze.py。这个脚本的任务是:读 CSV,做清洗和汇总,把 JSON 结果写到指定文件。脚本要特别注意三件事:参数从命令行接收、错误信息写到标准错误、返回非零退出码表示失败。
#!/usr/bin/env python3 import csv, json, sys from collections import defaultdict def main(): if len(sys.argv) != 3: print("usage: analyze.py <input_csv> <output_json>", file=sys.stderr) sys.exit(1) input_csv, output_json = sys.argv[1], sys.argv[2] try: rows = [] with open(input_csv, newline='', encoding='utf-8') as f: reader = csv.DictReader(f) for r in reader: if not r.get('amount'): continue try: rows.append({ 'date': r['date'], 'product': r['product'], 'quantity': int(r['quantity']), 'amount': float(r['amount']), }) except (ValueError, KeyError) as e: print(f"字段解析失败: {e}, 原始行: {r}", file=sys.stderr) sys.exit(2) # 这里简化处理:按最新一条数据的日期往前推 7 天作为窗口 rows.sort(key=lambda x: x['date']) latest = rows[-1]['date'] # 不展开日期计算细节 week_rows = rows # 本示例省略具体窗口过滤逻辑 result = { "total_amount": round(sum(r['amount'] for r in week_rows), 2), "order_count": len(week_rows), } with open(output_json, 'w', encoding='utf-8') as f: json.dump(result, f, ensure_ascii=False, indent=2) except Exception as e: print(f"处理失败: {e}", file=sys.stderr) sys.exit(3) if __name__ == '__main__': main()代码并不复杂,但它把“哪些行要被丢、金额怎么汇总、结果怎么输出”全部固化了。之后模型再也不会凭感觉处理这些数据。至于 assets/weekly_report_template.md,就是一个普通的 Markdown 模板:
## 销售周报 - 统计窗口:{{window}} - 总销售额:{{total_amount}} 元 - 订单数:{{order_count}} ...模板的作用是约束最终产出格式,不让模型自由发挥排版。
3.4 注册技能并做第一轮测试
把上面三个文件放进skills/generate_sales_weekly_report/目录后,就轮到注册和测试。不同 Agent 框架的注册方式略有差异,但通常都是把 skills 根目录路径告诉框架,框架会自动扫描子目录。我习惯的做法是在本地建一个最小测试项目:
mkdir -p my_agent/skills/generate_sales_weekly_report/{scripts,assets} cp analyze.py my_agent/skills/generate_sales_weekly_report/scripts/ cp template.md my_agent/skills/generate_sales_weekly_report/assets/然后准备一份故意带坑的测试数据,至少包含:正常行、缺 amount 的行、非法日期行。跑一次对话,观察智能体是否触发技能、脚本是否正常执行、周报模板是否被正确套用。第一轮我几乎每次都发现问题——不是脚本路径不对,就是模板里变量没被替换干净,所以一定要留出迭代时间。
3.5 版本管理与后续迭代
技能目录最好纳入版本管理。我个人的项目里会给技能加一个CHANGELOG.md,记录每次改动:改了描述、加了脚本参数、还是换了模板。原因很实际:技能被触发时模型只是加载最新版,但如果换了个框架、换了团队,没有人知道这个技能为什么长成这样,改起来会非常痛苦。
迭代节奏上,我建议“先跑通再优化”。技能只要能稳定完成任务,就先收手。不要一开始就给技能加一堆参数、做一堆兼容,那样反而把说明文件塞得太复杂,模型反而不知道该从哪一步开始。
4. 注册与调用机制:模型到底怎么找到技能的
4.1 描述信息与检索触发
很多人第一次接触技能时最好奇的问题是:“模型怎么知道该用哪个技能?” 说白了,在大多数实现里,每次对话开始或每次用户消息进来,系统的调度层会把所有技能的名字和描述整理成一份清单,作为上下文的一部分。模型看到清单后,根据当前用户请求去做“语义匹配”,如果命中某个技能,就加载技能正文并进入执行模式。
这就解释了一个神奇的现象:有些技能明明写得很好,用户需求也很明确,但模型就是不用。十有八九是描述写得和用户请求的“说法”对不上。比如用户说“帮我算一下这周卖了多少”,你的技能描述却写“生成销售周报”,模型可能在犹豫。我的对策是在描述里加几个触发同义词:“也可用于回答总销售额、订单量、销售趋势相关问题。” 这个细节帮我提高了不少触发率。
4.2 调用过程中的上下文传递
技能被触发后,并不需要把技能目录里的所有文件一次性塞给模型。通常的做法是“渐进式加载”:
- 先注入 SKILL.md 的正文,让模型知道整体流程;
- 模型按流程执行到“调用脚本”这一步时,框架负责运行脚本,把参数传进去;
- 脚本的标准输出和结果文件路径暴露给模型,模型据此生成最终答案。
这种设计的用意是省上下文窗口。如果每个技能都把脚本源码、模板全文、样本数据一股脑注入,对话还没开始就超了。所以我在封装技能时也会刻意控制 SKILL.md 的长度,正文超过 300 行就要考虑拆分。
4.3 冲突处理与优先级
当技能数量变多,会出现“两个技能描述很像、模型选错”的问题。我遇到过最典型的一次:一个叫convert_image_format,一个叫batch_resize_images,用户说“把图片改成 PNG”,模型选了半天选到 resize 那个。排查后我发现,问题出在convert_image_format的描述里没有写“也处理格式转换、扩展名修改这类请求”。
优化方式有两个层面。一是描述层面加边界和触发词,一句话讲清楚“什么时候用我、什么时候别用我”。二是调度层面,在框架里给技能配置优先级字段,比如格式转换类优先级高于批处理类。这个字段在不同框架里名字不同,但目的都一样:让模型在模糊情境下先选更具体、更纯的那个技能。
5. 常见问题与排查技巧实录
5.1 技能一直不触发
这是出现频率最高的问题。我在排查时先做三件事:
- 检查技能目录是否放对了位置、是否被框架成功扫描到。可以通过框架自带的技能列表命令(通常是
list_skills之类)确认。 - 检查描述是否足够“可匹配”。太抽象(“这个技能用来处理数据”)和太具体(只有英文一个说法)都是常见雷区。
- 换一种用户表述去测试:如果用户说“生成周报”能触发,说“这周卖了多少”就不触发,那就在描述里补充同义触发词。
如果以上都没问题,我还会打开调试日志,看模型在决策时到底有没有把技能描述列入考量。这个操作在不同框架里叫 verbose 或 debug 模式,打开后能看到每次决策的候选技能清单和最终选择。
5.2 脚本执行失败但模型硬着头皮继续
比不触发更危险的是“脚本已经报错了,模型还在自由发挥”。我见过模型在统计脚本失败后,直接根据文件名猜了一个数字出来,还一本正经写进周报。这属于说明文件里的异常路径没写清楚。
解决办法:在 SKILL.md 正文里写死“脚本退出码非 0 时必须停止生成结果,并把标准错误内容转述给用户”,同时在代码层面配合——脚本要把异常原因写得足够明确。我把两者配合好之后,这类情况几乎绝迹。
5.3 上下文被输出结果撑爆
脚本如果输出一整份几十 KB 的统计明细,模型再把所有内容读进上下文,很快就到极限。我采用三种缓解手段:
- 脚本只输出摘要(比如 Top5、总额、数量),完整明细写到结果文件;
- 模板里的可选字段保持最少,能不放图就不放图;
- 如果一定要建模读长文本,让脚本先做一次“切块”,模型按需分段读取。
这个原则总结成一句话:尽量让脚本做压缩,让模型做解读。
5.4 多技能协作时的顺序混乱
有些任务是串行多步的,比如“先清洗数据、再生成图表、最后写周报”。如果技能之间没有协作约定,模型可能跳步。我在这种情况下会额外写一个“流程编排技能”(比如run_weekly_pipeline),描述里写清楚先后顺序,然后在每一步内部调用对应技能。这样既保留了各技能的单一职责,又给了模型一个明确的执行路线。
5.5 我的排错工具清单
最后,把我在这个项目里沉淀的排错次序整理成一张表:
| 问题现象 | 优先排查点 | 常用手段 |
|---|---|---|
| 技能不触发 | 描述与用户请求是否匹配 | 加触发词、改名称、开调试日志 |
| 脚本报错 | 环境依赖、路径是否正确 | 打印标准错误、本地手动执行脚本 |
| 输出格式不符 | 模板变量是否被正确替换 | 先跑一次纯脚本流程,再联调模型 |
| 上下文超限 | SKILL.md 或脚本输出过大 | 压缩输出、分块读取、精简正文 |
| 选错技能 | 描述重叠或边界不清 | 加边界描述、设置优先级字段 |
6. 进阶经验:这几个点很少有人明说
6.1 安全边界要提前设计
技能能执行脚本,这本身就说明它的权限比普通提示词大。所以接外部数据源、或者让技能处理敏感文件时,我会坚持两条原则:一是脚本里不硬编码任何账号、口令、访问令牌;二是脚本的运行环境尽量隔离,最小权限启动。解释给团队用的时候我会强调:技能是“能力扩展”,不是“信任扩展”。你现在觉得脚本没风险,等技能数量超过 20 个、执行频率变高,任何一条硬编码密钥都会变成隐患。
6.2 技能的价值在于“流程资产化”
做了十几个技能之后,我最大的感受是:技能真正值钱的不是那几行脚本,而是你用代码和说明文件把“自己的判断”固化下来了。比如我知道周报的环比应该怎么算、异常阈值设多少、模板哪几个字段不能删,这些原本只存在我脑子里的经验,现在变成了一个团队都能调用的标准化资产。新人接手时,不再需要追问一堆细节,直接看技能说明文件就能上岗。
6.3 小步迭代胜过一次性完美
最后一个建议听起来普通,但真的重要:先做一个只覆盖 80% 场景的“能用版”,跑两个星期,再把暴露出的边界和异常补进说明文件。我在第一个技能上就犯了追求完美的错误,花三天做了 20 个参数的结果,上线后发现其中一半根本没被模型调用过,还让描述变得特别难懂。后来我把没用到的参数全部删掉,技能反而更稳定了。
如果你正打算在自己项目里引入技能体系,我的建议是从“你每周都要重复三次以上、且有明确输入输出”的任务开始,按这篇文章的路子先做一个出来跑跑看。做完第一个,你自然就知道第二个、第三个该怎么设计和取舍了。