Skill 深度解析:从案例到 OpenClaw 源码实现

25 阅读15分钟

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 AgentSkill 支持方式特点
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 是第一等公民:

Image

Skill 系统负责:

  1. 加载(Load):从多个来源扫描和解析 SKILL.md 文件
  2. 过滤(Filter):根据平台、环境变量、配置进行筛选
  3. 执行(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 实现
PromptSKILL.md 中的指令和示例formatSkillsForPrompt()
Workflow使用场景 / 不适用场景模型根据描述自主决策
MCP调用外部工具的能力pi-bundle-mcp-tools.ts
Script代码块/代码文件工具执行,带安全策略

三、OpenClaw 源码深度解析-整体流程

3.1 完整执行链路

当用户在 QQ 中发送消息时,Skill 的执行链路如下:

Image

3.2 代码层面的执行流程

// src/auto-reply/reply/agent-runner.ts  
/**  
 * 获取回复的主入口  
 */  
exportasyncfunctiongetReply(paramsGetReplyParams): 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: { dirstring; sourcestring; maxBytes?: number }): {  
skillsSkill[];  
} {  
// 解析传入目录为绝对路径,统一路径格式  
const rootDir = path.resolve(params.dir);  
  
// 定义真实路径变量,用于后续路径安全校验  
letrootRealPathstring;  
  
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: {  
  skillDirstring;  
  sourcestring;  
  rootRealPathstring;  
  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(  
entriesSkillEntry[],  
  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(entrySkillEntry): 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(entrySkillEntry, 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(skillsSkill[]): 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 执行流程图

Image

5.3 核心代码解析

/**  
 * 运行 Agent 回合(带模型回退支持)  
 *  
 * 这是 Agent 执行的核心函数,负责:  
 * 1. 管理模型回退链 - 当主模型失败时自动尝试备用模型  
 * 2. 处理各种错误场景 - 上下文溢出、限流、计费错误、会话损坏等  
 * 3. 支持流式回复和块回复两种输出模式  
 * 4. 管理会话状态持久化  
 * 5. 处理工具调用和事件流  
 */  
exportasyncfunctionrunAgentTurnWithFallback(params: {  
  commandBodystring;              // 用户输入的命令/消息内容  
  followupRun: FollowupRun;         // 后续运行配置  
  sessionCtx: TemplateContext;      // 会话模板上下文  
  replyOperation?: ReplyOperation;  // 回复操作对象  
  opts?: GetReplyOptions;           // 获取回复的选项  
  typingSignals: TypingSignaler;    // 打字信号发送器  
  blockReplyPipeline: BlockReplyPipelinenull;  
  blockStreamingEnabledboolean;  
  // ... 其他参数  
}): 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,  
        runasync (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,  
            // 事件处理回调  
            onPartialReplyasync (payload) => {  
              const textForTyping = awaithandlePartialForTyping(payload);  
              await params.opts?.onPartialReply?.({ text: textForTyping });  
            },  
            onAgentEventasync (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: { textSILENT_REPLY_TOKEN },  
        };  
      }  
        
      // 3. 处理重启生命周期错误  
      const restartLifecycleError = resolveRestartLifecycleError(err);  
      if (restartLifecycleError) {  
        return {  
          kind"final",  
          payload: {  
            textbuildRestartLifecycleReplyText(),  
          },  
        };  
      }  
        
      // 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,  
    runasync (provider, model, runOptions) => {  
    // 执行实际的 Agent 运行  
    const result = await runEmbeddedPiAgent({ ... });  
    ... 其他运行处理  
    return result;  
  },  
});

3. 工具执行循环

// 在 runEmbeddedPiAgent 内部  
 onAgentEventasync (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()显示友好的计费提示
会话损坏特定错误消息匹配删除损坏会话,重置状态
瞬态 HTTPisTransientHttpError()延迟后重试一次
模型切换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 对比

特性OpenClawClaude Code
架构Gateway + 独立 Sandbox 容器原生集成在 CLI 中
BackendDocker / SSH / OpenShellbubblewrap (Linux) / Seatbelt (macOS)
文件系统隔离挂载指定目录allowWrite/denyWrite 规则
网络隔离Docker 网络控制代理服务器域名白名单
浏览器隔离可选 sandboxed browser无原生支持
CLI 命令openclaw sandbox/sandbox

关键区别:

维度OpenClawClaude Code
隔离强度⭐⭐⭐⭐⭐⭐⭐⭐⭐
复杂度⭐⭐⭐⭐⭐
远程支持⭐⭐⭐⭐

适用场景:

  • OpenClaw:
    • 多租户环境
    • 需要完整容器隔离
    • 远程 agent 执行
    • 需要浏览器自动化隔离
  • Claude Code:
    • 本地开发
    • 轻量级隔离
    • 快速启用
    • 项目级控制

6.3 哪些工具会被 Sandbox 隔离

✅ 会被隔离:

  • exec (命令执行)
  • read/write/edit/apply_patch (文件操作)
  • process (进程管理)
  • browser (浏览器自动化,可选)

❌ 不被隔离:

  • Gateway 进程本身
  • tools.elevated (提权执行,强制在 host 运行)

七、总结

通过深入分析 OpenClaw 的源码,我们可以得出以下关键洞察:

  1. Skill 的本质:不是独立可运行的程序,而是需要 AI Agent 作为载体的"能力描述文件"
  2. OpenClaw 的 Skill 系统架构:
    • 加载层:多源扫描、动态更新、错误隔离
    • 过滤层:平台兼容、依赖检查、权限控制
    • 执行层:提示词注入、工具调用、安全沙箱
  3. 核心实现文件:
    • skill-loader.ts:Skill 加载与解析
    • skill-filter.ts:环境过滤
    • skill-formatter.ts:提示词格式化
    • agent-runner-execution.ts:Agent 执行核心
    • pi-embedded.ts:嵌入式 Agent 实现
  4. 关键设计思想:
    • 多源加载与合并
    • 渐进式 Skill 披露
    • 完善的错误处理和恢复机制
    • 多层安全防护
    • 智能模型回退
    • 灵活的扩展能力

对于希望在自己的系统中实现 Skill 机制的开发者,OpenClaw 的源码提供了极佳的参考实现。关键在于理解 Skill 作为"可复用 AI 工作流单元"的本质,以及 AI Agent 作为"Skill 运行时载体"的定位。

参考资源

本文基于 OpenClaw 开源代码分析编写,部分代码片段为简化展示,实际实现请参考官方源码。


本系列说明:在本系列中我们将通过不同 AI Agent(如 Code Buddy、OpenClaw、OpenCode、Hermes Agent 等)作为载体,结合具体业务案例进行实战,以深入剖析 Skill 的实现机制和底层原理。

—End—

本文作者:柚子大哥

本文原载:公众号“木昆子记录AI”