Partie 5 de la série “Développement IA avec Ollama et .NET” : Partie 1 – Ollama et .NET | Partie 2 – RAG local | Partie 3 – Agents IA | Partie 3.5 – Serveur MCP | Partie 4 – Microsoft Agent Framework | English
Dans le reste de la série, Ollama tournait à la main : installé localement sur la machine, le ollama pull fait manuellement, et l’application .NET qui pointait sur l’adresse locale d’Ollama codée en dur. Ça fonctionne, mais c’est plusieurs étapes manuelles à refaire sur chaque poste. Aspire prend en charge cette configuration. J’ai voulu voir ce que ça change.
Le code complet est disponible sur GitHub.
Le problème avec Ollama géré à la main
Prenez une application Blazor qui parle à un modèle, le modèle qui roule dans Ollama, et vous au milieu pour faire le pont. Vous démarrez Ollama, vous vérifiez qu’il écoute sur le bon port, vous faites le pull du modèle si c’est une nouvelle machine, et vous mettez l’URL dans appsettings.json. Quand un collègue clone le dépôt, il refait tout ça à sa manière.
Et rien de tout ça n’est visible. Si une réponse prend 12 secondes, rien n’indique si c’est le pull, le chargement en mémoire ou un prompt trop long.
Aspire permet de déclarer chaque morceau comme une ressource et de les afficher dans un dashboard.
Ollama comme ressource dans l’AppHost
On part d’un projet AppHost Aspire (dotnet new aspire-apphost, ou le template complet). On ajoute l’intégration d’hébergement Ollama du Community Toolkit :
dotnet add package CommunityToolkit.Aspire.Hosting.Ollama
Dans le AppHost.cs, Ollama devient une ressource et le modèle, une sous-ressource :
var builder = DistributedApplication.CreateBuilder(args);
// Le serveur Ollama tourne dans un conteneur géré par Aspire
var ollama = builder.AddOllama("ollama")
.WithDataVolume(); // on persiste les modèles téléchargés
// Le modèle est une ressource à part entière : Aspire le télécharge au démarrage
var chat = ollama.AddModel("chat", "llama3.2");
// L'app Blazor reçoit la connexion vers le modèle et attend qu'il soit prêt
builder.AddProject<Projects.Web>("web")
.WithReference(chat)
.WaitFor(chat);
builder.Build().Run();
Regardez AddModel("chat", "llama3.2"). Le premier argument est le nom de la ressource, celui qu’on référence du côté de l’application. Le deuxième est le tag du modèle Ollama. Aspire fait le pull au démarrage, et le WaitFor empêche l’application de partir avant que le modèle soit chargé. L’application ne démarre plus avant que le modèle soit prêt.
Brancher l’app Blazor
Du côté de l’application Blazor, on ajoute l’intégration client OllamaSharp :
dotnet add package CommunityToolkit.Aspire.OllamaSharp
Dans le Program.cs, une ligne enregistre le client Ollama et un IChatClient branchés sur la ressource chat :
builder.AddOllamaApiClient("chat").AddChatClient();
Le nom "chat" correspond à la ressource déclarée dans l’AppHost. Pas d’URL, pas de port, pas de configuration en dur : Aspire fait la découverte de service et l’application reçoit la bonne adresse au démarrage. Ces appels (AddOllamaApiClient(...).AddChatClient()) demandent CommunityToolkit.Aspire.OllamaSharp 13.4.0 ou plus; les versions antérieures utilisaient AddOllamaSharpChatClient, maintenant déprécié. Pour le pipeline complet de Microsoft.Extensions.AI, on peut empiler les middlewares :
builder.AddOllamaApiClient("chat")
.AddChatClient()
.UseFunctionInvocation()
.UseOpenTelemetry(configure: t => t.EnableSensitiveData = builder.Environment.IsDevelopment())
.UseLogging();
EnableSensitiveData fait enregistrer les prompts et les réponses dans les traces. Ce flag aide à déboguer, mais il faut l’éviter en production, où il finirait par stocker des données personnelles. On le limite donc au développement avec builder.Environment.IsDevelopment().
Dans un composant Blazor, on injecte IChatClient et on l’utilise directement :
@inject IChatClient ChatClient
@code {
private async Task<string> Demander(string question)
{
var reponse = await ChatClient.GetResponseAsync(question);
return reponse.Text;
}
}
Le composant ne connaît pas Ollama, il connaît IChatClient. Cette abstraction a son importance pour la production, j’y reviens plus loin.
Le dashboard Aspire
On lance avec aspire run (ou F5) et le dashboard Aspire s’ouvre. On y voit chaque ressource avec son état : le conteneur Ollama qui démarre, le modèle qui se télécharge, l’application Blazor qui attend.
Au démarrage : Ollama tourne, le modèle se télécharge, l’app web attend.
Une fois le modèle téléchargé, les trois ressources passent à Running.
L’onglet des traces est le plus utile. Comme le IChatClient passe par UseOpenTelemetry(), chaque appel au modèle devient une trace avec le temps total, le temps de chargement et les tokens. Quand une réponse prend 12 secondes, la trace montre où va le temps. Les logs du conteneur Ollama sont dans le même dashboard, sans avoir à fouiller dans docker logs.
Mettre en place cette télémétrie à la main demandait plus de travail, et c’était souvent remis à plus tard.
La lenteur du premier run
Le premier aspire run sur une machine neuve est lent. Aspire télécharge l’image du conteneur Ollama, puis le modèle par-dessus. Un llama3.2 pèse quelques gigaoctets, donc le téléchargement prend un moment.
Le .WithDataVolume() monte un volume sur /root/.ollama dans le conteneur, là où Ollama garde ses modèles. Sans volume, chaque redémarrage du conteneur recommence le pull au complet. Avec le volume, le modèle reste entre les démarrages et le deuxième run est presque instantané. Ajoutez-le dès le départ pour éviter de re-télécharger le modèle à chaque redémarrage.
L’accélération GPU
Sur une machine avec une carte Nvidia, on active l’accélération avec une ligne :
var ollama = builder.AddOllama("ollama")
.WithDataVolume()
.WithGPUSupport(OllamaGpuVendor.Nvidia);
Aspire passe la configuration GPU au conteneur. Ça fonctionne bien quand le poste a les pilotes Nvidia et le nvidia-container-toolkit installés. Sur un portable sans GPU dédié, on laisse tomber et Ollama roule sur le CPU, plus lentement mais assez pour développer. .WithOpenWebUI() ajoute une console web de chat à côté, utile pour valider le modèle sans passer par votre application.
Le passage en production
Aspire est excellent dans la boucle de développement, et c’est d’abord conçu pour ça : le F5, la configuration entre les services, le dashboard, la découverte de service.
Pour déployer, aspire publish génère les artefacts (un manifeste) qu’on peut cibler vers Azure Container Apps ou Docker Compose. L’AppHost lui-même ne roule pas en production : c’est un modèle d’orchestration pour le développement et une étape de publication. En production, l’orchestrateur, c’est Container Apps, Kubernetes ou Compose.
En production, l’hébergement d’Ollama en conteneur soulève plusieurs questions. L’image ollama/ollama est grosse. Le modèle pèse plusieurs gigaoctets et il faut décider où il vit : l’inclure dans l’image, qui devient énorme; le mettre sur un volume persistant, qui demande du stockage qui suit l’application; ou le télécharger au démarrage, avec un démarrage à froid plus lent. L’inférence demande aussi un GPU, sinon chaque réponse traîne. Héberger son propre LLM demande de vraies décisions d’infrastructure.
L’abstraction IChatClient aide pour ce passage. En développement, le client pointe sur Ollama local. En production, on le remplace par un endpoint hébergé, comme Azure AI Foundry ou un autre service géré, en changeant l’enregistrement, sans toucher aux composants Blazor qui appellent GetResponseAsync. Le code d’appel reste identique, seule l’implémentation derrière l’interface change. Pour la configuration sensible, on passe par les paramètres Aspire plutôt que des clés dans appsettings.json.
En pratique, j’utilise Aspire pour orchestrer le développement et garder le tout cohérent, Ollama en local pour itérer sans coûts d’API, et un endpoint géré derrière le même IChatClient une fois en production. La décision d’héberger un modèle ne se pose qu’au moment du déploiement.
Bon déploiement, et si le premier aspire run a l’air figé, c’est juste le modèle qui se télécharge en arrière-plan, laissez-lui deux minutes.
Cet article a été rédigé avec l’aide de l’IA et révisé par moi.