【实战】吃透龙虾🦞,你写的Agent也能支持Skills渐进式披露

3 阅读9分钟

这是我的专栏《春哥的Agent通关秘籍》系列文章的第19篇,希望系统性跟着我一起学AI-Agent编码的同学可以关注一下我的这个专栏


新技术从来不是凭空产生的,技术是阶段性进步的,所有新奇的概念和思路都是迭代的产物。——春哥说

龙虾🦞OpenClaw 当然也不是,它的进化过程清晰可见。

龙虾诞生在Skills普及之后,不能说是没有原因的,因为Skills至少解决了龙虾面临的几个核心问题:

  1. 进化:学会的技能用什么形式沉淀
  2. 技能共享:老王沉淀的技能怎么以标准的形式发给老李,让老李也用上
  3. 技能容量变大:可以只在上下文里常驻Skill索引,核心内容在需要时才加载

甚至可以说,没有Skills标准的盛行,龙虾🦞OpenClaw 的爆发也不会这么有力。

看完龙虾的源码,我只能说,撕!手撕!太容易了!

一、什么是Skills

为防止部分同学还不太清楚 Skills 是什么,我先解释一下,一句话总结:

Skills 是一组提供给智能体的,模块化的、可扩展的能力包。

一个Skills通常包含以下部分:

  • 元数据:名词,描述,环境 等
  • 核心提示词:用来作为提示词主体和入口的提示词部分
  • 引用模块:被核心提示词引用的提示词片段,或者被核心提示词引用的脚本工具

以上以 anthropicspptx 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系统呢?

我们主要把它分成四个阶段即可:

  1. Skills读取模块:扫描文件夹,结构化读取Skills的元数据与核心提示词。
  2. 把元数据塞到LLM的系统提示词里,并提醒LLM在合适的时候调用来使用它们
  3. 查找到核心提示词到上下文,推送到LLM上下文里
  4. 根据提示词里的引用,可以逐步加载更多的提示词或者工具脚本,推送到LLM上下文里

只要根据这四步,我们分别设计对应的模块和实现即可。

三、Skills读取模块

读取Skills的核心逻辑并不复杂:

  1. 扫描特定文件夹下的 */SKILL.md,对于匹配的文件作为入口即可。
  2. 使用File System库读取 SKILL.md 内容
  3. 使用代码分割出 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 技能索引数组,把其中的 namedescription掏出来,组成一个 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 的源码,一些在想象中可能很复杂的内容,实际实现起来却非常顺畅。

这就是阅读开源项目的意义所在!

后面,我们还将做更多有意义的学习和实践!

敬请期待!