Una vez comprendidos los conceptos básicos de Spring AI para interactuar con los modelos de la forma más sencilla, vamos a revisar conceptos más avanzados para poder crear aplicaciones más potentes, exprimiendo muchas de las grandes ventajas de la IA.

Algunas de las features que exploraremos a continuación pasan desapercibidas en la gran mayoría de los casos, pero son importantes de cara a la experiencia de usuario o integración con otras aplicaciones. Es el caso del contexto de los prompts, de forma que cuando realizamos múltiples preguntas a un chatbot, pueda comprender las alusiones indirectas a ciertos términos:

simonrodriguez@simonrodriguez:~$ ollama run llama3.2:1b »>> cual es la población de España hasta el año 2023? Solo indica el número de habitantes Según los datos del Instituto Nacional de Estadística (INE) de España, en el año 2023 la población total de España es de aproximadamente 47.761.000 habitantes. »>> y que idioma se habla? El idioma oficial de España es el español, y está dividido en varias variantes regionales. Algunos de los idiomas más comunes son: * Español (castellano) * Catalán (catalan) * Gallego (gallego) * Balear (barcelonesa) * Vasconés (vascuno) * Aranés * Asturian * Basque Además, también hay dialectos y variedades regionales del español que no son oficiales pero se hablan en diferentes partes de España.

En este ejemplo se puede ver cómo, en la segunda pregunta, no necesitamos especificar que nos referimos a España.

Otra de las funcionalidades que se revisará es la opción de usar “herramientas” (tools) para enriquecer la información que posee el modelo. Esto se debe a que los LLMs son modelos preentrenados, lo que significa que tienen información hasta una fecha concreta, no pudiendo ofrecer resultados en tiempo real.

simonrodriguez@simonrodriguez:~$ ollama run llama3.2:1b >>> tell me your cut off date My knowledge cutoff is currently December 2023, but I have access to more recent information via internet search. >>> Send a message (/? for help)

Las aplicaciones más conocidas, como ChatGPT, DeepSeek y demás, también obtienen dichos datos de tiempo real por funcionalidades externas al propio LLM.

Advisors

Los Advisors permiten interceptar, modificar y enriquecer las interacciones con el modelo. Entre los beneficios se incluyen el uso de patrones comunes de IA, transformar los datos enviados y recibidos de los LLMs y portabilidad entre modelos y casos de uso.

Componentes principales

Como se puede ver en la siguiente imagen, el API se basa en las clases CallAroundAdvisor y CallAroundAdvisorChain (y sus homólogas para el uso de streaming), además de AdvisedRequest y AdvisedResponse para interactuar con la petición y respuesta. Estas últimas entidades, además, incluyen un adviseContext para compartir el estado a través de la cadena de ejecución.

el API se basa en las clases CallAroundAdvisor y CallAroundAdvisorChain (y sus homólogas para el uso de streaming), además de AdvisedRequest y AdvisedResponse para interactuar con la petición y respuesta. Estas últimas entidades además incluyen un adviseContext para compartir el estado a través de la cadena de ejecución.

NextAroundCall y nextAroundStream son los métodos clave para poder examinar los datos del prompt, customizarlos, invocar la siguiente entidad en la cadena de ejecución de advisors, bloquear la petición, examinar la respuesta y poder lanzar excepciones. También existen métodos como getOrder (indica el orden del Advisor en la cadena de ejecución) y getName (nombre único del Advisor).

La cadena de ejecución creada por Spring AI permite la ejecución secuencial de múltiples advisors ordenados por el método getOrder. Los valores más bajos se ejecutan primero, siendo el último Advisor el que envía la petición al LLM. En la siguiente imagen se puede ver el flujo de interacción entre los Advisors y el modelo.

Spring AI crea una AdvisedRequest a partir del Prompt del usuario con un AdvisorContext vacío.  Cada Advisor en la cadena procesa la petición, pudiendo modificarla o bloquearla, debiendo en este caso crear la respuesta.  El último Advisor creado por el framework, envía la petición al Chat Model.  La respuesta del modelo se envía de vuelta a la cadena de Advisors convirtiéndola en AdvisedResponse.  Cada Advisor puede procesar o modificar la respuesta.  La AdvisedResponse se retorna al cliente.
  1. Spring AI crea una AdvisedRequest a partir del prompt del usuario con un AdvisorContext vacío.
  2. Cada Advisor en la cadena procesa la petición, pudiendo modificarla o bloquearla, debiendo en este caso crear la respuesta.
  3. El último Advisor creado por el framework, envía la petición al Chat Model.
  4. La respuesta del modelo se envía de vuelta a la cadena de Advisors convirtiéndola en AdvisedResponse.
  5. Cada Advisor puede procesar o modificar la respuesta.
  6. La AdvisedResponse se retorna al cliente.

Orden de Advisors

El orden de ejecución de los Advisors en la cadena viene determinado por el método getOrder. Algunos de los puntos a tener en cuenta para su ejecución son:

En el caso de que se necesite ejecutar un Advisor primero en la petición y en la respuesta:

Implementar un Advisor

Para crear un Advisor se tiene que implementar CallAroundAdvisor con su método aroundCall:

public class CustomAdvisor implements CallAroundAdvisor {

    @Override
    public String getName() {
        return this.getClass().getSimpleName();
    }

    @Override
    public int getOrder() {
        return 0;
    }

    @Override
    public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
        AdvisedResponse originalResponse = chain.nextAroundCall(this.beforeCall(advisedRequest));
        return this.afterCall(originalResponse);
    }
//Metodos para completar el comportamiento
}

Advisors proporcionados por el framework

Spring AI proporciona algunos Advisors ya implementados:

  1. Chat Memory:
memoryChatClient = chatClientBuilder
    .clone()
    .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
    .build();
  1. Question Answering:
  1. Content Safety:

Mejores prácticas

Structured Output

Este API ayuda a convertir la salida de un modelo a un formato estructurado para facilitar la integración con otros componentes/aplicaciones.

Antes de realizar la llamada al modelo, el Converter incluye instrucciones al prompt para que el modelo genere la respuesta en el formato deseado. Posteriormente el Converter transforma la salida en instancias del formato requerido, lo que implica transformar texto en la estructura de datos correspondiente, como puede ser JSON, XML o entidades de dominio.

Antes de realizar la llamada al modelo, el Converter incluye instrucciones al prompt para que el modelo genere la respuesta en el formato deseado. Posteriormente, el Converter transforma la salida en instancias del formato requerido, lo que implica transformar texto en la estructura de datos correspondiente, como puede ser JSON, XML o entidades de dominio.

Converters proporcionados por Spring AI

En estos momentos Spring AI proporciona las siguientes implementaciones de Converters:

AbstractConversionServiceOutputConverter, AbstractMessageOutputConverter, BeanOutputConverter, MapOutputConverter, ListOutputConverter
@GetMapping("/map")
Map<String, Object> getStandardOutputMap() {


    MapOutputConverter outputConverter = new MapOutputConverter();

    String format = outputConverter.getFormat();
    String template = """
            Dame una lista de las 3 criptomonedas mas importantes con su nombre, su abreviatura y una breve descripcion.
            Ejemplo: Bitcoin -> "abreviatura": "BTC", "descripcion": "la criptomoneda mas importante".
            {format}
            """;


    PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("format", format));
    Prompt prompt = new Prompt(promptTemplate.createMessage());


    return chatClient.prompt(prompt)
            .call()
            .entity(outputConverter);
}

Tool Calling

La integración con “herramientas” permite al modelo ejecutar funciones de cliente para acceder a información o ejecutar tareas cuando sea necesario. Sus principales usos son:

Es importante indicar que este patrón no es una funcionalidad que proporcionen los modelos, sino una que proporciona la aplicación en sí. El modelo realiza la petición a la “herramienta” con los argumentos, pero quien ejecuta la función es la aplicación.

Funcionamiento

Los conceptos y componentes importantes para el funcionamiento del Tool Calling son:

Incluir la definición de la “herramienta” que se quiere proporcionar al modelo. Esta definición está compuesta por: nombre, descripción y el esquema de los parámetros de entrada  Cuando el modelo decide llamar a una “herramienta”, envía una respuesta con el nombre de la herramienta y los parámetros de entrada adaptados según el esquema definido.  La aplicación ejecuta la “herramienta”.  El resultado de la “herramienta” es procesado por la aplicación.  La aplicación envía el resultado al modelo.  El modelo genera la respuesta final usando la respuesta de la “herramienta” como contexto adicional.
  1. Incluir la definición de la “herramienta” que se quiere proporcionar al modelo. Esta definición está compuesta por: nombre, descripción y el esquema de los parámetros de entrada.
  2. Cuando el modelo decide llamar a una “herramienta”, envía una respuesta con el nombre de la herramienta y los parámetros de entrada adaptados según el esquema definido.
  3. La aplicación ejecuta la “herramienta”.
  4. El resultado de la “herramienta” es procesado por la aplicación.
  5. La aplicación envía el resultado al modelo.
  6. El modelo genera la respuesta final usando la respuesta de la “herramienta” como contexto adicional.

A continuación veremos varias formas de definir y ejecutar las “herramientas”.

Métodos como herramientas

Se proporcionan dos formas de crear “herramientas” desde métodos:

@Slf4j
public class BitcoinTool {

    @Tool(description = "Get the current price of bitcoin cripto currency in euros")
    Long getBitcoinPriceInEuros() {

        BitcoinInfo bitcoinInfo = null;
        String apiResponse = "";
        try {
            apiResponse = RestClient.create().get().uri(new URI("https://cex.io/api/last_price/BTC/EUR")).retrieve()
                    .body(String.class);

            bitcoinInfo = new ObjectMapper().readValue(apiResponse, BitcoinInfo.class);

        } catch (URISyntaxException | JsonProcessingException e) {
            e.printStackTrace();
        }

        log.info("The current bitcoin price in euros is {}", bitcoinInfo.getLprice());
        return bitcoinInfo.getLprice();
    }
}
@GetMapping("/btc")
String toolCallingBtcPrice() {


    return this.chatClient.prompt().user("Give me the current bitcoin price")
    .tools(new BitcoinTool()).call().content();
    }

También se pueden indicar las “herramientas” por defecto en el ChatClient.Builder con el método defaultTools() (así la herramienta tiene efecto sobre todas las peticiones), teniendo un comportamiento similar los casos para la clase ChatModel.

@Slf4j
public class PurchaseOrderTool {
    
    public void createBitcoinPurchaseOrder(@ToolParam(description = "Bitcoin amount") Integer bitcoinAmount, 
            @ToolParam(description = "Current bitcoin euros price") Long currentBitcoinEurosPrice) {
        log.info("Create a purchase order for {} btc with each btc at {} euros price", bitcoinAmount, currentBitcoinEurosPrice);
    }
}
@GetMapping("/btc-purchase-order")
String toolCallingBtcPriceSavePurchaseOrder() {


    Method method = ReflectionUtils.findMethod(PurchaseOrderTool.class, "createBitcoinPurchaseOrder", Integer.class, Long.class);
    ToolCallback toolCallback = MethodToolCallback.builder()
        .toolDefinition(ToolDefinition.builder(method)
            .description("Create bitcoin purchase order of bitcoin amount at current bitcoin euros price")
            .build())
        .toolMethod(method)
        .toolObject(new PurchaseOrderTool())
        .build();


    return this.chatClient.prompt().user("Give me the current bitcoin price in euros and create a bitcoin purchase order of 10 bitcoins at this current bitcoin euros price")
        .tools(new BitcoinTool())
        .tools(toolCallback)
        .call()
        .content();
}

Por otro lado, existen las siguientes limitaciones en el uso de métodos como “herramientas”. Básicamente, los siguientes tipos no se soportan como parámetros ni retornos de los métodos:

Funciones como herramientas

Otra forma de crear “herramientas” es a través de funciones. De la misma forma que los métodos, existen dos formas:

@Configuration(proxyBeanMethods = false)
class WeatherTools {
    
    public static final String CURRENT_WEATHER_TOOL = "currentWeather";
WeatherService weatherService = new WeatherService();

    @Bean(CURRENT_WEATHER_TOOL)
    @Description("Get the weather in location")
    Function<WeatherRequest, WeatherResponse> currentWeather() {
        return weatherService;
    }
}

Aún con todo esto, existen las siguientes limitaciones en el uso de funciones como “herramientas”, no soportando los siguientes tipos como parámetros de entrada o salida de las funciones:

Nota: se debe remarcar la importancia de crear una buena descripción para las “herramientas”, puesto que esto puede llevar a que el modelo no entienda que tiene que usar esa “herramienta” o usarla de forma incorrecta para generar una respuesta coherente.

Ejecución de herramientas

La ejecución de las “herramientas” se realiza mediante un proceso manejado por la interfaz ToolCallingManager. La implementación por defecto es a través de la clase DefaultToolCallingManager aunque se puede personalizar creando un bean de ToolCallingManager.

En la siguiente imagen se puede ver el flujo por defecto:

Se incluye la definición de la “herramienta” en la petición de chat (ChatRequest) y se llama al modelo que recibe dicha petición.  Cuando el modelo decide llamar a una “herramienta” envía una respuesta (ChatResponse) con el nombre de la “herramienta” y los parámetros correspondientes.  ChatModel envía la petición al ToolCallingManager.  El ToolCallingManager identifica y ejecuta la “herramienta”.  El resultado de la ejecución se devuelve al ToolCallingManager.  El ToolCallingManager devuelve el resultado al ChatModel.  El ChatModel envía el resultado al modelo.  El modelo genera la respuesta final usando el resultado de la “herramienta” como información adicional.
  1. Se incluye la definición de la “herramienta” en la petición de chat (ChatRequest) y se llama al modelo que recibe dicha petición.
  2. 2 Cuando el modelo decide llamar a una “herramienta”, envía una respuesta (ChatResponse) con el nombre de la “herramienta” y los parámetros correspondientes.
  3. ChatModel envía la petición al ToolCallingManager.
  4. El ToolCallingManager identifica y ejecuta la “herramienta”.
  5. El resultado de la ejecución se devuelve al ToolCallingManager.
  6. El ToolCallingManager devuelve el resultado al ChatModel.
  7. El ChatModel envía el resultado al modelo.
  8. El modelo genera la respuesta final usando el resultado de la “herramienta” como información adicional.

Descubrimiento de herramientas

La principal forma de usar las “herramientas” es de la forma que se ha explicado en las secciones anteriores (Métodos y funciones como herramientas), pero Spring AI también ofrece la opción de obtener “herramientas” de forma dinámica en tiempo de ejecución con la interfaz ToolCallbackResolver y los nombres de las “herramientas”.

public interface ToolCallbackResolver {

    @Nullable
    ToolCallback resolve(String toolName);

}

Por defecto, las instancias que implementan ToolCallbackResolver serían:

Especificación

En la documentación también se detalla la especificación de cara a tener más control sobre la personalización de las “herramientas”.

Demo

Creamos una app con los siguientes endpoints para ejemplificar lo visto hasta ahora:

Hola, me llamo Jose. ¡Hola! bienvenido a nuestro espacio. en qué puedo ayudarte hoy, josé? tienes alguna pregunta o necesitas algo en particular? estoy aquí para escucharte y responder a tus preguntas de manera amable y objetiva.
¿Cuál es mi nombre? No puedo conocer tu nombre. ¿Hay algo más en lo que pueda ayudarte?
Hola, mi nombre es Jose. ¡Hola, Jose! ¿En qué puedo ayudarte hoy? ¿Hay algo en particular que te gustaría hablar o necesitas información sobre algún tema en particular? Estoy aquí para escucharte y responder a tus preguntas.
¿Cuál es mi nombre? Tu nombre es Jose, como mencionaste anteriormente. ¿Qué tal? ¿Te parece bien?
Dame información de España. Escribe solo 1 línea. hasta mi fecha de conocimiento en diciembre de 2023, españa era un país europeo y una monarquía constitucional con una monarquía constitucional, gobernada por el rey felipe vi desde el año 1975.
c.e.s.d.s.advisors.CustomAdvisor Input transformed DAME INFORMACION DE ESPANA. ESCRIBE SOLO 1 LINEA. c.e.s.d.s.advisors.CustomAdvisor Peticion al modelo AdvisedRequest chatModel=org.springframework.a1. c.e.s.d.s.advisors.CustomAdvisor Orignal response AdvisedResponse response=ChatResponse metadata={
{ "Criptomonedas": { "Bitcoin": { "abreviatura": "ВТС", "descripcion": "la criptomoneda mas importante" { "Ethereum": { "abreviatura": "ETH", "descripcion": "la segunda criptomoneda mas importante" "Tether": { "abreviatura": "USDT", "descripcion": "moneda de reserva fuerte" }
"ethereum": "{"abreviatura":"ETH", "descripcion":"la segunda criptomoneda mas importante"}", "litecoin": "{"abreviatura":"LTC","descripcion":"la tercera criptomoneda mas importante"}", "bitcoin": "{"abreviatura":"BTC", "descripcion":"la criptomoneda mas importante"}"
“Bitcoin”, “Ethereum”, “Bitcoin Cash”, “Litecoin”, “Dogecoin”,
I'm not able to provide real-time financial data or current prices for cryptocurrencies like Bitcoin. However, I can suggest some options for you to find the current Bitcoin price. You can check popular cryptocurrency exchange websites such as Coinbase, Binance, or Kraken, which often provide up-to-date pricing information for Bitcoin and other cryptocurrencies. You can also try searching online for "Bitcoin price" or "current Bitcoin price" to find various sources that may have the information you're looking for. Additionally, you can check reputable financial news websites and apps, such as Bloomberg, CNBC, or CoinMarketCap, which often provide up-to-date pricing information for cryptocurrencies, including Bitcoin.
The current Bitcoin price is approximately 63600 EUR. Please note that cryptocurrency prices can fluctuate rapidly and may change significantly over time. This information is provided for informational purposes only and should not be considered as investment advice. If you would like to know the current price of Bitcoin in USD, I can try to provide an update.
c.e.s.d.springai demo.tools.BitcoinTool : The current bitcoin price in euros is 63600 c.e.s.d.s.tools.Purchase0rderTool : Create a purchase order for 10 btc with each btc at 1000 euros price
Creating bitcoin purchase order with function criptomarketservice criptomarketResponse/currency=BC. amount=2, currencyPrice=80000, totalPrice=160000)

En este enlace se puede descargar el código de la aplicación de ejemplo.

Conclusión

En este capítulo de Spring AI se han analizado distintas formas de enriquecer o transformar la entrada y salida del modelo para la generación de respuestas coherentes, incluso con información en tiempo real, aunque es cierto que todavía quedan detalles por pulir.

En siguientes posts nos centraremos en el famoso RAG y las funcionalidades que lo soportan.

Referencias

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete