直接说结论:这几年做智能体相关的东西,越来越多人开始提“Skills”这个概念。你可能在不少技术社区看到过这个词,它指的是给大模型配的一类可复用的能力模块,有点像一个带说明书的工具箱。以前大家把复杂任务一股脑塞进提示词,结果模型要么顾此失彼,要么输出格式漂移,根本没法在实际工程里稳定复用。而技能模块化之后,每个能力独立封装、独立测试、独立维护,想用的时候直接声明一句“调用XX技能”,模型就知道该翻哪本说明书、跑哪个脚本。这篇文章不聊虚的,我会从零开始拆解一个技能模块的内部结构、手把手写完一个可用的技能,再把调试、版本管理、团队协作这些真实工程里绕不开的坑都过一遍。适合正在做智能体应用、或者想把大模型任务从“提示词一团糟”状态里解放出来的开发者看。
1. 技能模块的设计逻辑
1.1 先搞清楚“技能”到底是什么
通俗点说,大模型本身是个很聪明但“手很空”的大脑,它能理解语言、能推理、能生成文本,但没法直接读你本地文件、没法帮你批量跑命令、也没法稳定按照某个行业模板输出内容。技能模块干的活,就是给这个大脑装上“手”和“工具包”。
一个典型的技能模块,通常包含两部分:
- 一份用自然语言写成的说明文件,告诉模型“什么情况下用我、按什么顺序操作、输出什么格式”;
- 一个或多个真正干活的脚本或资源文件,比如Python脚本、Shell命令、数据模板。
拿我自己的经验举例子。之前做一个自动化运维相关的智能体,经常需要检查服务器上某个目录的占用情况、找出异常日志。如果每次都让模型“直接看服务器”,它根本做不到;但如果只把一段长长的命令写在提示词里,下次换个场景又会失效。后来我把这套检查逻辑封装成一个技能模块,模型只需要识别出用户意图是“体检服务器”,就自动去翻我写好的操作手册,按手册调用脚本,最后把结果整理成固定格式的报告。这个体验和之前相比,完全两码事。
1.2 为什么“自然语言指令 + 可执行脚本”是最稳的组合
有人会问:既然模型能力这么强,让它在对话里自己生成命令、自己执行不就行了?这里存在三个问题:
- 稳定性差。模型每次生成的命令可能有细微差异,一个错误的引号、一个错误的参数,就可能导致失败。
- 安全性差。让模型自由发挥去执行系统命令,等于让一个实习生在没有操作规范的情况下去碰生产环境,风险完全不可控。
- 无法沉淀。就算这次跑通了,下次呢?整个过程没有被固化成资产,知识没有积累。
技能模块采用“自然语言说明 + 可执行脚本”的组合,本质上就是把不确定的推理过程和确定的执行逻辑分开。模型负责判断“该不该用、参数怎么填”,脚本负责“用确定的方式完成任务”。这样既保留了模型的灵活性,又拿到了工程上需要的确定性和安全性。
1.3 模块化设计带来的额外好处
这一点在团队协作时体现得特别明显。之前项目里四个人共建同一个智能体,如果所有逻辑都堆在提示词里,根本没法并行开发。改成技能模块之后,每个人负责几个独立的技能目录,各自的代码和说明互不干扰,测试也互不影响。
另外,技能模块天然适合版本管理。每个技能单独升级、单独回滚,出问题时候不会拖累整个系统。这一点在后续调试那部分我会详细讲。
2. 从零搭建一个可用的技能模块
2.1 明确技能边界:不要贪多
动手写之前最重要的一件事,不是写代码,而是划清边界。一个技能只做一件事,而且这件事必须能被清晰地描述出来。
我见过不少新手最容易犯的错,就是做一个“全能技能”,又查日志又做数据分析又发通知。结果说明文件写了一大篇,模型根本搞不清什么时候该启用它,甚至连输出格式都会互相打架。
以我这次要做的示例技能为例。我打算做一个“仓库健康体检”技能,任务边界非常明确:
- 输入:一个Git仓库的本地路径;
- 执行:分析该仓库的状态,包括未提交改动、未推送提交、近期提交频率、文件变更情况;
- 输出:一份结构化的仓库健康报告。
这个技能不负责修复问题,也不负责推送代码,边界极其清晰。实际做的时候,你会发现边界越清晰,说明文件越好写,模型也越容易理解。
2.2 准备项目骨架目录
技能模块通常以一个独立目录存在,目录名就是技能名。我的习惯是统一用skills/作为根目录,技能名称全部小写加短横线,比如skills/repo-health-check/。
下面是一个推荐的骨架结构:
skills/ └── repo-health-check/ ├── SKILL.md ├── scripts/ │ └── analyze_repo.py └── assets/ └── report_template.mdSKILL.md:技能说明文件,模型最先读的就是它,所以命名和描述极其重要;scripts/:存放执行具体逻辑的脚本,Python也好、Shell也好,关键是保证可独立运行;assets/:存放辅助资源,比如输出报告用的模板、静态配置等。
这个结构不是拍脑袋定的。它对模型很友好,因为模型在判断是否调用时,第一步就是扫目录和描述,一个好的目录结构能节省大量调试时间。
2.3 编写技能说明文件:给模型看的操作手册
SKILL.md是技能能不能好用的灵魂所在。模型就是通过这份文档来理解技能的用途、约束和操作流程的。写这份文档的核心,是把它当成给你的同事的操作手册,而不是给机器读的接口文档。它需要足够清晰,让模型知道“我是谁”“什么时候出手”“按什么顺序做什么”“最终交出什么”。
下面是我为仓库健康体检技能写的说明文件示例:
# 仓库健康体检技能 ## 用途 检查一个本地Git仓库的运行状态,输出健康报告。 当用户询问仓库是否正常、有哪些改动未提交、推送是否滞后、近期变更是否频繁时, 优先使用本技能。 ## 使用条件 - 已经有一个本地Git仓库的路径; - 用户需要的是仓库状态汇总,而不是修改代码。 ## 执行步骤 1. 将用户提供的仓库路径传入脚本 `scripts/analyze_repo.py`; 2. 按照脚本输出内容生成最终报告; 3. 如果脚本报错,检查是否为合规的Git仓库。 ## 输出格式 返回报告时,包含以下板块(使用Markdown无序列表): - 当前分支 - 未提交的改动数 - 未推送的提交数 - 最近一周的提交次数 - 工作区状态:干净 / 有改动 / 有冲突 ## 注意事项 - 不要执行 `git push`、`git commit` 等任何写操作; - 如果路径不存在,直接告诉用户路径有误,不要尝试其他命令; - 如果 `.git` 目录丢失,说明“该目录不是有效的Git仓库”,停止执行。别看内容不长,每一句话其实都有讲究。“使用条件”和“注意事项”就是给模型上紧箍咒,防止它在错误场景下乱调用。尤其是“不要执行写操作”这种约束,直接,比在代码里做一万次防御都管用。
2.4 写确定性逻辑脚本:把脏活累活交给代码
SKILL.md是大脑,脚本就是手脚。脚本的目标是接收明确的入参、输出格式化结果。为了保持确定性,我尽量少依赖外部库,把功能集中在标准库能解决的范围内。
下面是scripts/analyze_repo.py的一个简化版本:
#!/usr/bin/env python3 import subprocess import sys import json def run_cmd(cmd, cwd): result = subprocess.run( cmd, cwd=cwd, capture_output=True, text=True, encoding="utf-8" ) if result.returncode != 0: return {"ok": False, "error": result.stderr.strip()} return {"ok": True, "output": result.stdout.strip()} def main(repo_path): # 1. 检查目录是否存在 import os if not os.path.isdir(repo_path): print(json.dumps({"ok": False, "error": "目录不存在"})) return # 2. 检查是否有 .git 目录 git_dir = os.path.join(repo_path, ".git") if not os.path.exists(git_dir): print(json.dumps({"ok": False, "error": "不是有效的Git仓库"})) return report = {} # 当前分支 res = run_cmd(["git", "branch", "--show-current"], repo_path) report["branch"] = res["output"] if res["ok"] else "unknown" # 未提交的改动数 res = run_cmd(["git", "status", "--porcelain"], repo_path) if res["ok"]: report["uncommitted_changes"] = len([l for l in res["output"].splitlines() if l.strip()]) else: report["uncommitted_changes"] = -1 # 未推送的提交数 res = run_cmd(["git", "log", "--oneline", "@{u}..HEAD"], repo_path) if res["ok"]: report["unpushed_commits"] = len([l for l in res["output"].splitlines() if l.strip()]) else: report["unpushed_commits"] = 0 # 没有上游分支时默认0 # 最近一周提交次数 res = run_cmd(["git", "log", "--oneline", "--since=1 week ago"], repo_path) if res["ok"]: report["commits_last_week"] = len([l for l in res["output"].splitlines() if l.strip()]) else: report["commits_last_week"] = 0 report["ok"] = True print(json.dumps(report, ensure_ascii=False, indent=2)) if __name__ == "__main__": if len(sys.argv) != 2: print(json.dumps({"ok": False, "error": "请传入仓库路径"})) sys.exit(1) main(sys.argv[1])这个脚本最大的特点就是稳。它不依赖模型在现场编写命令,所有命令都被固定封装在函数里。模型只需要负责把用户给到的路径填进去,然后解读JSON结果即可。
2.5 技能描述信息:决定模型能不能找到你
除了SKILL.md,大多数智能体框架还要求给技能配一份简短的描述信息,用于在运行时就完成匹配。这份描述通常包括技能名、用处、触发场景。别小看这段内容,它决定了模型在什么情况下能“想起”这个技能。
以本技能为例,描述就可以写成:
name: repo-health-check description: 检查本地Git仓库健康状态,包括当前分支、未提交改动、未推送提交、近期提交频率,适合用户询问代码是否同步、仓库是否正常等场景。描述里的关键词“检查健康状态”“未提交改动”“仓库是否正常”都是触发器。经验是:把用户可能会用到的口语化表达尽量多地放在描述里。用户说“我的代码怎么还没同步啊”,模型才能通过“同步”“仓库”这类词联想到这个技能。
3. 技能调试、版本管理与团队共享
3.1 调试的层次感:从脚本到指令再到大模型行为
写技能容易,调试技能才真正考验人。我的调试路线通常分三层,每层都有一套独立的验证方法。
第一层,纯脚本调试。落地到本地,把示例参数传进去,确认脚本本身的输出符合预期。比如我先跑一句:
python skills/repo-health-check/scripts/analyze_repo.py /tmp/test-repo看命令行输出是否是合法的JSON、分支名是否正确、改动数是否符合预期。这一层不过关,其他都免谈。
第二层,说明文件与脚本匹配度检查。重点检查SKILL.md里描述的步骤、参数格式、输出模板和实际脚本行为是否一致。这一层经常出问题,比如说明文件里写“返回最近一周的提交次数”,脚本里却统计的是“最近一天的提交次数”,这种不一致会让模型产生幻觉输出。
第三层,真实场景下的端到端验证。把它放到智能体对话环境里,用不同的说法问同一个问题。比如分别问“帮我看看这个仓库状态怎么样”“仓库是不是有问题”“我改了东西还没提交的有多少”,看模型能否稳定命中技能、填充参数是否正确、报告格式是否统一。
这三层调试能沉淀成一套可复用的回归清单。以后每次改技能,只要跑一遍清单,就能快速判断改动是否引入回归。
3.2 版本管理的落地方式
技能本质上是代码,因此也需要纳入版本管理。我的方式很直接:每个技能就是一个子目录,一个参数仓库存放所有技能,技能名称和目录名一一对应。
每次修改技能时,遵循三条规则:
- 一个技能独立提交,不要和无关修改混在一起;
SKILL.md和脚本一起更新,保持说明与实现一致;- 提交信息里写明修改原因,例如“优化触发场景描述”“修复未设置上游分支时的错误处理”。
很多人会忽略第2条。实际项目里,最常见的问题就是脚本升级了,说明书没跟着改,结果模型还按老办法操作,输出自然对不上。我自己就因为这种事情踩过坑,所以现在只要动脚本,必然检查说明文档,缺一不可。
3.3 团队共享与协作机制
当技能数量多了以后,就能沉淀出内部技能库。这时候最核心的协作机制是“技能评审”。我见过比较有效的做法是:新技能提PR,团队成员重点评审三个方面:
- 技能边界是否清晰,会不会和已有技能冲突;
- 脚本是否处理了常见异常输入;
- 说明文件是否具备可读性,换一个不了解背景的同事能不能看懂。
这套机制看起来繁琐,其实能帮所有人减少返工。团队里每个人都可能用别人写的技能,如果技能质量参差不齐,最后受苦的是大模型和下游用户。评审不是走形式,而是把“独门秘笈”变成“公共设施”的必经之路。
另外,共享时要注意技能之间的相互独立性。我建议技能之间不要共享私有脚本,如果有公共逻辑,要么直接在技能内复制一份,要么单独做成公共依赖。原因很简单:过度耦合会让技能库越来越难维护,模型也可能因为依赖关系而无法在离线环境使用某个技能。
4. 常见问题与排查实录
4.1 高频问题速查表
在实际构建技能模块的过程中,几乎每个人都会遇到以下几类问题。我把最常见的整理成了一个速查表,方便你在踩坑现场对照。
| 故障现象 | 可能原因 | 排查思路 |
|---|---|---|
| 模型聊到无关话题也触发技能 | 技能描述里的关键词过于宽泛 | 在描述里增加“当且仅当”“用户明确提到”等限定词;补充负面触发词 |
| 模型不按说明文件的步骤执行 | 说明文件步骤太笼统,模型有歧义 | 给每一步补充具体的参数示例、预期输出;明确数字编号,不要让模型自行排序 |
| 脚本本地能跑,接入智能体后立刻报错 | 环境变量、当前工作目录、权限与本地不一致 | 在脚本内部用绝对路径定位;对所有外部依赖做显式检查;先打印系统上下文再跑逻辑 |
| 同一个问题不同措辞,有时命中有时不中 | 技能描述覆盖度不够,模型召回不稳定 | 扩充同义表达;对照历史对话里用户的实际问法,把高频说法写进描述 |
| 技能多了以后互相污染 | 多个技能描述中存在重叠关键词 | 梳理技能边界,重叠部分合并;为每个技能增加“这个技能不负责什么”的说明 |
| 调用技能后输出格式五花八门 | 输出格式只写了“用Markdown”,不够具体 | 给出一个完整的输出示例,模型会优先模仿示例格式 |
这六类问题覆盖了我遇到过的大多数真实故障。其中“输出格式五花八门”最容易被人忽视,因为你可能觉得格式不重要,实际上在自动化流水线里,一个字段的缺失就可能让下游任务中断。
4.2 一个值得分享的排查案例
之前做一个项目,模型调用技能时总是自己脑补参数,比如仓库路径明明有,它非要往路径里加一个不存在的子目录。当时我试了很久没解决,后来才反应过来:说明文件的“执行步骤”里没有明确告诉模型“仓库路径直接使用用户提供的路径原文,不要拼接任何内容”。
这种问题属于典型的隐性知识缺失。对开发者来说,“不要拼接路径”是常识,但对模型来说,它没有这个常识。解决办法就是把这些行业内默认的“潜规则”显式写进说明文件的注意事项里。
类似的情况还有:脚本明明会自己处理某种异常,但模型因为不知道而提前终止了。后来我把所有可能的异常分支都写在注意事项里,模型才学会“先让脚本跑一遍,再判断问题”。
4.3 一个小技巧:让模型先展示分析过程再执行
我在调试中发现,很多技能失败并不是因为脚本写错了,而是模型在调用前对入参的理解跑偏了。一个很有效的缓解手段,是在SKILL.md里写明:
执行过程中,先简要说明你的分析和计划,再调用脚本。 最终输出中,必须包含实际执行结果,而不是你的推测。不要小看这句话。它逼着模型把“怎么理解用户请求、打算怎么填参数”显式外化出来,开发者一眼就能看出模型哪里想偏了。这个习惯练成的效果,要比反复修改描述词有效得多。
这套技能模块化的工作方式,我实际操作下来最大的体会是:它解决的不仅是模型的工具能力问题,更是工程协作中的知识沉淀问题。以前经验都长在个人脑子里,现在可以长在仓库里,新人进组看技能库就能了解系统能做什么。如果你正在做智能体相关的东西,不妨从一个小技能开始,把最常用、最稳定的流程固化下来,慢慢你会感觉到,那些原本又“脆”又不听话的提示词工程,终于能长出确定性的筋骨了。