Intégration des LLM dans des applications web

Intégration des LLM dans des applications web

Introduction

Ce tutoriel vous guidera à travers la création d’une application web complète intégrant des LLM. Nous utiliserons React pour le frontend et Node.js pour le backend, avec l’API OpenAI comme service de LLM.

Architecture de l’application

Structure du projet

llm-web-app/
├── frontend/
│   ├── src/
│   │   ├── components/
│   │   ├── services/
│   │   ├── styles/
│   │   └── App.jsx
│   ├── package.json
│   └── vite.config.js
├── backend/
│   ├── src/
│   │   ├── controllers/
│   │   ├── services/
│   │   ├── routes/
│   │   └── server.js
│   ├── package.json
│   └── .env
└── README.md

Backend (Node.js/Express)

1. Configuration du serveur

// backend/src/server.js
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import chatRoutes from './routes/chat.routes.js';

dotenv.config();

const app = express();
const PORT = process.env.PORT || 3000;

app.use(cors());
app.use(express.json());
app.use('/api/chat', chatRoutes);

app.listen(PORT, () => {
  console.log(`Serveur démarré sur le port ${PORT}`);
});

2. Service LLM

// backend/src/services/llm.service.js
import OpenAI from 'openai';

class LLMService {
  constructor() {
    this.openai = new OpenAI({
      apiKey: process.env.OPENAI_API_KEY
    });
  }

  async generateResponse(prompt, context = []) {
    try {
      const response = await this.openai.chat.completions.create({
        model: "gpt-3.5-turbo",
        messages: [
          { role: "system", content: "Tu es un assistant utile et précis." },
          ...context,
          { role: "user", content: prompt }
        ],
        temperature: 0.7,
        max_tokens: 150
      });

      return response.choices[0].message.content;
    } catch (error) {
      console.error('Erreur LLM:', error);
      throw new Error('Erreur lors de la génération de la réponse');
    }
  }
}

export default new LLMService();

3. Contrôleur

// backend/src/controllers/chat.controller.js
import LLMService from '../services/llm.service.js';

class ChatController {
  async handleChat(req, res) {
    try {
      const { message, context } = req.body;
      
      if (!message) {
        return res.status(400).json({ error: 'Message requis' });
      }

      const response = await LLMService.generateResponse(message, context);
      
      res.json({ response });
    } catch (error) {
      console.error('Erreur contrôleur:', error);
      res.status(500).json({ error: 'Erreur serveur' });
    }
  }
}

export default new ChatController();

4. Routes

// backend/src/routes/chat.routes.js
import express from 'express';
import ChatController from '../controllers/chat.controller.js';

const router = express.Router();

router.post('/message', ChatController.handleChat);

export default router;

Frontend (React)

1. Service API

// frontend/src/services/api.service.js
const API_URL = 'http://localhost:3000/api';

export const chatService = {
  async sendMessage(message, context = []) {
    try {
      const response = await fetch(`${API_URL}/chat/message`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ message, context }),
      });

      if (!response.ok) {
        throw new Error('Erreur réseau');
      }

      return await response.json();
    } catch (error) {
      console.error('Erreur API:', error);
      throw error;
    }
  }
};

2. Composant Chat

// frontend/src/components/Chat.jsx
import React, { useState, useRef, useEffect } from 'react';
import { chatService } from '../services/api.service';
import '../styles/Chat.css';

const Chat = () => {
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');
  const [loading, setLoading] = useState(false);
  const messagesEndRef = useRef(null);

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  };

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!input.trim()) return;

    const userMessage = input.trim();
    setInput('');
    setLoading(true);

    // Ajouter le message de l'utilisateur
    setMessages(prev => [...prev, { role: 'user', content: userMessage }]);

    try {
      const response = await chatService.sendMessage(userMessage, messages);
      
      // Ajouter la réponse du LLM
      setMessages(prev => [...prev, { role: 'assistant', content: response.response }]);
    } catch (error) {
      console.error('Erreur chat:', error);
      // Ajouter un message d'erreur
      setMessages(prev => [...prev, { 
        role: 'system', 
        content: 'Désolé, une erreur est survenue.' 
      }]);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="chat-container">
      <div className="messages-container">
        {messages.map((message, index) => (
          <div 
            key={index} 
            className={`message ${message.role}`}
          >
            {message.content}
          </div>
        ))}
        {loading && (
          <div className="message assistant loading">
            En train de réfléchir...
          </div>
        )}
        <div ref={messagesEndRef} />
      </div>

      <form onSubmit={handleSubmit} className="input-form">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Tapez votre message..."
          disabled={loading}
        />
        <button type="submit" disabled={loading}>
          Envoyer
        </button>
      </form>
    </div>
  );
};

export default Chat;

3. Styles CSS

/* frontend/src/styles/Chat.css */
.chat-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  height: 100vh;
  display: flex;
  flex-direction: column;
}

.messages-container {
  flex-grow: 1;
  overflow-y: auto;
  padding: 20px;
  background: #f5f5f5;
  border-radius: 8px;
  margin-bottom: 20px;
}

.message {
  margin: 10px 0;
  padding: 10px 15px;
  border-radius: 8px;
  max-width: 70%;
}

.message.user {
  background: #007bff;
  color: white;
  margin-left: auto;
}

.message.assistant {
  background: white;
  border: 1px solid #ddd;
  margin-right: auto;
}

.message.system {
  background: #ff4444;
  color: white;
  margin: 10px auto;
  text-align: center;
}

.input-form {
  display: flex;
  gap: 10px;
}

input {
  flex-grow: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
}

button {
  padding: 10px 20px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}

button:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.loading {
  opacity: 0.7;
  font-style: italic;
}

Fonctionnalités avancées

1. Gestion du contexte

// frontend/src/hooks/useChatContext.js
import { useState, useCallback } from 'react';

export const useChatContext = () => {
  const [context, setContext] = useState([]);

  const addToContext = useCallback((message) => {
    setContext(prev => [...prev, message]);
  }, []);

  const clearContext = useCallback(() => {
    setContext([]);
  }, []);

  return {
    context,
    addToContext,
    clearContext
  };
};

2. Gestion des erreurs

// frontend/src/utils/errorHandler.js
export const handleError = (error) => {
  if (error.response) {
    // Erreur serveur
    return `Erreur serveur: ${error.response.status}`;
  } else if (error.request) {
    // Erreur réseau
    return 'Erreur de connexion au serveur';
  } else {
    // Autre erreur
    return 'Une erreur inattendue est survenue';
  }
};

3. Mise en cache des réponses

// frontend/src/services/cache.service.js
class CacheService {
  constructor() {
    this.cache = new Map();
  }

  set(key, value, ttl = 3600000) { // TTL par défaut: 1 heure
    const item = {
      value,
      expiry: Date.now() + ttl
    };
    this.cache.set(key, item);
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;
    
    if (Date.now() > item.expiry) {
      this.cache.delete(key);
      return null;
    }
    
    return item.value;
  }
}

export default new CacheService();

Déploiement

1. Configuration de production

// backend/src/config/production.js
export const productionConfig = {
  port: process.env.PORT || 3000,
  cors: {
    origin: process.env.FRONTEND_URL,
    credentials: true
  },
  rateLimit: {
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100 // limite chaque IP à 100 requêtes par fenêtre
  }
};

2. Script de déploiement

#!/bin/bash
# deploy.sh

# Build frontend
cd frontend
npm run build

# Déploy backend
cd ../backend
npm install --production
pm2 start src/server.js --name "llm-web-app"

Tests

1. Tests unitaires

// backend/src/tests/llm.service.test.js
import { jest } from '@jest/globals';
import LLMService from '../services/llm.service';

describe('LLMService', () => {
  test('generateResponse retourne une réponse valide', async () => {
    const prompt = 'Test prompt';
    const response = await LLMService.generateResponse(prompt);
    
    expect(response).toBeDefined();
    expect(typeof response).toBe('string');
    expect(response.length).toBeGreaterThan(0);
  });
});

2. Tests d’intégration

// backend/src/tests/chat.controller.test.js
import request from 'supertest';
import app from '../server';

describe('Chat Controller', () => {
  test('POST /api/chat/message', async () => {
    const response = await request(app)
      .post('/api/chat/message')
      .send({ message: 'Test message' });
    
    expect(response.status).toBe(200);
    expect(response.body).toHaveProperty('response');
  });
});

Sécurité

1. Protection des clés API

// backend/src/middleware/apiKey.js
export const validateApiKey = (req, res, next) => {
  const apiKey = req.headers['x-api-key'];
  
  if (!apiKey || apiKey !== process.env.API_KEY) {
    return res.status(401).json({ error: 'Clé API invalide' });
  }
  
  next();
};

2. Rate Limiting

// backend/src/middleware/rateLimit.js
import rateLimit from 'express-rate-limit';

export const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limite chaque IP à 100 requêtes par fenêtre
  message: 'Trop de requêtes, veuillez réessayer plus tard'
});

Conclusion

Ce tutoriel vous a montré comment créer une application web complète intégrant des LLM. Vous avez appris à :

Dans le prochain tutoriel, nous explorerons des cas d’utilisation plus avancés et des techniques d’optimisation pour des scénarios spécifiques.