1. Introduzione
Questo codelab si concentra sul modello linguistico di grandi dimensioni (LLM) Gemini, ospitato su Vertex AI su Google Cloud. Vertex AI è una piattaforma che comprende tutti i prodotti, i servizi e i modelli di machine learning su Google Cloud.
Utilizzerai Java per interagire con l'API Gemini utilizzando il framework LangChain4j. Vedrai esempi concreti per sfruttare l'LLM per rispondere a domande, generare idee, estrarre entità e contenuti strutturati, utilizzare la Retrieval-Augmented Generation e chiamare funzioni.
Che cos'è l'AI generativa?
L'AI generativa si riferisce all'uso dell'intelligenza artificiale per creare nuovi contenuti, come testo, immagini, musica, audio e video.
L'AI generativa sfrutta modelli linguistici di grandi dimensioni (LLM) in grado di eseguire più operazioni contemporaneamente e di eseguire operazioni pronte all'uso, tra cui riepilogo, domande e risposte, classificazione e altro ancora. Con un addestramento minimo, i modelli di base possono essere adattati per casi d'uso mirati con pochissimi dati di esempio.
Come funziona l'AI generativa?
L'AI generativa utilizza un modello di machine learning per apprendere i pattern e le relazioni in un set di dati di contenuti creati dall'uomo. Quindi utilizza i pattern appresi per generare nuovi contenuti.
Il modo più comune per addestrare un modello di AI generativa consiste nell'utilizzare l'apprendimento supervisionato. Al modello viene assegnato un set di contenuti creati dall'uomo ed etichette corrispondenti. Poi, impara a generare contenuti simili a quelli creati dall'uomo.
Quali sono le applicazioni comuni dell'AI generativa?
L'IA generativa può essere utilizzata per:
- Migliorare le interazioni con i clienti grazie a esperienze avanzate di chat e ricerca.
- Esplora enormi quantità di dati non strutturati attraverso interfacce di conversazione e riassunti.
- Assistere nelle attività ripetitive come rispondere alle richieste di proposta, localizzare i contenuti di marketing in diverse lingue, verificare la conformità dei contratti con i clienti e altro ancora.
Quali sono le offerte di AI generativa di Google Cloud?
Con Vertex AI, puoi interagire con i modelli di base, personalizzarli e incorporarli nelle tue applicazioni con competenze di machine learning minime o nulle. Puoi accedere ai foundation model su Model Garden, ottimizzarli tramite una semplice UI su Vertex AI Studio oppure utilizzarli in un notebook di data science.
Vertex AI Search and Conversation offre agli sviluppatori il modo più rapido per creare motori di ricerca e chatbot basati sull'AI generativa.
Gemini in Google Cloud, basato su Gemini, è un collaboratore basato sull'AI disponibile in Google Cloud e negli IDE per aiutarti a fare di più in meno tempo. Gemini Code Assist fornisce il completamento del codice, la generazione di codice, le spiegazioni del codice e ti consente di chattare con lui per porre domande tecniche.
Cos'è Gemini?
Gemini è una famiglia di modelli di AI generativa sviluppati da Google DeepMind progettati per casi d'uso multimodali. Multimodale significa che può elaborare e generare diversi tipi di contenuti, come testo, codice, immagini e audio.

Gemini è disponibile in diverse varianti e dimensioni:
- Gemini 2.0 Flash: le nostre funzionalità di nuova generazione più recenti e funzionalità migliorate.
- Gemini 2.0 Flash-Lite: un modello Gemini 2.0 Flash ottimizzato per l'efficienza dei costi e la bassa latenza.
- Gemini 2.5 Pro: il nostro modello di ragionamento più avanzato finora.
- Gemini 2.5 Flash: un modello di pensiero che offre funzionalità complete. È progettato per offrire un equilibrio tra prezzo e prestazioni.
Funzionalità principali:
- Multimodalità: la capacità di Gemini di comprendere e gestire più formati di informazioni è un passo avanti significativo rispetto ai tradizionali modelli linguistici basati solo sul testo.
- Prestazioni: Gemini 2.5 Pro supera l'attuale stato dell'arte in molti benchmark ed è stato il primo modello a superare gli esperti umani nel difficile benchmark MMLU (Massive Multitask Language Understanding).
- Flessibilità: le diverse dimensioni di Gemini lo rendono adattabile a vari casi d'uso, dalla ricerca su larga scala all'implementazione su dispositivi mobili.
Come posso interagire con Gemini su Vertex AI da Java?
Sono disponibili due opzioni:
- La libreria ufficiale Vertex AI Java API per Gemini.
- framework LangChain4j.
In questo codelab utilizzerai il framework LangChain4j.
Che cos'è il framework LangChain4j?
Il framework LangChain4j è una libreria open source per l'integrazione di LLM nelle tue applicazioni Java, orchestrando vari componenti, come l'LLM stesso, ma anche altri strumenti come database vettoriali (per ricerche semantiche), caricatori e splitter di documenti (per analizzare i documenti e imparare da loro), analizzatori di output e altro ancora.
Il progetto è stato ispirato dal progetto Python LangChain, ma con l'obiettivo di servire gli sviluppatori Java.

Cosa imparerai a fare
- Come configurare un progetto Java per utilizzare Gemini e LangChain4j
- Come inviare il tuo primo prompt a Gemini in modo programmatico
- Come mostrare progressivamente le risposte di Gemini
- Come creare una conversazione tra un utente e Gemini
- Come utilizzare Gemini in un contesto multimodale inviando sia testo che immagini
- Come estrarre informazioni strutturate utili da contenuti non strutturati
- Come manipolare i modelli di prompt
- Come eseguire la classificazione del testo, ad esempio l'analisi del sentiment
- Come chattare con i tuoi documenti (Retrieval Augmented Generation)
- Come estendere i chatbot con le chiamate di funzione
- Come utilizzare Gemma localmente con Ollama e TestContainers
Che cosa ti serve
- Conoscenza del linguaggio di programmazione Java
- Un progetto Google Cloud
- Un browser, ad esempio Chrome o Firefox
2. Configurazione e requisiti
Configurazione dell'ambiente autonomo
- Accedi alla console Google Cloud e crea un nuovo progetto o riutilizzane uno esistente. Se non hai ancora un account Gmail o Google Workspace, devi crearne uno.



- Il nome del progetto è il nome visualizzato per i partecipanti a questo progetto. È una stringa di caratteri non utilizzata dalle API di Google. Puoi sempre aggiornarlo.
- L'ID progetto è univoco in tutti i progetti Google Cloud ed è immutabile (non può essere modificato dopo l'impostazione). La console Cloud genera automaticamente una stringa univoca, di solito non ti interessa di cosa si tratta. Nella maggior parte dei codelab, dovrai fare riferimento all'ID progetto (in genere identificato come
PROJECT_ID). Se l'ID generato non ti piace, puoi generarne un altro casuale. In alternativa, puoi provare a crearne uno e vedere se è disponibile. Non può essere modificato dopo questo passaggio e rimane per tutta la durata del progetto. - Per tua informazione, esiste un terzo valore, un numero di progetto, utilizzato da alcune API. Scopri di più su tutti e tre questi valori nella documentazione.
- Successivamente, devi abilitare la fatturazione in Cloud Console per utilizzare le risorse/API Cloud. Completare questo codelab non costa molto, se non nulla. Per arrestare le risorse ed evitare addebiti oltre a quelli previsti in questo tutorial, puoi eliminare le risorse che hai creato o il progetto. I nuovi utenti di Google Cloud possono beneficiare del programma prova senza costi di 300$.
Avvia Cloud Shell
Sebbene Google Cloud possa essere gestito da remoto dal tuo laptop, in questo codelab utilizzerai Cloud Shell, un ambiente a riga di comando in esecuzione nel cloud.
Attiva Cloud Shell
- Nella console Cloud, fai clic su Attiva Cloud Shell
.

Se è la prima volta che avvii Cloud Shell, viene visualizzata una schermata intermedia che ne descrive le funzionalità. Se è stata visualizzata una schermata intermedia, fai clic su Continua.

Bastano pochi istanti per eseguire il provisioning e connettersi a Cloud Shell.

Questa macchina virtuale è caricata con tutti gli strumenti di sviluppo necessari. Offre una home directory permanente da 5 GB e viene eseguita in Google Cloud, migliorando notevolmente le prestazioni e l'autenticazione della rete. Gran parte del lavoro per questo codelab, se non tutto, può essere svolto con un browser.
Una volta eseguita la connessione a Cloud Shell, dovresti vedere che il tuo account è autenticato e il progetto è impostato sul tuo ID progetto.
- Esegui questo comando in Cloud Shell per verificare che l'account sia autenticato:
gcloud auth list
Output comando
Credentialed Accounts
ACTIVE ACCOUNT
* <my_account>@<my_domain.com>
To set the active account, run:
$ gcloud config set account `ACCOUNT`
- Esegui questo comando in Cloud Shell per verificare che il comando gcloud conosca il tuo progetto:
gcloud config list project
Output comando
[core] project = <PROJECT_ID>
In caso contrario, puoi impostarlo con questo comando:
gcloud config set project <PROJECT_ID>
Output comando
Updated property [core/project].
3. Preparazione dell'ambiente di sviluppo
In questo codelab, utilizzerai il terminale Cloud Shell e l'editor di Cloud Shell per sviluppare i tuoi programmi Java.
Abilita le API Vertex AI
Nella console Google Cloud, assicurati che il nome del progetto sia visualizzato nella parte superiore della console Google Cloud. In caso contrario, fai clic su Seleziona un progetto per aprire il selettore di progetti e seleziona il progetto che ti interessa.
Puoi abilitare le API Vertex AI dalla sezione Vertex AI della console Google Cloud o dal terminale Cloud Shell.
Per abilitare l'API dalla console Google Cloud, vai prima alla sezione Vertex AI del menu della console Google Cloud:

Fai clic su Abilita tutte le API consigliate nella dashboard di Vertex AI.
Verranno abilitate diverse API, ma la più importante per il codelab è aiplatform.googleapis.com.
In alternativa, puoi abilitare questa API anche dal terminale Cloud Shell con il seguente comando:
gcloud services enable aiplatform.googleapis.com
Clona il repository GitHub
Nel terminale Cloud Shell, clona il repository per questo codelab:
git clone https://github.com/glaforge/gemini-workshop-for-java-developers.git
Per verificare che il progetto sia pronto per l'esecuzione, puoi provare a eseguire il programma "Hello World".
Assicurati di trovarti nella cartella di primo livello:
cd gemini-workshop-for-java-developers/
Crea il wrapper Gradle:
gradle wrapper
Corri con gradlew:
./gradlew run
Dovresti vedere l'output seguente:
.. > Task :app:run Hello World!
Apri e configura Cloud Editor
Apri il codice con l'editor di Cloud Code da Cloud Shell:

Nell'editor di Cloud Code, apri la cartella di origine del codelab selezionando File -> Open Folder e indica la cartella di origine del codelab (ad es. /home/username/gemini-workshop-for-java-developers/).
Configura le variabili di ambiente
Apri un nuovo terminale nell'editor di Cloud Code selezionando Terminal -> New Terminal. Configura due variabili di ambiente necessarie per eseguire gli esempi di codice:
- PROJECT_ID: l'ID del tuo progetto Google Cloud
- LOCATION: la regione in cui viene eseguito il deployment del modello Gemini
Esporta le variabili come segue:
export PROJECT_ID=$(gcloud config get-value project) export LOCATION=us-central1
4. Prima chiamata al modello Gemini
Ora che il progetto è configurato correttamente, è il momento di chiamare l'API Gemini.
Dai un'occhiata a QA.java nella directory app/src/main/java/gemini/workshop:
package gemini.workshop;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.model.chat.ChatLanguageModel;
public class QA {
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.build();
System.out.println(model.generate("Why is the sky blue?"));
}
}
In questo primo esempio, devi importare la classe VertexAiGeminiChatModel, che implementa l'interfaccia ChatModel.
Nel metodo main, configura il modello linguistico della chat utilizzando il builder per VertexAiGeminiChatModel e specifica:
- Progetto
- Località
- Nome modello (
gemini-2.0-flash).
Ora che il modello linguistico è pronto, puoi chiamare il metodo generate() e passare il prompt, la domanda o le istruzioni da inviare all'LLM. Qui, fai una domanda semplice sul perché il cielo è blu.
Non esitare a modificare questo prompt per provare domande o attività diverse.
Esegui il campione nella cartella principale del codice sorgente:
./gradlew run -q -DjavaMainClass=gemini.workshop.QA
Dovresti visualizzare un output simile a questo:
The sky appears blue because of a phenomenon called Rayleigh scattering. When sunlight enters the atmosphere, it is made up of a mixture of different wavelengths of light, each with a different color. The different wavelengths of light interact with the molecules and particles in the atmosphere in different ways. The shorter wavelengths of light, such as those corresponding to blue and violet light, are more likely to be scattered in all directions by these particles than the longer wavelengths of light, such as those corresponding to red and orange light. This is because the shorter wavelengths of light have a smaller wavelength and are able to bend around the particles more easily. As a result of Rayleigh scattering, the blue light from the sun is scattered in all directions, and it is this scattered blue light that we see when we look up at the sky. The blue light from the sun is not actually scattered in a single direction, so the color of the sky can vary depending on the position of the sun in the sky and the amount of dust and water droplets in the atmosphere.
Congratulazioni, hai effettuato la tua prima chiamata a Gemini.
Risposta in streaming
Hai notato che la risposta è stata data in una volta sola, dopo qualche secondo? È anche possibile ottenere la risposta in modo progressivo, grazie alla variante di risposta in streaming. La risposta in streaming, il modello restituisce la risposta pezzo per pezzo, man mano che diventa disponibile.
In questo codelab, ci atterremo alla risposta non in streaming, ma diamo un'occhiata alla risposta in streaming per vedere come si può fare.
In StreamQA.java nella directory app/src/main/java/gemini/workshop puoi vedere la risposta di streaming in azione:
package gemini.workshop;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiStreamingChatModel;
import static dev.langchain4j.model.LambdaStreamingResponseHandler.onNext;
public class StreamQA {
public static void main(String[] args) {
StreamingChatLanguageModel model = VertexAiGeminiStreamingChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.maxOutputTokens(4000)
.build();
model.generate("Why is the sky blue?", onNext(System.out::println));
}
}
Questa volta importiamo le varianti della classe di streaming VertexAiGeminiStreamingChatModel che implementa l'interfaccia StreamingChatLanguageModel. Devi anche importare staticamente LambdaStreamingResponseHandler.onNext, un metodo pratico che fornisce StreamingResponseHandler per creare un gestore di streaming con espressioni lambda Java.
Questa volta, la firma del metodo generate() è leggermente diversa. Anziché restituire una stringa, il tipo restituito è void. Oltre al prompt, devi superare un gestore di risposte di streaming. Qui, grazie all'importazione statica menzionata in precedenza, possiamo definire un'espressione lambda da passare al metodo onNext(). L'espressione lambda viene chiamata ogni volta che è disponibile una nuova parte della risposta, mentre quest'ultima viene chiamata solo se si verifica un errore.
Esegui:
./gradlew run -q -DjavaMainClass=gemini.workshop.StreamQA
Riceverai una risposta simile a quella della classe precedente, ma questa volta noterai che la risposta viene visualizzata progressivamente nella shell, anziché attendere la visualizzazione della risposta completa.
Configurazione aggiuntiva
Per la configurazione, abbiamo definito solo il progetto, la località e il nome del modello, ma ci sono altri parametri che puoi specificare per il modello:
temperature(Float temp): per definire il livello di creatività della risposta (0 indica una creatività bassa e spesso più oggettiva, mentre 2 indica risultati più creativi)topP(Float topP): per selezionare le parole possibili la cui probabilità totale è pari a quel numero in virgola mobile (compreso tra 0 e 1)topK(Integer topK): per selezionare in modo casuale una parola tra un numero massimo di parole probabili per il completamento del testo (da 1 a 40)maxOutputTokens(Integer max): per specificare la lunghezza massima della risposta fornita dal modello (in genere, 4 token rappresentano circa 3 parole)maxRetries(Integer retries): se superi la quota di richieste per periodo di tempo o se la piattaforma presenta un problema tecnico, puoi fare in modo che il modello riprovi la chiamata 3 volte
Finora hai fatto una sola domanda a Gemini, ma puoi anche avere una conversazione in più turni. Questo è ciò che esplorerai nella sezione successiva.
5. Prova Gemini
Nel passaggio precedente, hai posto una sola domanda. È il momento di avere una conversazione reale tra un utente e il modello LLM. Ogni domanda e risposta può basarsi su quelle precedenti per formare una vera discussione.
Dai un'occhiata a Conversation.java nella cartella app/src/main/java/gemini/workshop:
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.service.AiServices;
import java.util.List;
public class Conversation {
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.build();
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20)
.build();
interface ConversationService {
String chat(String message);
}
ConversationService conversation =
AiServices.builder(ConversationService.class)
.chatLanguageModel(model)
.chatMemory(chatMemory)
.build();
List.of(
"Hello!",
"What is the country where the Eiffel tower is situated?",
"How many inhabitants are there in that country?"
).forEach( message -> {
System.out.println("\nUser: " + message);
System.out.println("Gemini: " + conversation.chat(message));
});
}
}
Un paio di nuove importazioni interessanti in questo corso:
MessageWindowChatMemory: una classe che aiuterà a gestire l'aspetto multi-turno della conversazione e a conservare in memoria locale le domande e le risposte precedentiAiServices: una classe di astrazione di livello superiore che collegherà il modello di chat e la memoria della chat
Nel metodo main, configurerai il modello, la memoria della chat e il servizio AI. Il modello viene configurato come di consueto con le informazioni su progetto, posizione e nome del modello.
Per la memoria della chat, utilizziamo lo strumento di creazione di MessageWindowChatMemory per creare una memoria che conservi gli ultimi 20 messaggi scambiati. Si tratta di una finestra scorrevole sulla conversazione il cui contesto viene mantenuto localmente nel nostro client di classe Java.
Quindi, crei l'AI service che associa il modello di chat alla memoria della chat.
Nota come il servizio AI utilizzi un'interfaccia ConversationService personalizzata che abbiamo definito, che LangChain4j implementa e che accetta una query String e restituisce una risposta String.
Ora è il momento di conversare con Gemini. Innanzitutto, viene inviato un semplice saluto, poi una prima domanda sulla Torre Eiffel per sapere in quale paese si trova. Nota che l'ultima frase è correlata alla risposta alla prima domanda, in quanto ti chiedi quanti abitanti ci sono nel paese in cui si trova la Torre Eiffel, senza menzionare esplicitamente il paese indicato nella risposta precedente. Mostra che le domande e le risposte precedenti vengono inviate con ogni prompt.
Esegui il campione:
./gradlew run -q -DjavaMainClass=gemini.workshop.Conversation
Dovresti visualizzare tre risposte simili a queste:
User: Hello! Gemini: Hi there! How can I assist you today? User: What is the country where the Eiffel tower is situated? Gemini: France User: How many inhabitants are there in that country? Gemini: As of 2023, the population of France is estimated to be around 67.8 million.
Puoi porre domande in un solo turno o avere conversazioni in più turni con Gemini, ma finora l'input è stato solo di testo. E le immagini? Esploriamo le immagini nel passaggio successivo.
6. Multimodalità con Gemini
Gemini è un modello multimodale. Non solo accetta testo come input, ma anche immagini o persino video. In questa sezione vedrai un caso d'uso per combinare testo e immagini.
Pensi che Gemini riconoscerà questo gatto?

Immagine di un gatto nella neve tratta da Wikipedia
Dai un'occhiata a Multimodal.java nella directory app/src/main/java/gemini/workshop:
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.TextContent;
public class Multimodal {
static final String CAT_IMAGE_URL =
"https://upload.wikimedia.org/wikipedia/" +
"commons/b/b6/Felis_catus-cat_on_snow.jpg";
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.build();
UserMessage userMessage = UserMessage.from(
ImageContent.from(CAT_IMAGE_URL),
TextContent.from("Describe the picture")
);
Response<AiMessage> response = model.generate(userMessage);
System.out.println(response.content().text());
}
}
Negli import, noterai che distinguiamo tra diversi tipi di messaggi e contenuti. Un UserMessage può contenere sia un oggetto TextContent che un oggetto ImageContent. Questa è la multimodalità in azione: la combinazione di testo e immagini. Non inviamo solo una semplice stringa di prompt, ma un oggetto più strutturato che rappresenta un messaggio utente, composto da un elemento di contenuti di immagine e un elemento di contenuti di testo. Il modello restituisce un Response che contiene un AiMessage.
Recuperi quindi AiMessage dalla risposta tramite content() e il testo del messaggio grazie a text().
Esegui il campione:
./gradlew run -q -DjavaMainClass=gemini.workshop.Multimodal
Il nome dell'immagine ti ha sicuramente dato un indizio su cosa conteneva, ma l'output di Gemini è simile al seguente:
A cat with brown fur is walking in the snow. The cat has a white patch of fur on its chest and white paws. The cat is looking at the camera.
La combinazione di prompt di immagini e testo apre casi d'uso interessanti. Puoi creare applicazioni che possono:
- Riconoscere il testo nelle immagini.
- Controllare se un'immagine è sicura da visualizzare.
- Creare didascalie per le immagini.
- Cerca in un database di immagini con descrizioni in testo normale.
Oltre a estrarre informazioni dalle immagini, puoi estrarre informazioni anche da testo non strutturato. Questo è ciò che imparerai nella sezione successiva.
7. Estrai informazioni strutturate da testo non strutturato
Esistono molte situazioni in cui informazioni importanti vengono fornite in documenti di report, email o altri testi in formato lungo in modo non strutturato. Idealmente, vorresti essere in grado di estrarre i dettagli chiave contenuti nel testo non strutturato sotto forma di oggetti strutturati. Vediamo come fare.
Supponiamo che tu voglia estrarre il nome e l'età di una persona, data una biografia, un curriculum vitae o una descrizione della persona. Puoi chiedere all'LLM di estrarre JSON da testo non strutturato con un prompt modificato in modo intelligente (questa operazione è comunemente chiamata "prompt engineering").
Nell'esempio riportato di seguito, invece di creare un prompt che descriva l'output JSON, utilizzeremo una potente funzionalità di Gemini chiamata output strutturato, o a volte generazione vincolata, che impone al modello di restituire solo contenuti JSON validi, seguendo uno schema JSON specificato.
Dai un'occhiata a ExtractData.java in app/src/main/java/gemini/workshop:
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;
import static dev.langchain4j.model.vertexai.SchemaHelper.fromClass;
public class ExtractData {
record Person(String name, int age) { }
interface PersonExtractor {
@SystemMessage("""
Your role is to extract the name and age
of the person described in the biography.
""")
Person extractPerson(String biography);
}
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.responseMimeType("application/json")
.responseSchema(fromClass(Person.class))
.build();
PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);
String bio = """
Anna is a 23 year old artist based in Brooklyn, New York. She was born and
raised in the suburbs of Chicago, where she developed a love for art at a
young age. She attended the School of the Art Institute of Chicago, where
she studied painting and drawing. After graduating, she moved to New York
City to pursue her art career. Anna's work is inspired by her personal
experiences and observations of the world around her. She often uses bright
colors and bold lines to create vibrant and energetic paintings. Her work
has been exhibited in galleries and museums in New York City and Chicago.
""";
Person person = extractor.extractPerson(bio);
System.out.println(person.name()); // Anna
System.out.println(person.age()); // 23
}
}
Diamo un'occhiata ai vari passaggi di questo file:
- Un record
Personè definito per rappresentare i dettagli che descrivono una persona (nome ed età). - L'interfaccia
PersonExtractorè definita con un metodo che, data una stringa di testo non strutturato, restituisce un'istanzaPerson. extractPerson()è annotato con un'annotazione@SystemMessageche associa un prompt di istruzioni. Questo è il prompt che il modello utilizzerà per guidare l'estrazione delle informazioni e restituire i dettagli sotto forma di documento JSON, che verrà analizzato e convertito in un'istanzaPerson.
Ora esaminiamo i contenuti del metodo main():
- Il modello di chat è configurato e istanziato. Utilizziamo due nuovi metodi della classe Model Builder:
responseMimeType()eresponseSchema(). Il primo indica a Gemini di generare un JSON valido nell'output. Il secondo metodo definisce lo schema dell'oggetto JSON che deve essere restituito. Inoltre, quest'ultimo delega a un metodo pratico in grado di convertire una classe o un record Java in uno schema JSON appropriato. - Un oggetto
PersonExtractorviene creato grazie alla classeAiServicesdi LangChain4j. - A questo punto, puoi semplicemente chiamare
Person person = extractor.extractPerson(...)per estrarre i dettagli della persona dal testo non strutturato e ottenere un'istanzaPersoncon il nome e l'età.
Esegui il campione:
./gradlew run -q -DjavaMainClass=gemini.workshop.ExtractData
Dovresti vedere l'output seguente:
Anna 23
Sì, mi chiamo Anna e ho 23 anni.
Con questo approccio AiServices, operi con oggetti fortemente tipizzati. Non interagisci direttamente con il modello LLM. Al contrario, utilizzi classi concrete, come il record Person per rappresentare le informazioni personali estratte, e hai un oggetto PersonExtractor con un metodo extractPerson() che restituisce un'istanza Person. La nozione di LLM è astratta e, in qualità di sviluppatore Java, manipoli solo classi e oggetti normali quando utilizzi questa interfaccia PersonExtractor.
8. Strutturare i prompt con i modelli di prompt
Quando interagisci con un LLM utilizzando un insieme comune di istruzioni o domande, una parte del prompt non cambia mai, mentre altre parti contengono i dati. Ad esempio, se vuoi creare ricette, potresti utilizzare un prompt come "Sei un cuoco di talento, crea una ricetta con i seguenti ingredienti: ..." e poi aggiungere gli ingredienti alla fine del testo. A questo servono i modelli di prompt, simili alle stringhe interpolate nei linguaggi di programmazione. Un modello di prompt contiene segnaposto che puoi sostituire con i dati giusti per una determinata chiamata all'LLM.
Più concretamente, studiamo TemplatePrompt.java nella directory app/src/main/java/gemini/workshop:
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.model.output.Response;
import java.util.HashMap;
import java.util.Map;
public class TemplatePrompt {
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.maxOutputTokens(500)
.temperature(1.0f)
.topK(40)
.topP(0.95f)
.maxRetries(3)
.build();
PromptTemplate promptTemplate = PromptTemplate.from("""
You're a friendly chef with a lot of cooking experience.
Create a recipe for a {{dish}} with the following ingredients: \
{{ingredients}}, and give it a name.
"""
);
Map<String, Object> variables = new HashMap<>();
variables.put("dish", "dessert");
variables.put("ingredients", "strawberries, chocolate, and whipped cream");
Prompt prompt = promptTemplate.apply(variables);
Response<AiMessage> response = model.generate(prompt.toUserMessage());
System.out.println(response.content().text());
}
}
Come di consueto, configuri il modello VertexAiGeminiChatModel con un alto livello di creatività con una temperatura elevata e valori elevati di topP e topK. Poi crei un PromptTemplate con il relativo metodo statico from(), passando la stringa del prompt e utilizzando le variabili segnaposto tra doppie parentesi graffe: {{dish}} e {{ingredients}}.
Crea il prompt finale chiamando apply(), che accetta una mappa di coppie chiave/valore che rappresentano il nome del segnaposto e il valore stringa con cui sostituirlo.
Infine, chiami il metodo generate() del modello Gemini creando un messaggio utente da questo prompt, con l'istruzione prompt.toUserMessage().
Esegui il campione:
./gradlew run -q -DjavaMainClass=gemini.workshop.TemplatePrompt
Dovresti visualizzare un output generato simile a questo:
**Strawberry Shortcake** Ingredients: * 1 pint strawberries, hulled and sliced * 1/2 cup sugar * 1/4 cup cornstarch * 1/4 cup water * 1 tablespoon lemon juice * 1/2 cup heavy cream, whipped * 1/4 cup confectioners' sugar * 1/4 teaspoon vanilla extract * 6 graham cracker squares, crushed Instructions: 1. In a medium saucepan, combine the strawberries, sugar, cornstarch, water, and lemon juice. Bring to a boil over medium heat, stirring constantly. Reduce heat and simmer for 5 minutes, or until the sauce has thickened. 2. Remove from heat and let cool slightly. 3. In a large bowl, combine the whipped cream, confectioners' sugar, and vanilla extract. Beat until soft peaks form. 4. To assemble the shortcakes, place a graham cracker square on each of 6 dessert plates. Top with a scoop of whipped cream, then a spoonful of strawberry sauce. Repeat layers, ending with a graham cracker square. 5. Serve immediately. **Tips:** * For a more elegant presentation, you can use fresh strawberries instead of sliced strawberries. * If you don't have time to make your own whipped cream, you can use store-bought whipped cream.
Puoi modificare i valori di dish e ingredients nella mappa e regolare la temperatura, topK e tokP, quindi eseguire di nuovo il codice. In questo modo potrai osservare l'effetto della modifica di questi parametri sul LLM.
I modelli di prompt sono un buon modo per avere istruzioni riutilizzabili e parametrizzabili per le chiamate LLM. Puoi trasmettere dati e personalizzare i prompt per valori diversi forniti dagli utenti.
9. Classificazione del testo con la progettazione dei prompt few-shot
I LLM sono piuttosto bravi a classificare il testo in categorie diverse. Puoi aiutare un LLM in questa attività fornendo alcuni esempi di testi e le relative categorie. Questo approccio è spesso chiamato progettazione dei prompt few-shot.
Apriamo TextClassification.java nella directory app/src/main/java/gemini/workshop per eseguire un particolare tipo di classificazione del testo: l'analisi del sentiment.
package gemini.workshop;
import com.google.cloud.vertexai.api.Schema;
import com.google.cloud.vertexai.api.Type;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;
import java.util.List;
public class TextClassification {
enum Sentiment { POSITIVE, NEUTRAL, NEGATIVE }
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.maxOutputTokens(10)
.maxRetries(3)
.responseSchema(Schema.newBuilder()
.setType(Type.STRING)
.addAllEnum(List.of("POSITIVE", "NEUTRAL", "NEGATIVE"))
.build())
.build();
interface SentimentAnalysis {
@SystemMessage("""
Analyze the sentiment of the text below.
Respond only with one word to describe the sentiment.
""")
Sentiment analyze(String text);
}
MessageWindowChatMemory memory = MessageWindowChatMemory.withMaxMessages(10);
memory.add(UserMessage.from("This is fantastic news!"));
memory.add(AiMessage.from(Sentiment.POSITIVE.name()));
memory.add(UserMessage.from("Pi is roughly equal to 3.14"));
memory.add(AiMessage.from(Sentiment.NEUTRAL.name()));
memory.add(UserMessage.from("I really disliked the pizza. Who would use pineapples as a pizza topping?"));
memory.add(AiMessage.from(Sentiment.NEGATIVE.name()));
SentimentAnalysis sentimentAnalysis =
AiServices.builder(SentimentAnalysis.class)
.chatLanguageModel(model)
.chatMemory(memory)
.build();
System.out.println(sentimentAnalysis.analyze("I love strawberries!"));
}
}
Un'enumerazione Sentiment elenca i diversi valori per un sentimento: negativo, neutro o positivo.
Nel metodo main(), crei il modello Gemini Chat come di consueto, ma con un numero massimo di token di output ridotto, in quanto vuoi solo una risposta breve: il testo è POSITIVE, NEGATIVE o NEUTRAL. Per limitare il modello in modo che restituisca solo questi valori, puoi sfruttare il supporto dell'output strutturato che hai scoperto nella sezione sull'estrazione dei dati. Per questo motivo viene utilizzato il metodo responseSchema(). Questa volta non utilizzerai il metodo pratico di SchemaHelper per dedurre la definizione dello schema, ma utilizzerai lo strumento di creazione Schema per capire come appare una definizione dello schema.
Una volta configurato il modello, crei un'interfaccia SentimentAnalysis che AiServices di LangChain4j implementerà per te utilizzando l'LLM. Questa interfaccia contiene un metodo: analyze(). Prende il testo da analizzare come input e restituisce un valore enum Sentiment. In questo modo, manipoli solo un oggetto fortemente tipizzato che rappresenta la classe di sentiment riconosciuta.
Poi, per fornire gli "esempi few-shot" per spingere il modello a svolgere il lavoro di classificazione, crei una memoria della chat per passare coppie di messaggi utente e risposte dell'AI che rappresentano il testo e il sentimento associato.
Uniamo tutto con il metodo AiServices.builder(), passando la nostra interfaccia SentimentAnalysis, il modello da utilizzare e la memoria della chat con gli esempi few-shot. Infine, chiama il metodo analyze() con il testo da analizzare.
Esegui il campione:
./gradlew run -q -DjavaMainClass=gemini.workshop.TextClassification
Dovresti vedere una sola parola:
POSITIVE
Sembra che amare le fragole sia un sentimento positivo.
10. Retrieval-Augmented Generation
Gli LLM vengono addestrati su una grande quantità di testo. Tuttavia, le sue conoscenze coprono solo le informazioni che ha visto durante l'addestramento. Se vengono rilasciate nuove informazioni dopo la data limite di addestramento del modello, questi dettagli non saranno disponibili per il modello. Pertanto, il modello non sarà in grado di rispondere a domande su informazioni che non ha visto.
Ecco perché approcci come la generazione aumentata dal recupero (RAG), che verranno trattati in questa sezione, contribuiscono a fornire le informazioni aggiuntive che un LLM potrebbe dover conoscere per soddisfare le richieste dei suoi utenti, per rispondere con informazioni più aggiornate o su informazioni private non accessibili al momento dell'addestramento.
Torniamo alle conversazioni. Questa volta potrai porre domande sui tuoi documenti. Creerai un chatbot in grado di recuperare informazioni pertinenti da un database contenente i tuoi documenti suddivisi in parti più piccole ("chunk"). Queste informazioni verranno utilizzate dal modello per basare le sue risposte, anziché fare affidamento esclusivamente sulle conoscenze contenute nel suo addestramento.
In RAG, ci sono due fasi:
- Fase di importazione: i documenti vengono caricati in memoria, suddivisi in blocchi più piccoli e vengono calcolati e archiviati in un database vettoriale in grado di eseguire ricerche semantiche gli embedding vettoriali (una rappresentazione vettoriale multidimensionale dei blocchi). Questa fase di importazione viene normalmente eseguita una sola volta, quando è necessario aggiungere nuovi documenti al corpus di documenti.

- Fase di query: ora gli utenti possono porre domande sui documenti. Anche la domanda verrà trasformata in un vettore e confrontata con tutti gli altri vettori nel database. I vettori più simili sono in genere correlati semanticamente e vengono restituiti dal database vettoriale. Successivamente, all'LLM viene fornito il contesto della conversazione, i blocchi di testo che corrispondono ai vettori restituiti dal database e gli viene chiesto di basare la sua risposta su questi blocchi.

Preparare i documenti
Per questo nuovo esempio, porrai domande su un modello di auto fittizio di un produttore di auto anch'esso fittizio: l'auto Cymbal Starlight. L'idea è che un documento su un'auto fittizia non debba far parte delle conoscenze del modello. Se Gemini è in grado di rispondere correttamente alle domande su questa auto, significa che l'approccio RAG funziona: è in grado di cercare nel documento.
Implementare il chatbot
Vediamo come creare l'approccio in due fasi: prima con l'importazione del documento e poi con il momento della query (chiamato anche "fase di recupero") quando gli utenti pongono domande sul documento.
In questo esempio, entrambe le fasi vengono implementate nella stessa classe. Normalmente, avresti un'applicazione che si occupa dell'importazione e un'altra che offre l'interfaccia del chatbot agli utenti.
Inoltre, in questo esempio utilizzeremo un database vettoriale in memoria. In uno scenario di produzione reale, le fasi di importazione e query sarebbero separate in due applicazioni distinte e i vettori vengono archiviati in un database autonomo.
Importazione di documenti
Il primo passaggio della fase di importazione dei documenti consiste nell'individuare il file PDF relativo alla nostra auto fittizia e preparare un PdfParser per leggerlo:
URL url = new URI("https://raw.githubusercontent.com/meteatamel/genai-beyond-basics/main/samples/grounding/vertexai-search/cymbal-starlight-2024.pdf").toURL();
ApachePdfBoxDocumentParser pdfParser = new ApachePdfBoxDocumentParser();
Document document = pdfParser.parse(url.openStream());
Anziché creare prima il solito modello linguistico di chat, crei un'istanza di un modello di incorporamento. Si tratta di un modello particolare il cui ruolo è creare rappresentazioni vettoriali di parti di testo (parole, frasi o persino paragrafi). Restituisce vettori di numeri in virgola mobile anziché risposte di testo.
VertexAiEmbeddingModel embeddingModel = VertexAiEmbeddingModel.builder()
.endpoint(System.getenv("LOCATION") + "-aiplatform.googleapis.com:443")
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.publisher("google")
.modelName("text-embedding-005")
.maxRetries(3)
.build();
Successivamente, avrai bisogno di alcune classi con cui collaborare per:
- Carica e dividi il documento PDF in blocchi.
- Crea i vector embedding per tutti questi blocchi.
InMemoryEmbeddingStore<TextSegment> embeddingStore =
new InMemoryEmbeddingStore<>();
EmbeddingStoreIngestor storeIngestor = EmbeddingStoreIngestor.builder()
.documentSplitter(DocumentSplitters.recursive(500, 100))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
storeIngestor.ingest(document);
Viene creata un'istanza di InMemoryEmbeddingStore, un database vettoriale in memoria, per archiviare i vector embedding.
Il documento è suddiviso in blocchi grazie alla classe DocumentSplitters. Il testo del file PDF verrà suddiviso in snippet di 500 caratteri, con una sovrapposizione di 100 caratteri (con il blocco successivo, per evitare di tagliare parole o frasi, in frammenti).
L'inserimento dello store collega lo splitter di documenti, il modello di embedding per calcolare i vettori e il database vettoriale in memoria. Dopodiché, il metodo ingest() si occuperà dell'importazione.
Ora la prima fase è terminata, il documento è stato trasformato in blocchi di testo con i relativi vector embedding e memorizzato nel database vettoriale.
Fare domande
È ora di prepararsi a fare domande. Crea un modello di chat per iniziare la conversazione:
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.maxOutputTokens(1000)
.build();
Hai anche bisogno di una classe di recupero per collegare il database vettoriale (nella variabile embeddingStore) al modello di embedding. Il suo compito è eseguire query sul database vettoriale calcolando un vector embedding per la query dell'utente, in modo da trovare vettori simili nel database:
EmbeddingStoreContentRetriever retriever =
new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);
Crea un'interfaccia che rappresenti un assistente esperto di auto, ovvero un'interfaccia che la classe AiServices implementerà per interagire con il modello:
interface CarExpert {
Result<String> ask(String question);
}
L'interfaccia CarExpert restituisce una risposta stringa racchiusa nella classe Result di LangChain4j. Perché utilizzare questo wrapper? In questo modo non solo otterrai la risposta, ma potrai anche esaminare i blocchi del database restituiti dal recupero dei contenuti. In questo modo, puoi mostrare all'utente le fonti del documento o dei documenti utilizzati per basare la risposta finale.
A questo punto, puoi configurare un nuovo servizio AI:
CarExpert expert = AiServices.builder(CarExpert.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.contentRetriever(retriever)
.build();
Questo servizio lega insieme:
- Il modello di lingua della chat che hai configurato in precedenza.
- Una memoria della chat per tenere traccia della conversazione.
- Il recuperatore confronta una query di vector embedding con i vettori nel database.
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
.contentInjector(DefaultContentInjector.builder()
.promptTemplate(PromptTemplate.from("""
You are an expert in car automotive, and you answer concisely.
Here is the question: {{userMessage}}
Answer using the following information:
{{contents}}
"""))
.build())
.contentRetriever(retriever)
.build())
Finalmente puoi fare le tue domande.
List.of(
"What is the cargo capacity of Cymbal Starlight?",
"What's the emergency roadside assistance phone number?",
"Are there some special kits available on that car?"
).forEach(query -> {
Result<String> response = expert.ask(query);
System.out.printf("%n=== %s === %n%n %s %n%n", query, response.content());
System.out.println("SOURCE: " + response.sources().getFirst().textSegment().text());
});
Il codice sorgente completo si trova in RAG.java nella directory app/src/main/java/gemini/workshop.
Esegui il campione:
./gradlew -q run -DjavaMainClass=gemini.workshop.RAG
Nell'output dovresti vedere le risposte alle tue domande:
=== What is the cargo capacity of Cymbal Starlight? === The Cymbal Starlight 2024 has a cargo capacity of 13.5 cubic feet. SOURCE: Cargo The Cymbal Starlight 2024 has a cargo capacity of 13.5 cubic feet. The cargo area is located in the trunk of the vehicle. To access the cargo area, open the trunk lid using the trunk release lever located in the driver's footwell. When loading cargo into the trunk, be sure to distribute the weight evenly. Do not overload the trunk, as this could affect the vehicle's handling and stability. Luggage === What's the emergency roadside assistance phone number? === The emergency roadside assistance phone number is 1-800-555-1212. SOURCE: Chapter 18: Emergencies Roadside Assistance If you experience a roadside emergency, such as a flat tire or a dead battery, you can call roadside assistance for help. Roadside assistance is available 24 hours a day, 7 days a week. To call roadside assistance, dial the following number: 1-800-555-1212 When you call roadside assistance, be prepared to provide the following information: Your name and contact information Your vehicle's make, model, and year Your vehicle's location === Are there some special kits available on that car? === Yes, the Cymbal Starlight comes with a tire repair kit. SOURCE: Lane keeping assist: This feature helps to keep you in your lane by gently steering the vehicle back into the lane if you start to drift. Adaptive cruise control: This feature automatically adjusts your speed to maintain a safe following distance from the vehicle in front of you. Forward collision warning: This feature warns you if you are approaching another vehicle too quickly. Automatic emergency braking: This feature can automatically apply the brakes to avoid a collision.
11. Chiamata di funzione
Esistono situazioni in cui vorresti che un LLM avesse accesso a sistemi esterni, come un'API web remota che recupera informazioni o esegue un'azione, o a servizi che eseguono un qualche tipo di calcolo. Ad esempio:
API web remote:
- Monitorare e aggiornare gli ordini dei clienti.
- Trova o crea un ticket in un issue tracker.
- Recuperare dati in tempo reale come quotazioni azionarie o misurazioni di sensori IoT.
- Inviare un'email.
Strumenti di calcolo:
- Una calcolatrice per problemi di matematica più avanzati.
- Interpretazione del codice per l'esecuzione del codice quando i LLM hanno bisogno di una logica di ragionamento.
- Converti le richieste in linguaggio naturale in query SQL in modo che un LLM possa eseguire query su un database.
La chiamata di funzioni (a volte chiamata strumenti o utilizzo di strumenti) è la capacità del modello di richiedere l'esecuzione di una o più chiamate di funzioni per suo conto, in modo da poter rispondere correttamente al prompt di un utente con dati più aggiornati.
Dato un particolare prompt di un utente e la conoscenza delle funzioni esistenti che possono essere pertinenti a quel contesto, un LLM può rispondere con una richiesta di chiamata di funzione. L'applicazione che integra l'LLM può quindi chiamare la funzione per suo conto e rispondere all'LLM con una risposta, che a sua volta interpreta rispondendo con una risposta testuale.
Quattro passaggi della chiamata di funzione
Vediamo un esempio di chiamata di funzione: ottenere informazioni sulle previsioni meteo.
Se chiedi a Gemini o a un altro LLM informazioni sul meteo a Parigi, ti risponderà che non ha informazioni sulle previsioni meteo attuali. Se vuoi che l'LLM abbia accesso in tempo reale ai dati meteo, devi definire alcune funzioni che può richiedere di utilizzare.
Dai un'occhiata al seguente diagramma:

1️⃣ Innanzitutto, un utente chiede informazioni sul meteo a Parigi. L'app chatbot (che utilizza LangChain4j) sa di avere a disposizione una o più funzioni per aiutare l'LLM a soddisfare la query. Il chatbot invia sia il prompt iniziale sia l'elenco delle funzioni che possono essere chiamate. Qui, una funzione chiamata getWeather() che accetta un parametro stringa per la posizione.

Poiché l'LLM non conosce le previsioni meteo, anziché rispondere tramite testo, invia una richiesta di esecuzione della funzione. Il chatbot deve chiamare la funzione getWeather() con "Paris" come parametro di località.
2️⃣ Il chatbot richiama la funzione per conto dell'LLM e recupera la risposta della funzione. In questo caso, immaginiamo che la risposta sia {"forecast": "sunny"}.

3️⃣ L'app chatbot invia la risposta JSON al LLM.

4️⃣ L'LLM esamina la risposta JSON, interpreta le informazioni e alla fine risponde con il testo che indica che a Parigi c'è il sole.

Ogni passaggio come codice
Innanzitutto, configura il modello Gemini come di consueto:
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.maxOutputTokens(100)
.build();
Definisci una specifica dello strumento che descrive la funzione che può essere chiamata:
ToolSpecification weatherToolSpec = ToolSpecification.builder()
.name("getWeather")
.description("Get the weather forecast for a given location or city")
.parameters(JsonObjectSchema.builder()
.addStringProperty(
"location",
"the location or city to get the weather forecast for")
.build())
.build();
Vengono definiti il nome della funzione, nonché il nome e il tipo del parametro, ma nota che sia la funzione che i parametri hanno una descrizione. Le descrizioni sono molto importanti e aiutano l'LLM a capire davvero cosa può fare una funzione e quindi a valutare se questa funzione deve essere chiamata nel contesto della conversazione.
Iniziamo con il passaggio 1, inviando la domanda iniziale sul meteo a Parigi:
List<ChatMessage> allMessages = new ArrayList<>();
// 1) Ask the question about the weather
UserMessage weatherQuestion = UserMessage.from("What is the weather in Paris?");
allMessages.add(weatherQuestion);
Nel passaggio 2, passiamo lo strumento che vorremmo che il modello utilizzi e il modello risponde con una richiesta di esecuzione dello strumento:
// 2) The model replies with a function call request
Response<AiMessage> messageResponse = model.generate(allMessages, weatherToolSpec);
ToolExecutionRequest toolExecutionRequest = messageResponse.content().toolExecutionRequests().getFirst();
System.out.println("Tool execution request: " + toolExecutionRequest);
allMessages.add(messageResponse.content());
Passaggio 3. A questo punto, sappiamo quale funzione vuole che chiamiamo l'LLM. Nel codice, non stiamo effettuando una chiamata reale a un'API esterna, ma restituiamo direttamente una previsione meteo ipotetica:
// 3) We send back the result of the function call
ToolExecutionResultMessage toolExecResMsg = ToolExecutionResultMessage.from(toolExecutionRequest,
"{\"location\":\"Paris\",\"forecast\":\"sunny\", \"temperature\": 20}");
allMessages.add(toolExecResMsg);
Nel passaggio 4, l'LLM apprende il risultato dell'esecuzione della funzione e può quindi sintetizzare una risposta testuale:
// 4) The model answers with a sentence describing the weather
Response<AiMessage> weatherResponse = model.generate(allMessages);
System.out.println("Answer: " + weatherResponse.content().text());
Il codice sorgente completo si trova in FunctionCalling.java nella directory app/src/main/java/gemini/workshop.
Esegui il campione:
./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCalling
Dovresti vedere un output simile al seguente:
Tool execution request: ToolExecutionRequest { id = null, name = "getWeatherForecast", arguments = "{"location":"Paris"}" }
Answer: The weather in Paris is sunny with a temperature of 20 degrees Celsius.
Nell'output sopra puoi vedere la richiesta di esecuzione dello strumento, nonché la risposta.
12. LangChain4j gestisce la chiamata di funzione
Nel passaggio precedente, hai visto come le normali interazioni di domanda/risposta di testo e richiesta/risposta di funzione sono intervallate e, nel frattempo, hai fornito direttamente la risposta alla funzione richiesta, senza chiamare una funzione reale.
Tuttavia, LangChain4j offre anche un'astrazione di livello superiore che può gestire le chiamate di funzioni in modo trasparente per te, gestendo la conversazione come di consueto.
Chiamata di funzione singola
Diamo un'occhiata a FunctionCallingAssistant.java, pezzo per pezzo.
Per prima cosa, crea un record che rappresenterà la struttura dei dati di risposta della funzione:
record WeatherForecast(String location, String forecast, int temperature) {}
La risposta contiene informazioni sulla posizione, sulle previsioni e sulla temperatura.
Poi crei una classe che contiene la funzione effettiva che vuoi rendere disponibile al modello:
static class WeatherForecastService {
@Tool("Get the weather forecast for a location")
WeatherForecast getForecast(@P("Location to get the forecast for") String location) {
if (location.equals("Paris")) {
return new WeatherForecast("Paris", "Sunny", 20);
} else if (location.equals("London")) {
return new WeatherForecast("London", "Rainy", 15);
} else {
return new WeatherForecast("Unknown", "Unknown", 0);
}
}
}
Tieni presente che questa classe contiene una sola funzione, ma è annotata con l'annotazione @Tool, che corrisponde alla descrizione della funzione che il modello può richiedere di chiamare.
Anche i parametri della funzione (in questo caso uno solo) sono annotati, ma con questa breve annotazione @P, che fornisce anche una descrizione del parametro. Puoi aggiungere tutte le funzioni che vuoi per renderle disponibili al modello per scenari più complessi.
In questa classe, restituisci alcune risposte predefinite, ma se volessi chiamare un servizio esterno reale di previsioni meteo, è nel corpo di questo metodo che faresti la chiamata a quel servizio.
Come abbiamo visto quando hai creato un ToolSpecification nell'approccio precedente, è importante documentare cosa fa una funzione e descrivere a cosa corrispondono i parametri. In questo modo, il modello comprende come e quando può essere utilizzata questa funzione.
Successivamente, LangChain4j ti consente di fornire un'interfaccia che corrisponde al contratto che vuoi utilizzare per interagire con il modello. In questo caso, si tratta di un'interfaccia semplice che accetta una stringa che rappresenta il messaggio dell'utente e restituisce una stringa corrispondente alla risposta del modello:
interface WeatherAssistant {
String chat(String userMessage);
}
È anche possibile utilizzare firme più complesse che coinvolgono UserMessage (per un messaggio utente) o AiMessage (per una risposta del modello) di LangChain4j o persino un TokenStream, se vuoi gestire situazioni più avanzate, poiché questi oggetti più complessi contengono anche informazioni aggiuntive come il numero di token consumati e così via. Ma per semplicità, prenderemo in input e output solo stringhe.
Concludiamo con il metodo main(), che lega insieme tutti i pezzi:
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.build();
WeatherForecastService weatherForecastService = new WeatherForecastService();
WeatherAssistant assistant = AiServices.builder(WeatherAssistant.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.tools(weatherForecastService)
.build();
System.out.println(assistant.chat("What is the weather in Paris?"));
System.out.println(assistant.chat("Is it warmer in London or in Paris?"));
}
Come di consueto, configuri il modello Gemini Chat. Poi istanzi il servizio di previsioni meteo che contiene la "funzione" che il modello ci chiederà di chiamare.
Ora, utilizza di nuovo la classe AiServices per associare il modello di chat, la memoria della chat e lo strumento (ovvero il servizio di previsioni meteo con la relativa funzione). AiServices restituisce un oggetto che implementa l'interfaccia WeatherAssistant che hai definito. L'unica cosa che rimane da fare è chiamare il metodo chat() dell'assistente. Quando lo richiami, vedrai solo le risposte di testo, ma le richieste di chiamata di funzione e le risposte di chiamata di funzione non saranno visibili allo sviluppatore e queste richieste verranno gestite automaticamente e in modo trasparente. Se Gemini ritiene che una funzione debba essere chiamata, risponderà con la richiesta di chiamata della funzione e LangChain4j si occuperà di chiamare la funzione locale per tuo conto.
Esegui il campione:
./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCallingAssistant
Dovresti vedere un output simile al seguente:
OK. The weather in Paris is sunny with a temperature of 20 degrees.
It is warmer in Paris (20 degrees) than in London (15 degrees).
Questo era un esempio di una singola funzione.
Chiamate di funzioni multiple
Puoi anche avere più funzioni e lasciare che LangChain4j gestisca più chiamate di funzioni per tuo conto. Dai un'occhiata a MultiFunctionCallingAssistant.java per un esempio di più funzioni.
Ha una funzione per convertire le valute:
@Tool("Convert amounts between two currencies")
double convertCurrency(
@P("Currency to convert from") String fromCurrency,
@P("Currency to convert to") String toCurrency,
@P("Amount to convert") double amount) {
double result = amount;
if (fromCurrency.equals("USD") && toCurrency.equals("EUR")) {
result = amount * 0.93;
} else if (fromCurrency.equals("USD") && toCurrency.equals("GBP")) {
result = amount * 0.79;
}
System.out.println(
"convertCurrency(fromCurrency = " + fromCurrency +
", toCurrency = " + toCurrency +
", amount = " + amount + ") == " + result);
return result;
}
Un'altra funzione per ottenere il valore di un titolo azionario:
@Tool("Get the current value of a stock in US dollars")
double getStockPrice(@P("Stock symbol") String symbol) {
double result = 170.0 + 10 * new Random().nextDouble();
System.out.println("getStockPrice(symbol = " + symbol + ") == " + result);
return result;
}
Un'altra funzione per applicare una percentuale a un determinato importo:
@Tool("Apply a percentage to a given amount")
double applyPercentage(@P("Initial amount") double amount, @P("Percentage between 0-100 to apply") double percentage) {
double result = amount * (percentage / 100);
System.out.println("applyPercentage(amount = " + amount + ", percentage = " + percentage + ") == " + result);
return result;
}
Puoi quindi combinare tutte queste funzioni e una classe MultiTools e porre domande come "Qual è il 10% del prezzo delle azioni AAPL convertito da USD a EUR?"
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.maxOutputTokens(100)
.build();
MultiTools multiTools = new MultiTools();
MultiToolsAssistant assistant = AiServices.builder(MultiToolsAssistant.class)
.chatLanguageModel(model)
.chatMemory(withMaxMessages(10))
.tools(multiTools)
.build();
System.out.println(assistant.chat(
"What is 10% of the AAPL stock price converted from USD to EUR?"));
}
Esegui il comando nel seguente modo:
./gradlew run -q -DjavaMainClass=gemini.workshop.MultiFunctionCallingAssistant
Dovresti vedere le più funzioni chiamate:
getStockPrice(symbol = AAPL) == 172.8022224055534 convertCurrency(fromCurrency = USD, toCurrency = EUR, amount = 172.8022224055534) == 160.70606683716468 applyPercentage(amount = 160.70606683716468, percentage = 10.0) == 16.07060668371647 10% of the AAPL stock price converted from USD to EUR is 16.07060668371647 EUR.
Verso gli agenti
La chiamata di funzione è un ottimo meccanismo di estensione per i modelli linguistici di grandi dimensioni come Gemini. Ci consente di creare sistemi più complessi, spesso chiamati "agenti" o "assistenti AI". Questi agenti possono interagire con il mondo esterno tramite API esterne e con servizi che possono avere effetti collaterali sull'ambiente esterno (come l'invio di email, la creazione di ticket e così via).
Quando crei agenti così potenti, devi farlo in modo responsabile. Prima di intraprendere azioni automatiche, ti consigliamo di prendere in considerazione un approccio human-in-the-loop. È importante tenere presente la sicurezza quando si progettano agenti basati su LLM che interagiscono con il mondo esterno.
13. Esecuzione di Gemma con Ollama e TestContainers
Finora abbiamo utilizzato Gemini, ma esiste anche Gemma, il suo modello gemello.
Gemma è una famiglia di modelli aperti leggeri e all'avanguardia creati sulla base della stessa ricerca e tecnologia utilizzata per creare i modelli Gemini. L'ultimo modello Gemma è Gemma3, disponibile in quattro dimensioni: 1B (solo testo), 4B, 12B e 27B. I loro pesi sono disponibili senza costi e le loro dimensioni ridotte ti consentono di eseguirli in autonomia, anche sul laptop o in Cloud Shell.
Come si esegue Gemma?
Esistono molti modi per eseguire Gemma: nel cloud, tramite Vertex AI con un clic di un pulsante o GKE con alcune GPU, ma puoi anche eseguirlo localmente.
Una buona opzione per eseguire Gemma localmente è Ollama, uno strumento che ti consente di eseguire modelli di piccole dimensioni, come Llama, Mistral e molti altri, sulla tua macchina locale. È simile a Docker, ma per gli LLM.
Installa Ollama seguendo le istruzioni per il tuo sistema operativo.
Se utilizzi un ambiente Linux, devi prima attivare Ollama dopo averlo installato.
ollama serve > /dev/null 2>&1 &
Una volta installato localmente, puoi eseguire comandi per estrarre un modello:
ollama pull gemma3:1b
Attendi che il modello venga estratto. L'operazione può richiedere un po' di tempo.
Esegui il modello:
ollama run gemma3:1b
Ora puoi interagire con il modello:
>>> Hello! Hello! It's nice to hear from you. What can I do for you today?
Per uscire dal prompt, premi Ctrl+D.
Esecuzione di Gemma in Ollama su TestContainers
Anziché dover installare ed eseguire Ollama localmente, puoi utilizzare Ollama all'interno di un container, gestito da TestContainers.
TestContainers non è utile solo per i test, ma puoi utilizzarlo anche per eseguire i container. Esiste anche un OllamaContainer specifico di cui puoi usufruire.
Ecco il quadro completo:

Implementazione
Diamo un'occhiata a GemmaWithOllamaContainer.java, pezzo per pezzo.
Innanzitutto, devi creare un contenitore Ollama derivato che recuperi il modello Gemma. Questa immagine esiste già da un'esecuzione precedente o verrà creata. Se l'immagine esiste già, devi solo comunicare a TestContainers che vuoi sostituire l'immagine Ollama predefinita con la variante basata su Gemma:
private static final String TC_OLLAMA_GEMMA3 = "tc-ollama-gemma3-1b";
public static final String GEMMA_3 = "gemma3:1b";
// Creating an Ollama container with Gemma 3 if it doesn't exist.
private static OllamaContainer createGemmaOllamaContainer() throws IOException, InterruptedException {
// Check if the custom Gemma Ollama image exists already
List<Image> listImagesCmd = DockerClientFactory.lazyClient()
.listImagesCmd()
.withImageNameFilter(TC_OLLAMA_GEMMA3)
.exec();
if (listImagesCmd.isEmpty()) {
System.out.println("Creating a new Ollama container with Gemma 3 image...");
OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.7.1");
System.out.println("Starting Ollama...");
ollama.start();
System.out.println("Pulling model...");
ollama.execInContainer("ollama", "pull", GEMMA_3);
System.out.println("Committing to image...");
ollama.commitToImage(TC_OLLAMA_GEMMA3);
return ollama;
}
System.out.println("Ollama image substitution...");
// Substitute the default Ollama image with our Gemma variant
return new OllamaContainer(
DockerImageName.parse(TC_OLLAMA_GEMMA3)
.asCompatibleSubstituteFor("ollama/ollama"));
}
Successivamente, crea e avvia un container di test Ollama, quindi crea un modello di chat Ollama puntando all'indirizzo e alla porta del container con il modello che vuoi utilizzare. Infine, invoca model.generate(yourPrompt) come di consueto:
public static void main(String[] args) throws IOException, InterruptedException {
OllamaContainer ollama = createGemmaOllamaContainer();
ollama.start();
ChatLanguageModel model = OllamaChatModel.builder()
.baseUrl(String.format("http://%s:%d", ollama.getHost(), ollama.getFirstMappedPort()))
.modelName(GEMMA_3)
.build();
String response = model.generate("Why is the sky blue?");
System.out.println(response);
}
Esegui il comando nel seguente modo:
./gradlew run -q -DjavaMainClass=gemini.workshop.GemmaWithOllamaContainer
La prima esecuzione richiederà un po' di tempo per creare ed eseguire il container, ma una volta completata, dovresti vedere la risposta di Gemma:
INFO: Container ollama/ollama:0.7.1 started in PT7.228339916S
The sky appears blue due to Rayleigh scattering. Rayleigh scattering is a phenomenon that occurs when sunlight interacts with molecules in the Earth's atmosphere.
* **Scattering particles:** The main scattering particles in the atmosphere are molecules of nitrogen (N2) and oxygen (O2).
* **Wavelength of light:** Blue light has a shorter wavelength than other colors of light, such as red and yellow.
* **Scattering process:** When blue light interacts with these molecules, it is scattered in all directions.
* **Human eyes:** Our eyes are more sensitive to blue light than other colors, so we perceive the sky as blue.
This scattering process results in a blue appearance for the sky, even though the sun is actually emitting light of all colors.
In addition to Rayleigh scattering, other atmospheric factors can also influence the color of the sky, such as dust particles, aerosols, and clouds.
Gemma è in esecuzione in Cloud Shell.
14. Complimenti
Congratulazioni, hai creato la tua prima applicazione di chat di AI generativa in Java utilizzando LangChain4j e l'API Gemini. Nel corso del tempo, hai scoperto che i modelli linguistici di grandi dimensioni multimodali sono piuttosto potenti e in grado di gestire varie attività come domande/risposte, anche nella tua documentazione, estrazione di dati, interazione con API esterne e altro ancora.
Passaggi successivi
Ora tocca a te migliorare le tue applicazioni con potenti integrazioni LLM.
Further reading
- Casi d'uso comuni dell'AI generativa
- Risorse di formazione sull'AI generativa
- Interagire con Gemini tramite Generative AI Studio
- AI responsabile