这是我的专栏《春哥的Agent通关秘籍》系列文章的第19篇,希望系统性跟着我一起学AI-Agent编码的同学可以关注一下我的这个专栏
新技术从来不是凭空产生的,技术是阶段性进步的,所有新奇的概念和思路都是迭代的产物。——春哥说
龙虾🦞OpenClaw 当然也不是,它的进化过程清晰可见。
龙虾诞生在Skills普及之后,不能说是没有原因的,因为Skills至少解决了龙虾面临的几个核心问题:
- 进化:学会的技能用什么形式沉淀
- 技能共享:老王沉淀的技能怎么以标准的形式发给老李,让老李也用上
- 技能容量变大:可以只在上下文里常驻Skill索引,核心内容在需要时才加载
甚至可以说,没有Skills标准的盛行,龙虾🦞OpenClaw 的爆发也不会这么有力。
看完龙虾的源码,我只能说,撕!手撕!太容易了!
一、什么是Skills
为防止部分同学还不太清楚 Skills 是什么,我先解释一下,一句话总结:
Skills 是一组提供给智能体的,模块化的、可扩展的能力包。
一个Skills通常包含以下部分:
- 元数据:名词,描述,环境 等
- 核心提示词:用来作为提示词主体和入口的提示词部分
- 引用模块:被核心提示词引用的提示词片段,或者被核心提示词引用的脚本工具
以上以 anthropics 的 pptx skills为例,介绍了它的各个部分和主要能力,一图胜千言,相信以各位天才的理解能力,看完这张图就能理解skills的组成和能力了。
日常运行情况下,只有元数据会被放到LLM的对话上下文里,这样能避免Token上下文被迅速塞满。
当LLM意识到自己当前需要解决的问题和某个Skills非常契合时,才会去加载该Skill,并根据需要一点点加载模块。
这样一来,就极大地扩充了LLM的能力数量上限,也解决了提示词太长带来的种种困扰:
- 提示词超出最大长度
- 提示词太长,LLM的注意力被分散
关于引用文件与工具的规范
Agent Skills 规范要求使用相对路径在 Markdown 中引用文件,所有引用保持一级深度(即直接从 SKILL.md 链接到子目录中的文件,避免嵌套链)。
这确保代理能高效管理上下文——SKILL.md 保持简洁(建议不超过 500 行),详细内容移到其他文件,按需加载。
your-skill-name/
├── SKILL.md # 必须:YAML + Markdown 指令
├── scripts/ # 可选:可执行脚本(如 .py, .sh)
│ └── extract.py # 示例脚本
├── references/ # 可选:额外文档(如 .md 文件)
│ └── REFERENCE.md # 示例参考文件
└── assets/ # 可选:模板、图像、数据文件
└── template.md # 示例模板
标准强调模组化:引用文件不自动加载,只有在主体中明确提及时,代理才会读取或执行。
但这种标准并不严苛,也可以根据自己的需要选择文件组织方式:
your-skill-name/
├── SKILL.md # 核心文件
├── REFERENCE.md # 直接同级引用
├── run.py # 脚本也同级
└── LICENSE.txt # 其他辅助文件
ok,那么应该怎么实现这样一套Skills系统呢?
只是凭空思考的话,好像挺麻烦的,但当我吃完龙虾🦞OpenClaw的源码,才发现这套机制居然如此简单,我也可以轻松手撕!
二、思路梳理与设计
如何实现一套支持渐进式披露的Skills系统呢?
我们主要把它分成四个阶段即可:
- Skills读取模块:扫描文件夹,结构化读取Skills的元数据与核心提示词。
- 把元数据塞到LLM的系统提示词里,并提醒LLM在合适的时候调用来使用它们
- 查找到核心提示词到上下文,推送到LLM上下文里
- 根据提示词里的引用,可以逐步加载更多的提示词或者工具脚本,推送到LLM上下文里
只要根据这四步,我们分别设计对应的模块和实现即可。
三、Skills读取模块
读取Skills的核心逻辑并不复杂:
- 扫描特定文件夹下的
*/SKILL.md,对于匹配的文件作为入口即可。 - 使用File System库读取
SKILL.md内容 - 使用代码分割出
YAML元数据部分,把 Skills 索引的结构化的信息以结构化的方式存储到内存中,方便后续使用。
接下来撰写核心代码:
class SkillIndex:
"""Skill 索引"""
def __init__(self, name: str, description: str, location: str):
self.name = name
self.description = description
self.location = location
skill索引需要包含的内容并不多,需要的话还可以加上环境,系统等内容。
解析索引的代码也非常容易:
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 {}
只需要通过----把 markdown 文件切开,然后使用 pyyaml 库提供的能力解析就行。
拿到所有skills的索引之后,下一步就是构建能够认出Skills,并在合适的时机调用它们的系统提示词了。
四、能识别Skills的系统提示词
首先,遍历我们的 skill_indexes 技能索引数组,把其中的 name和description掏出来,组成一个 XML 格式的文本:
# 构建 XML 格式的索引
skills_xml = "<available_skills>\n"
for idx in skill_indexes:
skills_xml += f""" <skill>
<name>{idx.name}</name>
<description>{idx.description}</description>
</skill>
"""
skills_xml += "</available_skills>"
为什么是xml?没为什么,个人喜好,你喜欢用JSON,这里也可以是JSON,喜欢用YML,这里也可以是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它应该在什么时候用skills
- 有哪些skills
- skills里有引用
- 严格按skill里的指令执行
对,提示词工程就是这么朴实无华但好用。
有啥特别的需求,直接在提示词里加上,再补充能力即可。
五、让LLM读取Skill内容
上面的提示词里我们有这样一段话:
- 如果某个 skill 明显适用于用户请求 → 使用 `lookup_skill` 工具读取完整内容
噢?你提到了工具对不对?
还记不记得本系列教程前面提到的 reAct 思维范式和 Function Calling 机制,以及 langchain 天然支持通过反射机制来支持 Function Calling ?
对咯!Skill 读取的内容就是这么朴实无华且枯燥。
让我们进入 Agent 开发枯燥的环节之:“定义Tools”。
因此,我们需要这样定义一个方法(这里是简化伪代码):
@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()
并把这个方法注入到 create_react_agent 方法里,让LLM获取到方法的schema细节,知道什么时候应该调用 tools。
六、加载引用
不仅仅 “核心提示词” 是在需要的时候才加载的。
提示词中也允许引用其他文件,这样的引用也需要在必要的时候进行按需加载,渐进式披露。
因此,我们在优先级最高的系统提示词里,我们有这样的说明:
4. **引用处理**: skill 文档中可能引用其他文档(使用 路径 格式,如 ../docs/xxx.md),如需读取可使用 `read_reference` 工具
所以,和上一章一样,我们也需要定义 read_reference 的工具方法。
@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')
七、demo与验证
以上所有实现思路的代码,你都可以在这里获取到:github.com/zhangshichu…
你可以在这里体会到LLM按需加载的全流程与全过程。
八、小结
读完龙虾🦞OpenClaw 的源码,一些在想象中可能很复杂的内容,实际实现起来却非常顺畅。
这就是阅读开源项目的意义所在!
后面,我们还将做更多有意义的学习和实践!
敬请期待!