IoT e IA

RAG para datos industriales: combina manuales de equipos con telemetría MQTT en vivo

Una guía práctica de generación aumentada por recuperación para fábricas: indexa manuales y procedimientos, obtén valores actuales de topics MQTT y da a los técnicos respuestas basadas en ambos, con citas.

RAG para datos industriales que combina páginas del manual de un equipo con telemetría MQTT en vivo de una máquina en planta
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

El RAG para datos industriales responde a preguntas como "¿por qué está en alarma el compresor de la línea 3 y qué debo revisar?" combinando dos fuentes que un técnico normalmente tiene que manejar a la vez: la documentación del equipo y las lecturas actuales de la máquina. Esta guía muestra cómo indexar manuales y procedimientos, obtener valores en vivo de topics MQTT en el momento de la pregunta y montar un prompt que produzca respuestas fundadas con citas, además de los escollos que importan en la planta.

Por qué el RAG básico no basta en una planta

La generación aumentada por recuperación (RAG) recupera pasajes relevantes de tus documentos y se los pasa a un modelo de lenguaje para que responda a partir de tu contenido en lugar de su conocimiento general. Para documentos de oficina, eso es todo. En la industria se queda corto, porque la mayoría de las preguntas útiles dependen del estado actual de una máquina:

  • "¿Es normal este nivel de vibración para esta bomba?" necesita tanto el límite del manual como el valor actual.
  • "¿Qué significa el código de fallo E-214 y sigue activo?" necesita la tabla de fallos y el estado de alarma en vivo.
  • "¿Puedo reiniciar ya la enfriadora?" necesita el procedimiento de rearranque y las presiones y temperaturas actuales.

Un modelo solo con documentos citará límites sin conocer el valor. Un modelo solo con telemetría verá números sin saber qué significan. Combinar ambos es lo que hace que un asistente sea útil para un técnico de mantenimiento.

Arquitectura de RAG para datos industriales

Componente Función Notas
Almacén de documentos Manuales, procedimientos, tablas de fallos, órdenes de trabajo anteriores Se fragmenta y vectoriza una vez y se actualiza cuando cambian los documentos
Índice vectorial Encuentra pasajes similares a la pregunta Una biblioteca como NumPy basta para colecciones pequeñas; a escala, usa una base de datos vectorial
Registro de activos Relaciona los IDs de activo con documentos y topics MQTT El pegamento entre documentos y datos en vivo
Caché de valores en vivo Se suscribe a MQTT y guarda el último valor de cada topic Los retained messages la llenan al instante al arrancar
Constructor de prompts Combina pregunta, pasajes y valores en vivo Etiqueta cada pieza con su fuente
LLM Redacta la respuesta con citas Local o alojado

El registro de activos es la pieza que la mayoría de los tutoriales omite y la que hace que el sistema funcione. Cuando un técnico pregunta por el "compresor C-3", el registro te dice qué manual aplica (los números de modelo difieren entre máquinas por lo demás parecidas) y qué topics contienen sus lecturas.

{
  "C-3": {
    "description": "Screw air compressor, line 3",
    "model": "SC-75",
    "documents": ["sc75-operating-manual", "sc75-fault-codes", "wo-history-c3"],
    "topics": {
      "discharge_temp_c": "connectmq/demo/8b2e61/plant1/line3/c3/discharge_temp",
      "discharge_pressure_bar": "connectmq/demo/8b2e61/plant1/line3/c3/pressure",
      "active_fault": "connectmq/demo/8b2e61/plant1/line3/c3/fault"
    }
  }
}

Una estructura de topics que sigue los niveles de ISA-95 (empresa, planta, área, línea, celda) hace que esta correspondencia sea casi mecánica. Si tus máquinas publican Sparkplug B, ConnectMQ puede decodificarlo y republicar las métricas en topics ISA-95 legibles, que un sistema RAG puede consumir sin un decodificador de Protobuf. Nuestro artículo sobre Sparkplug B y el Unified Namespace explica ese modelo de topics.

Paso 1: preparar e indexar los documentos

La preparación de los documentos determina la calidad de las respuestas más que la elección del modelo.

  1. Extrae texto limpio. Convierte los PDF a texto y comprueba que las tablas han sobrevivido. Las tablas de códigos de fallo suelen ser el contenido más valioso y el que más probabilidades tiene de quedar destrozado.
  2. Fragmenta según la estructura. Divide por títulos y pasos de procedimiento, no por un número fijo de caracteres, para que cada procedimiento se mantenga unido. Limita los fragmentos a unos pocos cientos de palabras.
  3. Adjunta metadatos. Guarda con cada fragmento el ID del documento, el título, la sección, la página y el modelo del equipo.
  4. Vectoriza y almacena. Calcula un embedding por fragmento y guárdalo junto con los metadatos.

El ejemplo siguiente usa la API de embeddings de Ollama con un modelo de embeddings local y guarda el índice en NumPy. Instala con pip install ollama numpy "paho-mqtt>=2.0" y ollama pull nomic-embed-text.

# index_docs.py
import json
import re
from pathlib import Path

import numpy as np
import ollama

EMBED_MODEL = "nomic-embed-text"
DOCS = Path("docs")          # un archivo .md o .txt por documento, llamado <doc-id>.md


def chunks_for(path):
    text = path.read_text(encoding="utf-8")
    sections = re.split(r"\n(?=#{1,3} )", text)   # divide antes de los títulos markdown
    for i, section in enumerate(sections):
        section = section.strip()
        if len(section) < 40:
            continue
        title = section.splitlines()[0].lstrip("# ").strip()
        yield {"doc": path.stem, "section": title, "chunk": i, "text": section[:3000]}


records = [c for p in sorted(DOCS.glob("*.md")) for c in chunks_for(p)]
vectors = []
for start in range(0, len(records), 32):
    batch = records[start:start + 32]
    resp = ollama.embed(model=EMBED_MODEL, input=["search_document: " + r["text"] for r in batch])
    vectors.extend(resp.embeddings)

matrix = np.array(vectors, dtype=np.float32)
matrix /= np.linalg.norm(matrix, axis=1, keepdims=True)
np.save("index.npy", matrix)
Path("index.json").write_text(json.dumps(records), encoding="utf-8")
print(f"indexed {len(records)} chunks")

Los prefijos search_document: y search_query: son las instrucciones de tarea que espera este modelo de embeddings en particular; otros modelos de embeddings tienen sus propias convenciones, así que consulta la ficha del modelo que uses.

Paso 2: mantener una caché de valores en vivo desde MQTT

La caché se suscribe a los topics de los activos y guarda el último valor con su marca de tiempo. Como los dispositivos publican valores retenidos, la caché está completa a los pocos instantes de arrancar.

# live_cache.py
import json
import threading
import time
import paho.mqtt.client as mqtt

_cache, _lock = {}, threading.Lock()


def start(topics, client_id="rag-cache-8b2e61", host="broker.connectmq.com", port=1883):
    def on_connect(client, userdata, flags, reason_code, properties):
        for t in topics:
            client.subscribe(t, qos=1)

    def on_message(client, userdata, msg):
        raw = msg.payload.decode("utf-8", errors="replace")
        try:
            value = json.loads(raw)
        except json.JSONDecodeError:
            value = raw
        with _lock:
            _cache[msg.topic] = {"value": value, "receivedAt": time.time()}

    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=client_id)
    client.on_connect = on_connect
    client.on_message = on_message
    client.connect(host, port, keepalive=60)
    client.loop_start()
    return client


def snapshot(topic_map):
    now = time.time()
    out = {}
    with _lock:
        for name, topic in topic_map.items():
            entry = _cache.get(topic)
            out[name] = None if entry is None else {
                "value": entry["value"], "ageSeconds": round(now - entry["receivedAt"], 1)}
    return out

Indicar la antigüedad de cada valor es esencial. Una lectura que llegó hace dos horas no es "actual", y el modelo debe decirlo.

Paso 3: recuperar, combinar y responder

En el momento de la pregunta, identifica el activo, recupera pasajes solo de los documentos de ese activo, lee sus valores en vivo y construye un prompt en el que cada pieza esté etiquetada.

# ask.py
import json
from pathlib import Path

import numpy as np
import ollama

import live_cache

EMBED_MODEL, CHAT_MODEL = "nomic-embed-text", "llama3.1:8b"
REGISTRY = json.loads(Path("assets.json").read_text())
RECORDS = json.loads(Path("index.json").read_text(encoding="utf-8"))
MATRIX = np.load("index.npy")

live_cache.start([t for a in REGISTRY.values() for t in a["topics"].values()])

SYSTEM = """You assist maintenance technicians.
Answer using ONLY the DOCUMENT passages and LIVE values provided.
Cite documents as [doc-id, section] and live values as [live: name].
If a live value is missing or older than 300 seconds, say it is unavailable or stale.
If the documents do not cover the question, say so. Never invent limits, codes or procedures.
Safety instructions in the documents (lockout, PPE) must be repeated, not shortened."""


def retrieve(question, docs, k=4):
    q = ollama.embed(model=EMBED_MODEL, input=["search_query: " + question]).embeddings[0]
    q = np.array(q, dtype=np.float32)
    q /= np.linalg.norm(q)
    scores = MATRIX @ q
    order = [i for i in np.argsort(-scores) if RECORDS[i]["doc"] in docs]
    return [RECORDS[i] | {"score": float(scores[i])} for i in order[:k]]


def ask(asset_id, question):
    asset = REGISTRY[asset_id]
    passages = retrieve(question, set(asset["documents"]))
    live = live_cache.snapshot(asset["topics"])
    context = "\n\n".join(f"DOCUMENT [{p['doc']}, {p['section']}]\n{p['text']}" for p in passages)
    live_text = "\n".join(f"LIVE [{k}]: {json.dumps(v)}" for k, v in live.items())
    prompt = (f"ASSET: {asset_id} - {asset['description']} (model {asset['model']})\n\n"
              f"{live_text}\n\n{context}\n\nQUESTION: {question}")
    resp = ollama.chat(model=CHAT_MODEL, options={"temperature": 0.1},
                       messages=[{"role": "system", "content": SYSTEM},
                                 {"role": "user", "content": prompt}])
    return resp.message.content


if __name__ == "__main__":
    import time
    time.sleep(2)   # deja que lleguen los valores retenidos
    print(ask("C-3", "Discharge temperature looks high. Is it within limits and what should I check first?"))

Para probarlo sin equipos reales, publica valores de prueba retenidos en el servidor MQTT público gratuito, que es legible por cualquiera y no debe recibir datos de producción:

mosquitto_pub -h broker.connectmq.com -p 1883 -i seed-8b2e61 -r -q 1 -t "connectmq/demo/8b2e61/plant1/line3/c3/discharge_temp" -m "104.5"
mosquitto_pub -h broker.connectmq.com -p 1883 -i seed-8b2e61 -r -q 1 -t "connectmq/demo/8b2e61/plant1/line3/c3/pressure" -m "7.9"
mosquitto_pub -h broker.connectmq.com -p 1883 -i seed-8b2e61 -r -q 1 -t "connectmq/demo/8b2e61/plant1/line3/c3/fault" -m "\"none\""

No se garantiza que los datos retenidos en el broker público persistan, así que vuelve a sembrar los valores si desaparecen, y nunca pongas datos reales de planta en un broker público.

Escollos propios del RAG industrial

  1. Manual equivocado para la máquina correcta. Máquinas parecidas con firmware o números de modelo distintos tienen límites diferentes. Filtra la recuperación por la lista de documentos del activo, como arriba, en lugar de buscar en todo.
  2. Unidades y escalado. Si un topic publica cuentas en bruto o grados Fahrenheit mientras el manual usa Celsius, el modelo comparará peras con manzanas. Normaliza las unidades en la caché e indícalas en el nombre del campo (discharge_temp_c).
  3. Valores obsoletos. Incluye siempre marcas de tiempo o antigüedades, e indica al modelo que trate los valores antiguos como no disponibles.
  4. Tablas perdidas en la extracción. Verifica las tablas de códigos de fallo tras la conversión del PDF; plantéate guardarlas como JSON estructurado y buscar los códigos directamente en lugar de mediante embeddings.
  5. Contenido de seguridad. Los pasos de bloqueo y etiquetado nunca deben desaparecer en un resumen. Dilo en el prompt de sistema y pruébalo.
  6. Sin conjunto de evaluación. Reúne entre 30 y 50 preguntas reales de técnicos con sus respuestas esperadas y vuelve a ejecutarlas cada vez que cambies la fragmentación, los modelos o los prompts.

Del prototipo a la planta

La ruta de lectura de valores en vivo es ligera: un suscriptor por instancia del asistente con un puñado de suscripciones con comodines. Ejecutarla contra un broker MQTT en la nube que ya transporta tu Unified Namespace significa que el asistente ve los mismos datos que los dashboards, los historiadores o un gemelo digital 3D de la planta, por TLS y con sus propias credenciales. ConnectMQ lo ofrece como broker MQTT gestionado, con decodificación de Sparkplug B y una vista de dispositivos ISA-95 en el portal para comprobar qué topics publica un activo. Durante el desarrollo, el servidor público basta para valores de prueba sembrados.

Para llevar el asistente más lejos, deja que obtenga valores bajo demanda mediante un servidor MCP para MQTT, convierte las preguntas recurrentes en resúmenes de datos de sensores con LLM programados, o explora un copiloto de IA industrial completo. Nuestra guía de IA e IoT sitúa estos patrones en contexto.

Empieza con ConnectMQ

Alimenta tu asistente RAG con telemetría en vivo y bien estructurada desde ConnectMQ, con retained messages, detección de topics ISA-95 y decodificación de Sparkplug B integradas. Crea un broker Serverless gratuito, sin tarjeta de crédito, y fundamenta tu primera respuesta en datos reales de máquina.

Preguntas frecuentes

¿Qué diferencia hay entre RAG y fine-tuning para datos industriales?

El RAG recupera tus documentos y valores en vivo en el momento de la pregunta, así que las respuestas se mantienen actuales y pueden citar fuentes. El fine-tuning cambia los pesos del modelo y no puede conocer el estado actual de una máquina.

¿Por qué obtener los valores en vivo de MQTT en lugar de una base de datos?

Los mensajes MQTT retenidos dan al instante el último valor de cada topic, que es exactamente lo que necesita una pregunta sobre el estado actual. Las preguntas históricas siguen correspondiendo a una base de datos de series temporales.

¿Necesito una base de datos vectorial?

No para unos pocos miles de fragmentos; un índice NumPy en memoria es lo bastante rápido. Pasa a una base de datos vectorial cuando las colecciones crezcan mucho o muchos usuarios consulten a la vez.

¿Cómo evito que el asistente invente límites?

Indícale que responda solo a partir de los pasajes y valores en vivo proporcionados, exige citas, filtra la recuperación por los documentos del equipo correcto y prueba con un conjunto de evaluación fijo.

Artículos relacionados

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.