☰
智能体Skills机制实践:从Prompt膨胀到可复用技能库
2026/10/14 10:14:48 网站建设 项目流程

上个月我终于把一个用了一年的老项目里那坨两千多字的 Prompt 拆掉了。拆完之后主 Prompt 只剩下不到五百字,十几个重复场景全部变成独立模块,新同事接手时不用再啃我的历史包袱。这个转变的核心,就是现在智能体开发里越来越常见的 Skills 机制——把能力封装成可插拔、可复用、由模型在运行时按需调用的独立技能单元。

如果你也在做智能体应用,不管是内部自动化工具、内容生产管线还是问答机器人,应该都遇到过类似的问题:每次加一个新功能,就往主 Prompt 里塞一段工具说明和几个回调函数的签名;功能越来越多之后,提示词越来越长,模型越来越容易把 A 功能的参数填到 B 功能上;更难受的是,想把这套能力搬到新项目里,几乎等于重写一遍。

Skills 机制解决的就是这三件事:能力复用、上下文瘦身、权限可控。它把"工具代码 + 使用说明 + 输入输出约定"打包成一个技能文件,放进技能库;模型在推理时先看技能目录,判断当前任务应该调哪个,再加载对应说明去执行。

这篇文章我会从为什么要用技能系统讲起,然后拆解一个技能的标准结构,再用一个完整的实操案例带你把第一个技能从零建起来,最后分享我在这套机制上踩过的坑和排查技巧。内容不绑定某个特定框架,思路通用。

1. 为什么需要技能系统:从"写死逻辑"到"按需装配"

1.1 痛点:一个 Prompt 里什么都堆,迟早失控

没接触技能系统之前,我做智能体项目的方式很简单:所有能力说明、工具函数、参数格式全往主 Prompt 里塞。初期还好,二十来个工具也能跑,问题是项目一长,症状就全冒出来了。

第一个是提示词膨胀。每接一个新场景,就要在主 Prompt 里补一段说明,三个月下来,主 Prompt 从 500 字涨到 2000 多字。模型每轮推理都要重新处理这段超长上下文,先不说 token 成本,光是它"注意力的重心"都被稀释了——原来你精心设计的几个关键约束,混在一堆新增内容里,模型越来越容易忽略。

第二个是耦合。各个能力的提示词和内生逻辑搅在一起,改一个场景的描述,经常误伤另一个场景。我印象最深的一次:我在内容生产管线的 Prompt 里加了一条"输出必须保持 Markdown 格式"的约束,结果隔壁数据清洗场景的 JSON 输出结构被这条约束悄悄改坏了。定位这个问题花了我整整一个下午。

第三个是复用难。项目 A 里写好的文件批量处理、网页正文提取、格式校验,想搬到项目 B,只能复制粘贴,再把相关的提示词片段也搬一遍。稍有不慎,中间还夹带了项目 A 私有的上下文,项目 B 表现就不对。本质上是因为能力没有边界,和项目上下文粘在一起。

1.2 技能机制的本质:从"指令全量加载"变成"技能按需加载"

Skills 机制的核心,是把原本堆在 Prompt 里的"能力包"抽出来,变成独立的、可索引的、由模型按需加载的模块。我习惯把它理解成三件套:目录、描述、路由。

目录就是技能库里所有技能的索引,模型启动时就能看到"有哪些技能可用"。描述是每个技能用自然语言写清楚的"什么时候用、怎么用、参数是什么"。路由则是模型在推理过程中,根据当前对话内容,去匹配技能描述、选出最合适的那个技能来执行。

你可以类比成维修师傅的工具箱。一个维修师傅不会把所有工具都摊在地上,他看到"换水管"的任务,就只拿扳手和生料带;看到"装插座",就只拿改锥和电笔。智能体也是一样,它不需要在每轮对话里把所有技能的代码细节都过一遍,只需要看目录和简介,找到当前任务对应的那一个,然后加载它。

这个转换带来的直接收益是:主 Prompt 变成了真正的"总控",只保留目标、语气、通用约束;技能代码则独立演进、独立测试、独立复用。改动一个技能,不再需要全局排查会不会影响其他场景,因为其他技能在物理上就和它隔离了。

1.3 技能系统适合什么、不适合什么

技能不是银弹,我不建议把所有能力都往技能里装。先说适合的场景。

适合做成技能的典型特征有三个:流程稳定、输入输出可枚举、需要被反复执行。比如文件格式转换、批量重命名、网页正文提取、数据清洗、图像裁剪、定时任务触发、日志格式整理,这些任务边界清晰、结果可以验证,非常适合封装成技能。

不适合的场景也有。探索性的创意任务、需要多轮动态规划的任务,把技能作为主框架容易限制模型的发挥。比如"写一篇行业分析报告"这种开放任务,你很难预先定义一个固定技能来覆盖;但它内部可能包含"收集网页资料"这个原子动作,这个动作适合拆出来做技能。我的经验是,技能适合作为开放任务的"工具包",而不是开放任务本身的框架。

还有一点要注意:技能不是越大越好。我见过有人把一个完整的"项目生成器"做成单个技能,描述洋洋洒洒几百字,结果模型在匹配时经常犹豫——它不确定当前任务到底属不属于这个技能的管辖范围。技能设计的核心原则是"小、明确、单一职责",一个技能只做好一件事。

2. 技能的结构设计:元数据、描述文件与实现代码

2.1 最小技能目录长什么样

一个技能不是一段代码就完事了,它需要一套完整的结构,让模型和安全机制都能理解它。我的最小目录一般长这样:

skills/ doc-batch-archive/ SKILL.md archive_tool.py requirements.txt samples/ input_note.md output_result.json

每个文件都有它存在的理由。

SKILL.md 是给模型读的说明书,它的质量直接决定模型会不会在正确的时候调这个技能,是整个技能的灵魂。archive_tool.py 是实际执行的代码,负责真正完成任务。requirements.txt 声明技能依赖的第三方库,技能库加载时会按需安装或校验。samples 目录放样例输入和输出,帮助模型快速确认"我这个场景和这个技能是不是匹配"。

2.2 元数据里最关键的两个字段

先说 name 和 description,这是模型最先看到的东西。

name 是技能的唯一标识,我习惯用"动词 + 对象"的命名法。比如 doc-batch-archive 表示文档批量归档,webpage-text-extract 表示网页正文提取,batch-rename 表示批量重命名。命名要直观,避免用缩写谜语——模型看到 skill_b 这种名字根本猜不到它干什么,它在路由阶段就会跳过这个技能。

description 是决定路由准确率的核心。这个字段不是写给程序看的,是写给模型看的,所以要写"人话",要写清触发场景、能力边界、输入输出格式。

2.3 一个好的描述文件应该写清什么

我的 SKILL.md 通常分三个层次展开。

第一层是触发条件。明确哪些任务属于这个技能,最好给出关键词和典型问法。比如"用户说'把文件整理一下''归档''按类型分类'时,优先调用本技能"。这一层写得好,模型匹配的准确率会明显上升。

第二层是能力边界。写清这技能不能做什么,这一点特别重要。比如归档技能只处理文档类文件,不碰图片和视频;网页提取技能只抓正文,不处理动态渲染的页面。边界约束能避免模型在模糊任务上硬套技能。

第三层是输入输出约定。参数结构、返回格式、错误处理方式都写清楚。模型是拿着描述去组装参数、理解结果的,你的约定越明确,它传错参数的概率越低。

下面是一个我实际用过的归档技能的 SKILL.md 节选:

name: doc-batch-archive description: 将指定目录中的文档按扩展名归档到对应分类文件夹(如 md、txt、pdf、docx)。 触发场景:用户说"把文件整理一下""归档""按类型分类"时。 限制:只处理文档类文件,不处理图片、音频、视频文件。 输入: - source_dir: 源目录的绝对路径(必填) 输出: - 返回 JSON 数组,元素格式为 { "file": "原文件名", "target_folder": "目标分类名", "status": "moved|skipped|error" }

2.4 为什么模型读的是描述而不是代码

这是技能系统和传统函数库最大的差别。传统开发中,调用方是人,人能看懂接口签名和注释;但在技能系统里,"调用方"是大模型,它不执行你的代码,它只根据描述来理解"这个技能是什么、能不能用在这个场景、参数怎么填"。

所以,技能的描述质量甚至比代码质量更能决定整个系统表现。代码只要功能正确、参数校验严谨,剩下的战斗都在描述里。你要像写 API 文档一样去写 SKILL.md,读者是模型——你的描述要让它一眼就知道该选谁。

我见过最典型的现象:技能列表只有十几项,模型却总是选第一个或者最后一个,原因很简单,它们的 description 都写得太抽象,模型根本无从区分。判断描述写得好不好,可以用一个"双胞胎测试":把两个技能的 description 放在一起,打乱顺序,让模型随机看一个,如果它仍然能快速判断该调哪个,说明描述达标了。如果模型要反复读几遍才发现意思不明确,那这个描述就是不及格的。

3. 从零搭建一套技能库:实操步骤与代码示例

3.1 第一步:从重复劳动里选第一个技能

技能库的第一步,不是搭框架,而是选一个高频重复、边界清晰的任务来练手。我建议从"批量重命名文件"开始,因为它在大多数项目里都会用到,逻辑简单,风险低。

你可以先盘点一下自己的日常工作:有没有什么操作是你每个星期都要重复做的?比如把下载目录里一堆乱七八糟的文件按规则改名,或者把某个临时目录里的图片统一压缩成固定尺寸,再或者批量提取一批 HTML 文件里的正文。选一个你闭着眼都能讲清楚步骤的,作为第一个技能。

我第一次做技能库时,选的就是"批量重命名文件",原因是它几乎不需要依赖外部库,输入输出都非常直观,非常适合把整套流程跑通。等你熟悉了流程,再逐步往复杂技能过渡。

3.2 第二步:实现技能代码,注意无副作用设计

技能代码的核心要求是"可预测":相同的输入要产生相同的行为,尽量少做运行时副作用。技能入库之后,模型会在各种场景下反复调用它,如果代码随意读写全局状态,排查问题会非常痛苦。

我写的批量重命名技能大概是这样的:

# batch_rename.py import re from pathlib import Path from typing import Optional def rename_files( directory: str, pattern: str, replacement: str, dry_run: bool = True, ) -> list[dict]: """按正则规则批量重命名目录内的文件。 Args: directory: 目标文件夹绝对路径。 pattern: 正则表达式,用于匹配原文件名。 replacement: 替换目标,例如 project_\\1。 dry_run: 默认 True,只打印计划不实际改名。 Returns: 包含每条重命名前后对照和状态的列表。 """ results = [] target_dir = Path(directory) if not target_dir.is_dir(): raise ValueError(f"目录不存在: {directory}") for file_path in target_dir.iterdir(): if not file_path.is_file(): continue new_name = re.sub(pattern, replacement, file_path.name) if new_name == file_path.name: continue entry = { "file": file_path.name, "new_name": new_name, "status": "pending", } if not dry_run: file_path.rename(file_path.with_name(new_name)) entry["status"] = "done" results.append(entry) return results

注意两个细节。第一个是 dry_run 参数,默认 True。这个设计非常重要,因为模型调用工具时传错参数是常态,如果技能一上来就直接改文件名,出错了很难恢复。先跑一遍 dry_run 把计划列出来,再传 dry_run=False 实际执行,本身就是一道安全保险。

第二个是返回值的设计。我没有直接返回一句"成功",而是返回结构化数据,每一项的状态都清清楚楚。这样模型拿到结果后可以自己组装成用户能看懂的话,同时你也能在日志里完整审计。

3.3 第三步:编写描述文件,把使用说明书写足

代码写完之后,紧接着写 SKILL.md。在技能系统里,这一步的重要性不亚于代码本身,甚至更高。

我为 batch-rename 写的描述文件如下:

name: batch-rename description: 按正则规则批量重命名目录中的文件。适用场景:用户说"批量改名""把文件名统一""去掉文件里的日期前缀"等。 限制:只处理目录下的普通文件,不递归子目录;无法处理重名冲突,冲突时返回 error。 输入: - directory: 目录绝对路径(必填) - pattern: 正则表达式,例如 "_\d{8}" 表示匹配下划线加 8 位数字 - replacement: 替换后的内容,例如 "archive_\\1" - dry_run: 是否只做模拟预览,默认 true 输出: - 返回 JSON 数组,元素格式为 { "file": "旧文件名", "new_name": "新文件名", "status": "pending|done|error" } 默认行为: - 如果 dry_run 为 true,给出全部计划,不实际修改。 - 如果目录为空或无可改名文件,返回空数组。 失败替代策略: - 如果目录不存在,不报堆栈,建议用户检查路径是否正确。 - 如果替换后与现有文件重名,标记 status=error,并说明原因。

我特别想强调"失败替代策略"这一段。模型调用技能出错时,它自己往往不知道该怎么应对。如果描述里写了"目录不存在时建议用户检查路径",模型就会顺着你的引导给出友好提示,而不是直接复读一遍异常堆栈。这个小细节,对最终交互体验影响很大。

3.4 第四步:自检脚本,先自己人把关再上库

技能建好,别急着投入使用。先跑一个自检脚本,把输入输出验证一遍。我的自检一般包含两个维度:功能维度和模型路由维度。

功能维度很简单,构造几个固定样例,验证返回值符合预期。比如建一个临时目录,放几个带日期前缀的文件,跑 dry_run,确认新文件名正确。

模型路由维度可能很多人会忽略。我通常会拿一段模拟用户请求,让接入技能库的模型判断"该调用哪个技能"——如果模型选中了 batch-rename,路由就是成功的;如果模型犹犹豫豫选了别的,说明描述还不够清晰。这个验证过程可以在本地跑,不需要接线上环境。

这一步能帮你提前发现很多"代码没问题但模型就是不会用"的问题,比上线后返工划算得多。

3.5 第五步:注册、加载与测试

技能库的加载机制我习惯保持简单:启动时扫描技能目录,逐个读取 SKILL.md,校验格式,检查依赖,然后构建一个技能索引。运行时模型根据对话做路由匹配,选中后加载对应实现执行。

有一点要提醒:不要手动给技能配固定优先级。我见过一些项目在配置里写着"优先调用 A 技能,其次 B 技能",这种写死顺序的做法短期看着稳,长期看其实削弱了模型基于实际场景做判断的能力。技能的匹配逻辑应该交给模型按描述自己选,配置层的干预越少越好。

上线后还要做回归测试:每个技能至少要保留一组标准输入输出用例,每次改动技能代码都要重跑一遍。技能是半成品逻辑,模型生成的结果可能千差万别,标准用例能保证它的核心功能永远稳。

4. 实战落地中的问题与排查技巧

4.1 模型就是不调用技能?先看这几个原因

我做技能库初期,最常遇到的困惑就是"技能明明写得没问题,模型就是不用"。排查下来,原因通常出在这几个地方。

第一个是名称太泛。比如技能叫"文件服务",看起来没毛病,但模型不知道它管什么。改成 file-preview 或 csv-cleaner 这种动词加对象的命名后,调用率会明显提升。

第二个是描述缺少触发场景。模型看到一个看起来像"万金油"的描述,宁可自己去猜,也不会冒险调用。我后来强制要求每个技能必须在描述里写清"用户说什么话时你该用这个技能",调用率稳了很多。

第三个是缺少参数示例。模型见了参数说明但不清楚实际填什么,就算选中了技能也会传错参数。解决方式是给一个极简示例,我通常会在描述末尾补一行:

example: input: { "directory": "/tmp/test_files", "pattern": "(_\\d{8})", "replacement": "-\\1" }

4.2 技能粒度怎么定?过小是灾难,过大是累赘

技能粒度是技能系统里最考验经验的地方。拆得太细,技能库会爆炸,几十个技能堆在一起,模型匹配时负担很大;拆得太粗,一个技能塞了太多职责,描述写不清,模型经常误启用。

我个人的判断标准是:原子操作和复合流程分开管理。原子操作是一个技能只做一件事,比如 file-copy、html-to-text,代码基本在半个屏幕以内;复合流程是多个原子操作的编排,比如"归档并生成索引报表",内部会调用 file-move 和 markdown 表格生成。

在目录层面,我会给复合流程技能加一个"uses"字段,显式声明它依赖哪些原子技能,这样模型能更好理解技能之间的层级关系。如果一个技能的实现代码超过 200 行,我强烈建议你看一看是不是该拆了——它可能已经超过"一个能力模块"的健康粒度,描述必然模糊,路由必然犹豫。

4.3 技能与提示词之间的权责边界

技能不是提示词的替代品,它们各有适用场景。我踩过几次把"该用提示词解决的"做成技能的坑之后,现在有一条很清晰的边界。

凡是"一句话能说清楚的简单动作",用提示词解决,不建技能。比如让模型输出某段内容的概括、把结果格式转成 JSON,这些动作只有模型自己敏感,不涉及外部工具,也不需要代码执行,硬包一层技能反而多一层调用损耗。

凡是"需要多步骤、有输入输出依赖、会被反复复用"的功能,才值得做成技能。判断标准是:你是否愿意为这个功能写一份两页纸的使用说明书?如果不愿意,说明它还不值得做成技能。

另外要注意,一个技能如果只在某个项目的特定场景里出现,建议直接做成项目内脚本而不是通用技能。通用技能库里混入太多"伪通用"模块,会让索引越来越脏,最终拖累整个路由性能。

4.4 日志、审计与降级:我踩过的一个大坑

技能系统的日志和传统代码日志侧重点不同。传统日志关注的是"代码执行时发生了什么",技能日志还要额外关注"模型为什么选择了这个技能、传了什么参数、结果有没有被模型正确理解"。

我一开始没意识到日志的重要性,结果踩过一个很深的坑。当时我给一个图像处理服务接了一个压缩技能,技能执行结果直接写到服务器固定路径,没有任何结构化日志。上线一周后,用户反馈"图片被压坏了",我完全无法复现现场——没有日志,没有调用记录,没有参数快照,最后只能把功能整体回滚。

从那以后,我给所有技能都加了一段统一的日志逻辑,每次调用至少记录五个字段:技能名、入参、返回结果、耗时、异常信息。入参和返回结果要做成 JSON 快照,方便事后回放。

如果你的技能接入的是线上服务,日志还要做脱敏。用户输入里经常带敏感信息,技能日志里不能原样保存,涉及个人信息的字段直接截断或者替换成占位符。

4.5 技能描述与版本管理

技能不是一次写定就不动的。场景在变,模型能力在变,技能描述必须跟着迭代。但描述变更比代码变更更容易引发隐藏问题——你改了一个技能的表达,可能无形中改变了模型对它的理解,进而影响路由选择。

我把 SKILL.md 当 API 来管。每次修改描述,都要重跑一次模型路由测试,确认调用率没有异常波动。技能代码升级时,不在老技能目录里直接改,而是建一个新版本目录,比如 batch-rename-v2,让模型同时看到 v1 和 v2 两个版本,通过描述的自然竞争让模型自己选择更合适的那个。等 v2 表现稳定了再淘汰 v1,这样能有效降低升级带来的断层风险。

版本切换时还要注意依赖一致性。技能库加载时会检查依赖,如果 v2 新增了依赖库,要在 requirements.txt 里同步更新,否则模型选到 v2 时会直接执行失败。

我在实际使用中还有一个习惯:技能维护的频率比功能开发高得多。每次加新技能前,先看一遍现有技能列表,优先考虑是否能用已有技能的组合来解决,其次才是新建技能。技能库像衣柜,定期清理、及时归位,才能保证每次调用都精准高效。上面提到的技能目录结构、描述写法、自检流程,你照着跑一遍,基本就能搭起一套可用的技能库;剩下的问题,会在你真实项目里慢慢暴露,再一个个解决。

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

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

立即咨询