在上一篇文章中,我们已经搭建好了完整的开发环境,成功启动了 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 技能包含三个部分:
- 元数据:技能的名称、描述、作者、版本等信息
- 参数 Schema:使用 JSON Schema 定义技能的输入参数
- 执行函数:实际处理请求的代码逻辑
1.3 OpenClaw 技能调用流程
当用户提出一个问题时,OpenClaw 会执行以下步骤:
- 接收用户问题,分析意图
- 根据技能描述,判断是否需要调用工具
- 如果需要,生成符合参数 Schema 的工具调用请求
- 将请求发送给对应的执行节点
- 执行节点运行技能代码,返回结果
- 将工具返回的结果与用户问题一起发送给大模型
- 大模型生成最终回答,返回给用户
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. 接下来要做什么
在下一篇文章中,我们将:
- 集成向量数据库 Chroma
- 开发文档向量化技能,将解析后的文本转换为向量
- 实现向量存储和检索功能
- 完成 RAG 系统的核心检索模块
如果你迫不及待想提前准备,可以先了解一下 Chroma 向量数据库的基本概念和使用方法。
7. 小结
本篇文章我们完成了以下事情:
- 深入理解了 OpenClaw 的三层架构和核心工作原理
- 掌握了 MCP 协议的基本概念和技能开发流程
- 开发并测试了第一个 Hello World 技能
- 完成了实用的多格式文档解析技能,支持 TXT、Markdown、PDF 和 Word
- 学会了如何通过 REST API 调用 OpenClaw 技能
- 为后续的 RAG 检索模块开发打下了坚实的基础