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èle | Fenêtre max | Équivalent en mots | Équivalent en pages A4 |
|---|---|---|---|
| GPT-4o | 128 000 tokens | ~96 000 mots | ~384 pages |
| GPT-4o mini | 128 000 tokens | ~96 000 mots | ~384 pages |
| Claude 3.5 Sonnet | 200 000 tokens | ~150 000 mots | ~600 pages |
| Gemini 1.5 Pro | 1 000 000 tokens | ~750 000 mots | ~3 000 pages |
| Llama 3.1 70B | 128 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 :
- Stocker l’historique de chaque conversation
- Le transmettre à chaque nouvel appel API
- Le tronquer ou résumer quand il approche de la limite
- 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èle | Input (per 1M tokens) | Output (per 1M tokens) | Bon pour |
|---|---|---|---|
| GPT-4o mini | $0.15 | $0.60 | Volume élevé, cas simples |
| GPT-4o | $2.50 | $10.00 | Tâches complexes, haute qualité |
| Claude 3.5 Sonnet | $3.00 | $15.00 | Documents longs, code complexe |
| Claude 3 Haiku | $0.25 | $1.25 | Volume élevé, latence faible |
| Gemini 1.5 Flash | $0.075 | $0.30 | Le 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 :
- Lance une conversation sur un sujet technique (.NET, architecture, etc.)
- Après 10-15 échanges, observe le nombre de tokens affiché dans le prompt
- Continue jusqu’à déclencher le résumé automatique
- Vérifie que le modèle se “souvient” encore des informations clés après le résumé
- 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.