当前位置: 首页 > news >正文

ESP32上实现LLM思考过程可视化:用Brainscope调试微型语言模型

之前在调试嵌入式端的小型语言模型时,最痛苦的事情不是模型跑不起来,而是模型跑起来之后完全是一个“黑盒”。串口里刷出来一串串 token,你只知道它在生成文本,却不清楚每一步是怎么选的、模型对下一个字符有多少置信度、温度参数改大改小到底影响了什么。直到接触了 Brainscope 这类可视化调试工具,才真正体会到“看着单片机的 LLM 思考”是什么体验。

这篇文章围绕Brainscope/examples/ESP32 Watch a microcontroller's LLM think这个示例展开,从概念、环境、原理,到 ESP32 上跑通一个微型 LLM 并实现“思考过程可视化”的完整流程,都会讲到。新手可以照着搭建环境,有嵌入式开发经验的读者可以直接跳到代码部分复用思路。

1. 背景与核心概念

1.1 当 LLM 跑进单片机

提到大语言模型(LLM),很多人第一反应是云端 API、GPU 推理集群、几十 GB 的权重文件。但在端侧推理场景里,我们讨论的是另一条路线:把模型压缩到几十 MB 甚至几 MB,跑在 MCU 上。

云侧 LLM 和端侧 LLM 的核心区别在于:

对比维度云侧 LLM端侧 LLM
网络依赖强,离线不可用无,可完全离线
延迟受网络波动影响本地推理,延迟可控
隐私数据需上传数据不出设备
模型规模几十亿到几千亿参数几百万到几亿参数
算力要求高性能 GPUMCU / 边缘 NPU
典型设备服务器、PCESP32-S3、树莓派、手机端侧

在智能音箱、儿童玩具、传感器节点、工业控制面板这类场景中,很多需求并不需要 GPT-4 级别的能力,只需要在本地完成“分类、补全、纠错、简单对话”等任务。这时候,ESP32 这类低成本 Wi-Fi MCU 就成了很合适的载体。

1.2 为什么选择 ESP32

ESP32 并不是第一个能跑神经网络的单片机,但它有几个优势让它成为 LLM 端侧实验的热门平台:

  • 生态成熟:Arduino、PlatformIO、ESP-IDF 三套开发方式都可用。
  • 内存可选:带 PSRAM 的型号(如 ESP32-S3-WROOM-1-N16R8)可以挂载 8MB 甚至更大的外部 RAM,给 KV Cache 和激活值留出空间。
  • 算力够用:ESP32-S3 支持向量指令,对量化后的矩阵运算有一定加速效果。
  • 外设齐全:Wi-Fi、蓝牙、SPI、I2C、USB 都有,方便对接屏幕、传感器和数据通道。

这里要说明一点:ESP32 能跑的“LLM”并不是 ChatGPT 那种规模的大模型,而是微型语言模型,比如基于 llama2.c 架构的小型模型、字符级 RNN、或者经过大幅压缩的 TinyLlama 变体。模型能跑多大,主要取决于 PSRAM 容量和 Flash 大小。

1.3 Brainscope 解决什么问题

Brainscope 是一个偏“模型内部状态可视化”的调试工具。它的思路是:在模型推理的每一步,把关键内部状态采集出来,通过串口、WebSocket 或文件方式传给外部展示端,让开发者能看到:

  • 当前生成到第几个 token。
  • 候选 token 的概率分布。
  • logits 经过 softmax 之后的结果。
  • 温度、top-k、top-p 等采样参数对生成结果的影响。
  • 每一步的熵值,用来判断模型“确定”还是“不确定”。

官方 examples 目录里的 ESP32 示例,题目就叫 “Watch a microcontroller's LLM think”,本质上就是把“模型思考过程”这件事从抽象变具体。下面从环境准备开始,逐步实现这套链路。

2. 环境准备与版本说明

2.1 硬件清单

建议准备以下硬件:

  • 主控:ESP32-S3-DevKitC-1 开发板,或者任意带有 8MB PSRAM 的 ESP32-S3 模组。
  • Flash 容量:建议 16MB,方便存放模型权重。
  • 屏幕(可选):OLED 或 TFT 小屏,用来显示实时 token,增强演示效果。
  • 数据线:USB-C 数据线,注意要支持数据传输,不能只用充电线。

如果你的开发板没有 PSRAM,也可以跑但模型规模会受限,后面讲内存优化时会说明原因。

2.2 开发环境

本文示例以 Arduino IDE 为主,同时也兼容 PlatformIO。Arduino IDE 方式对新手更友好。

  1. 安装 Arduino IDE,建议使用 2.x 版本。
  2. 安装 ESP32 开发板支持包。在“文件 → 首选项 → 附加开发板管理器网址”中添加:
https://espressif.github.io/arduino-esp32/package_esp32_index.json
  1. 打开“工具 → 开发板 → 开发板管理器”,搜索esp32,安装 Espressif 官方支持包。

如果下载慢,可以在国内镜像源下离线安装包,具体操作网上已有较多教程,这里不展开。

2.3 依赖库

后续代码需要用到:

  • WiFi 库:ESP32 自带。
  • WebSocketsServer:搜索并安装Websockets,作者为 Markus Sattler。
  • ArduinoJson:搜索并安装ArduinoJson。注意 v7 和 v6 的 API 有差异,本文代码按 v7 写法演示,如果你用的是 v6,需要把JsonDocument换成DynamicJsonDocument

版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。

3. 核心原理拆解:LLM 是怎么“想”的

要理解“看模型思考”,先要理解自回归语言模型生成一个 token 的完整流程。

3.1 自回归生成循环

一个微型 LLM 的推理过程可以拆成三步循环:

  1. 输入当前的 token 序列(prompt 或已经生成的内容)。
  2. 模型前向计算,输出一个 logits 向量,向量长度等于词表大小。
  3. 对 logits 做 softmax 得到概率分布,再按采样策略选出下一个 token。

生成的 token 又被拼接到输入序列尾部,进入下一轮。这个过程叫自回归生成。

用伪代码表示:

input = prompt_tokens while not stopped: logits = model.forward(input) probs = softmax(logits) next_token = sample(probs, temperature, top_k, top_p) output.append(next_token) input.append(next_token)

可以看到,每一步都存在一个可供采集的概率分布,这正是 Brainscope 展示“思考过程”的数据基础。

3.2 logits、softmax 与采样参数

几个关键概念需要分清:

  • logits:模型输出的未归一化分数,可以是任意实数。数值越大,代表模型越倾向选择该 token。
  • softmax:把 logits 变成总和为 1 的概率分布。
  • temperature:对 logits 做缩放。temperature 越大,分布越平滑,生成越随机;越小,分布越尖锐,生成越确定。
  • top-k:只从概率最高的 k 个 token 中采样,过滤长尾。
  • top-p:从累计概率超过 p 的最小集合中采样,也叫核采样。
  • 熵(entropy):衡量分布的不确定性。熵接近 0 时,模型对某个 token 高度自信;熵很大时,模型处于“纠结”状态。

当你在可视化面板上看到某个位置有多个候选 token 的概率接近,就意味着模型在这里“犹豫”,这比只看最终文本有趣得多,也更有调试价值。

3.3 量化:FP32 到 INT8 / INT4

模型在 PC 上通常用 FP32 或 FP16 权重。但 ESP32 的浮点运算能力有限,而且权重体积大,所以必须量化。

以 100 万参数模型为例:

精度单个参数占用权重体积
FP324 字节约 4 MB
INT81 字节约 1 MB
INT40.5 字节约 0.5 MB

在 ESP32-S3 上,INT8 和 INT4 量化是常见做法。量化会带来少量精度损失,但微型模型本身能力有限,推理速度的提升和内存的节省通常更值得。

3.4 Brainscope 的采集链路

把“思考过程”可视化,需要一条完整的数据链路:

ESP32 模型推理 ↓ 每步采集 StepInfo(token、topK概率、熵、温度) ↓ 序列化 JSON WebSocket Server(ESP32 上运行) ↓ 广播 浏览器 Dashboard(PC / 手机)

采集端负责从模型推理循环中提取数据,传输端负责把数据发出去,展示端负责渲染。三者互相独立,可以分别替换。

4. 实战一:让 ESP32 跑一个微型 LLM

下面我们开始搭建实际工程。为了让读者既能理解模型侧逻辑,又不会被几百行模型实现淹没,本文把模型推理封装成一个LLMInference类,并给出一个用于测试链路的 Mock 实现。实际项目中,你可以把这个类的方法替换成真实模型的调用接口。

4.1 创建项目结构

在 Arduino IDE 中新建工程,命名为esp32_llm_brainscope。在工程目录下创建以下文件:

esp32_llm_brainscope/ ├── esp32_llm_brainscope.ino ├── llm_wrapper.h ├── llm_wrapper.cpp └── dashboard.html

其中dashboard.html是可视化面板,可以直接用浏览器打开。

4.2 定义模型接口

先定义StepInfo结构和LLMInference类。

// 文件路径:esp32_llm_brainscope/llm_wrapper.h #pragma once #include <Arduino.h> // 每个候选 token 的信息 struct Candidate { const char* piece; // token 文本 float prob; // 概率 }; // 模型每一步的“思考快照” struct StepInfo { uint32_t step; // 当前步数 const char* nextPiece; // 被选中的 token float temperature; // 当前温度 float entropy; // 当前熵值 uint8_t candidateCount; // 候选数量 Candidate topK[8]; // 概率最高的前 8 个候选 }; class LLMInference { public: bool begin(); // 执行一步推理,返回该步的思考快照 StepInfo step(); // 设置采样温度 void setTemperature(float t); private: float temperature = 0.8f; };

这个头文件是所有对接逻辑的核心。你的真实模型实现只要能产出StepInfo,可视化链路就可以直接复用。

4.3 Mock 模型实现

为了先跑通可视化链路,这里提供一个假的实现。它不真正推理语言模型,而是模拟每一步的概率分布,用来验证 WebSocket 和 Dashboard 是否正常工作。

// 文件路径:esp32_llm_brainscope/llm_wrapper.cpp #include "llm_wrapper.h" // 模拟词表中的几个候选词 static const char* vocab[] = {" the", " is", " a", " of", " you", " I", " and", " to"}; static const uint8_t vocabSize = 8; bool LLMInference::begin() { return true; } void LLMInference::setTemperature(float t) { if (t > 0.1f && t <= 2.0f) { temperature = t; } } StepInfo LLMInference::step() { StepInfo info; info.step = 0; info.nextPiece = ""; info.temperature = temperature; info.entropy = 0.0f; info.candidateCount = vocabSize; // 用伪随机数模拟 logits,然后做 softmax float logits[8]; float sum = 0.0f; for (int i = 0; i < 8; i++) { logits[i] = random(-20, 100) / 100.0f; logits[i] = logits[i] / temperature; // 温度缩放 sum += exp(logits[i]); } float maxProb = 0.0f; int maxIdx = 0; for (int i = 0; i < 8; i++) { float p = exp(logits[i]) / sum; info.topK[i].piece = vocab[i]; info.topK[i].prob = p; if (p > maxProb) { maxProb = p; maxIdx = i; } // 粗略计算熵,真实场景应按完整分布计算 info.entropy += p * log(p); } info.entropy = -info.entropy; info.nextPiece = vocab[maxIdx]; info.step = millis() / 50; // 用时间模拟步数,方便观察 return info; }

说明:这个 Mock 实现的熵计算没有覆盖完整词表,只作为演示。真实模型只需要把这里的step()内部换成model.forward()和真正的采样逻辑即可,外层传输代码不用改动。

4.4 主程序:Wi-Fi 与 WebSocket

接下来编写主程序。ESP32 启动后连接 Wi-Fi,启动 WebSocket 服务,浏览器通过 IP 访问 Dashboard。

// 文件路径:esp32_llm_brainscope/esp32_llm_brainscope.ino #include <WiFi.h> #include <WebSocketsServer.h> #include <ArduinoJson.h> #include "llm_wrapper.h" const char* ssid = "YOUR_WIFI_SSID"; const char* password = "YOUR_WIFI_PASSWORD"; WebSocketsServer webSocket(81); LLMInference llm; bool clientConnected = false; void sendStep() { StepInfo info = llm.step(); // ArduinoJson v7 写法;v6 请改为 DynamicJsonDocument doc(1024); JsonDocument doc; doc["step"] = info.step; doc["nextPiece"] = info.nextPiece; doc["temperature"] = info.temperature; doc["entropy"] = info.entropy; JsonArray topK = doc["topK"].to<JsonArray>(); for (uint8_t i = 0; i < info.candidateCount; i++) { JsonObject obj = topK.add<JsonObject>(); obj["piece"] = info.topK[i].piece; obj["prob"] = info.topK[i].prob; } String json; serializeJson(doc, json); webSocket.broadcastTXT(json); } void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) { switch (type) { case WStype_CONNECTED: clientConnected = true; Serial.printf("Client %u connected\n", num); break; case WStype_DISCONNECTED: Serial.printf("Client %u disconnected\n", num); clientConnected = false; break; case WStype_TEXT: { // 收到 Dashboard 发来的控制指令 JsonDocument doc; if (deserializeJson(doc, payload, length) == DeserializationError::Ok) { if (doc["setTemperature"].is<float>()) { llm.setTemperature(doc["setTemperature"].as<float>()); Serial.printf("Set temperature to %.2f\n", doc["setTemperature"].as<float>()); } if (doc["next"].as<bool>() == true) { sendStep(); } } break; } default: break; } } void setup() { Serial.begin(115200); WiFi.begin(ssid, password); Serial.print("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("ESP32 IP Address: "); Serial.println(WiFi.localIP()); webSocket.begin(); webSocket.onEvent(webSocketEvent); llm.begin(); Serial.println("LLM wrapper ready."); } void loop() { webSocket.loop(); // 与浏览器建立连接且没有待处理数据时,每 800ms 推一步 if (clientConnected && millis() % 800 < 20) { sendStep(); } delay(10); }

4.5 运行与验证

把代码里的 Wi-Fi 信息改成你自己的,编译烧录到 ESP32 开发板。

打开串口监视器,波特率 115200,会看到类似输出:

Connecting to WiFi........ ESP32 IP Address: 192.168.1.123 LLM wrapper ready.

到这里,模型“有状态”了,但还看不到思考过程,下一步给浏览器做一个展示面板。

5. 实战二:构建可视化“思考面板”

5.1 面板功能设计

Dashboard 需要展示三类信息:

  • 当前生成的文本流:把每个nextPiece拼起来。
  • Top-8 候选概率柱状图:直观显示模型每一步的置信度。
  • 熵值变化曲线:判断模型是果断还是犹豫。

同时提供两个控制项:温度调节滑块、手动触发下一步按钮。

5.2 HTML 页面

在工程目录下创建dashboard.html,用浏览器打开,把顶部 IP 改成你的 ESP32 IP。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ESP32 LLM 思考面板</title> <style> body { font-family: "PingFang SC", "Microsoft YaHei", sans-serif; margin: 20px; background: #f7f9fb; color: #222; } h1 { font-size: 22px; } .card { background: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); padding: 16px; margin-bottom: 16px; } #textStream { min-height: 60px; font-size: 18px; line-height: 1.8; word-break: break-all; border: 1px solid #eee; border-radius: 8px; padding: 12px; } #entropyText { font-size: 14px; color: #666; } canvas { display: block; max-width: 100%; } button { background: #10a37f; color: #fff; border: none; border-radius: 8px; padding: 8px 16px; font-size: 14px; cursor: pointer; } button:hover { opacity: 0.85; } input[type="range"] { width: 300px; } </style> </head> <body> <h1>ESP32 LLM 思考面板</h1> <div class="card"> <h2>状态</h2> <div id="connStatus">连接中...</div> <div id="entropyText">熵值:--</div> </div> <div class="card"> <h2>候选概率分布</h2> <canvas id="probChart" width="700" height="360"></canvas> </div> <div class="card"> <h2>文本流</h2> <div id="textStream"></div> </div> <div class="card"> <h2>采样参数控制</h2> <label>Temperature: <span id="tempLabel">0.8</span></label> <input type="range" id="tempSlider" min="0.1" max="2.0" step="0.1" value="0.8"> <br><br> <button id="nextBtn">触发下一步</button> </div> <script> // 改成你的 ESP32 IP const WS_URL = 'ws://192.168.1.123:81'; let ws = null; let history = []; function connect() { ws = new WebSocket(WS_URL); ws.onopen = function () { document.getElementById('connStatus').textContent = '已连接'; }; ws.onclose = function () { document.getElementById('connStatus').textContent = '连接断开,尝试重连中...'; setTimeout(connect, 2000); }; ws.onmessage = function (event) { handleMessage(JSON.parse(event.data)); }; } function handleMessage(data) { document.getElementById('entropyText').textContent = '熵值:' + data.entropy.toFixed(3); const streamEl = document.getElementById('textStream'); history.push(data.nextPiece); if (history.length > 200) { history.shift(); } streamEl.textContent = history.join(''); drawChart(data.topK); } function drawChart(topK) { const canvas = document.getElementById('probChart'); const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); const barWidth = 60; const gap = 20; const baseY = 330; const maxH = 280; // 坐标轴 ctx.strokeStyle = '#ccc'; ctx.beginPath(); ctx.moveTo(30, baseY); ctx.lineTo(690, baseY); ctx.stroke(); for (let i = 0; i < topK.length; i++) { const prob = topK[i].prob; const h = prob * maxH; const x = 50 + i * (barWidth + gap); ctx.fillStyle = '#10a37f'; ctx.fillRect(x, baseY - h, barWidth, h); ctx.fillStyle = '#222'; ctx.font = '14px sans-serif'; ctx.fillText(topK[i].piece, x, baseY + 18); ctx.font = '12px sans-serif'; ctx.fillStyle = '#888'; ctx.fillText((prob * 100).toFixed(1) + '%', x, baseY - h - 8); } } // 温度滑块 document.getElementById('tempSlider').addEventListener('input', function () { const val = parseFloat(this.value); document.getElementById('tempLabel').textContent = val.toFixed(1); if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ setTemperature: val })); } }); // 手动触发下一步 document.getElementById('nextBtn').addEventListener('click', function () { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ next: true })); } }); connect(); </script> </body> </html>

5.3 运行结果说明

浏览器打开dashboard.html,如果一切正常,会出现以下现象:

  • 状态区显示“已连接”。
  • 候选概率柱状图每 800ms 刷新一次,不同候选词的柱长会变化。
  • 文本流区域不断追加新 token。
  • 拖动温度滑块,ESP32 串口会打印新的温度值,后续概率分布会随之变化。

这就是“看模型思考”的基础体验。你把llm_wrapper.cpp中的 Mock 换成真实模型后,看到的就是真实推理过程。

5.4 真实模型对接建议

目前 ESP32-S3 社区常用的微型模型路线有几种:

  • llama2.c 移植版:把 Karpathy 的 llama2.c 移植到 ESP32-S3,配合 INT8 量化运行小型 GPT 模型。
  • ESP-DL / ESP-LLM:乐鑫官方的神经网络库和 LLM 示例,对 ESP32-S3 的指令集有优化。
  • 自训练字符级 RNN:把莎士比亚、古诗文等语料做成字符级模型,权重非常小,100MB Flash 可以装好几个。

无论选择哪种,核心对接方式都一样:在llm.step()内部调用模型的前向计算,把输出的 logits 转成概率,填入StepInfo。注意,真实模型的词表往往有几千到几万项,topK数组不可能全部传输,建议先排序再取前 8 个候选,熵值则基于完整词表计算。

6. 常见问题与排查思路

问题现象常见原因解决思路
编译时报JsonDocument未定义ArduinoJson 版本是 v6 而不是 v7v6 使用DynamicJsonDocument doc(1024),并在末尾调用serializeJson
烧录后串口无输出开发板选择错误或驱动未安装确认选择的是 ESP32S3 Dev Module,安装 CP2102/CH340 驱动
浏览器连不上 WebSocketESP32 IP 不一致或端口被占用检查串口打印的 IP,确认 dashboard 中 WS_URL 与之一致
概率柱状图不刷新mock 的millis() % 800 < 20逻辑持续时间窗口太短改为在loop()中用unsigned long lastSend做定时
模型加载后反复重启模型文件太大,Flash 分区或 PSRAM 不足确认开发板规格,使用带 PSRAM 的型号,检查分区表
生成速度太慢未使用量化模型,或没有启用编译优化使用 INT8/INT4 权重,PlatformIO 下开启-O2
Wi-Fi 连接不上路由器 5G 频段兼容问题部分模组对 5GHz 支持不好,改为 2.4GHz 频段
温度滑块拖动后效果不明显Mock 模型随机数据对温度不敏感换真实模型观察明显差异;也可增大 logits 随机范围

如果遇到编译期报错堆栈很长,优先检查库版本冲突。ESP32 生态里库的 API 变动很快,先看报错信息里是哪个库文件抛出的,再针对性调整。

7. 最佳实践与工程建议

7.1 采样参数不要写死

温度和 top-k 直接影响生成质量和延迟。建议把采样参数设计为可配置项,由外部面板动态调整,这样调试时就不需要反复烧录固件。生产环境如果不需要调试,可以把 WebSocket 服务和采样参数入口一起裁剪掉,减少固件体积。

7.2 通信协议建议使用 JSON-Lines 或二进制

WebSocket 的 JSON 消息很方便调试,但每帧都带完整 key 名会浪费带宽。ESP32 的串口和 Wi-Fi 带宽有限,建议:

  • 调试阶段:保留 JSON,方便肉眼观察。
  • 稳定阶段:改成紧凑的二进制格式,用 1 字节type字段 + 若干float数组,减少序列化和传输开销。

7.3 内存管理是嵌入式 LLM 的重点

ESP32-S3 的内部 SRAM 有限,大块数据尽量放在 PSRAM:

  • 权重矩阵放 PSRAM。
  • KV Cache 按 batch size 动态分配。
  • 避免在每一步都创建大的临时数组,提前分配好缓存区。
  • ArduinoJson 的文档大小要按实际消息规模设置,防止内存碎片。

7.4 安全边界

如果设备暴露在局域网中,WebSocket 服务默认没有鉴权。建议:

  • 不要在公网直接暴露设备端口。
  • 生产环境可增加简单 token 验证或限定 IP。
  • 只开放局域网使用,或在路由器层做隔离。

7.5 打日志要有“开关”

嵌入式设备的日志输出会影响推理时序。建议加一个编译期开关:

#define BRAINSCOPE_DEBUG 1 #if BRAINSCOPE_DEBUG #define LOG(fmt, ...) Serial.printf(fmt, ##__VA_ARGS__) #else #define LOG(fmt, ...) #endif

发布固件时关掉调试日志,推理速度会明显提升。

7.6 量化精度要结合任务验证

前面提到 INT8/INT4 量化会损失精度。不要只看模型体积,要在目标任务上对比量化前后的输出分布和准确率。对需要精确数字任务的场景,建议保留关键层为 FP16 或混合精度,其余层用 INT8。

8. 总结与学习路线

通过这篇文章,你应该理解了以下几个关键点:

  • 云侧 LLM 和端侧 LLM 的适用边界,ESP32 在端侧推理中的定位。
  • 一个微型 LLM 在 ESP32 上是如何逐步生成 token 的,logits、softmax、temperature、top-k、熵这些概念分别起什么作用。
  • Brainscope 这类工具的核心思路:把模型每一步的内部状态采集出来、传出去、渲染出来。
  • 从零搭建了一套可以跑通的演示链路:ESP32 模型包装类 + WebSocket 传输 + 浏览器可视化面板。
  • 基于 Mock 实现验证了链路,后续可以直接替换为真实模型。

如果你是从零开始,建议按这个顺序继续加深:

  1. 先用 Mock 把可视化链路跑通,理解数据从哪来、到哪去。
  2. 找一个 llama2.c 的 ESP32-S3 移植项目,把训练好的 TinyStories 模型转换成适合烧录的格式,跑出真实文本。
  3. 尝试用 PlatformIO 替代 Arduino IDE,通过menuconfig调整 PSRAM、Flash 分区和编译优化。
  4. 研究量化流程,把 FP32 权重转成 INT8,记录量化前后的体积、速度和生成差异。
  5. 最后可以试着把生成结果通过蓝牙串口或屏幕展示出来,做一个脱离 PC 也能演示的完整小设备。

在实际项目中,最优先关注三个风险:内存是否够用、生成延迟是否可接受、量化后的输出是否仍然满足业务需求。只要先把这三个问题量化清楚,ESP32 上的 LLM 应用就成功了一大半。

如果本文对你有帮助,可以收藏备用。接下来动手烧录一块 ESP32-S3,亲眼看一次单片机“思考”的过程,远比只看文章更有收获。

http://www.cnnetsun.cn/news/4304044.html

相关文章:

  • LlamaIndex 安装与快速上手指南:3条路线让私有文档接入大模型问答
  • 可持久化并查集:一片森林为何只需一个根?
  • trackerslist:BT 下载没节点?每天自动更新的公共 Tracker 列表配置指南
  • 网易Java实习生笔试题深度拆解:HashMap、并发与JVM实战
  • LangChain自定义工具失灵?拆解ReAct循环定位问题
  • 用Common Lisp实现LLM推理引擎:AVX2加速与多线程实战
  • 网络延迟与资源消耗的取舍
  • 软件定义汽车核心:车规级存储架构与设计实践
  • LX Music 桌面版:免费多源音乐聚合播放器完整使用指南
  • Twenty 内存持续增长时,如何用 Chrome DevTools 定位可疑对象
  • 为什么你的 YOLO11 RTSP 实时检测总是延迟飙升?从缓冲帧到容器资源的一份完整优化指南
  • STM32L4 UART DMA碰撞问题详解:原理、配置与排查实战
  • 算法服务故障复盘应留下什么
  • 如何将 Windows 容器占用从 15GB 压到 2.5GB:Windows X Lite 轻量化部署实战
  • 3 个内置技能搭起 AI 自动化测试闭环:claude-code-best-practice 使用指南
  • 树莓派5上YOLOv8人员检测实战:基于OpenCV DNN与ONNX的轻量部署
  • Fooocus 免费 AI 绘图:3 分钟出第一张图
  • Rclone 使用指南:5 分钟从安装到自动化,搞定多云存储备份与挂载
  • Nuxt 预加载优化完整指南:5 步把首屏跳转时间压到 1.5 秒内
  • Vorssaint文本片段教程:输入一个词展开完整地址,支持日期变量
  • Magisk Android 无伤 Root 与系统级定制完整指南
  • LLM长期记忆架构实验:向量检索、摘要压缩与混合记忆方案对比
  • Vibe Coding实战:用Claude Code与Codex CLI开启AI协作开发
  • LX Music 免费桌面播放器:五大音源聚合搜索,一个窗口搞定听歌到囤歌
  • 3分钟上手 RuView:不用摄像头做 WiFi 人体姿态追踪的完整指南
  • 5步用熟OBS Studio:从第一次录屏到开播的快速指南
  • 无屏AI硬件“甜甜圈”解析:从语音交互到嵌入式工程实践
  • C++八股勘误:const指针、shared_ptr线程安全与vector扩容真相
  • 纯CPU推理引擎llambda.lisp:Common Lisp实现AVX2加速的LLM推理
  • C#开发Basler工业相机读取教程:pylon SDK图像采集与触发配置