Implémentation avancée de GPT en Java : Fonctionnalités d'appel de fonctions et gestion de contexte conversationnel détaillée
Cet article se base sur les dernières fonctionnalités de l'API OpenAI GPT, combinées à des pratiques de design patterns, pour explorer en profondeur la mise en œuvre d'un système d'appel de fonctions et de gestion de contexte conversationnel en environnement Java.
1. Introduction : Un nouveau paradigme de développement GPT
Avec l'évolution rapide des modèles GPT, la simple complétion de texte ne suffit plus pour répondre aux besoins des scénarios d'application complexes. La fonction d'appel de fonctions (Function Calling) introduite par OpenAI en 2023 a radicalement transformé le mode d'interaction homme-machine, permettant à GPT de s'intégrer en profondeur avec des systèmes externes. Parallèlement, la gestion de contexte conversationnel multi-tour est devenue un défi technique central pour construire des systèmes de conversation intelligents.
Cet article se concentrera sur la pile technologique Java, en adoptant une approche par design patterns, pour mettre en œuvre complètement un cadre d'application GPT prenant en charge l'appel de fonctions et la gestion de conversations multi-tours.
2. Conception de l'architecture globale
Nous adoptons un modèle d'architecture en couches, divisant le système en couche de présentation, couche de service et couche d'infrastructure :
// Définition des interfaces principales
public interface ServiceChatGPT {
ReponseCompletionChat chat(DemandeChat demande);
ReponseCompletionChat chatAvecFonctions(DemandeChat demande, List<FonctionDefinie> fonctions);
}
public interface GestionnaireConversation {
void sauvegarderContexte(String idSession, ContexteConversation contexte);
ContexteConversation getContexte(String idSession);
void effacerContexte(String idSession);
}
3. Implémentation détaillée de l'appel de fonctions
3.1 Mécanisme de définition et d'enregistrement des fonctions
Tout d'abord, définissons la structure de données pour les fonctions :
@Data
@AllArgsConstructor
public class DefinitionFonction {
private String nom;
private String description;
private ParametresFonction parametres;
}
@Data
public static class ParametresFonction {
private String type = "object";
private Map<String, ProprieteParametre> proprietes;
private List<String> requis;
}
@Data
public static class ProprieteParametre {
private String type;
private String description;
private List<String> valeursEnum;
}
Implémentons un registre de fonctions, utilisant le pattern Registre pour gérer toutes les fonctions disponibles :
@Component
public class RegistreFonction {
private final Map<String, ExecuteurFonction> mapFonctions = new ConcurrentHashMap<>();
public void enregistrerFonction(String nom, ExecuteurFonction executeur) {
mapFonctions.put(nom, executeur);
}
public ExecuteurFonction getExecuteur(String nom) {
return mapFonctions.get(nom);
}
public List<DefinitionFonction> getFonctionsDisponibles() {
return mapFonctions.keySet().stream()
.map(this::creerDefinitionFonction)
.collect(Collectors.toList());
}
private DefinitionFonction creerDefinitionFonction(String nom) {
// Créer une définition basée sur la fonction réelle
return new DefinitionFonction(nom, "Description de fonction", null);
}
}
3.2 Conception de l'exécuteur de fonctions
Utilisons le pattern Commande pour implémenter une interface d'exécution de fonctions unifiée :
public interface ExecuteurFonction {
ResultatFonction executer(String arguments) throws ExceptionExecutionFonction;
DefinitionFonction getDefinition();
}
@Data
@AllArgsConstructor
public class ResultatFonction {
private boolean succes;
private String message;
private Object donnees;
}
Exemple d'implémentation spécifique - Météo :
@Component
public class FonctionMeteo implements ExecuteurFonction {
private final DefinitionFonction definition;
public FonctionMeteo() {
this.definition = creerDefinition();
}
@Override
public ResultatFonction executer(String arguments) {
try {
JsonNode params = JsonUtils.parse(arguments);
String ville = params.get("ville").asText();
// Simulation d'appel API météo
DonneesMeteo meteo = recupererDonneesMeteo(ville);
return new ResultatFonction(true,
String.format("Météo à %s: %s, Température: %s℃", ville, meteo.getCondition(), meteo.getTemperature()),
meteo);
} catch (Exception e) {
return new ResultatFonction(false, "Échec de la requête météo: " + e.getMessage(), null);
}
}
@Override
public DefinitionFonction getDefinition() {
return definition;
}
private DefinitionFonction creerDefinition() {
Map<String, DefinitionFonction.ProprieteParametre> proprietes = new HashMap<>();
proprietes.put("ville", new DefinitionFonction.ProprieteParametre("string", "Nom de la ville", null));
List<String> requis = Arrays.asList("ville");
return new DefinitionFonction("obtenir_meteo", "Obtenir les informations météo d'une ville",
new DefinitionFonction.ParametresFonction("object", proprietes, requis));
}
}
3.3 Flux de traitement des appels de fonctions
Implémentons le service GPT principal, intégrant les capacités d'appel de fonctions :
@Service
@Slf4j
public class ServiceFonctionGPT {
@Autowired
private ClientOpenAI clientOpenAI;
@Autowired
private RegistreFonction registreFonction;
@Autowired
private GestionnaireConversation gestionnaireConversation;
public ReponseCompletionChat traiterAvecFonctions(String messageUtilisateur, String idSession) {
// Obtenir le contexte conversationnel
ContexteConversation contexte = gestionnaireConversation.getContexte(idSession);
// Construire les messages
List<MessageChat> messages = construireMessages(contexte, messageUtilisateur);
// Obtenir les fonctions disponibles
List<DefinitionFonction> fonctions = registreFonction.getFonctionsDisponibles();
// Premier appel à GPT, pouvant retourner une demande d'appel de fonction
ReponseCompletionChat reponse = clientOpenAI.completionChat(
new DemandeCompletionChat(messages, fonctions));
// Traiter les appels de fonctions possibles
return gererAppelsFonction(reponse, messages, idSession);
}
private ReponseCompletionChat gererAppelsFonction(ReponseCompletionChat reponseInitiale,
List<MessageChat> messages,
String idSession) {
ReponseCompletionChat reponseCourante = reponseInitiale;
int maxIterations = 5; // Prévenir les boucles infinies
for (int i = 0; i < maxIterations; i++) {
MessageChat messageReponse = reponseCourante.getChoix().get(0).getMessage();
// Vérifier si un appel de fonction est nécessaire
if (messageReponse.getAppelFonction() == null) {
break; // Pas d'appel de fonction, retour direct
}
// Exécuter l'appel de fonction
AppelFonction appelFonction = messageReponse.getAppelFonction();
ResultatFonction resultat = executerFonction(appelFonction);
// Ajouter le résultat de la fonction à l'historique des messages
messages.add(new MessageChat("fonction", resultat.getMessage(),
appelFonction.getNom(), resultat.getDonnees()));
// Appeler à nouveau GPT avec le résultat de l'exécution
reponseCourante = clientOpenAI.completionChat(
new DemandeCompletionChat(messages, registreFonction.getFonctionsDisponibles()));
}
return reponseCourante;
}
private ResultatFonction executerFonction(AppelFonction appelFonction) {
try {
ExecuteurFonction executeur = registreFonction.getExecuteur(appelFonction.getNom());
if (executeur == null) {
return new ResultatFonction(false, "Fonction non trouvée: " + appelFonction.getNom(), null);
}
return executeur.executer(appelFonction.getArguments());
} catch (Exception e) {
log.error("Échec d'exécution de fonction: {}", appelFonction.getNom(), e);
return new ResultatFonction(false, "Échec d'exécution: " + e.getMessage(), null);
}
}
}
4. Gestion du contexte conversationnel multi-tour
4.1 Conception de la structure de données de contexte
@Data
public class ContexteConversation {
private String idSession;
private List<MessageChat> messages;
private Map<String, Object> metadonnees;
private long derniereActivite;
private int compteurTokens;
public void ajouterMessage(MessageChat message) {
this.messages.add(message);
this.derniereActivite = System.currentTimeMillis();
this.compteurTokens += estimerTokens(message.getContenu());
}
public void compresserContexte(int maxTokens) {
while (compteurTokens > maxTokens && messages.size() > 1) {
// Supprimer le plus ancien échange utilisateur/assistant, conserver les messages système
if (messages.size() > 2 && !messages.get(1).getRole().equals("system")) {
MessageChat supprime = messages.remove(1);
compteurTokens -= estimerTokens(supprime.getContenu());
} else {
break;
}
}
}
private int estimerTokens(String texte) {
// Estimation simplifiée des tokens
return texte.length() / 4;
}
}
4.2 Stratégies de stockage de contexte
Utilisons le pattern Stratégie pour implémenter plusieurs backends de stockage :
public interface StockageContexte {
void sauvegarderContexte(String idSession, ContexteConversation contexte);
ContexteConversation chargerContexte(String idSession);
void supprimerContexte(String idSession);
}
// Implémentation de stockage en mémoire
@Component
@Slf4j
public class StockageMemoireContexte implements StockageContexte {
private final Map<String, ContexteConversation> mapContexte = new ConcurrentHashMap<>();
private final ScheduledExecutorService nettoyageExecutor =
Executors.newSingleThreadScheduledExecutor();
@PostConstruct
public void init() {
// Nettoyer les sessions expirées périodiquement
nettoyageExecutor.scheduleAtFixedRate(this::nettoyerSessionsExpirees, 1, 1, TimeUnit.HOURS);
}
@Override
public void sauvegarderContexte(String idSession, ContexteConversation contexte) {
mapContexte.put(idSession, contexte);
}
@Override
public ContexteConversation chargerContexte(String idSession) {
return mapContexte.get(idSession);
}
@Override
public void supprimerContexte(String idSession) {
mapContexte.remove(idSession);
}
private void nettoyerSessionsExpirees() {
long maintenant = System.currentTimeMillis();
long tempsExpiration = 24 * 60 * 60 * 1000; // 24 heures
mapContexte.entrySet().removeIf(entry ->
maintenant - entry.getValue().getDerniereActivite() > tempsExpiration);
}
}
// Implémentation de stockage Redis
@Component
@ConditionalOnProperty(name = "conversation.stockage", havingValue = "redis")
public class StockageRedisContexte implements StockageContexte {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void sauvegarderContexte(String idSession, ContexteConversation contexte) {
redisTemplate.opsForValue().set(construireCle(idSession), contexte, Duration.ofHours(24));
}
@Override
public ContexteConversation chargerContexte(String idSession) {
return (ContexteConversation) redisTemplate.opsForValue().get(construireCle(idSession));
}
@Override
public void supprimerContexte(String idSession) {
redisTemplate.delete(construireCle(idSession));
}
private String construireCle(String idSession) {
return "gpt:contexte:" + idSession;
}
}
4.3 Gestionnaire de contexte intelligent
Implémentons la compression et la résumé du contexte :
@Service
public class GestionnaireConversationIntelligent implements GestionnaireConversation {
@Autowired
private StockageContexte stockageContexte;
@Autowired
private ServiceResuméGPT serviceResumé;
private static final int MAX_TOKENS = 4000;
private static final int SEUIL_COMPRESSION = 3000;
@Override
public ContexteConversation getContexte(String idSession) {
ContexteConversation contexte = stockageContexte.chargerContexte(idSession);
if (contexte == null) {
contexte = new ContexteConversation(idSession, new ArrayList<>(),
new HashMap<>(), System.currentTimeMillis(), 0);
}
// Vérifier si une compression est nécessaire
if (contexte.getCompteurTokens() > SEUIL_COMPRESSION) {
contexte = compresserContexte(contexte);
}
return contexte;
}
private ContexteConversation compresserContexte(ContexteConversation contexte) {
try {
// Résumer les conversations précoces
List<MessageChat> messagesCompresses = resumerConversationPrecoce(contexte.getMessages());
ContexteConversation compresse = new ContexteConversation(
contexte.getIdSession(),
messagesCompresses,
contexte.getMetadonnees(),
System.currentTimeMillis(),
calculerCompteurTokens(messagesCompresses)
);
stockageContexte.sauvegarderContexte(contexte.getIdSession(), compresse);
return compresse;
} catch (Exception e) {
log.warn("Échec de compression de contexte, utilisation de troncature simple", e);
return tronquerContexte(contexte);
}
}
private List<MessageChat> resumerConversationPrecoce(List<MessageChat> messages) {
if (messages.size() <= 3) {
return messages; // Conversation trop courte, pas besoin de compression
}
// Conserver le message système et les 3 derniers tours de conversation
int indexSeparation = Math.max(1, messages.size() - 6); // Conserver les 3 derniers (6 messages)
List<MessageChat> messagesPrecoces = messages.subList(0, indexSeparation);
List<MessageChat> messagesRecents = messages.subList(indexSeparation, messages.size());
// Générer un résumé de la conversation précoce
String resumé = serviceResumé.genererResumé(messagesPrecoces);
List<MessageChat> compresse = new ArrayList<>();
compresse.add(new MessageChat("system",
"Résumé de la conversation précédente : " + resumé + "\nVeuillez continuer la conversation en vous basant sur ce résumé."));
compresse.addAll(messagesRecents);
return compresse;
}
}
5. Exemple d'application complet
5.1 Configuraton Spring Boot
@Configuration
@EnableConfigurationProperties(ProprietesGPT.class)
public class ConfigurationAutomatiqueGPT {
@Bean
@ConditionalOnMissingBean
public ClientOpenAI clientOpenAI(ProprietesGPT proprietes) {
return new ClientOpenAI(proprietes.getCleAPI(), proprietes.getUrlBase());
}
@Bean
public RegistreFonction registreFonction() {
return new RegistreFonction();
}
@Bean
@ConditionalOnMissingBean
public StockageContexte stockageContexte() {
return new StockageMemoireContexte();
}
}
@Data
@ConfigurationProperties(prefix = "gpt")
public class ProprietesGPT {
private String cleAPI;
private String urlBase = "https://api.openai.com/v1";
private int maxTokens = 2000;
private double temperature = 0.7;
}
5.2 Interface REST API
@RestController
@RequestMapping("/api/gpt")
@Validated
@Slf4j
public class ControleurGPT {
@Autowired
private ServiceFonctionGPT serviceGPT;
@Autowired
private GestionnaireConversation gestionnaireConversation;
@PostMapping("/chat")
public ResponseEntity<ReponseChat> chat(
@RequestBody @Valid DemandeChat demande,
HttpServletRequest httpRequest) {
String idSession = obtenirOuCreerIdSession(httpRequest);
try {
ReponseCompletionChat reponse = serviceGPT.traiterAvecFonctions(
demande.getMessage(), idSession);
// Mettre à jour le contexte conversationnel
mettreAJourContexteConversation(idSession, demande.getMessage(), reponse);
return ResponseEntity.ok(ReponseChat.succes(reponse));
} catch (Exception e) {
log.error("Échec du traitement GPT", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ReponseChat.erreur(e.getMessage()));
}
}
private String obtenirOuCreerIdSession(HttpServletRequest request) {
HttpSession session = request.getSession(true);
return session.getId();
}
private void mettreAJourContexteConversation(String idSession, String messageUtilisateur,
ReponseCompletionChat reponse) {
ContexteConversation contexte = gestionnaireConversation.getContexte(idSession);
contexte.ajouterMessage(new MessageChat("utilisateur", messageUtilisateur));
contexte.ajouterMessage(new MessageChat("assistant",
reponse.getChoix().get(0).getMessage().getContenu()));
gestionnaireConversation.sauvegarderContexte(idSession, contexte);
}
}
5.3 Exemple de configuration d'application
application.yml
---------------
gpt:
cle-api: ${OPENAI_API_KEY}
url-base: https://api.openai.com/v1
max-tokens: 2000
temperature: 0.7
conversation:
stockage: redis # Options: memory, redis
expiration: 24h
spring:
redis:
host: localhost
port: 6379
6. Optimisation des performances et meilleures pratiques
6.1 Configuration de pool de connexions et délais
@Configuration
public class ConfigurationHttpClient {
@Bean
public ClientOpenAI clientOpenAI(ProprietesGPT proprietes) {
OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(Duration.ofSeconds(30))
.readTimeout(Duration.ofSeconds(60))
.writeTimeout(Duration.ofSeconds(30))
.connectionPool(new ConnectionPool(20, 5, TimeUnit.MINUTES))
.build();
return new ClientOpenAI(proprietes.getCleAPI(), proprietes.getUrlBase(), httpClient);
}
}
6.2 Support de traitement asynchrone
@Service
@Async
public class ServiceGPTAsynchrone {
@Autowired
private ServiceFonctionGPT serviceGPT;
@Async("tacheGPTExecutor")
public CompletableFuture<ReponseCompletionChat> traiterAsynchrone(String message, String idSession) {
return CompletableFuture.supplyAsync(() ->
serviceGPT.traiterAvecFonctions(message, idSession));
}
}
@Configuration
@EnableAsync
public class ConfigurationAsynchrone {
@Bean("tacheGPTExecutor")
public TaskExecutor tacheGPTExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setNomPrefixeTache("gpt-asynchrone-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialiser();
return executor;
}
}
7. Conclusion
Cet article a détaillé une solution complète pour implémenter l'appel de fonctions GPT et la gestion de contexte conversationnel multi-tour basée sur Java. En adoptant des design patterns appropriés, nous avons construit un système de conversation intelligente extensible et facile à maintenir. Les points clés d'implémentation comprennent :
- Mécanisme d'appel de fonctions : Réalisation d'une gestion flexible de fonctions via les patterns Registre et Commande
- Gestion de contexte : Support de stratégies de stockage multiples et algorithmes de compression intelligents
- Traitement asynchrone : Amélioration du débit et des performances de réponse du système
Cette solution a été validée dans un environnement de production et peut traiter efficacement des scénarios de conversation multi-tour complexes, fournissant une base technique solide pour la construction d'applications d'entreprise IA.
Les exemples de code complets ont été téléchargés sur GitHub, veuillez étoiler et contribuer : [adresse du projet]
Références :
- Documentation officielle de l'API OpenAI (version 2024 la plus récente)
- "Design Patterns: Elements of Reusable Object-Oriented Software"
- Documentation officielle du Spring Framework
- Guide des meilleures pratiques Redis
À propos de l'auteur : Architecte Java senior, spécialisé dans la pratique de l'ingénierie IA et l'intégration de modèles de langage à grande échelle.