☰
Agent开发实战:用Skills实现能力复用与沉淀
2026/10/11 5:14:32 网站建设 项目流程

我今年上半年的大部分业余时间都花在了折腾 Agent 这类应用上,前后换了不少思路。最大的感受不是模型能力不够,而是“能力复用”这件事没人帮我解决:同样一个功能,今天在这个项目里写一遍提示词,明天换个项目又得重写一遍;工具函数散落在各个工程里,没有说明没有入口,模型根本不知道该在什么时候用。后来我把注意力放到了 Skills 上——它不是什么玄乎的新框架,而是一套把“教模型做一件完整的事”打包成标准结构的实践方法。简单说,一个 Skill 就是一个带说明文档、脚本和示例资源的文件夹,Agent 遇到对应任务时会自动发现、读取并调用。

这篇文章就是我从零开始把 Skills 用起来之后沉淀下来的理解、操作步骤和避坑记录,适合正在做 Agent 应用、想给大模型工作流沉淀能力库的开发者参考。我不会讲太抽象的理论,尽量用我自己实际做过的例子说话。

1. 先说说我为什么要盯上 Skills 这个东西

1.1 从重复劳动说起:提示词和代码为什么治标不治本

先说个真实场景。我之前给某内部系统做了一套日志分析助手,需求很简单:把程序输出的原始日志喂给模型,让它找出异常、分类、给出建议。第一次做的时候很顺利,提示词写清楚,配合几个解析函数,基本效果就有了。但问题出在“下一次”:

第二个项目要解析的是 Nginx 访问日志,逻辑稍微不一样,于是我把之前的提示词复制过来,改改例子,又加了一轮新的工具函数。第三个项目要用模型做合同要素抽取,提示词结构又变了一次。半年下来,我的代码仓库里躺着六七套互不相通的提示词工程,每套都有自己的一组 Python 函数,变量命名都不统一。

我自己反思了一下,核心问题不是“提示词写得不好”,而是没有把能力当成独立模块来对待。提示词黏在业务代码里,工具函数散落在各个目录里,模型面对一个任务时,没有一个“能力索引”告诉它:你有哪些技能可以用、每个技能处理什么输入、输出什么结果。于是每次新开发都像从零开始。

Function Calling 算是一次进步,它把函数和描述一起暴露给模型,模型可以根据意图选择调用。但实际用下来你会发现,函数列表一旦变长,模型的选择准确率反而下降,而且这些函数只活在当前代码运行时的上下文里,换一个 Agent 平台就全部失效。

1.2 Skills 到底解决的是什么问题

Skills 的思路和 Function Calling 最大的不同,是它把“能力描述”和“能力实现”彻底绑定在一起,然后作为一个独立实体存放。一个 Skill 不再是一个孤零零的函数签名,而是一个文件夹,里面至少包含一份写给人(和模型)看的说明文档,以及可执行的脚本或配置。

这个设计把问题从“模型能不能调用到函数”变成了“模型能不能发现这个技能”。前者依赖运行时的上下文塞满各种函数定义,后者依赖一个统一标准:只要文件夹里结构正确、说明描述清楚,Agent 平台会自动把技能注册进可选能力池,由模型在合适的时机主动选择。

我还记得第一次用对这种方法时的感觉:终于不用在提示词里堆“请根据以下规则输出 JSON”了。模型读到 SKILL.md 之后会自己决定用什么脚本、按什么格式输出,我只需要在业务层做最终结果校验。

从工程角度说,Skills 还带来一个额外好处:版本管理和共享变得自然。一个技能就是一个文件夹,我可以单独给某个技能打标签、单独测试、单独分发给其他同事使用,不用把整个项目的代码都复制一遍。这比在巨大提示词文件里做修改要安全得多。

1.3 什么场景适合上 Skills,什么场景先别碰

我自己的判断标准大概是这样的:

适合的场景:一是重复出现的文本处理类任务,比如日志分析、摘要、格式转换、信息抽取;二是需要结合本地脚本才能完成的流程,比如读取某个文件、调用某个 API、执行某个数据清洗步骤;三是希望多个 Agent 共享能力的团队内部工具库。这些场景的共同特点是“输入有一定规律但又不完全固定”,恰好是模型擅长处理、而脚本难以穷举规则的结合点。

不适合的场景:一是一次性任务,花十分钟能做完的事,不值得你花一小时去封装技能;二是高度依赖人的主观判断的开放式创作,比如“帮我写一首诗”,这种场景不太稳定,封装了反而限制模型的发挥;三是强交互、多轮对话才能逐步明确需求的事情,技能能处理的是“明确任务”,而不是“探索需求”。

我见过有人为了让一个技能看起来“完整”,硬是把一个本来可以直接用提示词解决的问题拆成三四个脚本加一份复杂说明,结果模型加载技能的时候犹豫不决,效果反而变差。Skills 是给常用动作做的工具箱,不是给所有动作做的收纳盒。

2. 一个 Skill 的文件结构,我踩了两轮才踩明白

2.1 Skill 不是一个功能,是一个可以自我说明的文件夹

我第一次上手时犯的错误是,以为 Skill 就等于一段配置或者一个回调函数。后来参考了若干主流 Agent 框架的约定才搞清楚:关于什么是“标准”,不同平台略有差异,但核心思想高度统一——一个 Skill 在磁盘上表现为一个独立文件夹,里面有固定的文件布局和元信息。

典型的目录结构长这样:

my-skill/ ├─ SKILL.md ├─ scripts/ │ ├─ process.py │ └─ requirements.txt ├─ assets/ │ └─ template.json └─ tests/ └─ sample_input.txt

SKILL.md 是这个技能的入口,是所有元信息所在;scripts 放实际跑逻辑的脚本;assets 放只读数据,模板、样例、参考文本之类;tests 放验收输入和预期结果。有些平台还会要求 assets 里的文件必须在 SKILL.md 中显式引用,不能让 Agent 自己去猜这个文件夹里有什么。

我自己经历了从“写一个函数”到“建一个技能文件夹”的转变之后,最大的感悟是:文件夹本身就是一种隐式的能力发现机制。Agent 扫描到 skills 目录后,只需要打开 SKILL.md,就能判断要不要用、怎么用、需要什么参数。这个机制不依赖代码运行时上下文,也不依赖人肉去改模型系统提示词。

在这个结构里,我最看重的其实是 assets 和 tests。assets 不仅提供静态资源,还能在描述文档里被用来做“one-shot 示例展示”;tests 则是回归的安全网。我后期维护技能的时候,经常是先跑一遍 tests,然后根据失败结果判断是脚本坏了还是描述写得不清晰,而不是摸着脑袋瞎改。

2.2 描述文件里每一段都有用途

SKILL.md 是整个技能能否有效工作的关键。很多人把它当成 README 来写,写得又全又长,结果模型不知道什么条件下该用。我后来总结出一套写法,每一段都有自己的明确用途:

  • 名称和一句话简介:给技能一个一望即知的名称,简介控制在 30 字内,说明核心功能。
  • 使用条件(When to Use):明确列出触发该技能的场景和不触发该技能的场景。这是最重要的部分,模型就是靠它决定什么时候加载。
  • 输入参数(Inputs):以表格或列表形式声明参数名、类型、含义、是否必填,最好带上示例。
  • 输出格式(Output):约定严格的输出结构,尤其是 JSON 的 schema 或 Markdown 模板,避免模型自由发挥。
  • 使用步骤(How to Use):简要描述内部工作流程,比如先读取文件、再执行脚本、再做后处理。
  • 边界与限制(What to Avoid):写清楚这个技能不该做什么,防止模型在不合适的场景强行使用。
  • 示例(Examples):一到两个完整的最小示例,展示从输入到输出的全过程。
  • 版本和依赖(Meta):记录技能版本、依赖环境、作者或归属。

这样排版的好处是,模型通常只需要读“使用条件+输入输出”就能做决策,剩余部分只会在实际调用时被阅读。从工程角度说,这相当于给模型做了一个结构化索引,而不是让它在长篇大论里找重点。

我还习惯把“该技能可能失败的场景”写进 What to Avoid 里。比如“日志文件中若超过 10 万行,请先和用户确认分段执行”,这样能减少模型盲目处理超大数据导致脚本卡死的概率。这一点是我加了之后才意识到有多重要。

2.3 脚本和资产:到底应该放到什么粒度

脚本的粒度问题经常被忽略。初期我习惯把整个流程写进一个又长又复杂的 Python 脚本,导致任何一点变化都要回到脚本里改。后来我改成“说明文档做编排,脚本做单步操作”的方式:SKILL.md 里描述流程顺序,scripts 下放多个职责单一的小脚本,甚至可以互相调用。

比如日志分析这个技能,我拆成了 parse_log.py(负责把原始文本切成结构化的条目)、classify_error.py(负责根据规则或模型输出判断异常类型)、format_report.py(负责生成最终 Markdown 报告)。每一步单独调用,任何一步出问题都能定位。代价是脚本数量变多,但换来的是可测试性和可替换性。

资产的粒度也是一样,不要什么都往 assets 里塞。只放那些技能运行必需的静态文件。我见过有人把 30MB 的参考文档塞进技能目录,结果每次加载都要读一遍,慢得让人抓狂。assets 应该放的是小而关键的模板或白名单,大型语料应该另行管理,在 SKILL.md 里说明路径即可。

脚本资源放好之后,注意一个问题:脚本的可执行权限。在一些 Linux 环境下,Agent 平台会直接以子进程方式执行脚本,如果没给执行权限就会报 Permission Denied。我在交付一个技能模板给同事时遇到过这种情况,后来养成了在每个脚本提交前统一检查权限的习惯。

3. 手把手创建第一个 Skill:日志异常提取与结构化

3.1 先确定边界,不要一上来就写文件

很多人创建技能的第一个动作就是新建文件夹,觉得边写边想效率高。我实际试下来,应该先做的是定义输入输出的边界。拿日志异常提取这个技能举例,我花了一个小时想清楚的问题包括:输入是纯文本日志还是文件路径;日志格式是什么风格(自有格式、通用格式、还是混搭);输出是要严格 JSON 还是要 Markdown 报告;异常等级如何划分;遇到无法识别的行时是跳过还是加到“待人工确认”清单里。

这些边界如果在写完技能后再考虑,往往要在 SKILL.md 和脚本来回改。我先在文本编辑器里草拟了一份“输入输出规格”,类似接口设计文档,然后才动手建目录。这个过程可以帮我发现不少模糊点。

我当时在规格里定了三件事:输入是原始文本内容或文件路径二者之一;输出固定为 JSON,包含 total_lines、errors、warnings、unknown_entries 四个字段;异常等级只分 error 和 warning 两种,其余的归入 unknown。这样定义清楚后,后续所有描述和脚本都围绕这份规格展开,避免“这个也行那个也行”的含糊。

定义完边界,才开始写 SKILL.md。这种“先设计后编码”的顺序看起来老套,但在技能开发里特别值钱,因为 SKILL.md 本身就要做到让人和模型都能一眼看懂。

3.2 写描述文件的实操细节

SKILL.md 的开头几句话,决定了模型会不会选择这个技能,所以我把使用条件写得像“路由规则”一样具体。下面是我实际使用的一个简化版本(去掉了所有平台相关的字段):

# Log Anomaly Extractor 一句话简介:从应用日志中提取异常条目,并按严重级别输出结构化 JSON。 ## When to Use 当用户提供一段原始日志文本或日志文件路径,且期望知道“里面有什么异常、有哪些严重错误”时,使用本技能。 不要在处理结构化数据时使用本技能(如 CSV 转其他格式),也不要用于实时日志监控。 ## Inputs - text: 原始日志内容(字符串)。如果用户给的是文件路径,请在调用脚本前读取文件内容再传入。 - level_threshold: 可选,严重级别阈值,默认 warning。 ## Output 返回一个 JSON,结构为: { "total_lines": 123, "errors": ["...", "..."], "warnings": ["...", "..."], "unknown_entries": ["..."] }

这里我故意把“不要用于实时日志监控”写进去了。因为如果不写,模型很可能用户说一句“帮我看看日志有没有问题”就会调用这个技能,但实际上用户只是想要人肉看一眼文本内容。负向约束能显著提高模型选择技能的准确率。

格式上,我一直坚持用 Markdown 表格和列表来描述参数,而不是在一大段话里自然语言描述。模型对结构化的参数列表解析效果更稳,而且后期人维护也容易。

还有一个细节:SKILL.md 里如果涉及调用脚本,我会明确写上脚本的运行环境。比如“需要 Python 3.10 以上,依赖见 scripts/requirements.txt”。这看起来是写给人看的,但实际上模型也会读到,并在调用前自检环境,减少运行时错误。

3.3 写配套脚本和参数约定

写完 SKILL.md,再写实际干活的脚本。我的分工是:脚本不负责做智能判断,只负责确定性处理和格式化;智能判断(比如某个日志片段算不算真正的异常)由模型在读取脚本输出后完成。

以下是我日志分析脚本的核心思路(Python 代码,只贴关键部分):

import json import re from typing import List def extract_log_entries(text: str) -> List[dict]: lines = text.splitlines() entries = [] for line in lines: m = re.match(r"^(\S+ \S+) (\w+) (.*)$", line) if m: timestamp, level, message = m.groups() entries.append({"timestamp": timestamp, "level": level, "message": message}) else: entries.append({"timestamp": None, "level": "unknown", "message": line}) return entries def analyze(entries: List[dict], threshold: str = "warning"): errors = [] warnings = [] unknown = [] for e in entries: level = e["level"].lower() if level in ("error", "critical", "fatal"): errors.append(e) elif level == "warning": warnings.append(e) else: unknown.append(e) return { "total_lines": len(entries), "errors": errors[:20], "warnings": warnings[:20], "unknown_entries": unknown[:10], } if __name__ == "__main__": # 从 stdin 读取原始日志文本,按固定格式输出 JSON import sys data = sys.stdin.read() res = analyze(extract_log_entries(data)) print(json.dumps(res, ensure_ascii=False, indent=2))

脚本里我把“各类别最多输出多少条”写死成了上限值。这个细节很关键,不然模型拿到的结果可能因为日志太长而爆掉上下文。脚本可以做截断,但必须在输出里说明“只返回前几条”,模型知道信息有截断后,可以主动询问用户是否需要更多细节。

参数约定方面,我在 SKILL.md 里声明了 level_threshold 参数,脚本里则用默认值兜底。这样就可以做到“模型按说明传参,脚本按参数执行;参数缺失时也不会崩溃”。脚本入口统一走 stdin 读入、stdout 输出 JSON,方便 Agent 平台捕获标准输出。

3.4 验证和迭代:怎么算好用

技能写得差不多之后,验证环节不能省。我先准备了三份测试日志:一份是常规格式但级别齐全的样例,一份是混入乱码和异常缩短行的噪声样例,一份是空文件和超长行边界样例。

第一次测试就发现了一个问题:脚本对“日志行中包含多个空格”的解析不准确,导致大量行被归类到 unknown。因为我用了一个贪心的正则,它把多余的空格吞进了 message 字段,而某些“时间戳格式不标准”的日志行没能匹配。解决办法是把时间戳正则放宽,并加上一行规范化处理。

这类问题靠人眼很难提前发现,只有靠边界样例才能暴露。我建议所有技能开发都保留 tests 目录和样例输入,不管是自己迭代还是将来交给别人,都会省很多时间。

当技能通过样例验证后,我会在真实日志文件上再跑一轮,观察模型是否主动调用了这个技能、调用后输出是否稳定。如果模型在一个明显匹配的任务场景下都不调用,通常说明 SKILL.md 里的描述和常见提问方式之间的关联不够紧密。我会把那类提问方式本身写进 When to Use 的举例里,比如“用户可能说‘帮我扫一眼这个日志有没有报错’,这时候也应该触发”。

4. 用 Skills 这半年,我踩过的坑和排查方法

4.1 描述文件写得像论文,Agent 根本不加载

我见过最普遍的问题:SKILL.md 写得极其完整,职责范围阐述得很充分,但模型在对话中就是不调用它。排除了平台故障之后,问题几乎都出在描述文件对“触发关键词”的匹配效果上。

模型判断是否调用技能,主要是把用户意图和 SKILL.md 的描述文本做语义匹配。如果你的描述用的是抽象的、偏后台术语的话,用户用口语提问时匹配度就很低。比如我最初写的是“本技能适用于日志体系中的异常元数据提取”,用户实际说的是“帮我看看这个日报告里面那段红色的是什么”,能匹配上才怪。

后来我改成“当用户希望快速了解一段日志或日志文件中包含哪些异常时使用本技能”,并且明确写上常见的用户表达方式,加载率就上来了。我还养成了一个习惯:每次从同事或用户那里听到一种新的口语化提问,就把它补进 When to Use 的例子里。这就是一个“描述文件持续优化”的过程,不是写完就结束。

排查建议:如果你发现模型始终不调用某个技能,先在另一个干净的对话里用触发场景提问,观察有没有加载日志或提示输出;如果没有,把描述文件里的句子逐句换成口语化表达再试。

4.2 参数类型不一致导致脚本原地爆炸

这个问题在接入外部脚本时特别常见。SKILL.md 里声明输入是整数类型,但 Agent 平台从用户语句里抽取出来的参数永远是字符串。模型照着说明传一个字符串过来的例子:我让某个技能接收 max_items=5,脚本里直接把它拿去 range() 用了,结果 TypeError 导致整个流程中断。

排查方法是在脚本入口统一做类型转换和默认值兜底。我后来给所有脚本写上这样一段防御逻辑:

import argparse parser = argparse.ArgumentParser() parser.add_argument("--max-items", type=int, default=10) parser.add_argument("--level-threshold", type=str, default="warning") args = parser.parse_args()

这样即便 SKILL.md 里写得不清楚,模型传了字符串也能正确转换。更保险的做法是在 SKILL.md 的参数表格里给每个参数加一列“类型”,并且示例里面把“字符串形式的数字”也列出来,让模型不至于理解偏差。

还建议在所有脚本入口增加参数校验和基准测试。哪怕内容只是打印“参数缺失,请检查”,也比模型拿到一堆乱码后自己脑补强。

4.3 安全边界:你以为 Agent 只读,它可能真会执行

Agent 调用技能时,本质上是在你的机器或服务器上执行代码。这是 Skills 模式最大的安全风险点。当我设计一个需要读取日志文件的技能时,脚本里如果带上了“删除临时文件”的逻辑,模型可能会在用户指令的影响下,拿着一个奇怪路径去调用,导致不可预期行为。

我的安全底线有几个:技能目录尽量运行在沙箱或受限目录下;不写任何包含删除目录、全局替换、curl 外部地址等高危操作的脚本;如果确实需要写入文件,写入路径硬编码为固定相对路径,不接收用户传入的任意绝对路径;脚本里禁止执行 shell 拼接命令,只用静态命令加参数列表。

我给自己定了一个简单的分级:只读技能、写临时文件技能、需要网络请求技能,三级的审核口径完全不同。只读技能基本可以直接放心跑;后两级必须人工 review 一遍,并且限制运行环境权限。

经验之谈:不要因为某个功能“只在内网临时用”就放松检查。内网环境下模型同样可能被诱导执行恶意指令,安全实践不能省略。

4.4 Skill 多了以后的版本与依赖管理

当技能从两三个增加到二十多个之后,新的问题出现了:同名冲突、依赖不一致、旧版技能行为和新需求不匹配。我曾在一个通用技能目录下放了两个都叫“summary”的文件夹,结果模型随机选择,输出风格完全不一样。

解法是给每个技能加上命名空间或前缀,比如 teamA-log-analyzer 和 teamB-report-summary,并从一开始就在 SKILL.md 头部写版本号和变更记录。依赖管理方面,每个独立技能都要带自己的 requirements.txt,不要依赖全局 Python 环境里的某个偶然安装过的库。

我后来还专门写了一个一次性的检查脚本,扫描所有技能目录,找出缺少 SKILL.md、SKILL.md 里缺少版本号、scripts 下有文件但没有被文档引用的目录。这个动作每两周跑一次,能及时清理掉“僵尸技能”。

关于共享:如果团队多个人同时维护技能,最好把它放到代码仓库里而不是各自本地复制。我在本地维护时被坑过一次:同事改完技能描述发给我,我没及时同步,结果两个人用同一套技能但是行为不一致。后来改成仓库统一管理,才把这个问题解决。

5. 是不是所有 Agent 应用都应该用 Skills,我的结论

5.1 什么人适合马上开始用

如果你满足下面任意一条,我认为可以直接上手尝试:

一是你已经写过两套以上类似的提示词工程或工具链,并且开始觉得重复度太高;二是你的 Agent 应用需要处理多种不同类型的输入,而且每种类型都有明确的处理流程;三是你需要和团队成员共享能力,而不是一个人默默在本地维护提示词文本。

Skills 带来的最大收益是“能力沉淀”。每新增一个技能,就相当于给 Agent 的世界里加了一个新功能按钮。三个月后再接到同类需求,不再是重写提示词,而是直接引用已有技能,省下来的时间非常可观。

我自己的实践体会是:会做合理的负面排除,比一味堆技能更重要。技能库里出现三五条互斥的日志类技能之后,模型可能不知道选哪个。所以要定期整理,把行为相似、触发场景重叠的技能合并,不能只加不减。

5.2 不要把 Skills 神话

Skills 不是银弹。它仍然依赖底层的模型能力、依赖你写的描述质量、也依赖运行时环境的稳定。有的场景下,一个精心设计的系统提示词比十个技能都好用;有的场景下,普通函数调用比技能文件夹轻量得多。我的建议是把它当作一套工程规范而不是架构必需品。

如果你遇到的输入非常固定、流程完全可枚举,用传统代码处理反而更快更稳;只有当你需要模型参与“理解与判断”时,Skills 的封装方式才明显占优势。说白了,Skills 擅长处理的是“规则边缘的灵活地带”。

不要为了技术架构的完整性而强行引入。你只需要解决眼下最痛的问题:哪个流程反复在做、哪个能力跨项目复用,就把哪个封装成技能。技能数量保持在一个可控范围,维护成本才不会失控。

5.3 最后分享一个我自己的使用小技巧

如果你也想开始实践,我建议你从今天就在新技能里固定写清楚版本号和运行环境,哪怕这个技能只给自己用。理由是,你永远不知道三个月后会不会因为一次系统升级,让某个脚本的依赖坏掉。到时候你翻 SKILL.md 看到写着版本和 requirements,排查会顺畅得多。

我自己的习惯是每个 SKILL.md 文件末尾放一块“Meta”信息:技能版本、依赖环境、是否存在外部 API 调用、是否需要网络。每次升级脚本,都在文件头更新版本说明,保持文档和代码同步。

现在回到最开始的那个问题:为什么我会盯上 Skills。因为它让我从“每次教模型怎么做”变成了“让模型自己找到怎么做”。这种转变不是靠一个更聪明的模型实现的,而是靠把能力组织成可以被发现、被复用、被维护的独立单元。这种组织方式带来的复利效果,只有在积累到一定数量之后才会显现。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询