Skill(技能)作为 AI Agent 生态中的重要概念,正在改变我们与 AI 交互的方式。但很多人对 Skill 的理解还停留在表面"下载下来就能用",往往忽略了其背后的运行机制和架构设计。
上一篇《用OpenClaw帮孩子成为真学霸—带你拆解AI Agent执行Skill+MCP的原理》文章中,我们依托OpenClaw作为Skill运行的AI Agent载体,详细介绍了一个“智能错题本”的业务案例是如何运行的,很多同学看完以后反馈还不解渴,想进一步了解OpenClaw的核心源码中Skill实现细节。
本文基于这个业务案例中的 Skill 作为抓手,通过深入解构 OpenClaw 的源代码, 分析AI Agent 是如何加载、检索和执行 Skill 的。无论你是想了解 Skill 的技术细节,还是希望在自己的智能体中构建类似的 Skill 运行机制,这篇文章都将为你提供有价值的参考。
一、Skill 的本质:必须依托 AI Agent 载体
1.1 一个常见的误解
很多人认为 Skill 是"通用"的,下载下来就能独立运行。但实际上:
Skill 必须依托一个 AI Agent 作为载体才能运行。
就像浏览器插件需要浏览器、VS Code 插件需要 VS Code 一样,Skill 需要 AI Agent 来提供:
- 运行时环境:解析 Skill 文件、管理依赖
- 工具调用能力:执行脚本、调用 MCP(Model Context Protocol)
- 上下文管理:维护对话状态、注入环境变量
- 安全沙箱:隔离执行、权限控制
1.2 支持 Skill 的 AI Agent 生态
目前主流的支持 Skill 的 AI Agent 包括:
| AI Agent | Skill 支持方式 | 特点 |
|---|---|---|
| OpenClaw | 本地 SKILL.md 文件 | 多源加载、安全沙箱、环境变量注入 |
| Claude Code | 通过 | Anthropic 官方,深度集成 |
| Codex CLI | 通过 | OpenAI 出品,支持 |
| Pi Coding Agent | 通过 | 轻量级,支持多 Provider |
| LangChain Deep Agent | 通过 Tools 机制 | 框架级支持,可编程性强 |
| Hermes Agent | 插件化 Skill 系统 | 企业级,支持复杂工作流 |
| OpenCode 等等 | ...... | ...... |
1.3 OpenClaw 中的 Skill 定位
在 OpenClaw 的架构中,Skill 是第一等公民:
Skill 系统负责:
- 加载(Load):从多个来源扫描和解析 SKILL.md 文件
- 过滤(Filter):根据平台、环境变量、配置进行筛选
- 执行(Execute):通过命令触发或模型自动调用
二、Skill 的组成与能力范围
2.1 学业评估师 Skill 配置形态结构解析
参考上篇文章“智能错题本”业务案例中的学业评估师Skill,其内容结构如下:
# 学业评估师 (Academic Evaluator)
## 技能概述
本技能通过调用 **试题批改 MCP Server**,实现试卷图片的端到端批改处理(OCR + 切题 + 评分 + 分析)。
| MCP Server | 职责 | 本技能使用的工具 |
|------------|------|----------------|
| **试题批改** (tencent-ocr-correction) | 试卷图片 OCR 识别、切题、批改、错误分析 | `correct_paper_sync` · `submit_paper_correction` · `get_correction_result` |
**适用场景**:
- 用户上传试卷/错题图片
- 复习卷答题图片批改
- "帮我批改这张试卷"
- "批改答题图片"
---
## 工作流程
### Step 1:接收试卷图片
从系统协调员获取试卷图片(URL 或 Base64)。
### Step 2:调用试题批改 MCP
```json
{
"tool": "correct_paper_sync",
"arguments": {
"image_url": "<试卷图片URL>",
"question_config_map": {"KnowledgePoints": true, "TrueAnswer": true}
}
}
```
### Step 3:处理批改结果
- 将返回的批改结果转换为错题本标准格式
- 映射错误类型(分析关键词 → 标准错误类型)
- 输出两份数据:
-**单题结果列表** → 给复习规划师
-**批改报告** → 给用户
> 详细的 MCP 调用参数和返回值说明见 `references/mcp_reference.md`。
> **为什么分离?**
> SKILL.md 会被注入到 LLM 的 System Prompt 中,过长会导致 token 浪费。
> 将详细的调用参数文档放在 reference 中,LLM 只在需要时才读取,既节省 token 又保证完整性。
2.2 Skill 能力范围公式
通过分析 OpenClaw 的源码,我们可以总结出 Skill 的能力范围:
Skill ≈ Prompt + Workflow + MCP + Script
| 组成部分 | 对应 Skill 内容 | OpenClaw 实现 |
|---|---|---|
| Prompt | SKILL.md 中的指令和示例 | formatSkillsForPrompt() |
| Workflow | 使用场景 / 不适用场景 | 模型根据描述自主决策 |
| MCP | 调用外部工具的能力 | pi-bundle-mcp-tools.ts |
| Script | 代码块/代码文件 | 工具执行,带安全策略 |
三、OpenClaw 源码深度解析-整体流程
3.1 完整执行链路
当用户在 QQ 中发送消息时,Skill 的执行链路如下:
3.2 代码层面的执行流程
// src/auto-reply/reply/agent-runner.ts
/**
* 获取回复的主入口
*/
exportasyncfunctiongetReply(params: GetReplyParams): Promise<ReplyResult> {
// 1. 加载 Skills
const skillEntries = opts?.entries ?? loadSkillEntries(workspaceDir, opts);
// 2. 过滤 Skills
const effectiveSkillFilter = resolveEffectiveWorkspaceSkillFilter(opts);
const eligible = filterSkillEntries(
skillEntries,
opts?.config,
effectiveSkillFilter,
opts?.eligibility,
)
// 3. 格式化 Skills 为提示词
const skillsPrompt = formatSkillsForPrompt(availableSkills);
// 4. 构建系统提示词
const prompt = [
remoteNote,
truncationNote,
compact ? formatSkillsCompact(skillsForPrompt) : formatSkillsForPrompt(skillsForPrompt),
]
.filter(Boolean)
.join("\n");
return { eligible, prompt, resolvedSkills };
// 5. 执行 Agent 运行
const runOutcome = awaitrunAgentTurnWithFallback({
commandBody,
followupRun,
sessionCtx,
replyOperation,
opts,
typingSignals,
// ...
});
}
接下来后续几章详细介绍Skill加载及运行的核心源码。
四、OpenClaw 源码深度解析-Skill加载流程
4.1 Skill Loader 加载层
local-loader.ts对应Skill Loader加载层,负责从多个来源加载和解析 Skill 文件:
// src/agent/skills/local-loader.ts, 技能文件安全加载器
// src/agent/skills/workspace.ts OpenClaw 技能系统核心逻辑
/**
* 从多个来源加载 Skill
*
* 1. 内置 Skills:~/.openclaw/skills/**
* 2. 工作区 Skills:./.openclaw/workspace/skills/**
* 3. 项目本地 Skills:./SKILL.md
* 4. 额外配置目录技能,config.ts 配置
*/
//配置目录技能(managed)
const managedSkillsDir = opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills");
//工作区项目技能(workspace)
const workspaceSkillsDir = path.resolve(workspaceDir, "skills");
//内置技能(bundled)
const bundledSkillsDir = opts?.bundledSkillsDir ?? resolveBundledSkillsDir();
//额外配置目录技能(extra)
const extraDirsRaw = opts?.config?.skills?.load?.extraDirs ?? [];
const extraDirs = extraDirsRaw
.map((d) => (typeof d === "string" ? d.trim() : ""))
.filter(Boolean);
const pluginSkillDirs = resolvePluginSkillDirs({
workspaceDir,
config: opts?.config,
});
const mergedExtraDirs = [...extraDirs, ...pluginSkillDirs];
const bundledSkills = bundledSkillsDir
? loadSkills({
dir: bundledSkillsDir,
source: "openclaw-bundled",
})
: [];
const extraSkills = mergedExtraDirs.flatMap((dir) => {
const resolved = resolveUserPath(dir);
returnloadSkills({
dir: resolved,
source: "openclaw-extra",
});
});
const managedSkills = loadSkills({
dir: managedSkillsDir,
source: "openclaw-managed",
});
const personalAgentsSkillsDir = path.resolve(os.homedir(), ".agents", "skills");
const personalAgentsSkills = loadSkills({
dir: personalAgentsSkillsDir,
source: "agents-skills-personal",
});
const projectAgentsSkillsDir = path.resolve(workspaceDir, ".agents", "skills");
const projectAgentsSkills = loadSkills({
dir: projectAgentsSkillsDir,
source: "agents-skills-project",
});
const workspaceSkills = loadSkills({
dir: workspaceSkillsDir,
source: "openclaw-workspace",
});
/**
* 安全地从指定目录加载技能(Skill),带路径安全校验、异常捕获,不会抛出错误
* 支持加载根目录技能 或 遍历子目录批量加载
*
* @param params - 加载参数
* @param params.dir - 要加载技能的根目录
* @param params.source - 技能来源标识(用于标记技能归属)
* @param params.maxBytes - 可选,技能文件最大大小限制(字节)
* @returns 返回加载到的技能数组(永远不会抛出异常,失败返回空数组)
*/
exportfunctionloadSkillsFromDirSafe(params: { dir: string; source: string; maxBytes?: number }): {
skills: Skill[];
} {
// 解析传入目录为绝对路径,统一路径格式
const rootDir = path.resolve(params.dir);
// 定义真实路径变量,用于后续路径安全校验
letrootRealPath: string;
try {
// 获取目录的真实绝对路径(解决软链接/../等路径问题,做安全防护)
rootRealPath = fs.realpathSync(rootDir);
} catch {
// 如果路径不存在/无权访问/解析失败,直接返回空技能列表
return { skills: [] };
}
// 尝试加载【根目录本身】作为单个技能目录
const rootSkill = loadSingleSkillDirectory({
skillDir: rootDir, // 技能目录
source: params.source, // 技能来源
rootRealPath, // 真实根路径(用于安全校验,防止目录穿越)
maxBytes: params.maxBytes,// 文件大小限制
});
// 如果根目录本身是一个合法技能,直接返回该技能
if (rootSkill) {
return { skills: [rootSkill] };
}
// ------------------------------
// 根目录不是技能 → 遍历子目录批量加载
// ------------------------------
// 1. 列出所有候选技能子目录
// 2. 逐个尝试加载每个子目录
// 3. 过滤掉加载失败的 null 值,只保留合法 Skill 类型
const skills = listCandidateSkillDirs(rootDir)
.map((skillDir) =>
loadSingleSkillDirectory({
skillDir,
source: params.source,
rootRealPath,
maxBytes: params.maxBytes,
}),
)
.filter((skill): skill is Skill => skill !== null);
// 返回最终加载到的所有技能
return { skills };
}
/**
* 解析 SKILL.md 文件
*/
functionloadSingleSkillDirectory(params: {
skillDir: string;
source: string;
rootRealPath: string;
maxBytes?: number;
}): Skill | null {
// 1. 读取 SKILL.md
const skillFilePath = path.join(params.skillDir, "SKILL.md");
const raw = readSkillFileSync(...)
if (!raw) returnnull;
// 2. 解析头部配置
const frontmatter = parseFrontmatter(raw);
// 3. 提取名称、描述
const name = frontmatter.name || 目录名;
const description = frontmatter.description;
// 4. 构建标准 Skill 对象并返回
return { name, description, filePath, baseDir, source, ... };
}
关键设计点:
1. 多源加载:支持用户级、工作区级、项目级三级 Skill
2. 动态更新:文件变化时自动重新加载
3. 错误隔离:单个 Skill 解析失败不影响其他 Skill
4. 缓存机制:已加载的 Skill 会被缓存以提高性能
4.2 Skill Filter 过滤层
config.ts过滤层负责根据当前环境筛选可用的 Skill:
// src/agents/skills/config.ts
/**
* 根据当前环境过滤 Skill
*/
functionfilterSkillEntries(
entries: SkillEntry[],
config?: OpenClawConfig,
skillFilter?: string[],
eligibility?: SkillEligibilityContext,
): SkillEntry[] {
let filtered = entries.filter((entry) =>shouldIncludeSkill({ entry, config, eligibility }));
// If skillFilter is provided, only include skills in the filter list.
if (skillFilter !== undefined) {
const normalized = normalizeSkillFilter(skillFilter) ?? [];
const label = normalized.length > 0 ? normalized.join(", ") : "(none)";
skillsLogger.debug(`Applying skill filter: ${label}`);
filtered =
normalized.length > 0
? filtered.filter((entry) => normalized.includes(entry.skill.name))
: [];
skillsLogger.debug(
`After skill filter: ${filtered.map((entry) => entry.skill.name).join(", ") || "(none)"}`,
);
}
return filtered;
}
functionisSkillVisibleInAvailableSkillsPrompt(entry: SkillEntry): boolean {
if (entry.exposure) {
return entry.exposure.includeInAvailableSkillsPrompt !== false;
}
if (entry.invocation) {
return entry.invocation.disableModelInvocation !== true;
}
return entry.skill.disableModelInvocation !== true;
}
exportfunctionshouldIncludeSkill(params: {
entry: SkillEntry;
config?: OpenClawConfig;
eligibility?: SkillEligibilityContext;
}): boolean {
const { entry, config, eligibility } = params;
const skillKey = resolveSkillKey(entry.skill, entry);
const skillConfig = resolveSkillConfig(config, skillKey);
const allowBundled = normalizeAllowlist(config?.skills?.allowBundled);
if (skillConfig?.enabled === false) {
returnfalse;
}
if (!isBundledSkillAllowed(entry, allowBundled)) {
returnfalse;
}
returnevaluateRuntimeEligibility({
os: entry.metadata?.os,
remotePlatforms: eligibility?.remote?.platforms,
always: entry.metadata?.always,
requires: entry.metadata?.requires,
hasBin: hasBinary,
hasRemoteBin: eligibility?.remote?.hasBin,
hasAnyRemoteBin: eligibility?.remote?.hasAnyBin,
hasEnv: (envName) =>
Boolean(
process.env[envName] ||
skillConfig?.env?.[envName] ||
(skillConfig?.apiKey && entry.metadata?.primaryEnv === envName),
),
isConfigPathTruthy: (configPath) =>isConfigPathTruthy(config, configPath),
});
}
exportfunctionisBundledSkillAllowed(entry: SkillEntry, allowlist?: string[]): boolean {
if (!allowlist || allowlist.length === 0) {
returntrue;
}
if (!isBundledSkill(entry)) {
returntrue;
}
const key = resolveSkillKey(entry.skill, entry);
return allowlist.includes(key) || allowlist.includes(entry.skill.name);
}
4.3 Skill 提示词格式化
skill-contract.ts负责将 Skill 转换为模型可理解的提示词格式:
// src/agent/skills/formatter.ts
// src/agents/skills/skill-contract.ts
/**
* 将 Skills 格式化为 XML 格式的提示词
*
* 输出格式示例:
* 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.
<available_skills>
<skill>
<name>github</name>
<description>Clone and manage GitHub repositories</description>
<location>~/skills/github/SKILL.md</location>
</skill>
</available_skills>
*/
exportfunctionformatSkillsForPrompt(skills: Skill[]): string {
if (skills.length === 0) {
return"";
}
const lines = [
"\n\nThe following skills provide specialized instructions for specific tasks.",
"Use the read tool to load a skill's file when the task matches its description.",
"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>",
];
for (const skill of skills) {
lines.push(" <skill>");
lines.push(` <name>${escapeXml(skill.name)}</name>`);
lines.push(` <description>${escapeXml(skill.description)}</description>`);
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
lines.push(" </skill>");
}
lines.push("</available_skills>");
return lines.join("\n");
}
五、OpenClaw 源码深度解析-Agent Runner 执行层
Agent Runner 执行层(agent-runner-execution.ts)是 OpenClaw 系统的核心执行引擎,负责管理 Agent 运行的完整生命周期。以下是对该模块的深度解析:
5.1 核心职责
// src/auto-reply/reply/agent-runner-execution.ts
/**
* Agent 运行执行模块 - 核心运行逻辑
*
* 本文件是 OpenClaw 系统中 Agent 回复生成的核心执行器。
* 主要职责:
* 1. 管理 Agent 运行的完整生命周期(启动、执行、错误处理、完成)
* 2. 处理模型回退(Model Fallback)- 当主模型失败时自动切换到备用模型
* 3. 处理各种错误场景(上下文溢出、限流、计费错误、会话损坏等)
* 4. 支持流式回复和块回复(Block Reply)两种输出模式
* 5. 管理会话状态持久化和回滚
*/
5.2 执行流程图
5.3 核心代码解析
/**
* 运行 Agent 回合(带模型回退支持)
*
* 这是 Agent 执行的核心函数,负责:
* 1. 管理模型回退链 - 当主模型失败时自动尝试备用模型
* 2. 处理各种错误场景 - 上下文溢出、限流、计费错误、会话损坏等
* 3. 支持流式回复和块回复两种输出模式
* 4. 管理会话状态持久化
* 5. 处理工具调用和事件流
*/
exportasyncfunctionrunAgentTurnWithFallback(params: {
commandBody: string; // 用户输入的命令/消息内容
followupRun: FollowupRun; // 后续运行配置
sessionCtx: TemplateContext; // 会话模板上下文
replyOperation?: ReplyOperation; // 回复操作对象
opts?: GetReplyOptions; // 获取回复的选项
typingSignals: TypingSignaler; // 打字信号发送器
blockReplyPipeline: BlockReplyPipeline | null;
blockStreamingEnabled: boolean;
// ... 其他参数
}): Promise<AgentRunLoopResult> {
// 常量与状态初始化
constTRANSIENT_HTTP_RETRY_DELAY_MS = 2_500;
let didLogHeartbeatStrip = false;
let autoCompactionCount = 0;
// 生成或复用运行 ID
const runId = params.opts?.runId ?? crypto.randomUUID();
// 媒体路径规范化与运行上下文注册
const normalizeReplyMediaPaths = createReplyMediaPathNormalizer({
cfg: params.followupRun.run.config,
sessionKey: params.sessionKey,
workspaceDir: params.followupRun.run.workspaceDir,
});
// 注册 Agent 运行上下文到全局事件系统
// 这使得其他组件可以获取当前运行的状态信息
if (params.sessionKey) {
registerAgentRunContext(runId, {
sessionKey: params.sessionKey,
verboseLevel: params.resolvedVerboseLevel,
isHeartbeat: params.isHeartbeat,
isControlUiVisible: shouldSurfaceToControlUi,
});
}
// 主执行循环
while (true) {
try {
// 调用模型回退机制执行 Agent
const fallbackResult = awaitrunWithModelFallback({
runId,
run: async (provider, model, runOptions) => {
// 通知模型选择完成
params.opts?.onModelSelected?.({ provider, model });
// 持久化回退候选选择
const rollbackFallbackCandidateSelection =
awaitpersistFallbackCandidateSelection(provider, model);
// 根据提供商类型选择执行路径
if (isCliProvider(provider, params.followupRun.run.config)) {
// CLI Agent 执行路径
returnawaitrunCliAgent({
sessionId: params.followupRun.run.sessionId,
prompt: params.commandBody,
provider,
model,
// ... 其他参数
});
}
// 嵌入式 Pi Agent 执行路径
const { embeddedContext, senderContext, runBaseParams } =
buildEmbeddedRunExecutionParams({ /* ... */ });
returnawaitrunEmbeddedPiAgent({
...embeddedContext,
prompt: params.commandBody,
// 事件处理回调
onPartialReply: async (payload) => {
const textForTyping = awaithandlePartialForTyping(payload);
await params.opts?.onPartialReply?.({ text: textForTyping });
},
onAgentEvent: async (evt) => {
// 处理各种事件流
if (evt.stream === "tool") {
await params.typingSignals.signalToolStart();
}
if (evt.stream === "compaction") {
// 处理上下文压缩事件
}
// ... 其他事件处理
},
// ... 其他回调和参数
});
},
});
// 处理执行结果
runResult = fallbackResult.result;
fallbackProvider = fallbackResult.provider;
fallbackModel = fallbackResult.model;
// 检查嵌入式错误(如上下文溢出)
const embeddedError = runResult.meta?.error;
if (embeddedError && isContextOverflowError(embeddedError.message)) {
// 尝试自动恢复
if (await params.resetSessionAfterCompactionFailure(embeddedError.message)) {
return {
kind: "final",
payload: {
text: "⚠️ 上下文限制超出,已重置会话,请重试。",
},
};
}
}
break; // 执行成功,退出循环
} catch (err) {
// 错误处理与恢复逻辑
// 1. 处理实时模型切换错误
if (err instanceofLiveSessionModelSwitchError) {
liveModelSwitchRetries += 1;
if (liveModelSwitchRetries > MAX_LIVE_SWITCH_RETRIES) {
return {
kind: "final",
payload: {
text: "⚠️ 模型切换失败,请稍后重试。",
},
};
}
// 切换到请求的模型并重试
params.followupRun.run.provider = err.provider;
params.followupRun.run.model = err.model;
continue;
}
// 2. 处理用户中止
if (isReplyOperationUserAbort(params.replyOperation)) {
return {
kind: "final",
payload: { text: SILENT_REPLY_TOKEN },
};
}
// 3. 处理重启生命周期错误
const restartLifecycleError = resolveRestartLifecycleError(err);
if (restartLifecycleError) {
return {
kind: "final",
payload: {
text: buildRestartLifecycleReplyText(),
},
};
}
// 4. 处理上下文压缩失败
if (isCompactionFailureError(message) && !didResetAfterCompactionFailure) {
didResetAfterCompactionFailure = true;
if (await params.resetSessionAfterCompactionFailure(message)) {
return {
kind: "final",
payload: {
text: "⚠️ 上下文压缩失败,已重置会话,请重试。",
},
};
}
}
// 5. 处理会话损坏(Gemini 特定问题)
if (isSessionCorruption) {
// 删除损坏的会话记录
awaitresetCorruptedSession(params.sessionKey);
return {
kind: "final",
payload: {
text: "⚠️ 会话历史损坏,已重置会话,请重试!",
},
};
}
// 6. 处理瞬态 HTTP 错误(502/521 等)
if (isTransientHttp && !didRetryTransientHttpError) {
didRetryTransientHttpError = true;
awaitsleep(TRANSIENT_HTTP_RETRY_DELAY_MS);
continue; // 重试
}
// 7. 其他错误 - 返回友好的错误信息
const fallbackText = classifyAndBuildErrorMessage(err);
return {
kind: "final",
payload: { text: fallbackText },
};
}
}
// 返回成功结果
return {
kind: "success",
runId,
runResult,
fallbackProvider,
fallbackModel,
fallbackAttempts,
didLogHeartbeatStrip,
autoCompactionCount,
};
}
5.4 关键执行步骤详解
1. 环境准备与上下文注入
// 构建嵌入式运行执行参数
const { embeddedContext, senderContext, runBaseParams } = buildEmbeddedRunExecutionParams({
run: params.followupRun.run,
sessionCtx: params.sessionCtx,
provider,
model,
runId,
});
2. 模型调用与响应处理
// 通过 runWithModelFallback 实现智能回退
const fallbackResult = await runWithModelFallback({
...resolveModelFallbackOptions(params.followupRun.run),
runId,
run: async (provider, model, runOptions) => {
// 执行实际的 Agent 运行
const result = await runEmbeddedPiAgent({ ... });
... 其他运行处理
return result;
},
});
3. 工具执行循环
// 在 runEmbeddedPiAgent 内部
onAgentEvent: async (evt) => {
const hasLifecyclePhase =
evt.stream === "lifecycle" && typeof evt.data.phase === "string";
if (evt.stream !== "lifecycle" || hasLifecyclePhase) {
notifyAgentRunStart();
}
// Trigger typing when tools start executing.
// Must await to ensure typing indicator starts before tool summaries are emitted.
if (evt.stream === "tool") {
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
const name = typeof evt.data.name === "string" ? evt.data.name : undefined;
if (phase === "start" || phase === "update") {
await params.typingSignals.signalToolStart();
await params.opts?.onToolStart?.({ name, phase });
}
}
...
4. 结果聚合与返回
// 处理最终结果
if (runResult) {
// 应用 OpenAI GPT Chat 简洁性控制
...
applyOpenAIGptChatReplyGuard({
provider: fallbackProvider,
model: fallbackModel,
commandBody: params.commandBody,
isHeartbeat: params.isHeartbeat,
payloads: runResult.payloads,
});
...
}
5. 错误处理机制:
| 错误类型 | 检测方式 | 处理策略 |
|---|---|---|
| 上下文溢出 | isContextOverflowError() | 自动压缩或重置会话 |
| 限流错误 | isRateLimitErrorMessage() | 显示等待时间,建议稍后重试 |
| 计费错误 | isBillingErrorMessage() | 显示友好的计费提示 |
| 会话损坏 | 特定错误消息匹配 | 删除损坏会话,重置状态 |
| 瞬态 HTTP | isTransientHttpError() | 延迟后重试一次 |
| 模型切换 | LiveSessionModelSwitchError | 切换模型后重试(最多2次) |
6. 安全防护机制:
- 心跳标记剥离:防止心跳信号泄露到用户可见的回复中
- 静默回复过滤:过滤掉内部使用的静默标记
- 错误信息清理:使用
sanitizeUserFacingText()清理用户可见的错误信息 - 最大重试限制:
MAX_LIVE_SWITCH_RETRIES = 2防止无限循环
六、OpenClaw安全沙箱隔离机制
6.1 Sandbox 机制概述
OpenClaw 可以在隔离的 sandbox 运行时中运行 agent,以提高安全性。Sandbox 是可选功能,通过 agents.defaults.sandbox 配置控制。
Sandbox 机制支持大部分 Agent 类型,包括:
- 主会话 (main session)
- 非主会话 (non-main sessions)
- Group/Channel 会话
- Sub-agent (子代理)
- ACP (Agent Coding Platform) 会话
6.2 OpenClaw vs Claude Code Sandbox 对比
| 特性 | OpenClaw | Claude Code |
|---|---|---|
| 架构 | Gateway + 独立 Sandbox 容器 | 原生集成在 CLI 中 |
| Backend | Docker / SSH / OpenShell | bubblewrap (Linux) / Seatbelt (macOS) |
| 文件系统隔离 | 挂载指定目录 | allowWrite/denyWrite 规则 |
| 网络隔离 | Docker 网络控制 | 代理服务器域名白名单 |
| 浏览器隔离 | 可选 sandboxed browser | 无原生支持 |
| CLI 命令 | openclaw sandbox | /sandbox |
关键区别:
| 维度 | OpenClaw | Claude Code |
|---|---|---|
| 隔离强度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 复杂度 | ⭐⭐⭐ | ⭐⭐ |
| 远程支持 | ⭐⭐⭐⭐ | ❌ |
适用场景:
- OpenClaw:
- 多租户环境
- 需要完整容器隔离
- 远程 agent 执行
- 需要浏览器自动化隔离
- Claude Code:
- 本地开发
- 轻量级隔离
- 快速启用
- 项目级控制
6.3 哪些工具会被 Sandbox 隔离
✅ 会被隔离:
exec(命令执行)read/write/edit/apply_patch(文件操作)process(进程管理)browser(浏览器自动化,可选)
❌ 不被隔离:
- Gateway 进程本身
tools.elevated(提权执行,强制在 host 运行)
七、总结
通过深入分析 OpenClaw 的源码,我们可以得出以下关键洞察:
- Skill 的本质:不是独立可运行的程序,而是需要 AI Agent 作为载体的"能力描述文件"
- OpenClaw 的 Skill 系统架构:
- 加载层:多源扫描、动态更新、错误隔离
- 过滤层:平台兼容、依赖检查、权限控制
- 执行层:提示词注入、工具调用、安全沙箱
- 核心实现文件:
skill-loader.ts:Skill 加载与解析skill-filter.ts:环境过滤skill-formatter.ts:提示词格式化agent-runner-execution.ts:Agent 执行核心pi-embedded.ts:嵌入式 Agent 实现
- 关键设计思想:
- 多源加载与合并
- 渐进式 Skill 披露
- 完善的错误处理和恢复机制
- 多层安全防护
- 智能模型回退
- 灵活的扩展能力
对于希望在自己的系统中实现 Skill 机制的开发者,OpenClaw 的源码提供了极佳的参考实现。关键在于理解 Skill 作为"可复用 AI 工作流单元"的本质,以及 AI Agent 作为"Skill 运行时载体"的定位。
参考资源
- OpenClaw 官方文档:docs.openclaw.ai/
- OpenClaw GitHub:github.com/openclaw/op…
- Pi Coding Agent:github.com/mariozechne…
本文基于 OpenClaw 开源代码分析编写,部分代码片段为简化展示,实际实现请参考官方源码。
本系列说明:在本系列中我们将通过不同 AI Agent(如 Code Buddy、OpenClaw、OpenCode、Hermes Agent 等)作为载体,结合具体业务案例进行实战,以深入剖析 Skill 的实现机制和底层原理。
—End—
本文作者:柚子大哥
本文原载:公众号“木昆子记录AI”