La fenêtre de contexte est probablement le concept le plus sous-estimé par les développeurs qui débutent avec les LLMs. On se concentre sur le choix du modèle, la qualité des prompts, la gestion des erreurs — mais la fenêtre de contexte, ses implications sur les coûts, la performance, et la cohérence des réponses est souvent découverte trop tard, en production.

Cette leçon couvre tout ce que tu dois savoir pour concevoir des applications .NET robustes autour de cette contrainte fondamentale.

La fenêtre de contexte — définition précise

La fenêtre de contexte est la quantité maximale de tokens qu’un LLM peut traiter en une seule requête, en entrée ET en sortie confondus. Quand on dit que GPT-4o a une fenêtre de 128 000 tokens, ça signifie :

Tokens entrants + Tokens sortants ≤ 128 000

Ce qui compte dans les tokens “entrants” :

  • Le system prompt
  • Tout l’historique de la conversation
  • Les documents ou données que tu injectes
  • Les résultats d’outils (function calling, RAG)
ModèleFenêtre maxÉquivalent en motsÉquivalent en pages A4
GPT-4o128 000 tokens~96 000 mots~384 pages
GPT-4o mini128 000 tokens~96 000 mots~384 pages
Claude 3.5 Sonnet200 000 tokens~150 000 mots~600 pages
Gemini 1.5 Pro1 000 000 tokens~750 000 mots~3 000 pages
Llama 3.1 70B128 000 tokens~96 000 mots~384 pages

Un LLM n’a AUCUNE mémoire entre les appels

C’est le point que la majorité des développeurs ratent au début : par défaut, un LLM repart de zéro à chaque appel API. Il n’y a aucun état serveur, aucune session, aucune mémoire persistante côté modèle.

ChatGPT “se souvient” de ta conversation parce que l’interface web de OpenAI renvoie tout l’historique à chaque message. C’est ton frontend ou ton backend qui maintient et transmet cet historique — pas le modèle.

Ce qui signifie que dans ton app .NET, c’est entièrement ta responsabilité de :

  1. Stocker l’historique de chaque conversation
  2. Le transmettre à chaque nouvel appel API
  3. Le tronquer ou résumer quand il approche de la limite
  4. Le persister entre les sessions si l’utilisateur revient plus tard

Implémentation : gestionnaire de contexte de conversation

// ConversationManager.cs
// Gestionnaire de conversation avec limite de tokens et persistance

using OpenAI;
using OpenAI.Chat;
using System.Text.Json;

public class ConversationMessage
{
    public string Role { get; set; } = "";   // "system", "user", "assistant"
    public string Content { get; set; } = "";
    public DateTime Timestamp { get; set; } = DateTime.UtcNow;
    public int ApproxTokens { get; set; }   // estimation : chars / 4
}

public class ConversationManager
{
    private readonly ChatClient _client;
    private readonly List<ConversationMessage> _history = new();
    private readonly string _systemPrompt;
    private readonly int _maxContextTokens;
    private const int _responseReserve = 2000; // tokens réservés pour la réponse

    public ConversationManager(
        string apiKey,
        string systemPrompt,
        int maxContextTokens = 120_000)  // ~6% de marge sur 128k
    {
        _client = new ChatClient("gpt-4o", apiKey);
        _systemPrompt = systemPrompt;
        _maxContextTokens = maxContextTokens;

        _history.Add(new ConversationMessage
        {
            Role = "system",
            Content = systemPrompt,
            ApproxTokens = EstimateTokens(systemPrompt)
        });
    }

    public async Task<string> SendMessageAsync(string userMessage)
    {
        var userMsg = new ConversationMessage
        {
            Role = "user",
            Content = userMessage,
            ApproxTokens = EstimateTokens(userMessage)
        };

        _history.Add(userMsg);

        // Tronquer si nécessaire avant l'appel
        await TrimContextIfNeededAsync();

        // Construire la liste de messages pour l'API
        var apiMessages = _history.Select(m => m.Role switch
        {
            "system"    => (ChatMessage)ChatMessage.CreateSystemMessage(m.Content),
            "user"      => ChatMessage.CreateUserMessage(m.Content),
            "assistant" => ChatMessage.CreateAssistantMessage(m.Content),
            _           => throw new InvalidOperationException($"Role inconnu: {m.Role}")
        }).ToList();

        var response = await _client.CompleteChatAsync(apiMessages);
        var assistantReply = response.Value.Content[0].Text;

        _history.Add(new ConversationMessage
        {
            Role = "assistant",
            Content = assistantReply,
            ApproxTokens = EstimateTokens(assistantReply)
        });

        return assistantReply;
    }

    private async Task TrimContextIfNeededAsync()
    {
        var totalTokens = _history.Sum(m => m.ApproxTokens);
        var available = _maxContextTokens - _responseReserve;

        if (totalTokens <= available) return;

        Console.WriteLine($"Contexte trop long ({totalTokens} tokens). Résumé en cours...");

        // Garder system prompt + 3 derniers échanges
        var systemMsg = _history.First();
        var recentMessages = _history.Skip(1).TakeLast(6).ToList();
        var oldMessages = _history.Skip(1).SkipLast(6).ToList();

        if (oldMessages.Count == 0) return;

        // Résumer les anciens messages avec le LLM
        var summaryPrompt = $"""
            Résume cette conversation de façon concise, en préservant :
            - Les informations importantes échangées
            - Les décisions prises
            - Le contexte nécessaire pour la suite
            
            Conversation à résumer :
            {string.Join("\n", oldMessages.Select(m => $"{m.Role}: {m.Content}"))}
            """;

        var summaryMessages = new List<ChatMessage>
        {
            ChatMessage.CreateSystemMessage("Tu résumes des conversations de façon concise et fidèle."),
            ChatMessage.CreateUserMessage(summaryPrompt)
        };

        var summaryResponse = await _client.CompleteChatAsync(summaryMessages,
            new ChatCompletionOptions { Temperature = 0.1f });
        var summary = summaryResponse.Value.Content[0].Text;

        // Reconstruire l'historique : system + résumé + messages récents
        _history.Clear();
        _history.Add(systemMsg);
        _history.Add(new ConversationMessage
        {
            Role = "system",
            Content = $"[Résumé de la conversation précédente]\n{summary}",
            ApproxTokens = EstimateTokens(summary)
        });
        _history.AddRange(recentMessages);

        Console.WriteLine($"Contexte réduit à {_history.Sum(m => m.ApproxTokens)} tokens.");
    }

    // Estimation rapide : ~4 caractères par token (approximation raisonnable)
    private static int EstimateTokens(string text) => text.Length / 4;

    public int CurrentTokenCount => _history.Sum(m => m.ApproxTokens);

    // Sauvegarder la conversation
    public string SerializeHistory() => JsonSerializer.Serialize(_history);

    // Restaurer une conversation précédente
    public static ConversationManager DeserializeHistory(
        string apiKey, string json)
    {
        var history = JsonSerializer.Deserialize<List<ConversationMessage>>(json)!;
        var manager = new ConversationManager(
            apiKey,
            history.First(m => m.Role == "system").Content
        );
        manager._history.Clear();
        manager._history.AddRange(history);
        return manager;
    }
}

// Usage
var manager = new ConversationManager(
    apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY")!,
    systemPrompt: "Tu es un assistant .NET expert. Tu réponds en français."
);

while (true)
{
    Console.Write($"\n[{manager.CurrentTokenCount} tokens] Toi: ");
    var input = Console.ReadLine();
    if (string.IsNullOrEmpty(input)) break;

    var response = await manager.SendMessageAsync(input);
    Console.WriteLine($"\nAssistant: {response}");
}

Le problème des coûts — comprendre la tarification

Les LLMs sont facturés au token — séparément pour l’entrée et la sortie. Comprendre ça est essentiel pour concevoir des applications économiquement viables.

ModèleInput (per 1M tokens)Output (per 1M tokens)Bon pour
GPT-4o mini$0.15$0.60Volume élevé, cas simples
GPT-4o$2.50$10.00Tâches complexes, haute qualité
Claude 3.5 Sonnet$3.00$15.00Documents longs, code complexe
Claude 3 Haiku$0.25$1.25Volume élevé, latence faible
Gemini 1.5 Flash$0.075$0.30Le moins cher du marché

Calcul concret : Une app de chat avec 1000 utilisateurs actifs par jour, chacun faisant 10 messages de 100 tokens, avec un historique moyen de 2000 tokens et des réponses de 300 tokens. Coût quotidien avec GPT-4o mini : 1000 × 10 × (2100 tokens input + 300 tokens output) / 1M × $0.33 moy ≈ $8/jour. Avec GPT-4o : environ 10x plus. Choisir le bon modèle selon la complexité de la tâche peut faire une différence de 10x sur la facture.

Stratégie : router les requêtes selon la complexité

// Router automatiquement vers le modèle approprié selon la complexité
public enum TaskComplexity { Simple, Medium, Complex }

public class SmartLLMRouter
{
    private readonly ChatClient _cheapClient;   // gpt-4o-mini
    private readonly ChatClient _powerClient;   // gpt-4o

    public SmartLLMRouter(string apiKey)
    {
        _cheapClient  = new ChatClient("gpt-4o-mini", apiKey);
        _powerClient  = new ChatClient("gpt-4o",      apiKey);
    }

    public async Task<string> RouteAndCallAsync(
        string userMessage,
        string systemPrompt)
    {
        var complexity = ClassifyComplexity(userMessage);
        var client     = complexity == TaskComplexity.Complex ? _powerClient : _cheapClient;
        var model      = complexity == TaskComplexity.Complex ? "gpt-4o" : "gpt-4o-mini";

        Console.WriteLine($"Complexité: {complexity} → Modèle: {model}");

        var messages = new List<ChatMessage>
        {
            ChatMessage.CreateSystemMessage(systemPrompt),
            ChatMessage.CreateUserMessage(userMessage)
        };

        var response = await client.CompleteChatAsync(messages);
        return response.Value.Content[0].Text;
    }

    private static TaskComplexity ClassifyComplexity(string message)
    {
        // Critères simples — à affiner selon ton domaine
        var wordCount    = message.Split(' ').Length;
        var hasCode      = message.Contains("```") || message.Contains("code");
        var hasAnalysis  = message.Contains("analyse") || message.Contains("compare") ||
                           message.Contains("explique en détail");
        var isSimpleQa   = wordCount < 15 && !hasCode && !hasAnalysis;

        if (isSimpleQa)                    return TaskComplexity.Simple;
        if (hasCode || hasAnalysis)        return TaskComplexity.Complex;
        return                                    TaskComplexity.Medium;
    }
}

Le “Lost in the Middle” problem

Des recherches (Liu et al., 2023) ont montré que les LLMs sont moins précis sur les informations qui se trouvent au milieu d’un long contexte. Ils sont bons sur le début et la fin, moins bons sur le milieu.

Ce qui signifie concrètement :

  • Si tu envoies un contrat de 50 pages, les clauses des pages 20-35 risquent d’être moins bien traitées
  • Si tu as plusieurs documents, le plus important doit être en premier ou en dernier
  • Les instructions critiques dans le prompt doivent être au début ET rappelées à la fin

3 stratégies de mémoire pour tes apps

Stratégie 1 — Fenêtre glissante (simple, adapté au chat) : garde les N derniers messages. Perds le début de la conversation. Simple à implémenter, fonctionne pour la majorité des chatbots.

Stratégie 2 — Résumé progressif (implémenté ci-dessus) : quand le contexte dépasse un seuil, demande au LLM de résumer les anciens échanges. Préserve les informations clés sur le long terme. Ajoute un appel API supplémentaire.

Stratégie 3 — RAG (Retrieval Augmented Generation — pour la production) : stocke toutes les informations dans une base vectorielle. À chaque message, récupère uniquement les chunks pertinents. C’est la solution la plus scalable et la plus utilisée en production. On dédie une leçon entière au RAG dans le Module 3.

Exercice pratique

Implémente le ConversationManager de cette leçon dans une Console App .NET. Puis :

  1. Lance une conversation sur un sujet technique (.NET, architecture, etc.)
  2. Après 10-15 échanges, observe le nombre de tokens affiché dans le prompt
  3. Continue jusqu’à déclencher le résumé automatique
  4. Vérifie que le modèle se “souvient” encore des informations clés après le résumé
  5. Ajoute la sérialisation pour persister la conversation entre deux lancements de l’app

Récapitulatif

  • Fenêtre de contexte = input + output, tout compris
  • Pas de mémoire entre les appels — c’est ton code qui gère l’historique
  • Coût = (tokens input × prix input) + (tokens output × prix output)
  • Le “lost in the middle” : mettre les infos critiques au début ou à la fin
  • 3 stratégies : fenêtre glissante (simple) → résumé (moyen) → RAG (production)
  • Router vers le modèle adapté à la complexité pour optimiser les coûts

Dernière leçon du Module 1 : on compare GPT-4o, Claude, et Gemini pour t’aider à choisir le bon modèle selon ton projet .NET.