OpenClaw 核心概念与第一个 MCP 技能开发

在上一篇文章中,我们已经搭建好了完整的开发环境,成功启动了 FastAPI 后端、OpenClaw AI 网关和 React 前端服务。这篇文章,我们将深入学习 OpenClaw 的核心机制,特别是 MCP(Model Context Protocol)协议,并开发我们的第一个实用技能——文档解析技能。

摘要

  • 理解 OpenClaw 的三层架构与核心工作原理
  • 掌握 MCP(Model Context Protocol)协议的基本概念
  • 学会开发、注册和测试 OpenClaw 自定义技能
  • 完成第一个实用技能:多格式文档解析技能
  • 实现通过 API 调用 OpenClaw 技能,为后续 RAG 集成铺路

1. OpenClaw 核心机制深度解析

在开始开发之前,我们需要先理解 OpenClaw 是如何工作的,这将帮助我们更好地设计和实现我们的知识库系统。

1.1 OpenClaw 三层架构

OpenClaw 采用了清晰的分层设计,确保系统的可扩展性和可靠性:

┌─────────────────────────────────────────────────────────┐
│                      控制层(Gateway 网关)              │
│  会话管理 │ 通道路由 │ 工具调度 │ 状态同步 │ 流式输出    │
└───────────────────────────┬─────────────────────────────┘
                            │
┌───────────────────────────┼─────────────────────────────┐
│                      执行层(Node/Agent 节点)           │
│  技能执行 │ 系统访问 │ 代码运行 │ 沙箱隔离 │ 错误处理    │
└───────────────────────────┬─────────────────────────────┘
                            │
┌───────────────────────────┼─────────────────────────────┐
│                      生态层(Skills & Workspace)        │
│  官方技能市场 │ 社区技能 │ 自定义技能 │ 工作空间配置    │
└─────────────────────────────────────────────────────────┘
  • 控制层:系统的"神经中枢",运行在 127.0.0.1:18789 端口,负责接收所有请求,管理会话生命周期,调度技能执行,并将结果返回给调用方
  • 执行层:分布在终端的"执行肢体",负责实际运行技能代码,可以安全地访问宿主系统的资源
  • 生态层:基于 MCP 协议的模块化插件系统,所有功能都以技能的形式存在,可以自由安装、卸载和开发

1.2 什么是 MCP 协议?

MCP(Model Context Protocol)是 OpenClaw 定义的一套标准化协议,用于大模型与外部工具之间的通信。它解决了传统工具调用中存在的以下问题:

  • 不同大模型的工具调用格式不统一
  • 工具描述不清晰,导致大模型不知道如何正确调用
  • 缺乏统一的错误处理和状态同步机制
  • 工具权限管理混乱

一个标准的 MCP 技能包含三个部分:

  1. 元数据:技能的名称、描述、作者、版本等信息
  2. 参数 Schema:使用 JSON Schema 定义技能的输入参数
  3. 执行函数:实际处理请求的代码逻辑

1.3 OpenClaw 技能调用流程

当用户提出一个问题时,OpenClaw 会执行以下步骤:

  1. 接收用户问题,分析意图
  2. 根据技能描述,判断是否需要调用工具
  3. 如果需要,生成符合参数 Schema 的工具调用请求
  4. 将请求发送给对应的执行节点
  5. 执行节点运行技能代码,返回结果
  6. 将工具返回的结果与用户问题一起发送给大模型
  7. 大模型生成最终回答,返回给用户

2. 开发第一个 MCP 技能:Hello World

让我们从最简单的 Hello World 技能开始,熟悉 MCP 技能的开发流程。

2.1 创建技能目录

openclaw 目录下创建一个 skills 文件夹,用于存放我们所有的自定义技能:

cd openclaw
mkdir skills
mkdir skills/hello-world

2.2 编写技能定义文件

每个技能都需要一个 skill.yaml 文件,用于定义技能的元数据和参数 Schema。

创建 skills/hello-world/skill.yaml

name: hello-world
description: 一个简单的 Hello World 技能,用于测试 OpenClaw 技能开发
author: Your Name
version: 0.1.0
category: 测试

parameters:
  type: object
  properties:
    name:
      type: string
      description: 要问候的人的名字
      default: World
  required: []

returns:
  type: string
  description: 问候语

2.3 编写技能执行代码

创建 skills/hello-world/index.ts

import { SkillContext, SkillResult } from 'openclaw';

export default async function execute(
  params: { name?: string },
  context: SkillContext
): Promise<SkillResult> {
  const name = params.name || 'World';
  const greeting = `Hello, ${name}! 这是来自 OpenClaw 技能的问候。`;
  
  context.logger.info(`成功生成问候语: ${greeting}`);
  
  return {
    success: true,
    data: greeting
  };
}

2.4 注册技能

现在我们需要告诉 OpenClaw 我们有一个新技能。编辑 OpenClaw 的配置文件 .claw/config.yaml,添加以下内容:

skills:
  - path: ../skills/hello-world
    enabled: true

2.5 重启 OpenClaw 并测试

重启 OpenClaw 网关以加载新技能:

# 按 Ctrl+C 停止之前的服务
pnpm run start

现在打开 OpenClaw Web 管理界面(通常会自动打开,地址是 http://localhost:18789),在聊天框中输入:

调用 hello-world 技能,名字是 OpenClaw

你应该会看到类似这样的回复:

正在调用 hello-world 技能...
技能调用成功:
Hello, OpenClaw! 这是来自 OpenClaw 技能的问候。

恭喜!你已经成功开发并运行了你的第一个 OpenClaw 技能。


3. 开发实用技能:文档解析技能

现在我们来开发一个对知识库系统至关重要的技能——文档解析技能。这个技能将能够读取本地文件系统中的文档,并将其内容提取为纯文本格式。

3.1 创建文档解析技能目录

mkdir skills/document-parser

3.2 编写技能定义文件

创建 skills/document-parser/skill.yaml

name: document-parser
description: 解析本地文件系统中的文档,提取纯文本内容。支持 TXT、Markdown、PDF、Word 等格式。
author: Your Name
version: 0.1.0
category: 文档处理

parameters:
  type: object
  properties:
    filePath:
      type: string
      description: 要解析的文档的绝对路径
    format:
      type: string
      description: 文档格式,如果不指定则根据文件扩展名自动识别
      enum: [txt, md, pdf, docx, auto]
      default: auto
  required:
    - filePath

returns:
  type: object
  properties:
    success:
      type: boolean
      description: 是否解析成功
    content:
      type: string
      description: 提取的纯文本内容
    metadata:
      type: object
      description: 文档元数据
      properties:
        fileName:
          type: string
          description: 文件名
        fileSize:
          type: number
          description: 文件大小(字节)
        format:
          type: string
          description: 文档格式
        pageCount:
          type: number
          description: 页数(仅适用于 PDF 和 Word)

3.3 安装依赖

我们需要一些库来解析不同格式的文档:

pnpm add fs-extra pdf-parse mammoth
pnpm add -D @types/fs-extra @types/pdf-parse

3.4 编写技能执行代码

创建 skills/document-parser/index.ts

import { SkillContext, SkillResult } from 'openclaw';
import fs from 'fs-extra';
import path from 'path';
import pdfParse from 'pdf-parse';
import mammoth from 'mammoth';

interface DocumentParserParams {
  filePath: string;
  format: 'txt' | 'md' | 'pdf' | 'docx' | 'auto';
}

interface DocumentMetadata {
  fileName: string;
  fileSize: number;
  format: string;
  pageCount?: number;
}

export default async function execute(
  params: DocumentParserParams,
  context: SkillContext
): Promise<SkillResult> {
  const { filePath, format } = params;
  
  try {
    // 检查文件是否存在
    if (!await fs.pathExists(filePath)) {
      throw new Error(`文件不存在: ${filePath}`);
    }
    
    // 获取文件信息
    const stats = await fs.stat(filePath);
    if (!stats.isFile()) {
      throw new Error(`不是一个文件: ${filePath}`);
    }
    
    const fileName = path.basename(filePath);
    const fileSize = stats.size;
    
    // 自动识别文件格式
    let actualFormat = format;
    if (actualFormat === 'auto') {
      const ext = path.extname(filePath).toLowerCase().slice(1);
      if (['txt', 'md', 'pdf', 'docx'].includes(ext)) {
        actualFormat = ext as any;
      } else {
        throw new Error(`不支持的文件格式: ${ext}`);
      }
    }
    
    context.logger.info(`开始解析文档: ${fileName}, 格式: ${actualFormat}`);
    
    let content: string;
    let pageCount: number | undefined;
    
    // 根据不同格式解析文档
    switch (actualFormat) {
      case 'txt':
      case 'md':
        content = await fs.readFile(filePath, 'utf-8');
        break;
        
      case 'pdf':
        const pdfBuffer = await fs.readFile(filePath);
        const pdfResult = await pdfParse(pdfBuffer);
        content = pdfResult.text;
        pageCount = pdfResult.numpages;
        break;
        
      case 'docx':
        const docxBuffer = await fs.readFile(filePath);
        const docxResult = await mammoth.extractRawText({ buffer: docxBuffer });
        content = docxResult.value;
        break;
        
      default:
        throw new Error(`不支持的文件格式: ${actualFormat}`);
    }
    
    // 清理文本内容
    content = content.replace(/\r\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
    
    const metadata: DocumentMetadata = {
      fileName,
      fileSize,
      format: actualFormat,
      pageCount
    };
    
    context.logger.info(`文档解析成功: ${fileName}, 大小: ${fileSize} 字节, 页数: ${pageCount || 'N/A'}`);
    
    return {
      success: true,
      data: {
        success: true,
        content,
        metadata
      }
    };
    
  } catch (error: any) {
    context.logger.error(`文档解析失败: ${error.message}`);
    
    return {
      success: false,
      error: error.message
    };
  }
}

3.5 注册文档解析技能

编辑 .claw/config.yaml,添加文档解析技能:

skills:
  - path: ../skills/hello-world
    enabled: true
  - path: ../skills/document-parser
    enabled: true

3.6 测试文档解析技能

重启 OpenClaw 网关,然后在 Web 界面中测试:

调用 document-parser 技能,filePath 是 "/path/to/your/test.md"

请将 /path/to/your/test.md 替换为你本地一个实际存在的 Markdown 文件的绝对路径。如果一切正常,你应该能看到文件的内容被成功提取出来。


4. 通过 API 调用 OpenClaw 技能

为了让 FastAPI 后端能够调用 OpenClaw 技能,我们需要了解 OpenClaw 的 REST API。

4.1 创建会话

首先,我们需要创建一个会话:

curl -X POST http://localhost:18789/api/sessions \
  -H "Content-Type: application/json" \
  -d '{
    "title": "文档解析测试会话"
  }'

你会得到一个包含会话 ID 的响应:

{
  "success": true,
  "data": {
    "id": "session-1234567890",
    "title": "文档解析测试会话",
    "createdAt": "2026-05-18T00:00:00.000Z"
  }
}

4.2 调用技能

使用上一步得到的会话 ID,调用文档解析技能:

curl -X POST http://localhost:18789/api/sessions/{session-id}/messages \
  -H "Content-Type: application/json" \
  -d '{
    "role": "user",
    "content": "调用 document-parser 技能,filePath 是 \"/path/to/your/test.md\"",
    "stream": false
  }'

你会得到一个包含技能调用结果的响应。

4.3 流式输出

如果需要流式输出,可以将 stream 参数设置为 true

curl -X POST http://localhost:18789/api/sessions/{session-id}/messages \
  -H "Content-Type: application/json" \
  -d '{
    "role": "user",
    "content": "总结一下这个文档的内容",
    "stream": true
  }'

OpenClaw 会以 SSE(Server-Sent Events)的方式返回结果,这对于实现前端的打字机效果非常有用。


5. 项目结构更新

至此,我们的项目目录已经更新为:

enterprise-knowledge-base/
├── .gitignore
├── backend/
│   ├── venv/
│   ├── main.py
│   └── requirements.txt
├── openclaw/
│   ├── node_modules/
│   ├── package.json
│   ├── .claw/
│   │   └── config.yaml
│   └── skills/
│       ├── hello-world/
│       │   ├── skill.yaml
│       │   └── index.ts
│       └── document-parser/
│           ├── skill.yaml
│           └── index.ts
└── frontend/
    ├── node_modules/
    ├── src/
    ├── index.html
    ├── package.json
    └── tsconfig.json

6. 接下来要做什么

在下一篇文章中,我们将:

  1. 集成向量数据库 Chroma
  2. 开发文档向量化技能,将解析后的文本转换为向量
  3. 实现向量存储和检索功能
  4. 完成 RAG 系统的核心检索模块

如果你迫不及待想提前准备,可以先了解一下 Chroma 向量数据库的基本概念和使用方法。


7. 小结

本篇文章我们完成了以下事情:

  • 深入理解了 OpenClaw 的三层架构和核心工作原理
  • 掌握了 MCP 协议的基本概念和技能开发流程
  • 开发并测试了第一个 Hello World 技能
  • 完成了实用的多格式文档解析技能,支持 TXT、Markdown、PDF 和 Word
  • 学会了如何通过 REST API 调用 OpenClaw 技能
  • 为后续的 RAG 检索模块开发打下了坚实的基础