El problema fundamental que aborda este artículo es la dificultad de traducir la intención humana a código funcional y correcto, especialmente en sistemas complejos como los distribuidos, donde la especificación inicial es inherentemente incompleta y el diseño se descubre iterativamente. Los Large Language Models (LLMs) pueden generar grandes volúmenes de código rápidamente, pero su fiabilidad disminuye con la ambigüedad del lenguaje natural y la vastedad del espacio de diseño en lenguajes de propósito general. La tesis central es que los Domain-Specific Languages (DSLs) actúan como un "arnés" efectivo para los LLMs, restringiendo el espacio de generación y permitiendo una validación determinista, lo que resulta en una mayor precisión y confiabilidad del código generado.
Esto se vuelve crítico en el contexto actual donde los LLMs son cada vez más capaces de generar código. Sin embargo, la revisión y validación de este código, especialmente en dominios con alta complejidad de estado como los sistemas distribuidos (ej. problemas de concurrencia, fallos de red, sesgos de reloj), es una tarea ardua. Los DSLs, al encapsular decisiones de diseño y proporcionar un vocabulario controlado, transforman el rol del LLM de un generador de código general a una interfaz de lenguaje natural para un modelo semántico bien definido, facilitando tanto la generación como la verificación.
Arquitectura del Sistema
La arquitectura propuesta se basa en la interacción de LLMs con un modelo semántico subyacente, a menudo expuesto a través de un DSL. El modelo semántico, como el de Tickloom, define las abstracciones clave del dominio (ej. Process, Replica, Network, Storage, Clock en sistemas distribuidos) y las decisiones de diseño fundamentales (ej. bucle de tick single-threaded, mensajes como Java records, coordinación de quórum). Este modelo reduce el espacio de diseño que el LLM debe explorar, permitiéndole enfocarse en la lógica del protocolo en lugar de detalles de implementación de bajo nivel como modelos de threading o patrones de red.
El DSL se construye sobre este modelo semántico, proporcionando una sintaxis restringida y declarativa para expresar conceptos y operaciones del dominio. En el ejemplo de Tickloom, esto se manifiesta en un DSL interno en Java para definir escenarios de prueba de sistemas distribuidos, donde el vocabulario del DSL incluye conceptos como 'servers', 'clients', 'writes', 'reads', 'partitions', 'delays'. La gramática del DSL es validada por el compilador del lenguaje anfitrión (Java en este caso) a través de interfaces progresivas, lo que garantiza que las generaciones malformadas resulten en errores de compilación en el nivel del dominio. Este enfoque permite que el LLM genere una representación intermedia (ej. un objeto Scenario compuesto de Steps y Actions) que luego es compilada o interpretada por el framework subyacente. La validación determinista del DSL (parser, JSON schema, type checker, compilador) es un componente clave que permite a un agente LLM operar en un bucle de generación y reparación autónomo.
Flujo de Generación y Verificación con DSL
- 1 Ingeniero Define la intención en lenguaje natural
- 2 LLM Genera código DSL basado en la intención y ejemplos in-context
- 3 Compilador/Validador DSL Verifica la sintaxis y semántica del código DSL generado
- 4 LLM (Agente) Repara errores de compilación/validación si los hay
- 5 Framework/Runtime Ejecuta el código DSL (ej. simula un escenario distribuido)
- 6 Ingeniero Revisa el resultado y refina la intención o el DSL
| Capa | Tecnología | Justificación |
|---|---|---|
| compute | LLMs (Large Language Models) | Generación de código DSL, co-diseño de abstracciones, interfaz de lenguaje natural |
| data-processing | Domain-Specific Languages (DSLs) | Restringir el espacio de generación de LLMs, proporcionar un vocabulario controlado, permitir validación determinista vs Lenguajes de propósito general (Java, Python) con prompts extensos |
| orchestration | Tickloom Framework (Java) | Modelo semántico para sistemas distribuidos, abstracciones de bajo nivel (Replica, Network, Storage, Clock), runtime determinista para simulación vs Simuladores de sistemas distribuidos genéricos, frameworks de pruebas unitarias tradicionales Bucle de tick single-threaded, tiempo lógico en ticks |
| observability | PlantUML / Mermaid / Graphviz | DSLs para modelado visual, utilizados por LLMs para generar diagramas a partir de descripciones vs Herramientas de dibujo manual, otras herramientas de diagramación |
Trade-offs
Ganancias
- ▲ Fiabilidad del código generado por LLMs
- ▲▲ Reducción del espacio de búsqueda para LLMs
- ▲ Facilidad de verificación y depuración
- ▲ Legibilidad y mantenibilidad del código DSL
Costes
- ▲ Costo inicial de diseño y mantenimiento del DSL y su modelo semántico
- △ Aplicabilidad limitada a dominios bien definidos y restringidos
Scenario<QuorumReplicaClient> scenario = QuorumStepBuilder.scenario("LWW lost update via server clock skew")
.servers(ATHENS, BYZANTIUM, CYRENE)
.clients(ALICE, BOB, READER)
.client(ALICE).connectedTo(ATHENS)
.client(BOB).connectedTo(BYZANTIUM)
.given(g -> g.serverTimeAt(ATHENS, 1_000L)
.serverTimeAt(BYZANTIUM, 2_000L))
.steps(s -> {
s.client(BOB).writes(KEY, "B").expectSuccess();
s.client(ALICE).writes(KEY, "A").expectSuccess();
s.client(READER).reads(KEY)
.expectResponse(v -> "B".equals(v));
});@Override
protected Map<MessageType, Handler> initialiseHandlers() {
return Map.of(
LWWMessageType.CLIENT_SET_REQUEST, this::handleClientSetRequest,
LWWMessageType.CLIENT_GET_REQUEST, this::handleClientGetRequest,
LWWMessageType.INTERNAL_SET_REQUEST, this::handleInternalSetRequest,
LWWMessageType.INTERNAL_GET_REQUEST, this::handleInternalGetRequest,
LWWMessageType.INTERNAL_SET_RESPONSE, this::handleInternalSetResponse,
LWWMessageType.INTERNAL_GET_RESPONSE, this::handleInternalGetResponse
);
}
private void handleClientGetRequest(Message message) {
var req = deserializePayload(message.payload(), ClientGetRequest.class);
var internalReq = new InternalGetRequest(req.key());
this.<InternalGetResponse>quorumRequest(LWWMessageType.INTERNAL_GET_REQUEST, internalReq)
.countResponseIf(r -> true) // any response is fine, just need a majority
.send()
.whenComplete((responses, error) -> {
if (error != null) {
send(createMessage(message.source(), message.correlationId(), new ClientGetResponse(req.key(), null, false), LWWMessageType.CLIENT_GET_RESPONSE));
return;
}
byte[] highestValue = null;
long highestTimestamp = -1;
for (InternalGetResponse r : responses.values()) {
if (r.value() != null && r.timestamp() > highestTimestamp) {
highestTimestamp = r.timestamp();
highestValue = r.value();
}
}
boolean found = highestValue != null;
send(createMessage(message.source(), message.correlationId(), new ClientGetResponse(req.key(), highestValue, found), LWWMessageType.CLIENT_GET_RESPONSE));
});
}Fundamentos Teóricos
La idea de los Domain-Specific Languages (DSLs) tiene raíces profundas en la informática, remontándose a los trabajos de John McCarthy sobre Lisp en los años 50, que ya permitía la creación de lenguajes para dominios específicos. El concepto de modelado de dominio y lenguaje ubicuo, central para la efectividad de los DSLs en este contexto, fue popularizado por Eric Evans en su libro "Domain-Driven Design" (2003). La aplicación de DSLs para mejorar la confiabilidad y la verificación en sistemas complejos, especialmente los distribuidos, se alinea con la investigación en verificación formal y pruebas de modelos (model checking), donde la reducción del espacio de estados es crucial. Trabajos como los de Leslie Lamport sobre Paxos (1998) y Diego Ongaro y John Ousterhout sobre Raft (2014) demuestran la complejidad inherente de los algoritmos de consenso y la necesidad de herramientas que simplifiquen su implementación y verificación, un problema que Tickloom busca abordar con un modelo semántico y un DSL. La noción de que el diseño se descubre a través de la implementación y la iteración resuena con la filosofía de "Design Thinking" y la ingeniería de software ágil, en contraste con el modelo de cascada de especificación upfront.