(a) Conceito e Arquitetura
Se a Parte 1 desta série ensinou o assistente a falar (rede, TLS, JSON), esta parte ensina a lembrar. Um assistente de bancada sem memória repete perguntas, esquece o nome do projeto e trata cada turno como se fosse o primeiro. Em um microcontrolador, “memória” não é uma coisa só: é uma hierarquia de camadas com custo e velocidade diferentes.
A camada mais rápida é a RAM, onde vive a janela de contexto ativa — pequena e volátil. Abaixo dela, o NVS (Non-Volatile Storage) guarda pares chave-valor persistentes: configurações, identificadores e um histórico curto de conversa. O littlefs é o sistema de arquivos em flash, ideal para logs maiores, notas e material que alimenta o RAG. E, no topo, a nuvem guarda o modelo. O ESP32 é o bibliotecário que decide o que sobe de camada a cada chamada.
O conceito central é orçamento de contexto. Cada token enviado custa dinheiro e latência, então o histórico não pode crescer indefinidamente. A técnica padrão é uma janela deslizante (mantém os últimos K turnos) combinada com um resumo enxuto dos turnos antigos. Sobre isso, plugamos uma camada leve de RAG: em vez de um banco vetorial, o MCU busca palavras-chave nas notas locais e injeta os trechos relevantes no prompt.
Vale quantificar para não trabalhar no escuro. Em modelos com janela de 128 k tokens, um assistente de bancada raramente precisa passar de 1 k a 2 k tokens por chamada: algumas centenas para o system prompt, o resumo em 1–2 frases, os últimos seis a oito turnos e três trechos de nota. Esse teto mantém a latência previsível e evita que uma conversa longa vire uma fatura surpresa. A regra de ouro é: o MCU decide o que entra na janela, nunca o modelo.
(b) BOM e camadas de armazenamento
Nenhum hardware novo é necessário em relação à Parte 1 — o mesmo ESP32-S3 (N16R8) com display e áudio serve. O que muda é o uso da flash interna.
| Camada | Chave / caminho | Uso | Observação |
|---|---|---|---|
| RAM | buffer de contexto | Janela ativa do prompt | Volátil; limitada por heap/PSRAM |
| NVS | namespace chat, chaves t0..t19 |
Histórico curto (ring de 20 turnos) | Wear leveling interno do NVS |
| NVS | namespace cfg |
API keys, modelo, preferências | Nunca em texto puro em produto |
| littlefs | /littlefs/notes/*.txt |
Notas locais para o RAG | Leitura por streaming |
| littlefs | /littlefs/log/ |
Logs técnicos e telemetria | Rotacionar para não esgotar flash |
(c) Flash e particionamento
Antes do código, é preciso reservar espaço na flash. Uma tabela de partições típica para esse assistente com 16 MB fica assim (recorte de partitions.csv):
| Nome | Tipo | Tamanho | Função |
|---|---|---|---|
| nvs | data/nvs | 0x6000 | Config e histórico curto |
| otadata | data/ota | 0x2000 | Seleção de slot OTA |
| phy_init | data/phy | 0x1000 | Calibração de RF |
| factory | app/factory | 0x400000 | Firmware principal |
| storage | data/littlefs | 0x100000 | Notas e logs (littlefs) |
Ampliar storage permite mais notas para o RAG; ampliar nvs permite histórico maior. Lembre que a NVS já faz wear leveling internamente, então gravações frequentes de turno são aceitáveis — desde que você evite reescrever a flash a cada token.
(d) Código — NVS, littlefs, resumo de contexto, RAG e montagem do prompt
Começamos pelo histórico curto em NVS, usando um anel de 20 chaves para não crescer sem limite.
#include "nvs_flash.h"
#include "nvs.h"
#define NS "chat"
// Grava o histórico curto como um blob/string JSON por turno (ring de N itens)
esp_err_t hist_put(int turno, const char *json_turno) {
nvs_handle_t h;
esp_err_t err = nvs_open(NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
char key[16];
snprintf(key, sizeof(key), "t%d", turno % 20); // ate 20 turnos
err = nvs_set_str(h, key, json_turno);
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
return err;
}
// Le um turno; retorna ESP_ERR_NVS_NOT_FOUND se nao existir
esp_err_t hist_get(int turno, char *out, size_t *len) {
nvs_handle_t h;
esp_err_t err = nvs_open(NS, NVS_READONLY, &h);
if (err != ESP_OK) return err;
char key[16];
snprintf(key, sizeof(key), "t%d", turno % 20);
err = nvs_get_str(h, key, out, len);
nvs_close(h);
return err;
}
As notas que alimentam o RAG moram no littlefs, montado a partir da partição storage:
#include "esp_littlefs.h"
void monta_fs(void) {
esp_vfs_littlefs_conf_t conf = {};
conf.base_path = "/littlefs";
conf.partition_label = "storage";
conf.format_if_mount_failed = true;
esp_vfs_littlefs_register(&conf);
}
// Guarda uma nota de bancada para o RAG local
void nota_salvar(const char *titulo, const char *texto) {
char path[96];
snprintf(path, sizeof(path), "/littlefs/notes/%s.txt", titulo);
FILE *f = fopen(path, "w");
if (f) { fputs(texto, f); fclose(f); }
}
Para montar o prompt, combinamos três pedaços: um resumo curto dos turnos antigos, os últimos turnos literais e os trechos trazidos pelo RAG. Um único snprintf monta o JSON final sem fragmentar a heap:
// Monta o prompt com contexto: system + resumo + ultimos K turnos + RAG
void monta_prompt(char *out, size_t out_sz,
const char *system, const char *resumo,
const char *rag, const char *user) {
snprintf(out, out_sz,
"{\"messages\":["
"{\"role\":\"system\",\"content\":\"%s%s%s\"},"
"{\"role\":\"user\",\"content\":\"%s\"}]}",
system,
(resumo && resumo[0]) ? " Contexto anterior: " : "",
(rag && rag[0]) ? rag : "",
user);
}
O RAG local é deliberadamente simples: sem embeddings nem banco vetorial no MCU. A busca por termos nas notas já resolve a maioria dos casos de bancada e cabe em poucos KB de código:
// RAG leve: busca por termos (sem embeddings) nas notas locais
int rag_buscar(const char *consulta, char *out, size_t out_sz) {
const char *dir = "/littlefs/notes";
DIR *d = opendir(dir);
if (!d) return 0;
struct dirent *e;
int achados = 0;
out[0] = 0;
while ((e = readdir(d)) && achados < 3) {
char path[160];
snprintf(path, sizeof(path), "%s/%s", dir, e->d_name);
FILE *f = fopen(path, "r");
if (!f) continue;
char buf[512];
size_t n = fread(buf, 1, sizeof(buf) - 1, f);
buf[n] = 0;
fclose(f);
if (strcasestr(buf, consulta)) { // match simples
strncat(out, buf, out_sz - strlen(out) - 1);
achados++;
}
}
closedir(d);
return achados;
}
O truque para o resumo é periódico: a cada N turnos, o próprio modelo recebe os turnos antigos e devolve 1–2 frases de resumo, que substituem os originais no envio seguinte. Assim o contexto enviado fica estável em tamanho, com custo previsível. Como o resumo também é um texto que vai e volta pela rede, ele deve ser curto por design — duas frases no máximo. Guarde-o na NVS sob a chave resumo para sobreviver a reinicializações, e invalide-o quando o usuário mudar de assunto de forma explícita, evitando que um resumo obsoleto contamine as próximas respostas.
(e) Troubleshooting
- NVS cheia: partição pequena demais gera
ESP_ERR_NVS_NO_FREE_PAGES. Aumente a partiçãonvsou use um anel menor de chaves. - Corrupção da NVS: apagões durante a escrita podem deixar a NVS inválida. Trate o erro na inicialização com
nvs_flash_erase()+ reinit, e limpe a chave comnvs_erase_key. - littlefs não monta: verifique o rótulo da partição e use
format_if_mount_failedapenas em desenvolvimento; em produção, prefira recuperar os dados. - Contexto grande demais: sintomas são latência alta, custo explodindo e estouro de buffer. Trunque por turno, resuma com mais frequência e limite o RAG a um top-3.
- Corte de UTF-8 no meio: truncar por bytes pode partir um caractere acentuado. Corte sempre em fronteira de caractere e valide antes de enviar.
(f) Aplicações reais
Com memória e contexto, o assistente deixa de ser um oráculo sem estado e passa a ser um companheiro de bancada: ele lembra que o projeto atual usa um sensor BMP280, recorda o estilo de código preferido e retoma o assunto de ontem. Em aplicações reais, isso viabiliza um diário técnico por voz que grava notas no littlefs e depois as consulta; um tutor de bancada que busca trechos de datasheets locais via RAG antes de responder; e um painel doméstico que mantém preferências de cada morador. Combinada com o 78/xiaozhi-esp32 como firmware de referência e um proxy ollama/ollama para reduzir custo, a arquitetura vira uma base sólida para assistentes embarcados persistentes.
Não some depois da matéria
Radar do hub: Núcleo Hits, Além do Oculto, Bobinho. Sem spray, sem lista comprada.
Radar do hub
Um e-mail quando sair matéria que importa — sem spray de newsletter genérica.

Deixe um comentário
Entre com Google ou Facebook para comentar. Nome e e-mail vêm da sua conta — sem preencher formulário.
Se o login falhar, configure Google e Facebook em Configurações → Nextend Social Login.
Outras opções de login