Implémentation des fonctionnalités avancées GPT en Java : design patterns pour l'appel de fonctions et la gestion du contexte conversationnel

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 :

  1. Mécanisme d'appel de fonctions : Réalisation d'une gestion flexible de fonctions via les patterns Registre et Commande
  2. Gestion de contexte : Support de stratégies de stockage multiples et algorithmes de compression intelligents
  3. 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 :

  1. Documentation officielle de l'API OpenAI (version 2024 la plus récente)
  2. "Design Patterns: Elements of Reusable Object-Oriented Software"
  3. Documentation officielle du Spring Framework
  4. 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.

Étiquettes: Java GPT Design Patterns Intelligence Artificielle Traitement du Langage Naturel

Publié le 20 juin à 00h59