做Agent开发这段时间,我最大的感受就是:模型越来越聪明,可一旦让它去真实环境里“干活”,翻车率依然高得离谱。你让它查一份数据,它能给你编一份;你让它改个配置,它能给你改坏;你让它调个接口,它连参数都拼不明白。问题不在模型本身的智力,而在Agent和现实世界之间少了一层“手艺”。Agent Skills这个概念,就是冲着这个缺口来的——它把模型调用工具、完成任务的隐性能力,封装成一套标准、可复用、可插拔的技能模块,让Agent不再是个只会聊天的嘴强王者。
这篇文章我想从概念讲到落地,从零手搓一个真实可用的Agent技能,把定义、实现、调试、踩坑的完整过程都摊开来讲。适合正在做Agent应用开发、被工具调用和任务编排折腾得头疼的工程师,也适合想搞明白Agent到底怎么“动手”的产品和技术负责人。
1. Agent Skills到底解决什么问题——别再用“插件思维”理解它
1.1 从“裸模型”到“能干活的Agent”,差在哪一层
很多人对Agent的理解是:把大模型的API接上,给它一个System Prompt,它就是一个Agent了。真这么简单的话,市面上就不会有那么多“Demo五分钟,上线两星期”的翻车现场了。
裸模型的能力边界很清晰:它只擅长 token 到 token 的映射,你给它文字,它还你文字。但现实世界的任务几乎都是非文字闭环的——你要查数据库,它得先能连上数据库驱动;你要生成报表,它得能跑Python脚本;你要操作Git仓库,它得能执行Git命令并解析输出。这一连串“感知-决策-执行-验证”的循环,模型自己完成不了,必须靠外部工具和代码把断点接起来。
传统的做法是Function Calling,让模型从预定义函数列表里挑一个调用。但在真实项目里,函数一多,你很快会遇到几个问题:模型选了函数但参数填错、函数执行结果太长把上下文撑爆、函数返回的格式不固定导致解析逻辑越写越复杂。更麻烦的是,Function Calling的粒度太细,它把“做事”拆成了“调接口”,而真正的任务往往是“调一堆接口并处理中间结果”。我在实战里经常遇到的情况是——为了完成一次代码评审,模型要连续调用七八个函数,中途任何一个失败,整个链路就断了。
Agent Skills的思路则完全不同。它管的是“一条完整的手艺”,而不是“一个单一的动作”。一个技能内部可以包含多步操作、多个脚本、预设的验证逻辑和异常兜底,Agent只需要判断“现在该用哪个技能”,然后把控制权交给这个技能去执行,就像人遇到故障时不是现场学修车,而是直接拿起一套标准维修流程。
1.2 和 Function Calling、Plugin、MCP 的边界与配合
很多读者会问:这玩意儿和OpenAI的Plugins、Anthropic的Claude Skills、社区炒得火热的MCP(Model Context Protocol)到底啥关系?我用一句话给你理清:Function Calling是“手”,Plugin是“把工具打包好给你装手上”,MCP是“给手统一了插线口”,而Agent Skills是“一本标准化操作手册+全套工具包”。
在实际系统里,它们不是替代关系,而是各管一层。我个人的推荐分层是:
- MCP负责打通工具连接层:让Agent统一通过协议去调外部资源,解决的是“连得上、连得规范”;
- Agent Skills负责封装任务执行层:把“怎么干、干完怎么验、出错怎么兜底”这些经验沉淀成可复用模块,解决的是“干得好、干得稳”。
打个比方,MCP是水电煤的接口标准,所有家电都能插;但Skills是菜谱,告诉你先放油还是先放盐,以及炒糊了该怎么补救。菜谱的价值不在接口,而在经验。这也是为什么我在实际项目中,即便已经接了MCP工具,依然要花大量精力去写Skills——工具的边界是“能用”,Skills的边界是“会用”。
2. 设计一套Agent Skills的正确姿势——核心是“接口”,不是“代码”
2.1 先把技能接口长什么样定义清楚
一套成熟的Agent Skills,最核心的文件不是实现脚本,而是一份技能说明书。业界常见的实践中,这个说明书通常叫SKILL.md,放在技能目录的根目录下。它的作用不是给人看的,而是给模型看的——模型通过阅读这份说明书来决定“这个技能在什么场景下适合用、怎么用、有什么禁忌”。
我用一个最小化的模板说明核心字段:
--- name: repo_inspection description: 对本地Git仓库执行健康检查,统计代码规模、提交活跃度、遗留TODO与风险点。适用于代码评审、接手新仓库、发布前检查等场景。 when_to_use: 当用户要求分析仓库质量、评估代码活跃度、检查技术债务遗留,或准备发布版本前需要一次全面巡检时。 when_not_to_use: 当任务只涉及单个文件的内容修改,或要求不运行任何命令时,不应调用本技能。 --- # 仓库健康检查技能 ## 执行步骤 1. 确认仓库路径参数 path 存在且为有效Git仓库 2. 执行 `git log --since=90.days --pretty=format:%H` 统计近90天提交数 3. 执行 `git ls-files | wc -l` 统计跟踪文件总数 4. 扫描代码中的 TODO/FIXME 标注并输出位置 ## 输出格式 以markdown表格输出:仓库路径、文件总数、近90天提交数、活跃开发者数、TODO总数、风险等级 ## 异常处理 - path无效或不是Git仓库时,返回错误码ERR_PATH并给出可用仓库的检测命令 - 命令执行超时超过30秒,终止当前操作并返回部分结果注意几个设计要点:when_to_use和when_not_to_use一定要写。这两个字段决定了模型从“技能库”里抓取技能时的命中率。你在开发的时候经常遇到“技能没被调用”的窘境,八成就是这篇说明书的关键事件写得不对——模型不知道怎么把用户需求映射到你的技能上。
2.2 技能描述怎么写,模型才真的看得懂
写技能描述这件事,完全是在写“提示词”。我踩过好几次坑之后总结出三条铁律:
**第一,用触发场景做描述,别用功能清单。**你写“支持Git仓库分析”,模型还是懵的;你写“当用户想了解一个代码库的整体健康状况时”,模型立刻能对上号。模型是对意图匹配,不是对关键词匹配。
**第二,明确写出不能用的场景。**这一点容易被忽略,却特别重要。when_not_to_use的作用是消除歧义。你不想让一个仓库分析技能在用户问“帮我改一下某个文件里的注释”时被调用,那么你明确写出来,模型的行为会稳健一大截。
**第三,执行步骤要像菜谱一样给足细节。**别只写“分析仓库”,要写清楚先跑什么命令,后解析什么输出,异常情况处理策略。我见过大量Agent技能实现得很简陋,说明书写了个标题就让模型自由发挥,结果就是每次执行的表现都像开盲盒。
技能描述的好坏,决定你得到的是一个稳定的交付物,还是一场不可控的实验。
3. 从0到1手搓一个技能:代码仓库健康体检
3.1 任务拆解:把“体检”变成可执行清单
光谈概念不过瘾,我直接带你做一个完整案例。这个技能的目标很简单:给任何本地Git仓库做一次“体检”,输出仓库的活跃度、规模、遗留风险和贡献者分布。
先把任务拆成四步:
- 确认仓库状态:路径是否存在、是否Git仓库、是否有未提交改动;
- 采集规模指标:跟踪文件总数、代码行数、目录深度;
- 采集活跃度指标:近90天提交数、活跃贡献者数、提交频率分布;
- 扫描风险点:找出代码中的
TODO、FIXME、XXX标注,以及大于某个阈值的超大文件。
这四步是“合格的人”会做的体检流程,现在要把它们变成模型可复用的技能。每个步骤都要有明确的执行命令和输出解析逻辑。
3.2 核心实现:SKILL.md + 执行脚本 + 测试样例
技能目录结构我建议这样组织:
repo_inspection/ ├── SKILL.md # 技能说明书 ├── inspect.py # 核心执行脚本 ├── tests/ # 测试样例和预期输出 │ ├── sample_repo/ # 一个小型模拟仓库 │ └── expected_output.md └── requirements.txt # 依赖列表核心脚本我直接用Python写,只依赖标准库和Git命令行,降低部署成本:
import subprocess import os import json import re import sys from pathlib import Path def run_git(repo_path: str, args: list[str]) -> str: result = subprocess.run( ["git", "-C", repo_path] + args, capture_output=True, text=True, timeout=30, check=False, ) if result.returncode != 0: raise RuntimeError(f"git {' '.join(args)} 失败: {result.stderr}") return result.stdout def inspect_repo(repo_path: str) -> dict: repo_path = Path(repo_path).expanduser().resolve() if not repo_path.exists(): return {"ok": False, "error": "ERR_PATH", "message": f"路径不存在: {repo_path}"} git_dir = repo_path / ".git" if not git_dir.exists() and not (repo_path / "HEAD").exists(): return {"ok": False, "error": "ERR_NOT_REPO", "message": "目录不是有效的Git仓库"} # 仓库基本状态 branch = run_git(str(repo_path), ["branch", "--show-current"]).strip() status = run_git(str(repo_path), ["status", "--porcelain"]).strip() has_uncommitted = len(status) > 0 # 规模指标 file_count = int(run_git(str(repo_path), ["ls-files"]).count("\n")) line_count = 0 pattern = re.compile(rb"[^\n]*\n") for file in run_git(str(repo_path), ["ls-files"]).split("\n"): if not file: continue try: with open(repo_path / file, "rb") as f: line_count += len(pattern.findall(f.read())) except (OSError, IsADirectoryError): pass # 活跃度指标:近90天 recent_log = run_git(str(repo_path), ["log", "--since=90.days", "--pretty=%H|%an|%ae"]) commits_90d = len([line for line in recent_log.split("\n") if line]) authors_90d = set() for line in recent_log.split("\n"): parts = line.split("|") if len(parts) == 3 and parts[2]: authors_90d.add(parts[2]) # 风险标注扫描 risk_refs = [] for file in run_git(str(repo_path), ["ls-files"]).split("\n"): if not file: continue if any(file.endswith(ext) for ext in [".md", ".txt", ".lock", ".png", ".jpg"]): continue try: with open(repo_path / file, "r", errors="ignore") as f: for idx, line in enumerate(f, start=1): for marker in ["TODO", "FIXME", "XXX", "HACK"]: if marker in line: risk_refs.append({"file": file, "line": idx, "marker": marker, "content": line.strip()[:80]}) break except OSError: pass # 超大文件(阈值默认 1000 行) large_files = [] threshold = int(os.environ.get("LARGE_FILE_THRESHOLD", 1000)) for file in run_git(str(repo_path), ["ls-files"]).split("\n"): if not file: continue try: with open(repo_path / file, "rb") as f: n = sum(1 for _ in f) if n > threshold: large_files.append({"file": file, "lines": n}) except OSError: pass return { "ok": True, "branch": branch, "has_uncommitted": has_uncommitted, "file_count": file_count, "line_count": line_count, "commits_90d": commits_90d, "active_authors_90d": sorted(authors_90d), "risk_refs": risk_refs[:50], "large_files": large_files[:20], "risk_level": estimate_risk(file_count, commits_90d, len(risk_refs)), } def estimate_risk(file_count: int, commits_90d: int, risk_count: int) -> str: if file_count > 5000 or commits_90d < 5 or risk_count > 30: return "high" if file_count > 1500 or commits_90d < 20 or risk_count > 10: return "medium" return "low" if __name__ == "__main__": path_arg = sys.argv[1] if len(sys.argv) > 1 else "." result = inspect_repo(path_arg) print(json.dumps(result, indent=2, ensure_ascii=False))这个脚本一点都不复杂,但把“体检”这个模糊要求变成了可复现的量化输出。测试样例方面,我建了一个只有十几个文件的小仓库放在tests/sample_repo下,专门验证:不是仓库能识别、路径错误能兜底、脚本能输出预期JSON字段。这些细节在实际调模型时帮了大忙。
3.3 实测调优:从“能用”到“好用”的三个细节
脚本跑通只是开始。我在实际接入Agent之后,陆续做了三处关键调优:
**第一处:给风险扫描加了上限。**一开始没有risk_refs[:50]和large_files[:20]的限制,遇到一个巨大的老仓库,输出直接几万行,对话上下文瞬间被塞满。这几乎是Agent实操里最致命的错误——上下文是有限资源,任何技能都应该有“输出预算”意识。
**第二处:把“活跃”从“近30天”改成“近90天”。**一开始我用了30天窗口,结果很多节奏正常的仓库都被判定为低活跃,误报率很高。改成90天之后,和团队实际的发布节奏就对齐了。这种参数不是拍脑袋定的,要看你服务的团队是什么样的迭代节奏。
**第三处:给异常加明确的错误码。**ERR_PATH、ERR_NOT_REPO这些返回值,可以让Agent在拿到结果后迅速做出“换路径、换技能、还是直接问用户”的决策,而不是对着一段堆栈发呆。让输出结构可被程序化消费,是Agent技能和人工脚本之间最大的区别。
4. 常见坑与排障实录——这些坑我几乎每次都会踩
4.1 模型“假装调用”技能,怎么发现
我碰到过很多诡异的情况:模型在回复里说“正在执行仓库检查”,但根本没有触发任何技能调用,然后下一句直接给出一段编造的检查结果。这就是所谓的幻觉性技能调用。
排查思路很简单:检查Agent运行日志里的工具调用序列。如果回复文本里出现了“执行”“分析”“我已经检查”这类词,但日志里没有对应的技能触发记录,基本可以判定为幻觉。解决办法有两个方向:
- 在系统提示词里明确写“未触发技能调用时,禁止描述执行过程,直接表示无法完成”;
- 在应用层加一道钩子,凡是文本中包含“已完成”且对应技能调用记录,就强制回退重试。
实际试验下来,第二种方法更稳,因为它把“防止幻觉”从提示词层面提到了工程层面。
4.2 技能环境依赖冲突,隔离方案比想象中重要
技能多了之后,Python依赖冲突是最烦的问题。技能A需要requests==2.28,技能B装了个新版本requests==3.x,结果A的技能悄悄挂了。这个坑我在接第八个技能的时候终于忍不住彻底解决。
方案很简单:**给技能配置独立的虚拟环境,而不是共享全局环境。**目录结构改成:
repo_inspection/ ├── .venv/ # 独立venv ├── requirements.txt ├── SKILL.md └── inspect.py启动时通过venv/bin/python执行,保证环境隔离。代价是每个技能多占几十MB磁盘,但换来的是稳定的隔离性,这笔账怎么算都划算。如果你用Docker跑Agent,那就更省心,每个技能一个容器镜像,边界更干净。
4.3 召回失败:为什么模型该用的时候不用
排名前三的失败原因,我逐个跟你说清楚:
**原因一:说明书里的触发器太抽象。**写“当用户关心项目健康状况时”不如写“当用户提到代码质量、仓库活跃、TODO清理、发布前风险检查时”。模型匹配的是具体表述,不是模糊概念。
**原因二:技能被另一个同类技能遮蔽。**如果技能库里有两个高度相似的技能,模型会随机选,或者选错。解决办法是把两个技能的when_to_use写得差异化,让它们分别覆盖互斥的场景。
**原因三:模型对技能调用产生犹豫。**AI大模型在低置信度时会选择“自己编答案”而不是“调用工具”。这种情况下我建议在系统Prompt里降低工具调用的心理门槛:“如果需要的信息在技能返回结果中,请优先调用技能;即使不确定,也请尝试调用后再回答。”
5. 进阶:技能的组合、版本化与安全边界
5.1 把多个技能编排成一个“复合技能”
技能之间可以组合。我现在做代码评审时,不是一个技能打天下,而是组合调用三个技能:repo_inspection做仓库体检、git_diff_analysis分析变更内容、style_checker扫描代码风格。为了让组合更顺畅,我通常再写一个code_review的编排技能,它的SKILL.md里不是自己实现逻辑,而是定义“先调A,再调B,最后调C”的编排规则。
这种做法的好处是把“流程”本身变成可复用资产。新项目来了,直接拉起code_review这个编排技能,就自动获得了整套评审流程,而不是每次都要模型现场规划。
5.2 技能仓库:团队沉淀与版本管理
当技能的规模超过10个之后,建议引入版本管理。我不但把每个技能放在独立的Git仓库里,还用Tag标记版本,在SKILL.md头部加version字段。这样当模型调用技能时,可以带上版本号,升级技能不影响正在运行的老任务。
团队协作时,技能仓库建议就放在代码仓库里,按skills/目录归集,大家用同样的技能库。配合CI,每次改动技能都会自动跑一遍tests/目录下的样例,确保老功能不被改坏。别小看这一步——技能也是代码,不测试的技能迟早会在生产环境“咬你一口”。
5.3 安全边界:权限最小化与沙箱
最后说安全,这是最不能省的部分。让Agent自由执行命令,等于把一把万能钥匙交给了实习生,必须设好护栏。我的建议有三条底线:
- 技能只能访问它明确需要的路径:进程内通过
chroot或用容器做文件系统隔离; - 禁止技能读取敏感环境变量:比如把API密钥、数据库密码从技能的执行环境里剥离出去,必要时通过
secrets接口按需注入; - 所有命令执行都要有超时和输出上限:防止某个技能写死循环,把整个Agent进程拖垮。
我做了一个简单的沙箱封装:用subprocess跑技能脚本时,传入一个最小化的环境变量白名单,网络请求默认禁止,需要联网时单独申请出口。这套规则写进团队技能开发规范后,事故率明显下降。
关于Agent Skills,我最后想说的
这半年多来,我最大的体会是:**Agent能不能干活,不取决于模型多大,而取决于你给它配了什么技能、技能写得多好、边界画得多清楚。**模型是发动机,Skills是变速箱——发动机再猛,没有变速箱的匹配,车轮子也转不到该去的速度。我强烈建议你从一个小场景开始,比如今天说的仓库体检,手写一个技能,再加到你的Agent里去。等你把第一个技能调顺了,你会发现整个Agent的稳定性上一个台阶,而且这套方法可以复用到几乎任何业务场景。