首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >打造迷你版 OpenClaw:一步步教你实现

打造迷你版 OpenClaw:一步步教你实现

作者头像
HELLO程序员
发布2026-06-26 21:22:39
发布2026-06-26 21:22:39
1380
举报

OpenClaw 绝对是今年最疯的 AI 工具之一。火到什么程度?大家都开始囤 Mac mini 就为跑它,创始人 Peter Steinberger 更是直接被 OpenAI 挖走了。

如果你还没听说过——OpenClaw 是一个免费开源的自主 AI 智能体,能帮你完成各种任务。它可以读收件箱、发邮件、管理日程、甚至帮你自动值机,而且这一切都能在 WhatsApp、Telegram 或者你常用的任何聊天软件里搞定。(官网就是这么吹的)

很多人甚至觉得,OpenClaw 已经摸到 AGI 的边了。这也是我决定从零复刻一个简易版、并手把手教你的原因之一(当然,我也真心想把它的零件拆开来研究一番)。

话不多说,咱们来造一个 「迷你 OpenClaw」

我们要造个啥?

开工前,先看看正版 OpenClaw 能干啥:

  • 能跑在 Mac / Windows / Linux 上,支持闭源大模型也支持本地大模型
  • 能用任意聊天软件跟它唠嗑
  • 能记住你的对话和偏好(也就是有持久记忆
  • 能上网、填表、从网页扒数据
  • 能完全访问你电脑上的所有文件
  • 能用各种「技能」——自己写的、社区做的都行,本质就是一堆指令、脚本和资源,帮 AI 完成特定任务

而我们的迷你版,就叫 Mini-OpenClaw,功能如下:

  • 能跑在 Mac / Windows / Linux
  • 能上网、填表、爬网页数据
  • 支持技能系统
  • 有持久记忆
  • 用 Telegram 跟它聊天

唯一区别:迷你版不会完全控制你的系统,毕竟安全风险太大,而且这篇教程本来就不是用来做生产级机器人的。

Mini-OpenClaw 的 8 大核心零件

写代码前,先搞懂它由哪 8 个模块组成:

  1. Telegram 通道相当于聊天平台适配器,把平台消息格式转成 AI 能看懂的标准格式。
  2. 会话管理器给每个用户单独开对话历史,互不干扰。
  3. 智能体运行时核心循环:给大模型喂提示词和上下文 → 需要工具就调用工具 → 返回最终答案。
  4. 记忆模块持久化存储,跨会话记住用户信息。
  5. SOUL.md一个 markdown 文件,定义 AI 的性格和行为准则。
  6. 技能(Skills)每个技能是一个文件夹,包含指令、脚本、资源,专门干某件事。
  7. 技能加载器启动时扫描所有技能,读取描述和工具,把大模型的工具调用路由到对应处理器。
  8. 上下文构建器把 SOUL.md、技能描述、用户记忆、当前时间揉成一团,喂给大模型。

下面我们一个个来搭。

新建项目

打开终端,一键搭好项目骨架:

代码语言:javascript
复制
# 建项目文件夹
mkdir mini-openclaw && cd mini-openclaw

# 建技能和前端目录
mkdir -p skills/datetime skills/memory_work skills/browser_use

# 核心组件文件
touch main.py agent_runtime.py context_builder.py session_manager.py telegram_channel.py memory_store.py skill_loader.py SOUL.md .env

# 日期时间技能
touch skills/datetime/SKILL.md skills/datetime/handler.py

# 记忆笔记技能
touch skills/memory_work/SKILL.md skills/memory_work/handler.py

# 浏览器技能
touch skills/browser_use/SKILL.md skills/browser_use/handler.py

然后建虚拟环境,用 uv 装依赖:

代码语言:javascript
复制
# 创建并激活虚拟环境
uv venv
source .venv/bin/activate

# 装包
uv pip install httpx python-dotenv python-telegram-bot playwright

# 给 Playwright 装浏览器内核
playwright install chromium

第一步:造记忆模块

Mini-OpenClaw 的记忆就是个简单的键值对存储,存在 JSON 文件里,用来存用户偏好和信息,方便大模型调用。

你想用 Redis 这种内存数据库也行,咱们图省事就用字典 + 文件。

代码语言:javascript
复制
# ./memory_store.py

import json
import os

class Memory:
    def __init__(self, path="MEMORY.json"):
        self.path = path
        if os.path.exists(path):
            with open(path) as f:
                self._data = json.load(f)
        else:
            self._data = {}

    def set(self, key, value):
        self._data[key] = value
        self._save()

    def get(self, key):
        return self._data.get(key)

    def keys(self):
        return list(self._data.keys())

    def _save(self):
        with open(self.path, "w") as f:
            json.dump(self._data, f, indent=2, default=str)

第二步:会话管理器

作用很简单:每个用户一个独立聊天记录,重启不丢失,存在 SESSIONS.json 里。

代码语言:javascript
复制
# ./session_manager.py

import json
import os
import time

class SessionManager:
    def __init__(self, path="SESSIONS.json"):
        self.path = path
        if os.path.exists(path):
            with open(path) as f:
                self.sessions = json.load(f)
            print("✅ 从磁盘恢复了历史会话!")
        else:
            self.sessions = {}

    def get_or_create_session(self, client_id, channel):
        session_id = f"{channel}:{client_id}"
        if session_id notin self.sessions:
            self.sessions[session_id] = {
                "client_id": client_id,
                "channel": channel,
                "created_at": time.time(),
                "history": [],
            }
        return session_id

    def add_message(self, session_id, message):
        session = self.sessions.get(session_id)
        if session:
            session["history"].append(message)
            self._save()

    def get_history(self, session_id):
        session = self.sessions.get(session_id)
        return session["history"] if session else []

    def _save(self):
        with open(self.path, "w") as f:
            json.dump(self.sessions, f, indent=2, default=str)

一个典型的会话长这样:

代码语言:javascript
复制
{
  "telegram:1191237804": {
    "client_id": "1191237804",
    "channel": "telegram",
    "created_at": 1774270494.7631621,
    "history": [
      {"role": "user", "content": "Hi!", "timestamp": 1774270494.7631638},
      {"role": "assistant", "content": "Hello! I'm your personal AI assistant...", "timestamp": 1774270497.646961},
      {"role": "user", "content": "Remember my name is ashish", "timestamp": 1774270609.259111},
      {"role": "assistant", "content": "Perfect! I've saved your name...", "timestamp": 1774270611.7315981}
    ]
  }
}

第三步:搭建技能系统

技能就是 AI 的「超能力」。每个技能文件夹里都有俩文件:

  • SKILL.md:告诉大模型这技能叫啥、能干啥
  • handler.py:真·干活的代码

我们做三个技能:

  1. 日期时间
  2. 记忆笔记
  3. 浏览器自动化

技能 1:日期时间

代码语言:javascript
复制
# skills/datetime/SKILL.md
---
name: datetime
description: 获取当前日期和时间。
---
代码语言:javascript
复制
# skills/datetime/handler.py
from datetime import datetime, timezone

tools = [
    {
        "name": "get_current_datetime",
        "description": "获取当前日期和时间。",
        "parameters": {
            "type": "object",
            "properties": {},
            "required": [],
        },
    }
]

asyncdef execute(tool_name, tool_input, context):
    if tool_name == "get_current_datetime":
        now = datetime.now(timezone.utc)
        return {
            "readable": now.strftime("%A, %B %d, %Y %I:%M:%S %p UTC"),
        }
    return {"error": f"未知工具:{tool_name}"}

技能 2:记忆笔记

代码语言:javascript
复制
# skills/memory_work/SKILL.md
---
name: memory_work
description: 把用户的个人信息存到记忆里。
---
代码语言:javascript
复制
# skills/memory_work/handler.py
tools = [
    {
        "name": "save_note",
        "description": "把关于用户的笔记存到记忆里。",
        "parameters": {
            "type": "object",
            "properties": {
                "key": {"type": "string", "description": "简短的键名"},
                "content": {"type": "string", "description": "笔记内容"},
            },
            "required": ["key", "content"],
        },
    }
]

asyncdef execute(tool_name, tool_input, context):
    memory = context["memory"]
    if tool_name == "save_note":
        memory.set(f"note:{tool_input['key']}", {
            "content": tool_input["content"],
        })
        return {"success": True, "key": tool_input["key"]}
    return {"error": f"未知工具:{tool_name}"}

技能 3:浏览器操控

代码语言:javascript
复制
# skills/browser_use/SKILL.md
---
name: browser_use
description: 网页浏览、提取文本、点击元素、填写并提交表单。用户让你访问网站、读页面、操作网页时使用。
---
代码语言:javascript
复制
# skills/browser_use/handler.py
from playwright.async_api import async_playwright

_browser = None
_page = None

asyncdef _get_page():
    global _browser, _page
    if _browser and _page:
        return _page
    pw = await async_playwright().start()
    _browser = await pw.chromium.launch(headless=True)
    _page = await _browser.new_page()
    return _page

tools = [
    {
        "name": "browse_url",
        "description": "访问网址并返回标题和文本内容",
        "parameters": {
            "type": "object",
            "properties": {"url": {"type": "string", "description": "要访问的URL"}},
            "required": ["url"],
        },
    },
    {
        "name": "click_element",
        "description": "通过选择器或文本点击页面元素",
        "parameters": {
            "type": "object",
            "properties": {"selector": {"type": "string", "description": "CSS 选择器或文本"}},
            "required": ["selector"],
        },
    },
    {
        "name": "fill_input",
        "description": "在输入框输入内容",
        "parameters": {
            "type": "object",
            "properties": {
                "selector": {"type": "string"},
                "text": {"type": "string"}
            },
            "required": ["selector", "text"],
        },
    },
    {
        "name": "get_page_content",
        "description": "获取当前页面文本",
        "parameters": {
            "type": "object",
            "properties": {"selector": {"type": "string", "description": "可选选择器"}},
            "required": [],
        },
    }
]

asyncdef execute(tool_name, tool_input, context):
    try:
        page = await _get_page()
        if tool_name == "browse_url":
            url = tool_input["url"]
            ifnot url.startswith("http"):
                url = "https://" + url
            await page.goto(url, wait_until="domcontentloaded", timeout=10000)
            title = await page.title()
            text = await page.inner_text("body")
            return {
                "title": title,
                "url": page.url,
                "content_preview": text.strip()[:3000],
            }
        elif tool_name == "click_element":
            await page.click(tool_input["selector"], timeout=3000)
            await page.wait_for_load_state("domcontentloaded")
            return {
                "clicked": tool_input["selector"],
                "new_url": page.url,
                "new_title": await page.title(),
            }
        elif tool_name == "fill_input":
            await page.fill(tool_input["selector"], tool_input["text"])
            return {
                "filled": tool_input["selector"],
                "text": tool_input["text"],
            }
        elif tool_name == "get_page_content":
            selector = tool_input.get("selector") or"body"
            text = await page.inner_text(selector)
            return {
                "url": page.url,
                "content": text.strip()[:5000],
            }
        return {"error": f"未知工具:{tool_name}"}
    except Exception as e:
        return {"error": str(e)}

第四步:技能加载器

启动时自动扫描所有技能,把工具信息喂给大模型,调用时找到对应执行函数。

代码语言:javascript
复制
# skill_loader.py
import os
import importlib.util

class SkillLoader:
    def __init__(self):
        self.skills = {}

    def load_from_directory(self, skills_dir):
        ifnot os.path.isdir(skills_dir):
            print("未找到技能目录")
            return
        for entry in os.listdir(skills_dir):
            skill_dir = os.path.join(skills_dir, entry)
            skill_md = os.path.join(skill_dir, "SKILL.md")
            handler_py = os.path.join(skill_dir, "handler.py")
            ifnot os.path.isdir(skill_dir):
                continue
            ifnot os.path.exists(skill_md) ornot os.path.exists(handler_py):
                continue
            try:
                with open(skill_md) as f:
                    name, desc = self._parse_skill_md(f.read())
                spec = importlib.util.spec_from_file_location(f"skill_{entry}", handler_py)
                module = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(module)
                self.skills[name] = {
                    "name": name,
                    "description": desc,
                    "tools": getattr(module, "tools", []),
                    "execute": getattr(module, "execute", None),
                }
                print(f"✅ 已加载技能:{name}")
            except Exception as e:
                print(f"❌ 加载失败 {entry}:{e}")

    def get_active_skills(self):
        return [{"name": s["name"], "description": s["description"]} for s in self.skills.values()]

    def get_tools(self):
        tools = []
        for s in self.skills.values():
            tools.extend(s["tools"])
        return tools

    asyncdef execute_tool(self, tool_name, tool_input, context):
        for s in self.skills.values():
            if any(t["name"] == tool_name for t in s["tools"]):
                if s["execute"]:
                    returnawait s["execute"](tool_name, tool_input, context)
        return {"error": f"未知工具:{tool_name}"}

    def _parse_skill_md(self, content):
        name = "unknown"
        desc = ""
        for line in content.split("\n"):
            if line.startswith("name:"):
                name = line.split(":", 1)[1].strip()
            elif line.startswith("description:"):
                desc = line.split(":", 1)[1].strip()
        return name, desc

第五步:给 AI 注入灵魂 —— SOUL.md

用一个 markdown 文件定义 AI 的性格和规矩。原版 OpenClaw 有好几个,咱们精简成一个。

代码语言:javascript
复制
# Soul
你是 Mini-OpenClaw,一个跑在用户本地机器上的私人 AI 助手。你可以调用已安装技能提供的工具。

## 性格
- 友好、简洁,偶尔皮一下
- 语气轻松,像跟聪明朋友聊天
- 不懂就直说,别瞎编

## 规则
- 存笔记时用简短统一的键,比如 name、location、job
- 上网搜索只用 DuckDuckGo,不用 Google
- 该用工具就用工具
- 执行危险操作前必须先问用户
- 没要求详细说明时,回复控制在 300 字以内

第六步:上下文构建器

把灵魂、技能、记忆、当前时间拼成系统提示词,每次都喂给大模型。

代码语言:javascript
复制
# context_builder.py
import os
from datetime import datetime, timezone

BASE_PROMPT = """你是一个由 Mini OpenClaw 驱动的贴心私人 AI 助手。
简洁、友好、乐于助人,该用工具就用工具。"""

def load_soul():
    path = os.path.join(os.path.dirname(__file__), "SOUL.md")
    try:
        with open(path, "r") as f:
            return f.read()
    except FileNotFoundError:
        return BASE_PROMPT

def build_system_prompt(active_skills, memory=None):
    prompt = load_soul()
    if active_skills:
        prompt += "\n\n## 可用技能\n"
        for s in active_skills:
            prompt += f"### {s['name']}\n{s['description']}\n\n"
    if memory:
        prefix = "note:"
        notes = {k[len(prefix):]: memory.get(k) for k in memory.keys() if k.startswith(prefix)}
        if notes:
            prompt += "\n\n## 你知道的用户信息\n"
            for k, v in notes.items():
                content = v.get("content", v) if isinstance(v, dict) else v
                prompt += f"- {k}:{content}\n"
    prompt += f"\n当前时间:{datetime.now(timezone.utc).isoformat()}"
    return prompt

第七步:智能体运行时 —— 大脑核心

基于经典的 ReAct 模式:思考 → 行动 → 观察 → 循环,直到给出最终答案。 最多循环 5 轮,避免死循环。

代码语言:javascript
复制
# agent_runtime.py
import json
import httpx
from context_builder import build_system_prompt

MAX_TOOL_ROUNDS = 5

class AgentRuntime:
    def __init__(self, provider, model, api_key, skills, memory):
        self.provider = provider
        self.model = model
        self.api_key = api_key
        self.skills = skills
        self.memory = memory

    asyncdef run(self, history, session_id, callbacks):
        on_token = callbacks.get("on_token")
        on_tool_use = callbacks.get("on_tool_use")

        system_prompt = build_system_prompt(self.skills.get_active_skills(), self.memory)
        messages = [{"role": m["role"], "content": m["content"]} for m in history]
        tools = self.skills.get_tools()

        response = ""
        rounds = 0

        while rounds < MAX_TOOL_ROUNDS:
            rounds += 1
            result = await self._call_anthropic(system_prompt, messages, tools)

            if result["tool_calls"]:
                messages.append({"role": "assistant", "content": result["raw_content"]})
                for tc in result["tool_calls"]:
                    if on_tool_use:
                        await on_tool_use(tc["name"], tc["input"])
                    tool_res = await self.skills.execute_tool(
                        tc["name"], tc["input"],
                        {"session_id": session_id, "memory": self.memory}
                    )
                    messages.append({
                        "role": "user",
                        "content": [{
                            "type": "tool_result",
                            "tool_use_id": tc["id"],
                            "content": json.dumps(tool_res),
                        }]
                    })
                continue

            if result["text"]:
                if on_token:
                    await on_token(result["text"])
                response = result["text"]
            break
        return response

    asyncdef _call_anthropic(self, system_prompt, messages, tools):
        body = {
            "model": self.model,
            "max_tokens": 4096,
            "system": system_prompt,
            "messages": messages,
        }
        if tools:
            body["tools"] = [{
                "name": t["name"],
                "description": t["description"],
                "input_schema": t["parameters"],
            } for t in tools]

        asyncwith httpx.AsyncClient(timeout=120) as client:
            res = await client.post(
                "https://api.anthropic.com/v1/messages",
                headers={
                    "Content-Type": "application/json",
                    "x-api-key": self.api_key,
                    "anthropic-version": "2023-06-01",
                },
                json=body
            )

        if res.status_code != 200:
            raise Exception(f"Anthropic API 报错 {res.status_code}:{res.text}")
        data = res.json()
        text_parts = []
        tool_calls = []
        for b in data["content"]:
            if b["type"] == "text":
                text_parts.append(b["text"])
            elif b["type"] == "tool_use":
                tool_calls.append({
                    "id": b["id"],
                    "name": b["name"],
                    "input": b["input"],
                })
        return {
            "text": "".join(text_parts),
            "tool_calls": tool_calls orNone,
            "raw_content": data["content"],
        }

第八步:Telegram 聊天通道

监听 Telegram 消息,传给 AI,再把回答发回去。

代码语言:javascript
复制
# telegram_channel.py
import time
import asyncio
from telegram import Update
from telegram.ext import Application, MessageHandler, filters

class TelegramChannel:
    def __init__(self, token, agent, sessions):
        self.token = token
        self.agent = agent
        self.sessions = sessions

    asyncdef start(self):
        app = Application.builder().token(self.token).build()
        app.add_handler(MessageHandler(filters.TEXT, self._on_message))
        await app.initialize()
        await app.start()
        await app.updater.start_polling()
        await asyncio.Future()

    asyncdef _on_message(self, update: Update, context):
        chat_id = str(update.effective_chat.id)
        user_text = update.message.text
        ifnot user_text:
            return

        session_id = self.sessions.get_or_create_session(chat_id, "telegram")
        self.sessions.add_message(session_id, {
            "role": "user",
            "content": user_text,
            "timestamp": time.time(),
        })

        await update.effective_chat.send_action("typing")

        try:
            history = self.sessions.get_history(session_id)
            full_response = ""

            asyncdef on_token(token):
                nonlocal full_response
                full_response += token

            asyncdef on_tool_use(name, input):
                await update.effective_chat.send_action("typing")

            await self.agent.run(history, session_id, {
                "on_token": on_token,
                "on_tool_use": on_tool_use,
            })

            if full_response:
                for i in range(0, len(full_response), 4096):
                    await update.message.reply_text(full_response[i:i+4096])

            self.sessions.add_message(session_id, {
                "role": "assistant",
                "content": full_response,
                "timestamp": time.time(),
            })
        except Exception as e:
            await update.message.reply_text(f"报错啦:{e}")

拿 Telegram Bot Token

  • 打开 Telegram 搜 @BotFather
  • /newbot
  • 起名字,用户名必须以 bot 结尾
  • 复制 token 放进 \.env

第九步:主程序入口

把所有零件拼起来,跑起来!

代码语言:javascript
复制
# main.py
import asyncio
import os
from dotenv import load_dotenv

from memory_store import Memory
from session_manager import SessionManager
from skill_loader import SkillLoader
from agent_runtime import AgentRuntime
from telegram_channel import TelegramChannel

load_dotenv()

asyncdef main():
    print("Mini OpenClaw 启动中……")

    memory = Memory()
    sessions = SessionManager()
    skills = SkillLoader()
    skills.load_from_directory(os.path.join(os.path.dirname(__file__), "skills"))

    agent = AgentRuntime(
        provider=os.getenv("MODEL_PROVIDER"),
        model=os.getenv("MODEL_NAME"),
        api_key=os.getenv("ANTHROPIC_API_KEY"),
        skills=skills,
        memory=memory
    )

    telegram = TelegramChannel(
        token=os.getenv("TELEGRAM_BOT_TOKEN"),
        agent=agent,
        sessions=sessions
    )

    print("\nMini OpenClaw 已经在 Telegram 上跑起来啦!")
    print("\nGO CLAW!!! 🦞")

    await telegram.start()

if __name__ == "__main__":
    asyncio.run(main())

最后一步:配置环境变量

新建 \.env

代码语言:javascript
复制
ANTHROPIC_API_KEY=<你的密钥>
MODEL_PROVIDER=anthropic
MODEL_NAME=claude-opus-4-6
TELEGRAM_BOT_TOKEN=<你的 Telegram 机器人 token>

完工!

到这儿,一个迷你但五脏俱全的自主 AI 智能体就造好了。 能聊天、能记东西、能上网、能调用工具,还能在 Telegram 上随便玩。

想继续整活?可以加更多技能、换本地大模型、接微信、加文件操作……玩法多到离谱。

#openclaw #agi #AIAgent

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 HELLO程序员 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 我们要造个啥?
  • Mini-OpenClaw 的 8 大核心零件
  • 新建项目
  • 第一步:造记忆模块
  • 第二步:会话管理器
  • 第三步:搭建技能系统
    • 技能 1:日期时间
    • 技能 2:记忆笔记
    • 技能 3:浏览器操控
  • 第四步:技能加载器
  • 第五步:给 AI 注入灵魂 —— SOUL.md
  • 第六步:上下文构建器
  • 第七步:智能体运行时 —— 大脑核心
  • 第八步:Telegram 聊天通道
    • 拿 Telegram Bot Token
  • 第九步:主程序入口
  • 最后一步:配置环境变量
  • 完工!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档