乐于分享
好东西不私藏

OpenClaw Skill 加载到 Prompt 的原理与调用过程

OpenClaw Skill 加载到 Prompt 的原理与调用过程

概述

OpenClaw 的 Skill 系统采用声明式 + 按需加载的设计。Skill 文件(SKILL.md)在会话启动时被扫描加载为元数据,但内容不会直接嵌入系统提示。系统提示中只包含 Skill 的”目录卡片”(名称、描述、路径、版本),由 LLM 自主判断是否需要读取某个 Skill 的完整内容。


核心设计原则

  1. 节省 Token — 系统提示中只保留 Skill 元数据列表,避免每次请求都携带大量 Skill 内容
  1. LLM 自主决策 — 系统提示明确告诉 LLM “如果任务匹配某个 Skill,就用 read 工具读取它”
  1. 按需读取 — LLM 通过 read 工具主动读取 SKILL.md 文件内容,然后按照其中指令执行
  1. 缓存与版本控制 — 通过 version(文件内容哈希)判断 Skill 是否变更,避免重复读取

Skill 文件结构示例

---
name: weather-planner
description: Plan outdoor activities based on weather forecasts
---

# Weather Planner

When the user wants to plan outdoor activities:

1. Use the `bash` tool to call `curl` and fetch weather data from wttr.in
2. Parse the output to extract temperature, precipitation, and wind
3. Recommend activities based on conditions:
   - Sunny & < 30°C: hiking, picnic, cycling
   - Rainy: indoor museums, shopping, movies
   - Windy > 30km/h: avoid outdoor sports
4. Always suggest a backup indoor option

完整调用链

1. 会话初始化(AgentSession 构造函数)

文件: src/agents/sessions/agent-session.ts

行号: 第 380-409 行

constructor(config: AgentSessionConfig) {
  // ... 初始化各种属性 ...

  // 订阅 Agent 事件(会话持久化、扩展、自动压缩、重试逻辑)
  this.unsubscribeAgent = this.agent.subscribe(this.handleAgentEvent);
  this.installAgentToolHooks();

  // 构建运行时环境(关键入口)
  this.buildRuntime({
    activeToolNames: this.initialActiveToolNames,
    includeAllExtensionTools: true,
  });
}

2. 构建运行时(buildRuntime)

文件: src/agents/sessions/agent-session.ts

行号: 第 2479-2531 行

private buildRuntime(options: {
  activeToolNames?: string[];
  flagValues?: Map<string, boolean | string>;
  includeAllExtensionTools?: boolean;
}): void {
  // 1. 创建基础工具定义(read/bash/edit/write)
  const baseToolDefinitions = createAllToolDefinitions(this.cwd, {
    read: { autoResizeImages },
    bash: { commandPrefix: shellCommandPrefix, shellPath },
  });

  // 2. 创建 ExtensionRunner(管理扩展生命周期)
  this.currentExtensionRunner = new ExtensionRunner(
    extensionsResult.extensions,
    extensionsResult.runtime,
    this.cwd,
    this.sessionManager,
    this.sessionModelRegistry,
  );

  // 3. 绑定扩展核心和 UI 上下文
  this.bindExtensionCore(this.currentExtensionRunner);
  this.applyExtensionBindings(this.currentExtensionRunner);

  // 4. 刷新工具注册表(关键:这里会触发系统提示重建)
  this.refreshToolRegistry({
    activeToolNames: baseActiveToolNames,
    includeAllExtensionTools: options.includeAllExtensionTools,
  });
}

3. 刷新工具注册表(refreshToolRegistry)

文件: src/agents/sessions/agent-session.ts

行号: 第 2380-2477 行

private refreshToolRegistry(options?: {
  activeToolNames?: string[];
  includeAllExtensionTools?: boolean;
}): void {
  // 1. 收集所有工具定义(内置 + 扩展 + 自定义)
  const allCustomTools = [...registeredTools, ...this.customTools];
  const definitionRegistry = new Map([...this.baseToolDefinitions, ...allCustomTools]);

  // 2. 提取工具的 Prompt Snippets 和 Guidelines
  this.toolPromptSnippets = new Map(...);
  this.toolPromptGuidelines = new Map(...);

  // 3. 包装工具(添加扩展钩子)
  const toolRegistry = new Map([...wrappedBuiltInTools, ...wrappedExtensionTools]);
  this.toolRegistry = toolRegistry;

  // 4. 确定下一个活跃工具列表
  const nextActiveToolNames = [...];

  // 5. 设置活跃工具(关键:这里触发系统提示重建)
  this.setActiveToolsByName([...new Set(nextActiveToolNames)]);
}

4. 设置活跃工具(setActiveToolsByName)

文件: src/agents/sessions/agent-session.ts

行号: 第 882-897 行

setActiveToolsByName(toolNames: string[]): void {
  const tools: AgentTool[] = [];
  const validToolNames: string[] = [];
  for (const name of toolNames) {
    const tool = this.toolRegistry.get(name);
    if (tool) {
      tools.push(tool);
      validToolNames.push(name);
    }
  }

  // 设置 Agent 的工具列表
  this.agent.state.tools = tools;

  // 重建基础系统提示(关键入口)
  this.baseSystemPrompt = this.rebuildSystemPrompt(validToolNames);
  this.agent.state.systemPrompt = this.baseSystemPrompt;
}

5. 重建系统提示(rebuildSystemPrompt)

文件: src/agents/sessions/agent-session.ts

行号: 第 1015-1049 行

private rebuildSystemPrompt(toolNames: string[]): string {
  const { validToolNames, toolSnippets, promptGuidelines } =
    this.collectActiveToolPromptMetadata(toolNames);

  // 从 ResourceLoader 获取各种资源
  const loaderSystemPrompt = this.sessionResourceLoader.getSystemPrompt();
  const loaderAppendSystemPrompt = this.sessionResourceLoader.getAppendSystemPrompt();
  const appendSystemPrompt =
    loaderAppendSystemPrompt.length > 0 ? loaderAppendSystemPrompt.join("\n\n") : undefined;

  // 获取已加载的 Skill 列表(关键)
  const loadedSkills = this.sessionResourceLoader.getSkills().skills;
  const loadedContextFiles = this.sessionResourceLoader.getAgentsFiles().agentsFiles;

  // 构建系统提示选项
  this.baseSystemPromptOptions = {
    cwd: this.cwd,
    skills: loadedSkills,              // ← Skill 列表传入
    contextFiles: loadedContextFiles,
    customPrompt: loaderSystemPrompt,
    appendSystemPrompt,
    selectedTools: validToolNames,
    toolSnippets,
    promptGuidelines,
  };

  // 调用 buildSystemPrompt 生成最终系统提示字符串
  return buildSystemPrompt(this.baseSystemPromptOptions);
}

6. 构建系统提示(buildSystemPrompt)

文件: src/agents/sessions/system-prompt.ts

行号: 第 28-75 行

export function buildSystemPrompt(options: BuildSystemPromptOptions): string {
  let prompt = "";

  // 1. 添加自定义提示(如果存在)
  if (options.customPrompt) {
    prompt += options.customPrompt;
  }

  // 2. 添加工具使用说明
  if (options.selectedTools.length > 0) {
    prompt += buildToolsSection(options.selectedTools, options.toolSnippets);
  }

  // 3. 添加工具使用指南
  if (options.promptGuidelines.length > 0) {
    prompt += buildGuidelinesSection(options.promptGuidelines);
  }

  // 4. 添加 Skill 部分(关键)
  const hasRead = options.selectedTools.includes("read");
  if (hasRead && options.skills.length > 0) {
    prompt += formatSkillsForPrompt(options.skills);  // ← 格式化 Skill 列表
  }

  // 5. 添加上下文文件
  if (options.contextFiles.length > 0) {
    prompt += buildContextFilesSection(options.contextFiles);
  }

  // 6. 添加追加提示
  if (options.appendSystemPrompt) {
    prompt += "\n\n" + options.appendSystemPrompt;
  }

  return prompt;
}

7. 格式化 Skill 列表(formatSkillsForPrompt)

文件: src/skills/loading/session.ts

行号: 第 349-352 行

export function formatSkillsForPrompt(skills: Skill[]): string {
  // 过滤掉 disableModelInvocation 的 Skill(只能显式通过 /skill:name 调用)
  const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
  return formatSkillContractForPrompt(visibleSkills);
}

8. 生成 XML 格式(formatSkillContractForPrompt)

文件: src/skills/loading/skill-contract.ts

行号: 第 53-75 行

export function formatSkillsForPrompt(skills: Skill[]): string {
  if (skills.length === 0) {
    return "";
  }

  // 生成 XML 格式的 Skill 列表
  const skillsXml = skills
    .map((skill) => {
      return [
        `  <skill>`,
        `    <name>${escapeXml(skill.name)}</name>`,
        `    <description>${escapeXml(skill.description)}</description>`,
        `    <location>${escapeXml(skill.filePath)}</location>`,
        `    <version>${escapeXml(skill.promptVersion ?? "")}</version>`,
        `  </skill>`,
      ].join("\n");
    })
    .join("\n");

  return [
    "",  // 空行分隔
    "The following skills provide specialized instructions for specific tasks.",
    "Use the read tool to load a skill's file when the task matches its description.",
    "If a skill's <version> differs from a previous turn, re-read its SKILL.md before using it.",
    "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
    "",
    "<available_skills>",
    skillsXml,
    "</available_skills>",
  ].join("\n");
}

9. XML 转义(escapeXml)

文件: src/skills/loading/skill-contract.ts

行号: 第 38-45 行

function escapeXml(str: string): string {
  return str
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");
}

生成的系统提示示例

You are an expert coding assistant...

The following skills provide specialized instructions for specific tasks.
Use the read tool to load a skill's file when the task matches its description.
If a skill's <version> differs from a previous turn, re-read its SKILL.md before using it.
When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.

<available_skills>
  <skill>
    <name>weather-planner</name>
    <description>Plan outdoor activities based on weather forecasts</description>
    <location>/home/user/.config/openclaw/skills/weather-planner/SKILL.md</location>
    <version>a1b2c3d</version>
  </skill>
  <skill>
    <name>git-commit-helper</name>
    <description>Write conventional commit messages</description>
    <location>/home/user/project/.openclaw/skills/git-commit/SKILL.md</location>
    <version>xyz789</version>
  </skill>
</available_skills>

Current date: 2026-07-06
Current working directory: /home/user/project

Skill 加载流程(ResourceLoader)

DefaultResourceLoader 初始化

文件: src/agents/sessions/resource-loader.ts

行号: 第 177-291 行

export class DefaultResourceLoader implements ResourceLoader {
  private skills: Skill[] = [];
  private skillDiagnostics: ResourceDiagnostic[] = [];

  constructor(options: DefaultResourceLoaderOptions) {
    this.cwd = options.cwd;
    this.agentDir = options.agentDir;
    this.settingsManager = options.settingsManager ?? new SettingsManager();
    // ... 初始化各种路径
  }

  getSkills(): { skills: Skill[]; diagnostics: ResourceDiagnostic[] } {
    return { skills: this.skills, diagnostics: this.skillDiagnostics };
  }

  async reload(): Promise<void> {
    // ... 重新加载所有资源
    const skillsResult = loadSkills({
      cwd: this.cwd,
      agentDir: this.agentDir,
      skillPaths,
      // ...
    });
    this.skills = skillsResult.skills;
    this.skillDiagnostics = skillsResult.diagnostics;
  }
}

loadSkills 函数

文件: src/skills/loading/session.ts

行号: 第 385-470 行(近似)

export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult {
  const { cwd, agentDir, skillPaths } = options;

  // 1. 扫描所有 skillPaths 目录
  const allSkillFiles: string[] = [];
  for (const path of skillPaths) {
    const files = scanDirectoryForSkills(path);  // 查找 SKILL.md 文件
    allSkillFiles.push(...files);
  }

  // 2. 解析每个 SKILL.md
  const skills: Skill[] = [];
  const diagnostics: ResourceDiagnostic[] = [];

  for (const filePath of allSkillFiles) {
    try {
      const content = fs.readFileSync(filePath, "utf-8");
      const { frontmatter, body } = parseFrontmatter(content);

      // 验证必填字段
      if (!frontmatter.name || !frontmatter.description) {
        diagnostics.push({ type: "error", message: `Missing name or description in ${filePath}` });
        continue;
      }

      // 计算版本(基于内容哈希)
      const promptVersion = computeSkillPromptVersion(content);

      skills.push({
        name: frontmatter.name,
        description: frontmatter.description,
        filePath,
        baseDir: path.dirname(filePath),
        promptVersion,
        sourceInfo: { source: determineSource(filePath) },
        disableModelInvocation: frontmatter["disable-model-invocation"] ?? false,
        source: determineSource(filePath),
      });
    } catch (error) {
      diagnostics.push({ type: "error", message: `Failed to load ${filePath}: ${error}` });
    }
  }

  // 3. 去重(同名 Skill 保留先找到的)
  const uniqueSkills = deduplicateSkills(skills);

  return { skills: uniqueSkills, diagnostics };
}

Skill 触发方式

方式一:被动触发(LLM 自主判断)

流程:

  1. 系统提示中包含 <available_skills> 列表
  1. LLM 看到用户输入后,判断哪个 Skill 匹配当前任务
  1. LLM 主动调用 read 工具读取 Skill 文件:

  “`json

  {

    “tool”: “read”,

    “params”: {

      “file_path”: “/home/user/.config/openclaw/skills/weather-planner/SKILL.md”

    }

  }

  “`

  1. LLM 读取到 Skill 内容后,按照其中指令执行

方式二:主动触发(用户命令)

文件: src/agents/sessions/agent-session.ts

行号: 第 1287-1320 行

private expandSkillCommand(text: string): string {
  if (!text.startsWith("/skill:")) {
    return text;
  }

  const spaceIndex = text.indexOf(" ");
  const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
  const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();

  // 查找 Skill
  const skills = this.sessionResourceLoader.getSkills().skills;
  const skill = skills.find((s) => s.name === skillName);

  if (!skill) {
    return text;  // Skill 不存在,返回原文
  }

  // 读取 SKILL.md 内容
  const content = fs.readFileSync(skill.filePath, "utf-8");
  const { frontmatter, body } = parseFrontmatter(content);

  // 包装成 <skill> 标签插入到用户消息中
  return `<skill name="${skill.name}" location="${skill.filePath}">\n` +
    `References are relative to ${skill.baseDir}.\n\n` +
    body +
    `</skill>\n\n` +
    args;
}

效果: 用户输入 /skill:weather-planner 明天想出去野餐,最终 LLM 收到的消息变为:

<skill name="weather-planner" location="/home/user/.config/openclaw/skills/weather-planner/SKILL.md">
References are relative to /home/user/.config/openclaw/skills/weather-planner.

# Weather Planner
When the user wants to plan outdoor activities:
1. Use the `bash` tool to call `curl` and fetch weather data from wttr.in
...
</skill>

明天想出去野餐

关键文件总结

文件 职责
`src/agents/sessions/agent-session.ts` AgentSession 类,协调整个会话生命周期
`src/agents/sessions/system-prompt.ts` 构建系统提示,插入 Skill 列表
`src/agents/sessions/resource-loader.ts` DefaultResourceLoader,加载 Skill 等资源
`src/skills/loading/session.ts` loadSkills 函数,扫描和解析 SKILL.md
`src/skills/loading/skill-contract.ts` Skill 类型定义和 XML 格式化
`src/skills/loading/skill-version.ts` 计算 Skill 版本哈希
`src/skills/loading/workspace.ts` 工作区 Skill 加载和索引

总结

OpenClaw 的 Skill 加载到 Prompt 的过程可以概括为:

  1. 扫描 — 会话启动时扫描 ~/.config/openclaw/skills/./.openclaw/skills/ 等目录
  1. 解析 — 读取每个 SKILL.md,提取 frontmatter(name、description)和计算版本哈希
  1. 缓存 — 将 Skill 元数据缓存到 DefaultResourceLoader
  1. 构建 — 在 buildSystemPrompt 中将 Skill 列表格式化为 XML 插入系统提示
  1. 传递 — 系统提示通过 agent.state.systemPrompt 传递给底层 LLM 运行时
  1. 触发 — LLM 自主判断或使用 /skill:name 命令触发 Skill 的完整内容读取