Artículo

API fluida de ChatClient en Spring AI

API fluida de ChatClient en Spring AI

1. Visión general

En este tutorial, exploraremos la API fluida de ChatClient, una característica del módulo Spring AI versión 1.0.0 M1.

La interfaz ChatClient del módulo Spring AI permite la comunicación con modelos de IA, permitiendo a los usuarios enviar indicaciones y recibir respuestas estructuradas. Sigue el patrón builder, ofreciendo una API similar a WebClient, RestClient y JdbcClient.

2. Ejecución de indicaciones mediante ChatClient

Podemos usar el cliente en Spring Boot como un bean autoconfigurado, o crear una instancia de forma programática.

Primero, agreguemos la dependencia spring-ai-openai-spring-boot-starter a nuestro pom.xml:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>

Con esto, podemos inyectar la instancia ChatClient.Builder en nuestros componentes gestionados por Spring:

@RestController
@RequestMapping("api/articles")
class BlogsController {

    private final ChatClient chatClient;

    public BlogsController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    // ...
}

Ahora, creemos un endpoint sencillo que acepte una pregunta como parámetro de consulta y reenvíe el prompt a la IA:

@GetMapping("v1")
String askQuestion(@RequestParam(name = "question") String question) {
    return chatClient.prompt()
      .user(question)
      .call()
      .chatResponse()
      .getResult()
      .getOutput()
      .getContent();
}

Como vemos, el fluido ChatClient nos permite crear fácilmente una solicitud de prompt a partir del String de entrada del usuario, llamar a la API y recuperar el contenido de la respuesta como texto.

Además, si solo nos interesa el cuerpo de la respuesta como un String y no necesitamos metadatos como códigos de estado o encabezados, podemos simplificar nuestro código utilizando el método content() para agrupar los últimos cuatro pasos. Refactoricemos el código y añadamos esta mejora:

@GetMapping("v1")
String askQuestion(@RequestParam(name = "question") String question) {
    return chatClient.prompt()
      .user(question)
      .call()
      .content();
}

Si enviamos una solicitud GET ahora, recibiremos una respuesta sin una estructura definida, similar a la salida predeterminada de ChatGPT cuando se accede a través de un navegador: request article recommedation

3. Mapeo de la respuesta a un formato específico

Como vemos, la interfaz ChatClient simplifica el proceso de reenviar consultas de usuarios a un modelo de chat y enviar la respuesta de vuelta. Sin embargo, en la mayoría de los casos, querremos que la salida del modelo esté en un formato estructurado, que luego puede serializarse a JSON.

La API expone un método entity(), que nos permite definir una estructura de datos específica para la salida del modelo. Revisemos nuestro código para asegurarnos de que devuelve una lista de objetos Article, cada uno con un título y un conjunto de etiquetas:

record Article(String title, Set<String> tags) {
}

@GetMapping("v2")
List<Article> askQuestionAndRetrieveArticles(@RequestParam(name = "question") String question) {
    return chatClient.prompt()
      .user(question)
      .call()
      .entity(new ParameterizedTypeReference<List<Article>>() {});
}

Si ejecutamos la solicitud ahora, esperamos que el endpoint devuelva la recomendación de Article en una lista JSON válida: request aritcles as json

4. Proveer contexto adicional

Hemos aprendido a usar el módulo Spring AI para crear prompts, enviarlos a un modelo de IA y recibir respuestas estructuradas. Sin embargo, las recomendaciones de artículos devueltas por nuestra API REST son ficticias y pueden no existir en realidad, en nuestro sitio web.

Para abordar esto, el ChatClient aprovecha el patrón Retrieval Augmented Generation (RAG), combinando la recuperación de datos de una fuente con un modelo generativo para proporcionar respuestas más precisas. Usaremos un vector store para aprovechar RAG y cargarlo con documentos relevantes para nuestro caso de uso.

Primero, crearemos un VectorStore y lo cargaremos con los datos aumentados desde un archivo local, durante la inicialización de la clase:

@RestController
@RequestMapping("api/articles")
public class BlogsController {

    private final ChatClient chatClient;
    private final VectorStore vectorStore;

    public BlogsController(ChatClient.Builder chatClientBuilder, EmbeddingModel embeddingModel) throws IOException {
        this.chatClient = chatClientBuilder.build();
        this.vectorStore = new SimpleVectorStore(embeddingModel);
        initContext();
    }

    void initContext() throws IOException {
        List<Document> documents = Files.readAllLines(Path.of("src/main/resources/articles.txt"))
          .stream()
          .map(Document::new)
          .toList();

        vectorStore.add(documents);
    }
  
    // ...
}

Como vemos, leemos todas las entradas de articles.txt y creamos un nuevo Document para cada línea de este archivo. No hace falta decir que no tenemos que depender de un archivo – podemos usar cualquier fuente de datos si lo necesitamos.

Después, proporcionaremos los datos aumentados al modelo envolviendo el VectorStore en un QuestionAnswerAdvisor:

@GetMapping("v3")
List<Article> askQuestionWithContext(@RequestParam(name = "question") String question) {
    return chatClient.prompt()
      .advisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()))
      .user(question)
      .call()
      .entity(new ParameterizedTypeReference<List<Article>>() {});
}

Como resultado, nuestra aplicación ahora devuelve datos exclusivamente del contexto aumentado: request articles using rag

5. Conclusión

En este artículo, exploramos el ChatClient de Spring AI. Comenzamos enviando consultas simples de usuarios al modelo y leyendo sus respuestas como texto plano. Luego, mejoramos nuestra solución al obtener la respuesta del modelo en un formato estructurado y específico.

Finalmente, aprendimos a cargar el contexto del modelo con una colección de documentos para proporcionar respuestas precisas basadas en nuestros propios datos. Logramos esto utilizando un VectorStore y un QuestionAnswerAdvisor.

Newsletter Semanal de Java

Cada viernes recibe lo más nuevo del ecosistema Java: frameworks, herramientas y mejores prácticas.

Sin spam. Cancela cuando quieras.

Compartir artículo