LLM Design Patterns : structurer son développement IA comme un vrai projet logiciel

L’intégration d’un LLM dans une application n’est pas qu’une affaire de prompt. Dès que l’on quitte l’expérimentation pour construire un vrai produit, il faut retrouver les réflexes d’architecture logicielle. Le modèle devient un service à part entière : il s’oriente, il se contraint, il s’intègre, et il doit répondre de manière prévisible.Le développement local avec **Ollama met encore plus en évidence cette discipline. On peut profiler le modèle, versionner son environnement, tester les flux comme on teste un service métier. L’IA s’intègre dans l’écosystème, elle n’est plus une magie externe.## Le Pattern Intent Router
Dans une architecture API, le point d’entrée unique facilite le contrôle.
Le client appelle toujours /ask, et c’est le router qui décide de la suite. Le LLM ne reçoit un message JSON structuré indiquant ce que l’utilisateur veut faire.Endpoint /askapp.MapPost("/ask", async (AskRequest request, IntentRouter router) => { var result = await router.RouteAsync(request); return Results.Json(result); });Analyse de l’intention par le LLM```
public async Task<string> ExtractIntent(string userPrompt)
{
var scaffold = new
{
instruction = "Détermine l'intention utilisateur. Réponds uniquement avec un mot clé.",
input = userPrompt
};
var response = await _ollama.GenerateJsonAsync(scaffold);
return response["intent"].GetValue<string>();
}
Cela réduit dramatiquement les ambiguïtés et s’intègre naturellement dans des workflows API.**Requête envoyée au backend**```
POST /ask
{
"action": "summarize",
"payload": {
"topic": "l'évolution de .NET",
"length": "short",
"format": "markdown"
}
}
```**Prompt interne généré pour Ollama**```
{
"instruction": "Tu génères un résumé structuré selon les paramètres fournis.",
"rules": {
"length": "short|medium|long",
"format": "markdown|text"
},
"input": {
"topic": "l'évolution de .NET",
"length": "short",
"format": "markdown"
}
}
```**Côté C#**```
public async Task<string> SummarizeAsync(SummaryPayload payload)
{
var prompt = new
{
instruction = "Génère un résumé strictement structuré.",
rules = new { },
input = payload
};
var result = await _ollama.GenerateMarkdownAsync(prompt);
return result;
}
```Le modèle n’improvise pas, il remplit un contrat JSON.## Le Pattern Tooling[](https://xavierraboteur.wordpress.com/wp-content/uploads/2025/12/image-10.png)Les outils exposés au modèle deviennent des actions internes de l’API comme récupérer la météo, lire un utilisateur en base, appeler un service externe.Le LLM ne fait que demander l’outil via un JSON normalisé et le backend exécute.
L’IA décide, l’API exécute, et rien n’est laissé au hasard.**Prompt interne :**```
{
"instruction": "Si l'utilisateur parle d'un profil, appelle l'outil get_user.",
"input": "Donne-moi les infos pour l'utilisateur jean@example.com",
"tools": [
{ "name": "get_user", "schema": { "email": "string" } }
]
}
```**Le modèle renverra :**```
{
"tool_call": "get_user",
"arguments": { "email": "jean@example.com" }
}
```**Côté C# :**```
if (response.ToolCall == "get_user")
{
var email = response.Arguments["email"].GetValue<string>();
return await _db.Users.FirstAsync(u => u.Email == email);
}
```## Le Pattern Memory[](https://xavierraboteur.wordpress.com/wp-content/uploads/2025/12/image-11.png)La mémoire n’est pas un vague historique, c’est un store structuré, consultable via le EndPoint ***/session/{id}/memory***.Le modèle s’appuie sur des données réelles, jamais sur une “intuition”.**Stockage**```
public async Task SaveMemoryAsync(string sessionId, LlmMemory memory)
{
await _db.Memories.UpsertAsync(new MemoryRecord(sessionId, memory));
}
```**Réinjection dans les prompts**À chaque appel **/ask**, on recharge le contexte :```
var memory = await _memory.LoadAsync(request.SessionId);
var prompt = new
{
instruction = "Tiens compte du contexte conversationnel fourni.",
memory,
input = request.Payload
};
```## Le Pattern Guardrail[](https://xavierraboteur.wordpress.com/wp-content/uploads/2025/12/image-12.png)Sortie non conforme → rejet immédiat.
L’API joue le rôle de pare-feu.Validation JSON stricte```
var isValid = JsonValidator.ValidateSchema(response, summarySchema);
if (!isValid)
{
return Results.BadRequest(new { error = "Réponse non conforme" });
}
```Les guardrails transforment une IA bavarde en un service fiable.## Le Pattern Multi-Pass[](https://xavierraboteur.wordpress.com/wp-content/uploads/2025/12/image-13.png)Pour améliorer la qualité de la réponse, le backend enchaîne plusieurs passes vers le modèle.
Tout passe toujours par l’**unique endpoint /ask**, mais en interne, l’**API orchestre plusieurs séquences**.**Exemple pour la création de contenu :**```
var plan = await _ollama.GenerateAsync(new { step = "plan", input = req });
var draft = await _ollama.GenerateAsync(new { step = "draft", plan });
var final = await _ollama.GenerateAsync(new { step = "refine", draft });
```Le client ne voit qu’un appel et le backend orchestre trois passes.## Le Pattern Deterministic Output[](https://xavierraboteur.wordpress.com/wp-content/uploads/2025/12/image-14.png)Quand l’API doit exploiter la sortie immédiatement, on impose au modèle un format stable.**Contrat JSON exigé par le LLM**```
{
"summary": "string",
"keywords": ["string"],
"score": "number"
}
```**Parsing fiable côté C#**```
var dto = JsonSerializer.Deserialize<StructuredSummary>(rawResponse);
```Avec un protocole clair, nous avons zéro surprise.## Architecture (API .NET + Ollama)[](https://xavierraboteur.wordpress.com/wp-content/uploads/2025/12/image-1.png)**Tout converge vers */ask*.** C’est le cerveau qui organise, pas le modèle.## Un développement IA réellement maîtriséAvec un LLM local, une API .NET et un point d’entrée unique, l’IA devient un service maîtrisé :
un flux propre, guidé, déterministe, intégrable comme n’importe quel composant applicatif.Ces patterns ne sont pas théoriques ; ils sont opérationnels. Ils permettent de construire une plateforme IA robuste, testable, prédictible, adaptée à la production.