【花雕学编程】MimiClaw 嵌入式AI Agent系统架构剖析——基于FreeRTOS的多线程通信与外设协同机制

2026-04-2148
AI 快速预览详细 收起
MimiClaw 是一个运行在 ESP32-S3 上的轻量级 AI Agent 固件,支持多通道接入(如 Telegram、飞书、WebSocket、串口命令行)并进行 LLM 推理和工具操作。系统架构采用分层 + 消息总线模式,通过 FreeRTOS 实现实时任务调度。初始化流程严格遵循依赖优先级,确保系统稳定运行。代码实现了精细的内存管理和错误处理策略,保证在资源受限环境下高效运行。

【花雕学编程】MimiClaw 嵌入式AI Agent系统架构剖析——基于FreeRTOS的多线程通信与外设协同机制
——实时操作系统任务调度策略/消息总线设计/硬件抽象层实现

 

 

MimiClaw官网与代码仓库
官方网站:https://mimiclaw.io
代码仓库:https://github.com/memovai/mimiclaw
国内镜像:https://gitcode.com/RealGao/mimiclaw

 

引言
随着大语言模型(LLM)的普及,将 AI 能力下沉到边缘设备已成为一个热门方向。今天要解读的 MimiClaw 正是一个运行在 ESP32-S3 上的轻量级 AI Agent 固件。它能够同时接入 Telegram、飞书、WebSocket、串口命令行等多个通道,接收用户指令,调用 LLM 进行推理,执行工具操作,并将响应原路返回。这里将以 MimiClaw 的主程序代码为例,系统架构、头文件职责、初始化流程、并发模型、模块交互、错误处理、内存管理七大核心维度,对 MimiClaw 主程序代码开展体系化、工程化、专业化解析。

 

阅读收获:你将看到一个资源受限的嵌入式设备如何通过精心设计的模块化架构,构建出高内聚、可扩展的 AI Agent 运行时。

 

00 (1).jpg

 

00 (2).jpg
00 (3).jpg

## 一、系统架构总览与整体定位


MimiClaw 基于 ESP-IDF 框架与 FreeRTOS 实时操作系统开发,整体采用 分层 + 消息总线 的架构模式:

 

05-.jpg


这种设计的核心优势在于解耦:通道模块只负责收发消息,Agent 核心只关心推理与调度,双方通过消息总线通信,互不干扰。

 

系统代码mimi.c(完整代码见文后附录)是 MimiClaw AI Agent 嵌入式固件的唯一主入口,硬件基于 ESP32‑S3 芯片,软件依托 ESP‑IDF 开发框架与 FreeRTOS 实时操作系统构建,是面向资源受限环境的本地化多模态智能代理核心程序。

 

系统核心能力:实现多通道接入、多模态交互的本地化 AI 代理,并行支持 Telegram、飞书、WebSocket、串口命令行等交互渠道;可接收用户指令,完成 LLM 推理调度、工具执行、定时任务触发、动态技能脚本调用,并通过原通道闭环返回响应结果。

 

核心设计目标:在 512KB 片内 SRAM + 外接 PSRAM 的资源约束下,打造高内聚、低耦合、易扩展、低功耗的嵌入式智能体运行时,兼顾稳定性、实时性与功能完整性。

## 二、头文件体系与模块职责划分

 

代码遵循分层解耦、职责单一的设计原则,头文件分为平台基础依赖与自研业务模块两大类,构建了清晰的软件边界。

 

2.1 平台基础头文件(ESP-IDF / 标准 C 核心依赖)

 

06.jpg

 

2.2 MimiClaw 自研模块头文件(核心业务组件)

自研模块严格遵循分层架构设计,无循环依赖,是系统高可扩展性的核心支撑:
 

07.jpg

 

设计亮点:模块间仅通过消息总线或标准函数接口交互,无直接交叉依赖,极大提升单元测试效率与模块替换灵活性。

## 三、关键初始化流程(依赖驱动型时序设计)

 

初始化流程严格遵循依赖优先级,杜绝资源争用与初始化失败,分为四大核心阶段:

 

3.1 第一阶段:基础存储初始化

 

代码
ESP_ERROR_CHECK(init_nvs());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(init_spiffs());

NVS 初始化:init_nvs() 中包含了自动修复逻辑——若分区损坏或版本变更,自动擦除后重建,避免启动失败。

事件循环创建:ESP-IDF 的默认事件总线,供 Wi-Fi、IP 等系统事件使用。

SPIFFS 挂载:挂载文件系统到指定路径(如 /spiffs),并设置 format_if_mount_failed = true,确保首次运行或损坏时自动格式化。

 

3.2 第二阶段:子系统顺序初始化

紧接着,代码按依赖关系严格顺序初始化各个模块:

代码
MessageBus → MemoryStore → SkillLoader → SessionMgr → WifiManager 
→ HttpProxy → TelegramBot → FeishuBot → LLMProxy → ToolRegistry 
→ CronService → Heartbeat → AgentLoop → SerialCLI

设计考量:

MessageBus 最早初始化,因为后续几乎所有模块都依赖它。

SerialCLI 在 Wi-Fi 之前启动,确保即使无网络也能通过串口进行调试和控制。

 

3.3 第三阶段:网络连接与任务启动

代码
wifi_manager_start();
if (wifi_manager_wait_connected(30000) == ESP_OK) {
    // 创建出站分发任务
    xTaskCreatePinnedToCore(outbound_dispatch_task, ...);
    // 启动网络依赖的服务
    agent_loop_start();
    telegram_bot_start();
    feishu_bot_start();
    cron_service_start();
    heartbeat_start();
    ws_server_start();
}

超时处理:等待 Wi-Fi 连接最多 30 秒,超时后仅打印警告,系统以离线模式继续运行(CLI 和本地功能仍可用)。
任务创建时机:先创建出站分发任务,再启动各通道和 Agent,防止早期生成的回复因无人接收而丢失。

 

## 四、并发模型与核心任务

 

MimiClaw 充分利用 FreeRTOS 的多任务能力,将不同职责分配到独立任务中运行。

 

4.1 出站分发任务 (outbound_dispatch_task)
这是系统中最关键的后台任务之一,其逻辑非常清晰:
 

 

代码
while (1) {
    mimi_msg_t msg;
    if (message_bus_pop_outbound(&msg, UINT32_MAX) != ESP_OK) continue;

    if (strcmp(msg.channel, MIMI_CHAN_TELEGRAM) == 0) {
        telegram_send_message(msg.chat_id, msg.content);
    } else if (strcmp(msg.channel, MIMI_CHAN_FEISHU) == 0) {
        feishu_send_message(msg.chat_id, msg.content);
    } else if (strcmp(msg.channel, MIMI_CHAN_WEBSOCKET) == 0) {
        ws_server_send(msg.chat_id, msg.content);
    }
    // ... 其他通道

    free(msg.content);  // 重要:释放消息内容内存
}

无限循环 + 阻塞读取:任务大部分时间处于挂起状态,不消耗 CPU。

单一职责:只负责将消息路由到正确的通道,不解析、不修改内容。

内存管理:消息内容由生产者(如 Agent 循环)分配,本任务消费后必须 free 释放,防止泄漏。

 

4.2 其他隐含任务
虽然未直接出现在主程序中,但各模块内部还会创建以下任务:

Telegram 轮询任务:长轮询获取机器人更新。
WebSocket 服务器任务:监听端口并处理连接。
Agent 主循环任务:核心推理与调度。
Cron 定时任务:基于软件定时器触发。

 

所有这些任务通过消息总线进行数据交换,形成了典型的生产者-消费者模式,避免了模块间的直接耦合。

## 五、消息总线与模块交互机制

 

消息总线是全系统的中枢神经,实现通道与 Agent 核心的完全解耦。


5.1 标准消息结构体(设计规范)

代码
typedef struct {
    char channel[MIMI_CHANNEL_MAX_LEN];  // 交互渠道标识
    char chat_id[MIMI_CHAT_ID_MAX_LEN];  // 唯一会话ID
    char *content;                       // 动态分配消息体
} mimi_msg_t;

5.2 消息分发逻辑
出站任务仅做路由转发,不解析、不修改、不处理业务逻辑:
匹配渠道:调用对应发送接口(Telegram / 飞书 / WebSocket);
系统消息:仅打印日志,用于内部调试;
未知渠道:告警并丢弃,避免无效资源消耗。
设计原则:单一职责、数据透明、松耦合。

## 六、错误处理与健壮性设计

 

针对嵌入式环境高可靠性要求,代码实现分级容错、降级运行策略,核心故障不导致系统崩溃:
 

08.jpg

 

## 七、内存管理(资源受限环境最优实践)

 

ESP32-S3 内存资源严格受限,代码实现精细化、分区化内存管理:
7.1 启动内存诊断

代码
heap_caps_get_free_size(MALLOC_CAP_INTERNAL)   // 片内SRAM
heap_caps_get_free_size(MALLOC_CAP_SPIRAM)     // 外接PSRAM

实时监控内存余量,为技能加载、LLM 上下文分配提供决策依据。


7.2 动态内存规范
消息内存:生产者 malloc 分配,消费者 free 释放,闭环管理;
文件缓存:技能 / 会话数据仅读取时分配内存,限制文件大小防溢出;
LLM 上下文:通过滑动窗口、摘要压缩控制内存占用。


7.3 任务栈配置
所有任务栈大小通过宏定义统一管理:
轻量任务(出站分发):4KB 栈即可;
重型任务(Agent 循环):≥8KB,适配 JSON 解析、HTTP 缓存等操作。

## 八、安全性与隐私设计

 

面向物联网设备的安全基线设计,杜绝敏感信息泄露:
凭据隔离:密钥、Token 等存储在独立机密文件,通过.gitignore 屏蔽,不硬编码;
存储加密:支持 NVS 分区加密、Flash 加密,保护本地配置;
传输安全:HTTP 代理默认开启 TLS 证书验证,禁用不安全传输。

 

## 九、扩展性设计亮点

 

代码具备插件化、动态化扩展能力,无需修改核心逻辑即可迭代功能:
渠道插件化:新增渠道仅需定义标识、实现发送接口、添加路由分支;
技能动态化:支持从 SPIFFS 加载脚本,动态注册工具,无需重新编译固件;
消息标准化:所有渠道消息归一化抽象,Agent 核心不感知具体渠道。

 

## 十、工程化优化建议

 

09.jpg

 

## 十一、总结

 

该代码是嵌入式 AI Agent 在资源受限平台的工程化标杆实现,完整构建了多通道智能体的启动、调度、交互、容错全流程:


✅ 多渠道闭环交互能力
✅ 消息总线解耦的高并发架构
✅ NVS+SPIFFS 双存储与离线降级能力
✅ 动态可扩展的技能 / 工具系统
✅ 精细化内存与错误管理


系统核心代码深度融合了 C 语言、FreeRTOS、ESP-IDF 的最佳实践,其模块化设计、资源权衡策略、健壮性保障方案,对嵌入式系统与边缘智能体开发者具备极高的参考与学习价值。

附录
mimi.c 完整核心代码

代码
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_event.h"
#include "esp_system.h"
#include "esp_heap_caps.h"
#include "esp_spiffs.h"
#include "nvs_flash.h"

#include "mimi_config.h"
#include "bus/message_bus.h"
#include "wifi/wifi_manager.h"
#include "channels/telegram/telegram_bot.h"
#include "channels/feishu/feishu_bot.h"
#include "llm/llm_proxy.h"
#include "agent/agent_loop.h"
#include "memory/memory_store.h"
#include "memory/session_mgr.h"
#include "gateway/ws_server.h"
#include "cli/serial_cli.h"
#include "proxy/http_proxy.h"
#include "tools/tool_registry.h"
#include "cron/cron_service.h"
#include "heartbeat/heartbeat.h"
#include "skills/skill_loader.h"

static const char *TAG = "mimi";

static esp_err_t init_nvs(void)
{
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_LOGW(TAG, "NVS partition truncated, erasing...");
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    return ret;
}

static esp_err_t init_spiffs(void)
{
    esp_vfs_spiffs_conf_t conf = {
        .base_path = MIMI_SPIFFS_BASE,
        .partition_label = NULL,
        .max_files = 10,
        .format_if_mount_failed = true,
    };

    esp_err_t ret = esp_vfs_spiffs_register(&conf);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "SPIFFS mount failed: %s", esp_err_to_name(ret));
        return ret;
    }

    size_t total = 0, used = 0;
    esp_spiffs_info(NULL, &total, &used);
    ESP_LOGI(TAG, "SPIFFS: total=%d, used=%d", (int)total, (int)used);

    return ESP_OK;
}

/* Outbound dispatch task: reads from outbound queue and routes to channels */
static void outbound_dispatch_task(void *arg)
{
    ESP_LOGI(TAG, "Outbound dispatch started");

    while (1) {
        mimi_msg_t msg;
        if (message_bus_pop_outbound(&msg, UINT32_MAX) != ESP_OK) continue;

        ESP_LOGI(TAG, "Dispatching response to %s:%s", msg.channel, msg.chat_id);

        if (strcmp(msg.channel, MIMI_CHAN_TELEGRAM) == 0) {
            esp_err_t send_err = telegram_send_message(msg.chat_id, msg.content);
            if (send_err != ESP_OK) {
                ESP_LOGE(TAG, "Telegram send failed for %s: %s", msg.chat_id, esp_err_to_name(send_err));
            } else {
                ESP_LOGI(TAG, "Telegram send success for %s (%d bytes)", msg.chat_id, (int)strlen(msg.content));
            }
        } else if (strcmp(msg.channel, MIMI_CHAN_FEISHU) == 0) {
            esp_err_t send_err = feishu_send_message(msg.chat_id, msg.content);
            if (send_err != ESP_OK) {
                ESP_LOGE(TAG, "Feishu send failed for %s: %s", msg.chat_id, esp_err_to_name(send_err));
            } else {
                ESP_LOGI(TAG, "Feishu send success for %s (%d bytes)", msg.chat_id, (int)strlen(msg.content));
            }
        } else if (strcmp(msg.channel, MIMI_CHAN_WEBSOCKET) == 0) {
            esp_err_t ws_err = ws_server_send(msg.chat_id, msg.content);
            if (ws_err != ESP_OK) {
                ESP_LOGW(TAG, "WS send failed for %s: %s", msg.chat_id, esp_err_to_name(ws_err));
            }
        } else if (strcmp(msg.channel, MIMI_CHAN_SYSTEM) == 0) {
            ESP_LOGI(TAG, "System message [%s]: %.128s", msg.chat_id, msg.content);
        } else {
            ESP_LOGW(TAG, "Unknown channel: %s", msg.channel);
        }

        free(msg.content);
    }
}

void app_main(void)
{
    /* Silence noisy components */
    esp_log_level_set("esp-x509-crt-bundle", ESP_LOG_WARN);

    ESP_LOGI(TAG, "========================================");
    ESP_LOGI(TAG, "  MimiClaw - ESP32-S3 AI Agent");
    ESP_LOGI(TAG, "========================================");

    /* Print memory info */
    ESP_LOGI(TAG, "Internal free: %d bytes",
             (int)heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
    ESP_LOGI(TAG, "PSRAM free:    %d bytes",
             (int)heap_caps_get_free_size(MALLOC_CAP_SPIRAM));

    /* Phase 1: Core infrastructure */
    ESP_ERROR_CHECK(init_nvs());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    ESP_ERROR_CHECK(init_spiffs());

    /* Initialize subsystems */
    ESP_ERROR_CHECK(message_bus_init());
    ESP_ERROR_CHECK(memory_store_init());
    ESP_ERROR_CHECK(skill_loader_init());
    ESP_ERROR_CHECK(session_mgr_init());
    ESP_ERROR_CHECK(wifi_manager_init());
    ESP_ERROR_CHECK(http_proxy_init());
    ESP_ERROR_CHECK(telegram_bot_init());
    ESP_ERROR_CHECK(feishu_bot_init());
    ESP_ERROR_CHECK(llm_proxy_init());
    ESP_ERROR_CHECK(tool_registry_init());
    ESP_ERROR_CHECK(cron_service_init());
    ESP_ERROR_CHECK(heartbeat_init());
    ESP_ERROR_CHECK(agent_loop_init());

    /* Start Serial CLI first (works without WiFi) */
    ESP_ERROR_CHECK(serial_cli_init());

    /* Start WiFi */
    esp_err_t wifi_err = wifi_manager_start();
    if (wifi_err == ESP_OK) {
        ESP_LOGI(TAG, "Scanning nearby APs on boot...");
        wifi_manager_scan_and_print();
        ESP_LOGI(TAG, "Waiting for WiFi connection...");
        if (wifi_manager_wait_connected(30000) == ESP_OK) {
            ESP_LOGI(TAG, "WiFi connected: %s", wifi_manager_get_ip());

            /* Outbound dispatch task should start first to avoid dropping early replies. */
            ESP_ERROR_CHECK((xTaskCreatePinnedToCore(
                outbound_dispatch_task, "outbound",
                MIMI_OUTBOUND_STACK, NULL,
                MIMI_OUTBOUND_PRIO, NULL, MIMI_OUTBOUND_CORE) == pdPASS)
                ? ESP_OK : ESP_FAIL);

            /* Start network-dependent services */
            ESP_ERROR_CHECK(agent_loop_start());
            ESP_ERROR_CHECK(telegram_bot_start());
            ESP_ERROR_CHECK(feishu_bot_start());
            cron_service_start();
            heartbeat_start();
            ESP_ERROR_CHECK(ws_server_start());

            ESP_LOGI(TAG, "All services started!");
        } else {
            ESP_LOGW(TAG, "WiFi connection timeout. Check MIMI_SECRET_WIFI_SSID in mimi_secrets.h");
        }
    } else {
        ESP_LOGW(TAG, "No WiFi credentials. Set MIMI_SECRET_WIFI_SSID in mimi_secrets.h");
    }

    ESP_LOGI(TAG, "MimiClaw ready. Type 'help' for CLI commands.");
}


创作许可协议

本项目采用 None(不开放任何权利,保留所有权利) 进行许可。

评论(0)
- 没有更多了 -