Les tests de performance sur FlowerDocs
Découvrez comment un outil interne basé sur AWS et CodeceptJS a permis de simuler des scénarios utilisateurs pour comparer les temps de réponse entre...
Comparez des documents avec des LLM dans un service Spring Boot modulaire. Clients IA et prompts sont configurables via YAML pour une intégration fluide.
Comparer deux documents volumineux pour en identifier les différences peut s’avérer une tâche fastidieuse et chronophage. En s’appuyant sur les grands modèles de langage (LLM), il devient possible d’automatiser ce processus et de générer des résumés lisibles mettant en évidence les différences entre les documents. Ce guide technique présente comment construire un service REST avec Spring Boot qui utilise des LLM pour comparer automatiquement des documents. L’accent est mis sur une architecture modulaire : la configuration des modèles IA et une configuration de prompts externalisée dans un fichier YAML, permettant une personnalisation facile et une intégration fluide avec différents fournisseurs d’IA sans modifier le code. Cette flexibilité permet aux développeurs de basculer entre différents modèles (OpenAI, modèles locaux via Ollama, Claude d’Anthropic, etc.) ou de modifier les prompts simplement via la configuration, rendant le système extensible et facile à maintenir.
Le comportement du service repose largement sur sa configuration, définie dans le fichier application.yml. Tous les paramètres essentiels relatifs au modèle d’IA et aux prompts y sont renseignés et liés à des classes de configuration Spring Boot. Cette approche centralise les paramètres critiques, si bien que les changements (par exemple, l’utilisation d’un autre modèle ou l’ajustement d’un paramètre comme la température) ne nécessitent aucune modification du code.
La section ai du fichier YAML définit les détails du fournisseur et du modèle IA. Les champs clés sont :
Exemple de configuration YAML pour différents fournisseurs :
Exemple OpenAI (application.yml) :
ai:
client: openaiClient
model: gpt-4-turbo
api-url: https://api.openai.com/v1/chat/completions
api-key: sk-...
parameters:
- temperature
values:
- 0.7
Exemple avec Ollama (modèle local Mistral) :
ai:
client: ollamaClient
model: mistral
api-url: http://localhost:11434/api/chat
parameters:
- temperature
values:
- 0.2
Exemple Anthropic Claude :
ai:
client: anthropicClient
model: claude-2.1
api-url: https://api.anthropic.com/v1/complete
api-key: <VOTRE_CLE_API_ANTHROPIC>
parameters:
- temperature
values:
- 0.5
Dans le même fichier YAML, une section de prompt distincte définit les modèles de prompt que le service utilisera pour interroger le LLM. Ce point est détaillé dans la section dédiée, mais en résumé, il permet de prédéfinir un prompt système (qui définit le contexte ou le rôle de l'IA) et un modèle de prompt utilisateur (qui inclut des espaces réservés pour le contenu des deux documents et d'autres paramètres dynamiques comme la langue ou le format).
Toutes ces propriétés sont mappées à des classes POJO via @ConfigurationProperties de Spring Boot. Par exemple, une classe AIProperties est liée au préfixe ai, et une classe PromptProperties est liée au préfixe de prompt. Cette liaison facilite l'injection de valeurs de configuration dans le service et les clients. Par exemple, la classe AIProperties pourrait ressembler à ceci :
@Component
@ConfigurationProperties(prefix = "ai")
public class AIProperties {
private String model;
private String apiUrl;
private String apiKey;
private String client;
private List<String> parameters;
private List<String> values;
// Getters and setters...
}
Grâce à cette approche pilotée par la configuration, vous pouvez modifier les fournisseurs d'IA ou les paramètres du modèle en modifiant simplement le fichier application.yml, sans modifier le code Java. Nous verrons ensuite comment les modèles de prompts sont définis dans la configuration et utilisés.
Afin de prendre en charge plusieurs fournisseurs de LLM (OpenAI, Ollama, Anthropic, etc.) de manière claire, le système est construit avec une abstraction client IA enfichable. La conception utilise une interface commune et une classe de base abstraite afin que les spécificités de chaque fournisseur soient isolées dans des classes de connecteur minimales. Les principaux composants de cette conception incluent :
Grâce à cette abstraction en couches, la prise en charge d'un nouveau fournisseur d'IA est simple et nécessite un minimum de modifications de code. Toute la configuration, comme les clés d'API, les URL, les noms de modèles et les paramètres, est fournie via YAML, ce qui permet à une nouvelle classe de connecteur de les exploiter sans avoir à coder en dur.
Voici un modèle de client d'IA personnalisé. Cet exemple présente un MyCustomClient hypothétique qui étend AbstractAIClient. Un développeur peut s'en servir comme point de départ pour intégrer un nouveau fournisseur LLM en personnalisant les méthodes extractContentFromResponse() et build() :
@Service("myCustomClient")
public class MyCustomClient extends AbstractAIClient {
public MyCustomClient(AIProperties props) {
super(props);
}
@Override
protected String extractContentFromResponse(Map<?, ?> response) {
List<Map<String, Object>> choices = (List<Map<String, Object>>) response.get("choices");
Map<String, Object> message = (Map<String, Object>) choices.get(0).get("message");
return (String) message.get("content");
}
@Override
protected void build() {
webClient = WebClient.builder()
.baseUrl(aiProperties.getApiUrl())
.defaultHeader("Authorization", "Bearer " + aiProperties.getApiKey())
.build();
}
}
Dans l'extrait ci-dessus, l'annotation @Service("myCustomClient") enregistre cette classe comme un bean Spring nommé "myCustomClient". En remplaçant la propriété YAML ai.client par "myCustomClient", le système utilisera ce nouveau client. La méthode extractContentFromResponse suppose que le JSON est structuré de manière similaire à celui d'OpenAI (avec une liste de choices contenant un message et un content), et devrait être adapté au format d'API du nouveau fournisseur. La méthode build() initialise le WebClient, en ajoutant ici un en-tête Authorization, par exemple pour les API nécessitant un jeton porteur.
Pour plus de contexte, voici à quoi ressemblent l'interface AIClient et une version simplifiée d'AbstractAIClient :
public interface AIClient {
String askModel(String systemPrompt, String userPrompt);
}
public abstract class AbstractAIClient implements AIClient {
protected AIProperties aiProperties;
protected WebClient webClient;
@Override
public String askModel(String systemPrompt, String userPrompt) {
Map<String, Object> body = buildBody(systemPrompt, userPrompt);
return webClient.post()
.bodyValue(body)
.retrieve()
.bodyToMono(Map.class)
.map(this::extractContentFromResponse)
.block();
}
private Map<String, Object> buildBody(String systemPrompt, String userPrompt) {
Map<String, Object> body = new HashMap<>();
body.put("model", aiProperties.getModel());
List<Map<String, String>> messages = new ArrayList<>();
messages.add(createMessage("system", systemPrompt));
messages.add(createMessage("user", userPrompt));
body.put("messages", messages);
List<String> params = aiProperties.getParameters();
List<String> vals = aiProperties.getValues();
if (params != null && vals != null) {
for (int i = 0; i < params.size(); i++) {
body.put(params.get(i), convertValue(vals.get(i)));
}
}
return body;
}
private Object convertValue(String value) {
if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) {
return Boolean.parseBoolean(value);
}
try {
return value.contains(".") ? Double.parseDouble(value) : Integer.parseInt(value);
} catch (NumberFormatException e) {
return value;
}
}
protected abstract String extractContentFromResponse(Map<?, ?> response);
protected abstract void build();
private Map<String, String> createMessage(String role, String content) {
Map<String, String> message = new HashMap<>();
message.put("role", role);
message.put("content", content);
return message;
}
}
En résumé, AbstractAIClient se charge de construire la requête (y compris l'insertion des prompts système et utilisateur dans une liste de messages et l'ajout de paramètres, comme la température, à partir de la configuration). Il utilise ensuite WebClient pour envoyer un POST à l'API URL et attend une réponse, attendue sous forme de mappage JSON. Le client concret se contente d'implémenter l'analyse de ce JSON (extractContentFromResponse) et de configurer WebClient (build). Cette architecture sépare clairement la logique spécifique au fournisseur du reste de l'application. Elle centralise également tous les identifiants et paramètres du fournisseur dans la configuration YAML, ce qui simplifie le changement ou l'ajout de nouvelles intégrations LLM.
Outre la configuration du client IA, les prompts eux-mêmes sont externalisés dans la configuration application.yml. Séparer les prompts du code permet aux non-développeurs d'ajuster la formulation et la structure de la requête IA sans toucher au code Java. Cela facilite également la maintenance de plusieurs modèles de prompts pour différents cas d'utilisation.
Dans le fichier YAML, les prompts sont généralement organisés dans une section « prompt ». Par exemple, nous pourrions avoir une section « prompt.compare » contenant les définitions de nos prompts de comparaison de documents. Chaque définition de prompt comprend au moins deux parties : system et utilisateur. Le système est une chaîne qui amorce l'IA avec une identité ou un comportement (par exemple, « Vous êtes une IA spécialisée dans l'analyse de documents. »), et l’utilisateur est une chaîne de modèle avec des espaces réservés où le contenu du document (et d'autres entrées dynamiques) sera inséré.
Par exemple, notre configuration pourrait définir deux variantes du prompt de comparaison : une générique (“generic”) et une détaillée (“detailed”). Le prompt générique peut permettre de spécifier dynamiquement une langue de sortie, tandis que le prompt détaillé peut être adapté à un domaine spécifique ou utiliser systématiquement une langue par défaut. Voici un extrait illustratif du fichier application.yml :
prompt:
compare:
detailed:
id: detailedContract
system: "Vous êtes une IA spécialisée dans l'analyse de documents."
user: |
Veuillez être exhaustif et fournir une comparaison très détaillée, point par point. Incluez toute différence subtile, même mineure.
Comparez les deux documents suivants :
Document A :
%s
Document B :
%s
generic:
id: genericComparison
system: "Vous êtes une IA spécialisée dans l'analyse de documents."
user: |
Répondez en %s. Comparez les deux documents suivants :
Document A :
%s
Document B :
%s
Le service remplace dynamiquement les placeholders (%s) avec le contenu textuel des documents (et éventuellement d’autres paramètres ex. : la langue).
Une fois la configuration et les clients en place, le contrôleur REST et la couche de service de l'application assurent la cohérence.
Le contrôleur REST expose un point de terminaison (par exemple, GET /ai-difference) qui déclenche la comparaison des documents. La couche de service contient la logique permettant de récupérer le contenu du document, d'appliquer le prompt et d'appeler le client IA. Contrôleur : Dans Spring Boot, une simple classe @RestController définit l'API. Par exemple :
@RestController
@RequestMapping("/api")
public class DocumentCompareController {
@Autowired
private ApplicationContext applicationContext;
@Autowired
private AIProperties aiProperties;
@Autowired
private CompareService compareService;
@GetMapping("/ai-difference")
public ResponseEntity<String> getAIDifference(
@RequestParam DocumentId leftDocumentId,
@RequestParam DocumentId rightDocumentId) {
AIClient client = initClient();
String result = compareService.compare(leftDocumentId, rightDocumentId, client);
return ResponseEntity.ok(result);
}
private AIClient initClient() {
AIClient selectedClient = (AIClient) applicationContext.getBean(aiProperties.getClient());
((AbstractAIClient) selectedClient).init(aiProperties);
return selectedClient;
}
}
Dans ce contrôleur :
Service : Le service de comparaison (appelons-le CompareService) contient la logique principale de récupération des données documentaires et d'interaction avec le LLM. Le pseudo-code de la méthode de comparaison du service pourrait ressembler à ceci :
@Service
public class CompareService {
@Autowired
private PromptProperties promptProperties;
@Autowired
private DocumentRepository documentRepository;
public String compare(DocumentId leftId, DocumentId rightId, AIClient client)
throws IOException {
// 1. Retrieve documents
String leftText = documentRepository.getText(leftId);
String rightText = documentRepository.getText(rightId);
// 2. Build prompt
Prompt prompt = promptProperties.getPromptMap().get("generic");
String systemPrompt = prompt.getSystem();
String userTemplate = prompt.getUser();
String userPrompt = String.format(userTemplate, "English", leftText, rightText);
// 3. Query the AI
String aiResponse = client.askModel(systemPrompt, userPrompt);
// 4. Return comparison
return aiResponse;
}
}
Quelques points à noter dans cette logique de service :
Cette conception assure une séparation claire des tâches : le contrôleur gère le protocole HTTP et le flux applicatif de haut niveau, le service gère la logique du domaine (récupération de documents et composition des prompts), et la couche client IA gère la communication avec le service IA externe.
Le contenu de prompts est externalisé dans la configuration et les spécificités du client IA sont encapsulées, ce qui facilite la maintenance et l'extension.
En suivant cette architecture, nous avons construit un service de comparaison de documents robuste et flexible alimenté par des LLM. La solution sépare proprement la configuration, l'incitation et l'intégration spécifique au fournisseur :
Similar posts
Découvrez comment un outil interne basé sur AWS et CodeceptJS a permis de simuler des scénarios utilisateurs pour comparer les temps de réponse entre...
Découvrez comment sécuriser les données et documents des caisses de retraite en conformité avec le RGPD. Apprenez l'importance du chiffrement, de la...
Optimisez la gestion des dossiers adhérents avec FlowerDocs eProcess ! Une plateforme unifiée alliant gestion documentaire et automatisation des...
Face à l’explosion documentaire, l’ECM devient un pilier stratégique pour l’Assurance, la Banque et les Mutuelles, boostant productivité, conformité,...
À partir du 1er octobre 2025, Microsoft interdira l'utilisation d'Office via licences SPLA ou sans utilisateur sur les clouds tiers (AWS, GCP,...
Cet article explore comment FlowerDocs 2025.0, avec une sécurité renforcée, des améliorations de performance et des fonctionnalités orientées...
Be the first to know about new B2B SaaS Marketing insights to build or refine your marketing function with the tools and knowledge of today’s industry.