写这篇文章之前,我刚从一段极其痛苦的调试中爬出来。起因是我们内部一个 Agent 项目里,模型偶尔会把"调用订单查询接口"理解成"直接修改订单状态"。问题不在模型本身,而在我们给模型暴露的"工具"太多、太杂,且彼此之间没有任何隔离和路由。后来我们把所有能力全部收敛进了一个统一的Skill 技能系统,这个问题才从根源上被解决。
这篇东西,就是我在把 Agent 从"能聊天"推向"能干活"的过程中,对 Skill 机制最核心的设计思考、落地实操和踩坑实录。考虑到近期Agent 开发、Skill这些词的热度,我尽量把从抽象概念到代码落地的完整链路写清楚,让你看完能直接动手改造自己的项目。
1. Skill 系统首先解决的不是"能力",而是"边界"
在聊 Skill 之前,我们必须先把一个认知对齐:大模型本身不是"干活"的主体,它是"决策"的主体。真正执行干活动作的,是那些拥有明确输入输出、内部逻辑可被信任的函数、脚本和流程。Skill 系统要做的事,就是在这两者之间搭建一条既通畅又可控的管道。
我见过不少团队,一开始给 Agent 加工具的方式非常野:直接把几十个函数塞进 System Prompt,让模型自己挑。结果就是模型经常挑错,要么参数格式不对,要么在反问用户,要么干脆用幻觉参数调用了函数。核心原因在于,你在让模型理解"一个未经约束的代码库",而不是理解"一组有边界的服务协议"。Skill 系统,本质上就是对能力进行"服务化封装"。
打个生活化的比方:你家里有很多电器,但每个电器的插头标准都不一样,你不可能让插座自己去适配所有电器。Skill 系统就像是那个统一的国标插座,每个电器(工具/函数/脚本)只需要把自己的插头(声明文件)插上去,模型只需要认识这个插座的样子,就能安全地使用所有电器。
1.1 Skill 和普通 Tool/Function 的本质区别
很多人搞不清这俩词的区别,我直接说结论:Tool 是点,Skill 是链。
- Tool:单次、无状态、原子化。比如"查询天气"、"翻译文本"。它完成一个动作,不做流程编排。
- Skill:复合、带状态、带流程编排。比如"生成月度运营报告",它内部可能包含:拉取数据 → 清洗数据 → 生成图表 → 撰写总结 → 输出 Markdown。这一串动作是一个完整的工作流,而不仅仅是单次调用。
从这个角度出发,Skill 的价值根本不是"加函数",而是把高频的、有明确步骤的业务动作固化成模板。当你的 Agent 不再需要每次都"自由发挥"来决定先调哪个接口、再调哪个接口时,它的可靠性和稳定性才会有质的飞跃。
1.2 用 Skill 之后,Agent 的行为逻辑发生了什么变化
这是我最想让你理解的部分。没有 Skill 系统时,Agent 收到"帮我把上个月的销售数据整理成图表"这个请求,会进入一段不可控的推理过程。它可能调分析工具、可能调画图工具、可能搞错时间范围、可能中途死循环。
接入 Skill 系统后,同样的请求会走另一条路:
- 模型先看到的是一个"技能菜单",上面写着技能1,叫"销售报表生成**"**,描述是"根据时间范围自动完成数据抽取、清洗、图表绘制与报告输出"。
- 模型所做的最关键决策,从"我怎么干活"变成了"我该用哪个技能干活"。
- 模型只需要正确填写这个技能需要的参数(时间范围、维度、输出格式),剩下的所有复杂动作,都由这段确定性代码去完成。
这带来的变化是革命性的:模型的自由度从"过程"收窄到了"决策"。而"决策"恰恰是大模型最擅长的,"过程"恰恰是最容易出错和产生幻觉的。这个思维转变,是整个技能系统设计的基石。
2. 核心机制拆解:Skill 的"生命周期"四要素
一个设计良好的 Skill,从定义到失效,必须经历四个阶段。这四个阶段分别对应了一套严谨的机制,缺一不可。我把它们称为Skill生命周期的四要素:注册(Register)、解析(Resolve)、执行(Execute)、回收(Recycle)。
2.1 注册:给能力上户口,而不是直接暴露
注册是整个系统的入口,也是最容易被做废的一步。我见过有团队把 Skill 注册做成了一张巨大的 JSON 配置表,塞了几百个技能,看起来大而全,用起来痛不欲生。
正确的注册逻辑应该包含三层信息:
- 技能标识层:技能名、技能ID、版本号。注意,技能名必须像变量名一样有约束感,要能代表它做的事。糟糕的例子是"数据处理",好的例子是"电商订单异常检测"。
- 技能调用协议层:这是模型的"说明书",包含功能描述、参数定义、返回定义。这一层必须写得极其详尽,因为模型是靠这层信息来做选择决策的。我后面会专门用一小节说明怎么写这份说明书。
- 技能执行体层:指向真正的 Python 函数、API 封装、或者脚本路径。这是最终干活的代码。
我在项目里普遍采用"目录即注册"的模式:每个 Skill 占用一个独立目录,目录内包含SKILL.md(协议层声明)和main.py(执行体),系统启动时遍历目录,统一加载到技能列表里。这样做的好处是极度符合直觉——新增一个技能,就是拷贝一个目录,改改声明和代码,重启加载器。
2.2 解析:从意图到技能的参数转换之旅
这是最核心、也最体现工程水平的环节。大模型传来的内容往往带着冗余信息,比如用户说"看一下近一周的销售情况,重点看华东区的回落趋势",解析器必须能从这句自然语言中提取出结构化参数:时间范围(近一周)、区域(华东区)、关注指标(销售额、趋势)。
参数解析这块,我的经验是:不要指望一次性把参数提取做完美,要做好"多轮补充"的设计。你可以在解析阶段做一个"必要参数校验器",如果必需的参数缺失,就生成一个追问返回给 Agent,让它向用户获取,而不是直接报错终止。
举个我们实际用的校验伪代码:
class SkillResolver: def __init__(self, skill_def): self.required_params = skill_def.get("required", []) self.param_schema = skill_def.get("parameters", {}) def resolve(self, model_output_json: dict): missing = [] for param in self.required_params: if param not in model_output_json or model_output_json[param] is None: missing.append(param) if missing: return { "status": "need_more_info", "missing": missing, "questions": [self.param_schema[p].get("prompt") for p in missing] } # 类型检查和格式校验,比如日期格式、枚举值范围 for param, value in model_output_json.items(): expected_type = self.param_schema.get(param, {}).get("type") if expected_type == "date" and not isinstance(value, str): return {"status": "type_error", "param": param} return {"status": "ready", "params": model_output_json}这个设计的好处是,把"模型可能产生的不确定性"导向了"可预期的对话流程"。Agent 拿到need_more_info的返回后,就知道自己该说什么话去问用户了,而不是干巴巴地报错:"缺少参数,无法执行"。
2.3 执行:带状态、带上下文、带反馈的长跑
Skill 执行不像普通函数那样"调用即返回",尤其是那些多步骤的 Skill,中间可能穿插着等待、数据处理、二次判断。执行机制的设计重点在两块:
第一,执行必须要有"过程可见性"。一个生成报告的 Skill 如果跑 10 秒,用户和 Agent 都会陷入焦虑。你得设计回调机制,比如"正在拉取数据...正在生成图表...正在排版",把这些推进状态实时吐给上层,上层再转成用户能感知的提示。这样即使用户等待,也知道系统在干活。
第二,执行必须支持"中断与恢复"。我自己被这个坑搞惨过:一个 Skill 跑到第 5 步发现外部 API 限流,整个任务失败,前面步骤白跑了。后来我引入了"检查点"机制,每完成一个子步骤,把中间产物写入临时存储,记录步骤编号。下次调用如果发现有一个未完成的检查点,就跳过热启动直接恢复。这个机制在长耗时的数据处理技能里价值极高。
def execute_with_checkpoint(skill_id, task_id, steps): checkpoint = load_checkpoint(skill_id, task_id) start_from = checkpoint.get("last_completed_step", 0) for i in range(start_from, len(steps)): result = steps[i].run() save_checkpoint(skill_id, task_id, last_completed_step=i+1, intermediate_result=result) clear_checkpoint(skill_id, task_id) return finalize(result)2.4 回收:善后比冲锋更重要
这个环节容易被忽略,但它决定了系统能不能长期稳定运行。Skill 执行过程中会产生临时文件、大对象、甚至是占用内存的数据结构,回收阶段要做的就是把它们全部清理干净。
我在项目里对回收的定义包括:关闭打开的文件句柄、删除临时目录、释放大对象引用、记录执行日志到独立存储(方便事后审计)。更重要的是,回收阶段必须生成一份"执行摘要",包括执行状态、消耗时间、关键输出指标的摘要,这些数据要喂回给 Agent,让它对后续对话有感知。
3. 实操手记:从零定义你自己的第一个 Skill
概念说清楚了,下面直接进入动手环节。我以"生成项目周报"这个超级经典的场景为例,带你走一遍完整流程。它非常典型——包含数据拉取、文本总结、格式输出三个子模块,且必须由代码确定性执行,而不是让模型自由发挥。
3.1 搭建技能目录结构
首先是最底层的目录组织。我会创建一个skills/根目录,里面放独立技能子目录。对于项目周报这个技能,结构如下:
skills/ └── weekly_report/ ├── SKILL.md ├── main.py ├── requirements.txt └── templates/ └── report_template.md为什么这么拆?因为每个技能可能有自己的数据依赖和模板,把它的所有资源全部圈在独立目录里,后续做权限控制、资源配额、版本管理都会非常方便。而且巡视代码的时候,一眼就能看出这个技能需要什么。
3.2 编写 SKILL.md:这是给模型看的关键文档
这一份文档的质量直接决定 Agent 的调用准确率。你要记住:这份文档的读者不是人,是大模型。所以它应该采用高度结构化、带有"防误解"的描述。
--- name: weekly_report_generator description: 根据用户提供的项目代码仓库和团队成员信息,自动分析 Git 提交记录, 生成包含工作内容、亮点、风险点和下周计划的周报。适用于项目复盘、 团队同步、向上汇报等场景。如果用户想写周报、复盘项目进度,使用此技能。 version: 1.0.0 --- ## Parameters - repo_path: 字符串,必填,项目本地仓库路径 - since: 字符串,必填,开始日期,格式 YYYY-MM-DD - until: 字符串,可选,结束日期,默认今天 - team_members: 数组,可选,团队 GitHub 用户名列表 ## Process 1. 调用 git log 分析 since 至 until 间的提交记录 2. 按成员分组,统计提交数量与代码变更行数 3. 使用大模型提取每个成员的核心工作要点 4. 结合仓库中的 Issue 状态,识别风险点 5. 渲染 Markdown 周报并返回 ## Output Format 标准 Markdown 表格,包含成员、核心贡献、亮点、风险与下周计划五列。 ## Notes - 如果 since 早于仓库首次提交时间,自动截断到首次提交时间 - 如果仓库路径无效,返回错误码 REPO_NOT_FOUND,不要尝试自动猜测路径注意三点细节。一是description的开头是什么,这决定了模型在理解用户模糊意图时的第一匹配依据,要写"如果用户想……使用此技能"这种决策导向的话。二是Parameters里给每个参数写明"必填/可选"和"格式约束",模型严格按照这个来填参数,错误率大降。三是Notes里有明确的异常处理指令,比如"不要尝试自动猜测路径",这能有效阻断模型的臆测行为。
3.3 实现 main.py:让代码保持"无脑执行"
当 SKILL.md 写清楚了之后,main.py 的逻辑应该非常简单、非常确定。它不需要什么高超的智能,只需要老老实实按参数执行。我把内部拆成三个函数,清晰对应三步流程:
# /weekly_report/main.py from datetime import datetime, timedelta import subprocess import json def collect_git_log(repo_path: str, since: str, until: str) -> list: cmd = ["git", "-C", repo_path, "log", "--since=" + since, "--until=" + until, "--pretty=format:%H|%an|%ad|%s", "--date=short"] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: raise RuntimeError("REPO_NOT_FOUND") commits = [] for line in result.stdout.strip().split("\n"): if not line: continue hash_, author, date, msg = line.split("|", 3) commits.append({"hash": hash_, "author": author, "date": date, "message": msg}) return commits def aggregate_by_member(commits: list) -> dict: members = {} for c in commits: members.setdefault(c["author"], []).append(c["message"]) return members def generate_report(repo_path: str, since: str, until: str, team_members: list = None) -> str: commits = collect_git_log(repo_path, since, until) members_data = aggregate_by_member(commits) # 此处可接入大模型做要点总结,但原则是:核心聚合计算绝不让模型直接执行 # 渲染模板,生成最终Markdown return render_template(members_data)在这个实现里有几个工程细节值得关注:
- 检测不到的仓库,直接抛
REPO_NOT_FOUND,而不是内置一些模糊逻辑。因为模糊逻辑会掩盖真实的路径问题,让排查难度倍增。 - 所有复杂计算都在 Python 里完成,大模型最多只做自然语言层面的组句总结。如果你让模型帮你做代码行数统计,那是在自找麻烦。
- 输出是一个完整的 Markdown 字符串,不回传中间结果,这样上层 Agent 拿到的已经是可交付的产物,而不是需要二次加工的半成品。
3.4 注册与加载:启动时扫目录
最后,在 Agent 系统启动时,加载器遍历skills/目录,把每个子目录的SKILL.md解析成一个结构化字典,同时确保main.py可以被导入。这个加载器通常几十行代码就够:
def load_skills(): skills = [] for skill_dir in SKILLS_ROOT.iterdir(): if not skill_dir.is_dir(): continue sk_md = skill_dir / "SKILL.md" if not sk_md.exists(): continue # 解析 frontmatter 和参数定义 meta = parse_frontmatter(sk_md) module = importlib.import_module(f"skills.{skill_dir.name}.main") skills.append({ "id": meta["name"], "description": meta["description"], "resolve": SkillResolver(meta), "execute": module.main, "meta": meta, }) return skills加载完毕的 skills 列表,会被序列化成一行行简短的技能摘要,注入到 Agent 的系统提示词中。注意是"摘要"而不是全部文档。因为模型上下文有限,我们要让它先知道"有什么技能",等到具体选中某个技能后,再把对应的完整 SKILL.md 文档填入上下文。这个"两级阅读"设计能显著减少 Prompt 长度和干扰项。
4. 从"能跑"到"好用":必须注意的三个隐性要素
很多人的 Skill 系统做到了上面那步,就以为大功告成了。但真正在实践中,还有三个隐性要素,决定了这套系统是"玩具"还是"生产系统"。
4.1 权限与沙箱:别让技能成为安全后门
Skill 的本质是让 Agent 执行代码,这就意味着你必须在执行环境上做严格管控。我在内部对不同的 Skill 划分了三种信任级别:
- 完全信任级别:运行在主进程内,通常是我们自己封装的官方工具,经过严格的评审。
- 部分信任级别:运行在子进程中,操作系统层做文件读写限制,不允许访问网络。
- 零信任级别:运行在容器中,连系统调用都受限,只暴露标准输入输出。社区贡献的 Skill 通常落在这层。
最让我记忆深刻的一次教训是,一个同事写的"PDF 表格提取"技能,内部用了一个带漏洞的第三方库,好在沙箱拦住了,不然攻击者可以借这个技能读取服务器上的任意文件。沙箱是最后一道防线,绝不能裸奔。
4.2 缓存与预加载:让高频技能快得飞起
有些技能是高频调用的,比如"格式化时间"、"翻译文本"、"URL 解析"。这类技能如果每次启动都要加载一堆依赖库、初始化模型会话,那延迟高得会让人怀疑人生。
我的做法是为高频技能单独开一个"常驻池",启动时直接预加载。比如翻译技能,我会预先把模型加载到内存里,技能被调用时直接从池子里取会话,响应能从秒级降到毫秒级。代价是内存占用上去了,但换取的是用户体验的稳定。这块本质上是在做取舍,你要根据你的预算量去平衡。
4.3 版本与回滚:技能也会迭代,别用旧版本卡死自己
只要你坚持用这套系统,技能一定会迭代。SKILL.md 里的描述可能写得不对,main.py 的处理逻辑可能有 BUG,你需要有快速回滚的能力。我的做法是给每个技能目录打上 git tag,注册表里记录的是当前生效的 commit hash。出了问题,一条命令就能整个技能回退到上一版,而不影响系统里其他正在运行的技能。
5. 常见问题与排查技巧实录
我把过去半年里团队踩过的高频问题整理成一个速查表,你可以直接对照排查。
| 症状 | 根因分析 | 解决方案 |
|---|---|---|
| 模型频繁选错技能 | SKILL.md 的 description 写得太泛或太窄 | 重写 description,用"如果用户想...使用此技能"句式,加入 2-3 个典型例句 |
| 参数解析总是失败 | 参数定义缺少枚举值和格式约束 | 在 schema 中明确 enum 列表、正则表达式、格式样例 |
| 技能执行超时 | 技能内部存在死循环或外部依赖过慢 | 增加超时控制,用signal.alarm或 asyncio wait_for 包裹执行体 |
| 技能污染上下文 | 执行产物过长,盲目回传所有数据 | 设计摘要函数,只回传核心指标,完整数据写入外部存储 |
| 多技能冲突 | 两个技能功能重叠,描述相似 | 会开技能评审,明确各自边界,必要时在 description 里写反例:"本技能不适用于XX场景" |
| 并发调用互相干扰 | Skill 内部用了共享全局变量 | 严格代码规范,skill 执行体应为纯函数,禁止读写模块级可变全局量 |
| 沙箱内文件权限异常 | 子进程工作目录不对,或挂载卷权限错误 | 日志中记录实际工作目录与 uid/gid,对照容器配置逐项检查 |
除了这张表,还有一个排查思路上的心得:当 Agent 表现诡异时,先查的是"模型看到了什么",而不是"代码做了什么"。把注入给模型的技能列表完整打印出来,看描述是否清晰、是否有歧义、是否符合预期。大概率问题就出在"模型视角"的信息质量上。这是我们调试多轮后总结出的优先级原则——先 SQL 后代码,先提示词后逻辑。
6. 多技能协同:Agent 从"单点功能"走向"流程编排"
单个 Skill 做得再好,也只是 Agent 工具箱里的一把称手工具。真正让 Agent 走向"干活"的,是多技能之间的协调机制。这一层我把它的演进分成三个阶段。
- 阶段一:顺序调用。Agent 按照用户请求,依次调用技能 A、技能 B、技能 C。例如"收集这周的数据,然后用周报技能生成报告,最后用电邮技能发出"。这是最基础也最可控的编排方式。
- 阶段二:条件分支。Agent 根据某个技能的输出结果来决定是否调用下一个技能。比如"分析订单异常,如果异常率超过 5%,则触发告警技能,否则只记录日志"。这需要系统提供清晰的决策信号给 Agent。
- 阶段三:并行编排。多个彼此独立的技能同时执行,最后汇总结果。这在处理"综合分析"类任务时非常有用。比如同时调用"销售数据技能"和"市场舆情技能",Agent 拿到两份结果后,再进行交叉分析。
我在实际项目中,比较提倡的做法是:先用阶段一的顺序调用跑通业务闭环,然后逐步嵌入条件分支。并行编排虽然强大,但会消耗大量 token 和资源,只有当你的技能稳定性足够高时才建议开启。
这块还想多说两句。技能编排协议的设计非常重要。每个技能的执行结果返回格式必须统一,我通常定义一个标准结构:status(成功/失败/需追问)、summary(无论成功失败都给一段简短摘要)、detail(完整详细结果,可选)。Agent 拿到这个结构,就能很平滑地决定下一步动作,而不是自己去 parse 乱七八糟的自由文本。
7. 对"Skill 技能系统"的延伸思考与经验收尾
写到这里,我想把话题稍微拉高一点。Skill 系统表面上是工程架构问题,本质上是产品思维的重组。当你开始用技能思维来组织 Agent 的能力时,你会不自觉地把自己的业务流程重新梳理一遍:什么环节是高频稳定的?什么环节需要人来兜底决策?什么环节是变化迅速的?这些思考远超技术本身。
从 Agent 进化的角度看,我个人的判断是:未来的 Agent 开发,会越来越像"组装和调试一组精密仪器",而不是"在对话里堆砌提示词"。谁能把业务拆成边界清晰、质量可控的 Skill,谁就掌握了让 Agent 真正可靠工作的钥匙。这个结论我在多个项目里都验证过。
最后分享一个非常有用的收尾技巧:给你的技能系统加一个"诊断模式"开关。打开后,系统会把每个技能被调用的完整链路——包括模型选的技能名、填入的参数、执行的每步日志、返回的摘要——都实时输出到一个调试面板。这能让你在做 Agent 调试时省下 80% 的时间,因为你不再需要盲猜 Agent 内部发生了什么。这比任何花哨的功能都实用,强烈建议你在动手的时候,第一件事就是把这个诊断面板搭好。