OpenClaw Skill 加载到 Prompt 的原理与调用过程
概述
OpenClaw 的 Skill 系统采用声明式 + 按需加载的设计。Skill 文件(SKILL.md)在会话启动时被扫描加载为元数据,但内容不会直接嵌入系统提示。系统提示中只包含 Skill 的”目录卡片”(名称、描述、路径、版本),由 LLM 自主判断是否需要读取某个 Skill 的完整内容。
核心设计原则
- 节省 Token — 系统提示中只保留 Skill 元数据列表,避免每次请求都携带大量 Skill 内容
- LLM 自主决策 — 系统提示明确告诉 LLM “如果任务匹配某个 Skill,就用 read 工具读取它”
- 按需读取 — LLM 通过
read工具主动读取SKILL.md文件内容,然后按照其中指令执行
- 缓存与版本控制 — 通过
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, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
生成的系统提示示例
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 自主判断)
流程:
- 系统提示中包含
<available_skills>列表
- LLM 看到用户输入后,判断哪个 Skill 匹配当前任务
- LLM 主动调用
read工具读取 Skill 文件:
“`json
{
“tool”: “read”,
“params”: {
“file_path”: “/home/user/.config/openclaw/skills/weather-planner/SKILL.md”
}
}
“`
- 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 的过程可以概括为:
- 扫描 — 会话启动时扫描
~/.config/openclaw/skills/和./.openclaw/skills/等目录
- 解析 — 读取每个
SKILL.md,提取 frontmatter(name、description)和计算版本哈希
- 缓存 — 将 Skill 元数据缓存到
DefaultResourceLoader中
- 构建 — 在
buildSystemPrompt中将 Skill 列表格式化为 XML 插入系统提示
- 传递 — 系统提示通过
agent.state.systemPrompt传递给底层 LLM 运行时
- 触发 — LLM 自主判断或使用
/skill:name命令触发 Skill 的完整内容读取
夜雨聆风