Ir al contenido

Aether: el arnés que convierte un LLM en un agente confiable

·897 palabras·5 mins
Michael Antonio Tomaylla
Autor
Michael Antonio Tomaylla
De la complejidad técnica a la simplicidad que genera valor

De un bot que responde a un agente que trabaja
#

Seamos honestos: conectar un modelo a Telegram y hacer que conteste mensajes es cosa de una tarde. Lo difícil viene después, cuando quieres que ese bot deje de ser un loro con buena dicción y empiece a encargarse de cosas por ti.

Esa es la historia de Aether: cogemos un bot que solo responde y le vamos dando mejoras hasta convertirlo en un agente con identidad, memoria y manos. Y lo hacemos sin frameworks mágicos, con piezas que ya conoces.

La caja de herramientas del día:

  • Java 25 y Spring AI 2 como runtime
  • GLM 5.2 servido en NVIDIA como modelo
  • Git (una rama por cada mejora)
  • Telegram como la cara del agente

Dos generaciones de agentes
#

Antes de escribir código conviene ubicarse. Los agentes han vivido dos generaciones.

La primera generación son pipelines orquestados: tú dibujas el grafo y el sistema lo recorre. Herramientas como LangGraph, AutoGen, CrewAI o LlamaIndex brillan aquí. El camino es fijo:

paso 1: buscar  →  paso 2: resumir  →  paso 3: responder

La segunda generación es el agent loop: le das un objetivo y un puñado de herramientas, y el modelo decide qué paso dar en cada turno. Es el patrón detrás de asistentes como Claude, GPT o Gemini.

Aether nace en la segunda generación: no le dibujamos el camino, se lo dejamos decidir.

La decisión clave: ¿dónde vive el ChatClient?
#

Toda la arquitectura se reduce a una pregunta: ¿dónde ponemos el ChatClient? Lo más tentador es conectar la entrada de Telegram directo al modelo y pasarle el mensaje tal cual. Eso es la Versión 0: el motor solo responde.

// El cerebro: un ChatClient de Spring AI
@Bean
ChatClient aether(ChatClient.Builder b) {
    return b.build();
}

// La cara: por cada mensaje que llega de Telegram…
String respuesta = aether.prompt()
        .user(entrada)   // ← texto del chat
        .call()
        .content();

responder(chatId, respuesta);   // → de vuelta al móvil

El ChatClient es el motor y Telegram es la cara. Funciona… para jugar. Para usarlo a diario le faltan cuatro cosas y en cuanto le pides algo real se nota:

  1. Identidad — no tiene personalidad estable.
  2. Contexto — no sabe para quién trabaja ni dónde está.
  3. Acción — no sabe qué puede tocar ni cuándo parar.

Responder mensajes es fácil. Encargarse de algo ya pide más piezas.

Harness engineering
#

Aquí entra el concepto central del post: el harness engineering (ingeniería del arnés).

Spring AI pone el runtime. El arnés decide cómo se usa.

Diseñar el arnés es decidir qué ve el agente, qué puede hacer, qué recuerda y cuándo pide permiso. El modelo es el músculo; el arnés es todo lo que lo convierte en algo confiable. Y lo bonito es que son pocas líneas.

Los 5 pilares del arnés
#

Cada pilar vive en su propia rama. Puedes hacer git checkout pilar-1-soul y ver al agente ganar superpoderes en vivo. Todo el código está en el repo: github.com/darkmtrance/aether.

Pilar 1 · Corazón — SOUL.md le da personalidad
#

🌿 Rama: pilar-1-soul

Dejamos por escrito cómo queremos que actúe cuando no estamos encima. El archivo se relee en cada turno, así que ajustas su carácter sin recompilar nada.

String respuesta = aether.prompt()
        .system(harness.readSoul())   // ← SOUL.md, en caliente
        .user(entrada)
        .call()
        .content();

Pilar 2 · Visor — USER.md + AGENTS.md le abren los ojos
#

🌿 Rama: pilar-2-context

Contexto vivo. El agente necesita saber para quién trabaja, dónde está y qué día es. Un CallAdvisor inyecta ese contexto en cada turno.

public ChatClientResponse adviseCall(
        ChatClientRequest req, CallAdvisorChain chain) {
    return chain.nextCall(req.mutate()
            .prompt(req.prompt().augmentSystemMessage(
                    contexto()))   // fecha + USER.md + AGENTS.md
            .build());
}

Pilar 3 · Manos — SKILLS.md le da herramientas reales
#

🌿 Rama: pilar-3-skills

El modelo elige la skill, pero el comando concreto sale del catálogo SKILLS.md (una allowlist), nunca del modelo. Así el agente ejecuta git, docker o mvn sin shell de por medio y con cero inyección.

@Tool(description = "Ejecuta una skill del catálogo")
public String ejecutarSkill(@ToolParam String nombre) {
    String comando = leerCatalogo().get(nombre);
    var p = new ProcessBuilder(tokenizar(comando)).start();
    // git, docker, mvn — sin shell, cero inyección
    ...
}

Pilar 4 · Mochila — MEMORY.md recuerda por él
#

🌿 Rama: pilar-4-memory

La memoria se genera sola, turno a turno. El hilo de la conversación vive en ChatMemory y lo que pasó ayer sigue ahí hoy.

var memoria = MessageWindowChatMemory.builder()
        .maxMessages(20).build();   // el hilo

builder.defaultAdvisors(
            contextAdvisor,
            MessageChatMemoryAdvisor.builder(memoria).build())
       .defaultTools(skillTools, memoryTools);   // MEMORY.md

Pilar 5 · Escudo — humano en el bucle
#

🌿 Rama: pilar-5-shield

Las acciones destructivas pasan por un gate de aprobación explícita. El agente propone, tú autorizas. Y el borrado real no lo dispara el LLM: lo dispara la app cuando confirmas.

@Tool(description = "Solicita borrar un archivo. NO lo borra")
public String solicitarBorradoArchivo(String ruta) {
    approvals.registrarBorradoPendiente(ruta);
    return "⚠️ Escribe \"Confirmo\" para proceder.";
}
// el borrado real lo dispara la app, no el LLM

El resultado
#

De pocas líneas a un agente con alma (SOUL.md), ojos (USER.md + AGENTS.md), manos (SKILLS.md), memoria (MEMORY.md) y freno (el gate de “Confirmo”).

Lo interesante no es el modelo —ese es intercambiable— sino el arnés que lo rodea. Ese arnés es tuyo: define cuánto le confías al agente y cuánto te reservas tú. El modelo sostiene el volante solo cuando y como tú lo decides.

Author: humano <yo>
Co-authored-by: IA — sostuvo el arnés, no tomó el volante.