Agent教程:实现Skills渐进式披露功能
Skills标准通过元数据索引与动态加载实现AI技能模块化共享。OpenClaw项目实践该方案,仅预加载轻量元数据,按需调用核心提示词与引用内容,从而管理上下文容量、保持模型专注。系统涵盖读取、提示词整合及工具调用等清晰步骤,降低了开发门槛。
在AI智能体开发中,技能的模块化与共享是核心挑战。Skills标准的出现为此提供了优雅方案,而OpenClaw项目则是这一理念的杰出实践。它的快速发展很大程度上得益于Skills概念的普及,后者有效解决了几个关键问题:
- 能力固化:智能体习得的新能力,如何以标准化形式保存下来?
- 技能复用:开发者A封装的功能,如何让开发者B以标准格式无缝调用?
- 资源优化:如何避免将所有技能描述都填入上下文导致令牌耗尽?Skills标准允许仅常驻索引,需要时才动态加载核心内容。
可以说,正是Skills标准的流行,推动了像OpenClaw这类项目的快速兴起。分析其源码后,你会发现其核心机制清晰明了,实现类似系统的门槛其实并不高。
Skills标准的核心概念
首先明确定义:Skills是一组提供给智能体的、模块化且可扩展的能力包。
一个标准Skill通常包含以下部分:
- 元数据:名称、描述、适用场景等基础信息。
- 核心提示词:定义技能主要逻辑和入口的指令部分。
- 引用模块:被核心提示词引用的其他提示词片段或需调用的脚本工具。
日常运行时,仅轻量级元数据会预加载到LLM上下文中。当LLM判定任务需要某个特定Skill时,才会触发加载其核心提示词,并根据任务进展按需加载引用模块。
这种“渐进式披露”策略,一举解决了传统长提示词的两大痛点:一是避免提示词长度超出模型限制,二是防止过长上下文分散模型注意力,影响核心任务表现。
引用文件与工具的组织规范
Agent Skills规范建议采用清晰的文件结构。通常,一个Skill目录包含必需的SKILL.md文件(内含YAML元数据和Markdown指令),以及可选的scripts/(存放可执行脚本)、references/(额外文档)、assets/(模板、图片等)子目录。
规范要求使用相对路径进行引用,并保持一级深度,避免复杂嵌套。这能确保SKILL.md文件本身保持简洁(建议不超过500行),将详细内容剥离到其他文件,实现按需加载。
当然,这套标准并非强制。你也可以根据项目实际需求,采用更扁平化的结构,例如将所有引用文件和脚本置于Skill目录根层级。关键在于保证引用路径的正确性和加载逻辑的一致性。
那么,如何亲手实现一套Skills系统?OpenClaw的源码揭示了答案——其核心机制出乎意料地简洁。
Skills系统的实现思路与设计
实现一套支持渐进式披露的Skills系统,可清晰划分为四个阶段:
- Skills读取模块:扫描指定文件夹,结构化读取每个Skill的元数据与核心提示词入口。
- 元数据注入:将Skill元数据(主要是名称和描述)嵌入系统提示词,告知LLM有哪些可用技能及使用时机。
- 核心内容加载:当LLM决定使用某个Skill时,通过工具调用查找并将其核心提示词内容推送到上下文中。
- 引用内容按需加载:根据核心提示词内的引用标记,逐步加载更深层的提示词片段或执行工具脚本。
接下来,我们按这四个阶段拆解关键实现。
Skills读取模块的实现
读取模块的核心逻辑直接明了:
- 扫描预设技能目录下的所有
*/SKILL.md文件,将其作为Skill入口。 - 读取
SKILL.md文件内容。 - 解析文件头部的YAML格式元数据(通常被
---分隔符包裹),将Skill索引信息结构化存储供后续查询。
首先,定义一个简单的Skill索引类:
class SkillIndex:
"""Skill 索引"""
def __init__(self, name: str, description: str, location: str):
self.name = name
self.description = description
self.location = location
解析YAML元数据的函数同样简洁:
def _parse_frontmatter(self, content: str) -> Dict[str, str]:
"""解析 YAML frontmatter"""
pattern = r'^---\n(.*)\n---'
match = re.match(pattern, content, re.DOTALL)
if not match:
return {}
try:
return yaml.safe_load(match.group(1)) or {}
except:
return {}
由此,我们获得了所有可用Skill的索引列表。下一步是让LLM“知晓”它们的存在。
构建能识别Skills的系统提示词
这是整个系统的“指挥中枢”。我们需要构建一段系统提示词,将Skill索引巧妙整合,并指导LLM的行为逻辑。
首先,将Skill索引数组转化为结构化文本格式,例如XML:
# 构建 XML 格式的索引
skills_xml = "\n"
for idx in skill_indexes:
skills_xml += f"""{idx.name} {idx.description} """
skills_xml += ""
选择XML、JSON或YAML格式均可,关键在于结构清晰、易于解析。接下来,组装核心系统指令:
# 构建指令 - 让 LLM 知道何时需要读取 skill
instructions = """
你是一个智能 AI 助手。你的职责是帮助用户完成各种任务。当你需要处理特定领域的问题时,可以使用可用的 skills。一般流程:
1. 理解用户请求
2. 判断是否需要使用 skill
3. 如需要,使用工具读取 skill 完整内容
4. 按照 skill 指令执行
如果用户请求不涉及任何已知的 skill 领域,直接回答即可。
## Skills 使用指南 (渐进式披露)
你可以通过 skills 来处理特定领域的任务。
### 可用 Skills
{skills_xml}
### 使用规则
1. **识别时机**: 当用户请求涉及特定技能领域时(如文件操作、Git操作、代码编写等),应考虑使用 skill
2. **按需读取**:
- 如果某个 skill 明显适用于用户请求 → 使用 `lookup_skill` 工具读取完整内容
- 如果多个 skill 都可能适用 → 选择最相关的一个读取
- 如果没有 skill 明显适用 → 不需要读取任何 skill
3. **执行指令**: 读取 skill 后,按照其中的命令和说明执行任务
4. **引用处理**: skill 文档中可能引用其他文档(使用 路径 格式,如 ../docs/xxx.md),如需读取可使用 `read_reference` 工具
### 注意事项
- 不要在初始阶段就读取所有 skill
- 只在确定需要某个 skill 后才读取
- 读取后严格按照 skill 的指令执行
""".format(skills_xml=skills_xml)
这段提示词的设计精髓在于“引导”而非“强制”。它明确了可用技能列表,规定了使用时机和流程(特别是通过特定工具按需加载),并强调了“渐进式披露”原则。剩下的则交由LLM的理解与推理能力。
实现LLM读取Skill内容的工具
注意到系统提示词中提到的lookup_skill工具了吗?这正是连接“意图”与“能力”的桥梁。
这里运用了智能体开发的基础设施:通过定义工具(Function),并利用LangChain等框架的反射机制将其schema暴露给LLM,LLM就能在合适时机发起调用。我们需要实现这样一个工具函数:
@tool
def lookup_skill(skill_name: str) -> str:
"""查找并读取指定 skill 的完整内容。"""
skill_md = find_location_from_indexes(str)
# 解析 frontmatter 获取 name
content = skill_md.read_text(encoding='utf-8')
pattern = r'^---\n.*\n---\n'
return re.sub(pattern, '', content, count=1, flags=re.DOTALL).strip()
此函数根据技能名称找到对应的SKILL.md文件,读取内容并剥离顶部YAML元数据,将纯指令内容返回给LLM。随后,LLM的上下文便包含了执行该技能所需的具体步骤。
实现引用内容的按需加载
“渐进式披露”不仅适用于Skill本身,也适用于Skill内部的引用。一个Skill的核心提示词中,可能会通过类似../docs/example.md的路径引用其他文件。
因此,我们还需要另一个工具read_reference,供LLM在遇到引用时调用,以加载更深层内容:
@tool
def read_reference(skill_name: str, reference: str) -> str:
"""读取 skill 内部引用的文档。"""
base_dir = find_location(skill_name)
target_path = (base_dir / reference).resolve()
return target_path.read_text(encoding='utf-8')
至此,一个支持动态加载、按需披露的Skills系统核心骨架便搭建完成。
系统演示与验证
将上述模块组合,你便能亲身体验LLM如何智能判断任务需求,动态加载并执行特定Skill的全过程。关键在于理解各部分如何通过提示词和工具调用协同工作。
总结与启示
分析OpenClaw源码的最大收获在于发现:许多听起来复杂精巧的设计,其实现内核往往清晰直接。Skills系统通过“元数据索引+动态加载”的核心思想,配合精心设计的系统提示词,便实现了能力的模块化与弹性扩展。
这正是阅读优秀开源项目的价值所在——它不仅提供现成解决方案,更能拆解复杂问题,展示背后简洁有力的设计逻辑。沿着这个思路,我们完全可以构建出适应自身需求的、更强大的智能体能力体系。
