StackPractices
advanced Por Mathias Paulenko

Patrón Blackboard

Un espacio de conocimiento compartido donde módulos especializados independientes colaboran para resolver problemas complejos contribuyendo soluciones parciales.

Descripción General

El Patrón Blackboard te da un espacio de conocimiento compartido — el blackboard — donde módulos independientes y especializados llamados Knowledge Sources trabajan juntos en un problema. Una fuente lee el tablero, se pregunta “¿puedo aportar algo útil aquí?”, y publica una solución parcial si la respuesta es sí.

El patrón se gana su lugar cuando ningún algoritmo por sí solo puede resolver el problema, pero un puñado de heurísticas especializadas pueden ir desgastándolo. Un componente Control ejecuta el bucle, decidiendo qué fuente actúa a continuación a partir del estado actual del blackboard.

Lo he visto aplicado en reconocimiento de voz, PLN, reconocimiento de imágenes y optimización por restricciones — dominios donde la respuesta emerge de muchas pequeñas contribuciones en lugar de un algoritmo decisivo.

Cuándo Usar

Usa el Patrón Blackboard cuando:

  • Ningún algoritmo determinístico puede resolver el problema
  • Varias heurísticas o algoritmos especializados pueden aportar soluciones parciales
  • El espacio del problema es suficientemente grande como para necesitar exploración
  • Módulos independientes pueden refinar incrementalmente una solución
  • Quieres una arquitectura extensible para un sistema de IA o heurísticas

Cuándo Evitar

  • Problemas que un solo algoritmo resuelve limpiamente
  • Presupuestos de latencia estrictos donde la sobrecarga de coordinación es inaceptable
  • Situaciones que necesitan comportamiento determinístico y predecible
  • Sistemas pequeños donde la coordinación cuesta más de lo que ahorra

Solución

Python

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import List, Dict, Any, Optional
from enum import Enum
import random

class Confidence(Enum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3

@dataclass
class Hypothesis:
    content: str
    confidence: Confidence
    source: str

@dataclass
class Blackboard:
    hypotheses: List[Hypothesis] = field(default_factory=list)
    final_solution: Optional[str] = None
    complete: bool = False

    def add_hypothesis(self, hypothesis: Hypothesis):
        self.hypotheses.append(hypothesis)

    def get_best_hypothesis(self) -> Optional[Hypothesis]:
        if not self.hypotheses:
            return None
        return max(self.hypotheses, key=lambda h: h.confidence.value)

    def set_solution(self, solution: str):
        self.final_solution = solution
        self.complete = True


class KnowledgeSource(ABC):
    def __init__(self, name: str):
        self.name = name

    @abstractmethod
    def can_contribute(self, blackboard: Blackboard) -> bool:
        pass

    @abstractmethod
    def contribute(self, blackboard: Blackboard):
        pass


class PatternMatcher(KnowledgeSource):
    def can_contribute(self, blackboard: Blackboard) -> bool:
        return len(blackboard.hypotheses) < 3

    def contribute(self, blackboard: Blackboard):
        bb = blackboard
        bb.add_hypothesis(Hypothesis(
            content="Patrón detectado: probablemente función",
            confidence=Confidence.MEDIUM,
            source=self.name
        ))


class SemanticAnalyzer(KnowledgeSource):
    def can_contribute(self, blackboard: Blackboard) -> bool:
        return any(h.source == "PatternMatcher" for h in blackboard.hypotheses)

    def contribute(self, blackboard: Blackboard):
        bb = blackboard
        bb.add_hypothesis(Hypothesis(
            content="Análisis semántico: intención de usuario es ejecución de query",
            confidence=Confidence.HIGH,
            source=self.name
        ))


class ConfidenceEvaluator(KnowledgeSource):
    def can_contribute(self, blackboard: Blackboard) -> bool:
        best = blackboard.get_best_hypothesis()
        return best is not None and best.confidence == Confidence.HIGH

    def contribute(self, blackboard: Blackboard):
        best = blackboard.get_best_hypothesis()
        if best:
            blackboard.set_solution(best.content)


class Controller:
    def __init__(self, blackboard: Blackboard, sources: List[KnowledgeSource]):
        self.blackboard = blackboard
        self.sources = sources

    def solve(self, max_iterations: int = 10):
        for _ in range(max_iterations):
            if self.blackboard.complete:
                break

            for source in self.sources:
                if source.can_contribute(self.blackboard):
                    source.contribute(self.blackboard)
                    break


# Uso
bb = Blackboard()
sources = [
    PatternMatcher("PatternMatcher"),
    SemanticAnalyzer("SemanticAnalyzer"),
    ConfidenceEvaluator("ConfidenceEvaluator"),
]
controller = Controller(bb, sources)
controller.solve()

print(f"Solución: {bb.final_solution}")
print(f"Todas las hipótesis: {[h.content for h in bb.hypotheses]}")

Java

import java.util.*;

enum Confidence { LOW, MEDIUM, HIGH }

record Hypothesis(String content, Confidence confidence, String source) {}

class Blackboard {
    private final List<Hypothesis> hypotheses = new ArrayList<>();
    private String finalSolution;
    private boolean complete = false;

    public void addHypothesis(Hypothesis h) { hypotheses.add(h); }
    public List<Hypothesis> getHypotheses() { return hypotheses; }
    public boolean isComplete() { return complete; }

    public Hypothesis getBestHypothesis() {
        return hypotheses.stream()
            .max(Comparator.comparingInt(h -> h.confidence().ordinal()))
            .orElse(null);
    }

    public void setSolution(String solution) {
        this.finalSolution = solution;
        this.complete = true;
    }

    public String getFinalSolution() { return finalSolution; }
}

abstract class KnowledgeSource {
    protected final String name;
    public KnowledgeSource(String name) { this.name = name; }
    public String getName() { return name; }

    public abstract boolean canContribute(Blackboard bb);
    public abstract void contribute(Blackboard bb);
}

class PatternMatcher extends KnowledgeSource {
    public PatternMatcher() { super("PatternMatcher"); }
    public boolean canContribute(Blackboard bb) { return bb.getHypotheses().size() < 3; }
    public void contribute(Blackboard bb) {
        bb.addHypothesis(new Hypothesis("Patrón detectado: probablemente función", Confidence.MEDIUM, name));
    }
}

class SemanticAnalyzer extends KnowledgeSource {
    public SemanticAnalyzer() { super("SemanticAnalyzer"); }
    public boolean canContribute(Blackboard bb) {
        return bb.getHypotheses().stream().anyMatch(h -> h.source().equals("PatternMatcher"));
    }
    public void contribute(Blackboard bb) {
        bb.addHypothesis(new Hypothesis("Semántica: intención de usuario es ejecución de consulta", Confidence.HIGH, name));
    }
}

class ConfidenceEvaluator extends KnowledgeSource {
    public ConfidenceEvaluator() { super("ConfidenceEvaluator"); }
    public boolean canContribute(Blackboard bb) {
        Hypothesis best = bb.getBestHypothesis();
        return best != null && best.confidence() == Confidence.HIGH;
    }
    public void contribute(Blackboard bb) {
        Hypothesis best = bb.getBestHypothesis();
        if (best != null) bb.setSolution(best.content());
    }
}

class Controller {
    private final Blackboard blackboard;
    private final List<KnowledgeSource> sources;

    public Controller(Blackboard blackboard, List<KnowledgeSource> sources) {
        this.blackboard = blackboard;
        this.sources = sources;
    }

    public void solve(int maxIterations) {
        for (int i = 0; i < maxIterations && !blackboard.isComplete(); i++) {
            for (KnowledgeSource source : sources) {
                if (source.canContribute(blackboard)) {
                    source.contribute(blackboard);
                    break;
                }
            }
        }
    }
}

// Uso
Blackboard bb = new Blackboard();
List<KnowledgeSource> sources = List.of(
    new PatternMatcher(),
    new SemanticAnalyzer(),
    new ConfidenceEvaluator()
);
Controller controller = new Controller(bb, sources);
controller.solve(10);
System.out.println("Solución: " + bb.getFinalSolution());

JavaScript

const Confidence = {
  LOW: 1,
  MEDIUM: 2,
  HIGH: 3,
};

class Blackboard {
  constructor() {
    this.hypotheses = [];
    this.finalSolution = null;
    this.complete = false;
  }

  addHypothesis(hypothesis) {
    this.hypotheses.push(hypothesis);
  }

  getBestHypothesis() {
    if (this.hypotheses.length === 0) return null;
    return this.hypotheses.reduce((best, h) =>
      h.confidence > best.confidence ? h : best
    );
  }

  setSolution(solution) {
    this.finalSolution = solution;
    this.complete = true;
  }
}

class KnowledgeSource {
  constructor(name) {
    this.name = name;
  }

  canContribute(blackboard) {
    throw new Error('Must implement canContribute');
  }

  contribute(blackboard) {
    throw new Error('Must implement contribute');
  }
}

class PatternMatcher extends KnowledgeSource {
  constructor() { super('PatternMatcher'); }
  canContribute(bb) { return bb.hypotheses.length < 3; }
  contribute(bb) {
    bb.addHypothesis({
      content: 'Patrón detectado: probablemente función',
      confidence: Confidence.MEDIUM,
      source: this.name,
    });
  }
}

class SemanticAnalyzer extends KnowledgeSource {
  constructor() { super('SemanticAnalyzer'); }
  canContribute(bb) {
    return bb.hypotheses.some(h => h.source === 'PatternMatcher');
  }
  contribute(bb) {
    bb.addHypothesis({
      content: 'Análisis semántico: intención de usuario es ejecución de query',
      confidence: Confidence.HIGH,
      source: this.name,
    });
  }
}

class ConfidenceEvaluator extends KnowledgeSource {
  constructor() { super('ConfidenceEvaluator'); }
  canContribute(bb) {
    const best = bb.getBestHypothesis();
    return best && best.confidence === Confidence.HIGH;
  }
  contribute(bb) {
    const best = bb.getBestHypothesis();
    if (best) bb.setSolution(best.content);
  }
}

class Controller {
  constructor(blackboard, sources) {
    this.blackboard = blackboard;
    this.sources = sources;
  }

  solve(maxIterations = 10) {
    for (let i = 0; i < maxIterations && !this.blackboard.complete; i++) {
      for (const source of this.sources) {
        if (source.canContribute(this.blackboard)) {
          source.contribute(this.blackboard);
          break;
        }
      }
    }
  }
}

// Uso
const bb = new Blackboard();
const sources = [
  new PatternMatcher(),
  new SemanticAnalyzer(),
  new ConfidenceEvaluator(),
];
const controller = new Controller(bb, sources);
controller.solve();
console.log('Solución:', bb.finalSolution);

Explicación

Tres componentes forman el Patrón Blackboard:

  • Blackboard: la estructura de datos compartida que mantiene el estado del problema y las hipótesis acumuladas
  • Knowledge Sources: módulos independientes que leen el blackboard, comprueban si sus condiciones aplican, y publican soluciones parciales
  • Controller: el bucle que elige qué fuente debe actuar a continuación

Las fuentes nunca hablan entre sí; solo leen y escriben en el blackboard. ¿Quieres extender el sistema? Suelta una fuente nueva — nada más necesita cambiar.

Arquitectura blackboard: el controlador inspecciona el estado, activa las knowledge sources, y estas leen y escriben hipótesis en el blackboard hasta converger en una solución.

Como contraste, el patrón Mediator centraliza la coordinación entre objetos conocidos en lugar de difundir mediante estado compartido, y el patrón Event Bus enruta eventos explícitos en lugar de dejar que las fuentes inspeccionen hipótesis compartidas.

Variantes

VarianteEstrategia de ControlCaso de Uso
OportunistaCualquier fuente contribuye cuando se cumplen sus condicionesSistemas simples y descentralizados
Basada en prioridadEl controller elige la fuente elegible de mayor prioridadSistemas complejos con muchas fuentes
Dirigida por eventosLas fuentes reaccionan a cambios del blackboardSistemas de IA reactivos
JerárquicaBlackboards en distintos niveles de abstracciónResolución de problemas por etapas

Lo que funciona

  • Mantén las fuentes independientes. Si las fuentes se llaman directamente entre sí, has construido un mediator, no un blackboard.
  • Dale una cola de prioridad al controller. Recorrer las fuentes en orden desperdicia ciclos — elige la que pinte más prometedora.
  • Puntúa la confianza de cada hipótesis. Los niveles de confianza le dan al controller algo real con lo que decidir.
  • Limita las iteraciones. No hay garantía de convergencia, así que necesitas una vía de escape.
  • Registra cada contribución. Cuando un sistema blackboard falla, la traza de contribuciones es cómo lo encuentras.

Una versión ejecutable de este patrón — tres knowledge sources clasificando texto sobre un blackboard compartido — está en el repositorio companion.

Errores Comunes

  • Acoplamiento fuerte entre fuentes. La comunicación directa anula el propósito del patrón.
  • Sin condiciones de terminación. Sin tope de iteraciones ni check de completitud, el bucle simplemente no para.
  • Sobreescribir hipótesis de alta confianza. Una contribución nueva no debería reemplazar ciegamente a una mejor.
  • Controller monolítico. Un controller que hace todo se vuelve el cuello de botella y el punto único de fallo.
  • Ignorar las condiciones de carrera. Las implementaciones multihilo necesitan acceso sincronizado al blackboard.

Ejemplos del Mundo Real

Sistemas de Reconocimiento de Voz

El reconocimiento de voz es la aplicación clásica del blackboard: modelos acústicos, modelos de lenguaje, diccionarios de pronunciación y analizadores contextuales publican hipótesis sobre lo que se dijo, y el sistema converge en la transcripción más probable.

Pipelines de Reconocimiento de Imágenes

En visión por computadora, detectores de bordes, reconocedores de formas y clasificadores semánticos publican interpretaciones parciales de una imagen en un tablero compartido — la misma idea con otra ropa.

Solvers de Optimización

Los solvers de restricciones usados en scheduling y routing ejecutan varias heurísticas que refinan iterativamente un espacio de solución compartido — blackboard en todo menos en el nombre.

Preguntas frecuentes

¿Cuál es la diferencia entre los patrones Blackboard y Pipeline?

Los pipelines mueven datos de forma lineal, etapa a etapa — cada paso solo ve la salida del anterior. Una fuente del blackboard contribuye cuando tiene algo que aportar, leyendo las hipótesis que haya en el tablero — el orden no existe.

¿Cómo decide el controller qué fuente activar?

Normalmente por orden de prioridad, umbrales de confianza, o pujas estilo subasta donde cada fuente puntúa su propia capacidad de contribuir.

¿Es el patrón Blackboard adecuado para sistemas de tiempo real?

Encaja mal. La sobrecarga de coordinación hace la latencia impredecible, así que los sistemas de tiempo real suelen preferir pipelines determinísticos o máquinas de estado.

¿Puedo usar Blackboard en un proyecto pequeño?

Puedes, pero rara vez compensa — con pocos componentes la sobrecarga de coordinación pesa más que la flexibilidad. Empieza simple e introdúcelo cuando el problema de verdad necesite hipótesis que compiten.

¿En qué se diferencia Blackboard de Mediator?

Mediator centraliza la comunicación entre objetos que se conocen entre sí. Blackboard los desacopla mediante estado compartido y contribución oportunista — las fuentes ni siquiera saben que existen otras.