Construire des agents IA avec Ollama et .NET

Agent IA avec des nœuds interconnectés représentant le raisonnement, la planification et l'exécution d'outils

Construire des agents IA avec Ollama et .NET

Partie 3 de 3 de la série “IA locale avec Ollama et .NET” : Partie 1 – Développement IA local | Partie 2 – RAG local | English

Dans les articles précédents, nous avons exploré l’exécution de LLM locaux avec Ollama et la construction de systèmes RAG. Maintenant, nous allons plus loin : des agents IA autonomes capables de raisonner, planifier et prendre des actions pour résoudre des tâches complexes.

Un exemple complet et fonctionnel est disponible ici : mongeon/code-examples · ai-agents-ollama-dotnet.

Que sont les agents IA ?

Un agent IA est plus qu’un chatbot. Alors qu’un chatbot répond à des prompts, un agent :

  • Raisonne sur les problèmes et les décompose en étapes
  • Planifie des solutions multi-étapes
  • Utilise des outils (API, bases de données, exécution de code, systèmes de fichiers)
  • S’auto-corrige en cas d’erreurs
  • Se souvient du contexte entre les interactions
  • Agit de manière autonome pour atteindre des objectifs

Pensez à la différence :

Chatbot : “Quel temps fait-il à Paris ?”
→ Réponse : “Je ne peux pas vérifier les données en temps réel.”

Agent : “Quel temps fait-il à Paris ?”
→ L’agent appelle WeatherAPI(“Paris”) → “Actuellement ensoleillé, 22°C”

L’agent a accès à des outils et sait quand les utiliser.

Composants de base d’un agent IA

1. Le moteur de raisonnement (LLM)

Le LLM (via Ollama) agit comme le “cerveau” de l’agent :

  • Analyse les requêtes des utilisateurs
  • Décide quels outils invoquer
  • Interprète les résultats
  • Planifie les prochaines étapes

Les modèles modernes comme Llama 3.3, Qwen 2.5 et Mistral excellent dans le raisonnement et l’appel de fonctions.

2. Outils (Fonctions)

Les outils sont les capacités que l’agent peut invoquer :

public interface ITool
{
    string Name { get; }
    string Description { get; }
    Task<string> ExecuteAsync(Dictionary<string, object> parameters);
}

Exemples d’outils :

  • SearchWeb(query) - Recherches Internet
  • ExecuteCode(code) - Exécution de code
  • QueryDatabase(sql) - Accès base de données
  • ReadFile(path) - Opérations sur fichiers
  • SendEmail(to, subject, body) - Communication externe
  • Calculate(expression) - Opérations mathématiques

Chaque outil a une description claire qui aide le LLM à décider quand l’utiliser.

3. Systèmes de mémoire

Mémoire à court terme : Contexte de la conversation actuelle

List<ChatMessage> conversationHistory;

Mémoire à long terme : Connaissance persistante (peut exploiter le RAG de la partie 2)

// Stockage dans Qdrant de l'article précédent
IVectorStore vectorMemory;

Mémoire de travail : Bloc-notes temporaire pendant l’exécution de tâches

Dictionary<string, object> workingMemory;

4. Boucle d’agent

La boucle d’exécution qui pilote le comportement de l’agent :

public async Task<string> RunAsync(string goal, int maxIterations = 10)
{
    var messages = new List<ChatMessage> 
    { 
        new ChatMessage("user", goal) 
    };
    
    for (int i = 0; i < maxIterations; i++)
    {
        var response = await llm.ChatAsync(messages, availableTools);
        
        if (response.ToolCalls.Any())
        {
            foreach (var toolCall in response.ToolCalls)
            {
                var result = await ExecuteToolAsync(toolCall);
                messages.Add(new ToolResultMessage(toolCall.Id, result));
            }
        }
        else if (response.IsComplete)
        {
            return response.Content;
        }
        
        messages.Add(response);
    }
    
    throw new MaxIterationsException();
}

Le pattern ReAct

ReAct (Raisonnement + Action) est le pattern d’agent fondamental :

┌─────────────────────────────────────────────────┐
│ 1. PENSÉE : Analyser le problème               │
│    "Je dois d'abord vérifier la météo"         │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 2. ACTION : Appeler un outil                    │
│    GetWeather("Paris")                          │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 3. OBSERVATION : Traiter le résultat            │
│    "Température : 22°C, Condition : Ensoleillé" │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 4. PENSÉE : Décider de la prochaine étape       │
│    "Bon temps, je peux recommander activités"   │
└─────────────────────────────────────────────────┘
              [Répéter ou Terminer]

L’agent alterne entre la réflexion (raisonnement) et l’action (utilisation d’outils) jusqu’à atteindre une conclusion.

Construire un agent simple en .NET

Construisons un agent de base avec des capacités d’appel d’outils en utilisant Ollama.

Étape 1 : Définir les outils

public class CalculatorTool : ITool
{
    public string Name => "calculator";
    public string Description => "Effectue des calculs mathématiques. Entrée : expression (string)";
    
    public Task<string> ExecuteAsync(Dictionary<string, object> parameters)
    {
        var expression = parameters["expression"].ToString();
        var result = EvaluateExpression(expression); // Utiliser NCalc ou similaire
        return Task.FromResult(result.ToString());
    }
    
    private double EvaluateExpression(string expr)
    {
        var e = new NCalc.Expression(expr);
        return Convert.ToDouble(e.Evaluate());
    }
}

public class WebSearchTool : ITool
{
    public string Name => "web_search";
    public string Description => "Recherche sur le web. Entrée : query (string)";
    
    private readonly HttpClient _client;
    
    public async Task<string> ExecuteAsync(Dictionary<string, object> parameters)
    {
        var query = parameters["query"].ToString();
        // Utiliser DuckDuckGo, Google Custom Search ou Bing API
        var results = await SearchAsync(query);
        return JsonSerializer.Serialize(results.Take(3));
    }
}

Étape 2 : Registre d’outils

public class ToolRegistry
{
    private readonly Dictionary<string, ITool> _tools = new();
    
    public void RegisterTool(ITool tool)
    {
        _tools[tool.Name] = tool;
    }
    
    public async Task<string> ExecuteAsync(string toolName, Dictionary<string, object> parameters)
    {
        if (!_tools.TryGetValue(toolName, out var tool))
            throw new ToolNotFoundException(toolName);
            
        return await tool.ExecuteAsync(parameters);
    }
    
    public List<ToolDefinition> GetToolDefinitions()
    {
        return _tools.Values.Select(t => new ToolDefinition
        {
            Name = t.Name,
            Description = t.Description
        }).ToList();
    }
}

Étape 3 : Implémentation de l’agent

public class OllamaAgent
{
    private readonly HttpClient _client;
    private readonly string _model;
    private readonly ToolRegistry _tools;
    private readonly List<ChatMessage> _history;
    
    public OllamaAgent(string model = "llama3.3")
    {
        _client = new HttpClient { BaseAddress = new Uri("http://localhost:11434") };
        _model = model;
        _tools = new ToolRegistry();
        _history = new List<ChatMessage>();
        
        // Enregistrer les outils
        _tools.RegisterTool(new CalculatorTool());
        _tools.RegisterTool(new WebSearchTool());
    }
    
    public async Task<string> RunAsync(string goal, int maxIterations = 10)
    {
        _history.Add(new ChatMessage 
        { 
            Role = "user", 
            Content = goal 
        });
        
        for (int iteration = 0; iteration < maxIterations; iteration++)
        {
            var response = await CallLLMAsync();
            
            // Vérifier si le modèle veut appeler un outil
            if (TryParseToolCall(response, out var toolCall))
            {
                Console.WriteLine($"[Agent] Appel d'outil : {toolCall.Name}");
                
                var result = await _tools.ExecuteAsync(
                    toolCall.Name, 
                    toolCall.Parameters
                );
                
                Console.WriteLine($"[Agent] Résultat : {result}");
                
                _history.Add(new ChatMessage
                {
                    Role = "tool",
                    Name = toolCall.Name,
                    Content = result
                });
            }
            else
            {
                // Réponse finale
                return response;
            }
        }
        
        throw new Exception("L'agent a dépassé le nombre maximum d'itérations");
    }
    
    private async Task<string> CallLLMAsync()
    {
        var systemPrompt = BuildSystemPrompt();
        
        var request = new
        {
            model = _model,
            messages = new[] 
            { 
                new { role = "system", content = systemPrompt } 
            }.Concat(_history.Select(m => new { role = m.Role, content = m.Content })),
            stream = false
        };
        
        var response = await _client.PostAsJsonAsync("/api/chat", request);
        var result = await response.Content.ReadFromJsonAsync<OllamaResponse>();
        
        var assistantMessage = result.Message.Content;
        _history.Add(new ChatMessage 
        { 
            Role = "assistant", 
            Content = assistantMessage 
        });
        
        return assistantMessage;
    }
    
    private string BuildSystemPrompt()
    {
        var toolDescriptions = string.Join("\n", 
            _tools.GetToolDefinitions()
                .Select(t => $"- {t.Name}: {t.Description}")
        );
        
        return $@"Vous êtes un agent IA utile avec accès à des outils.

Outils disponibles :
{toolDescriptions}

Pour utiliser un outil, répondez avec :
TOOL: nom_outil
PARAMETERS: {{""param"": ""valeur""}}

Quand vous avez une réponse finale, répondez normalement sans le préfixe TOOL.

Réfléchissez étape par étape et utilisez les outils au besoin.";
    }
    
    private bool TryParseToolCall(string response, out ToolCall toolCall)
    {
        // Parser le format d'appel d'outil de la réponse LLM
        if (response.Contains("TOOL:"))
        {
            // Parsing simple (en production, utiliser une sortie structurée)
            var lines = response.Split('\n');
            var toolName = lines.First(l => l.StartsWith("TOOL:"))
                .Replace("TOOL:", "").Trim();
            var paramsLine = lines.First(l => l.StartsWith("PARAMETERS:"))
                .Replace("PARAMETERS:", "").Trim();
            var parameters = JsonSerializer.Deserialize<Dictionary<string, object>>(paramsLine);
            
            toolCall = new ToolCall { Name = toolName, Parameters = parameters };
            return true;
        }
        
        toolCall = null;
        return false;
    }
}

Étape 4 : Utilisation

var agent = new OllamaAgent("llama3.3");

var result = await agent.RunAsync(
    "Quel est 15% de 2 450? Ensuite cherche le taux de change USD vers CAD actuel."
);

Console.WriteLine(result);

Sortie :

[Agent] Appel d'outil : calculator
[Agent] Résultat : 367.5
[Agent] Appel d'outil : web_search
[Agent] Résultat : [{"title":"USD to CAD","snippet":"1 USD = 1,42 CAD"}]
15% de 2 450 est 367,5. Le taux de change actuel est environ 1 USD = 1,42 CAD, 
donc 367,5 USD équivaut à environ 521,85 CAD.

Patterns d’agents avancés

1. Système multi-agents

Différents agents avec des rôles spécialisés :

public class MultiAgentSystem
{
    private readonly PlannerAgent _planner;
    private readonly ResearchAgent _researcher;
    private readonly CodingAgent _coder;
    private readonly ReviewAgent _reviewer;
    
    public async Task<string> SolveComplexTask(string task)
    {
        // Étape 1 : Décomposer la tâche
        var plan = await _planner.CreatePlanAsync(task);
        
        // Étape 2 : Exécuter les sous-tâches
        var results = new List<string>();
        
        foreach (var subtask in plan.Steps)
        {
            var result = subtask.Type switch
            {
                "research" => await _researcher.ExecuteAsync(subtask),
                "coding" => await _coder.ExecuteAsync(subtask),
                "review" => await _reviewer.ExecuteAsync(subtask),
                _ => throw new NotSupportedException()
            };
            
            results.Add(result);
        }
        
        // Étape 3 : Synthétiser la réponse finale
        return await _planner.SynthesizeAsync(results);
    }
}

2. Agent avec intégration RAG

Combiner les agents avec le système RAG de la partie 2 :

public class RAGAgent : OllamaAgent
{
    private readonly QdrantVectorStore _vectorStore;
    
    public RAGAgent(string model, QdrantVectorStore vectorStore) 
        : base(model)
    {
        _vectorStore = vectorStore;
        
        // Ajouter l'outil de recherche RAG
        RegisterTool(new KnowledgeBaseTool(_vectorStore));
    }
}

public class KnowledgeBaseTool : ITool
{
    public string Name => "search_knowledge_base";
    public string Description => "Recherche dans la base de connaissances de l'entreprise. Entrée : query (string)";
    
    private readonly QdrantVectorStore _vectorStore;
    
    public async Task<string> ExecuteAsync(Dictionary<string, object> parameters)
    {
        var query = parameters["query"].ToString();
        var results = await _vectorStore.SearchAsync(query, topK: 5);
        
        return string.Join("\n\n", results.Select(r => r.Content));
    }
}

Maintenant votre agent peut rechercher dans votre base de connaissances privée !

3. Agent autonome avec auto-évaluation

public class AutonomousAgent
{
    private readonly string _goal;
    private readonly List<string> _completedTasks;
    private readonly OllamaAgent _agent;
    
    public async Task RunUntilComplete()
    {
        while (!await IsGoalAchieved())
        {
            // Auto-évaluer la progression
            var status = await AssessProgressAsync();
            
            // Déterminer la prochaine tâche
            var nextTask = await PlanNextTaskAsync(status);
            
            Console.WriteLine($"[Autonome] Prochaine tâche : {nextTask}");
            
            // Exécuter avec l'agent
            var result = await _agent.RunAsync(nextTask);
            
            _completedTasks.Add($"{nextTask} -> {result}");
            
            // Stocker dans la mémoire à long terme
            await StoreInMemoryAsync(nextTask, result);
        }
    }
    
    private async Task<bool> IsGoalAchieved()
    {
        var prompt = $@"Objectif : {_goal}
Tâches complétées : {string.Join(", ", _completedTasks)}

L'objectif est-il entièrement atteint ? Répondez OUI ou NON uniquement.";
        
        var response = await _agent.RunAsync(prompt, maxIterations: 1);
        return response.Contains("OUI");
    }
}

Cas d’usage réels

1. Agent d’analyse de code

public class CodeAnalysisAgent : OllamaAgent
{
    public CodeAnalysisAgent() : base("deepseek-coder:33b")
    {
        RegisterTool(new ParseCodeFileTool());
        RegisterTool(new RunStaticAnalysisTool());
        RegisterTool(new SearchDocumentationTool());
        RegisterTool(new GenerateTestsTool());
    }
}

// Utilisation
var agent = new CodeAnalysisAgent();
var report = await agent.RunAsync(
    "Analyse Program.cs pour les vulnérabilités de sécurité et suggère des correctifs"
);

2. Assistant DevOps

public class DevOpsAgent : OllamaAgent
{
    public DevOpsAgent() : base("llama3.3")
    {
        RegisterTool(new CheckBuildStatusTool());
        RegisterTool(new QueryLogsTool());
        RegisterTool(new RestartServiceTool());
        RegisterTool(new SendAlertTool());
    }
}

// Utilisation
var agent = new DevOpsAgent();
await agent.RunAsync(
    "Surveille la production pendant la prochaine heure. Si le taux d'erreur dépasse 5%, rollback et alerte l'équipe."
);

3. Agent de support client

public class SupportAgent : OllamaAgent
{
    public SupportAgent(QdrantVectorStore knowledgeBase) : base("llama3.3")
    {
        RegisterTool(new SearchKnowledgeBaseTool(knowledgeBase));
        RegisterTool(new CheckOrderStatusTool());
        RegisterTool(new CreateTicketTool());
        RegisterTool(new EscalateToHumanTool());
    }
}

Bonnes pratiques et considérations

1. Sécurité et fiabilité

Timeouts : Prévenir les boucles infinies

using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
await agent.RunAsync(goal, cancellationToken: cts.Token);

Sandboxing : Isoler les opérations dangereuses

public class SafeCodeExecutionTool : ITool
{
    public async Task<string> ExecuteAsync(Dictionary<string, object> parameters)
    {
        // Exécuter dans un conteneur Docker ou environnement restreint
        var result = await DockerRunner.RunIsolatedAsync(code);
        return result;
    }
}

Humain dans la boucle : Exiger l’approbation pour les actions critiques

if (toolCall.RequiresApproval)
{
    Console.WriteLine($"L'agent veut : {toolCall.Description}");
    Console.Write("Approuver ? (o/n) : ");
    if (Console.ReadLine() != "o")
        return "Action refusée par l'utilisateur";
}

2. Observabilité

Journaliser toutes les décisions de l’agent :

public class ObservableAgent : OllamaAgent
{
    protected override async Task<string> CallLLMAsync()
    {
        var startTime = DateTime.UtcNow;
        var response = await base.CallLLMAsync();
        var duration = DateTime.UtcNow - startTime;
        
        _logger.LogInformation(
            "Appel LLM complété en {Duration}ms. Tokens : {Tokens}", 
            duration.TotalMilliseconds,
            CountTokens(response)
        );
        
        return response;
    }
}

3. Coût et performance

Mise en cache : Éviter les appels d’outils redondants

private readonly MemoryCache _toolResultCache = new();

public async Task<string> ExecuteToolAsync(ToolCall toolCall)
{
    var cacheKey = $"{toolCall.Name}:{JsonSerializer.Serialize(toolCall.Parameters)}";
    
    if (_toolResultCache.TryGetValue(cacheKey, out string cached))
        return cached;
    
    var result = await _tools.ExecuteAsync(toolCall.Name, toolCall.Parameters);
    _toolResultCache.Set(cacheKey, result, TimeSpan.FromMinutes(10));
    
    return result;
}

Sélection du modèle : Utiliser les modèles appropriés

  • Tâches simples : llama3.2:3b, phi-3
  • Raisonnement complexe : llama3.3:70b, qwen2.5:72b
  • Tâches de code : deepseek-coder, qwen2.5-coder

4. Tester les agents

[Fact]
public async Task Agent_CanCalculateAndSearchWeb()
{
    var agent = new OllamaAgent("llama3.3");
    var result = await agent.RunAsync(
        "Calcule 25 * 16 et cherche la capitale de la France"
    );
    
    Assert.Contains("400", result);
    Assert.Contains("Paris", result);
}

Utiliser des outils simulés pour les tests unitaires :

public class MockWeatherTool : ITool
{
    public Task<string> ExecuteAsync(Dictionary<string, object> parameters)
    {
        return Task.FromResult("{\"temp\": 22, \"condition\": \"ensoleillé\"}");
    }
}

Comparaison des frameworks d’agents

Microsoft Agent Framework

// Créer un agent avec backend Ollama
var agent = new Agent(
    new OllamaModelClient("llama3.3"),
    name: "WeatherAgent"
);

// Enregistrer les outils/fonctions
agent.RegisterTool(new WeatherTool());
agent.RegisterTool(new CalculatorTool());

// Exécuter avec appel d'outils automatique
var response = await agent.RunAsync(
    "Quel temps fait-il à Paris et combien fait 15% de 2450?"
);

Avantages :

  • Conçu spécifiquement pour le développement d’agents
  • Support natif multi-agents
  • Observabilité et télémétrie intégrées
  • Support Microsoft et développement actif

Inconvénients :

  • Framework plus récent (API en évolution)
  • Moins de contenu communautaire que Semantic Kernel

LangChain .NET

var agent = new AgentExecutor(
    llm: new OllamaLLM("llama3.3"),
    tools: new[] { weatherTool, calculatorTool },
    agentType: AgentType.ReAct
);

await agent.RunAsync("Tâche complexe");

Avantages :

  • Patterns établis
  • Parité avec Python

Inconvénients :

  • Moins mature que la version Python
  • Piloté par la communauté

Implémentation personnalisée (cet article)

Avantages :

  • Contrôle total
  • Dépendances minimales
  • S’intègre avec le code existant

Inconvénients :

  • Plus de code à écrire
  • Besoin d’implémenter les patterns soi-même

Directions futures

1. Agents multimodaux

Avec des modèles de vision (Llama 3.2 Vision, Qwen2-VL) :

RegisterTool(new AnalyzeImageTool());
RegisterTool(new GenerateDiagramTool());

2. Agents avec exécution de code

public class CodeInterpreterAgent : OllamaAgent
{
    public CodeInterpreterAgent() : base("deepseek-coder")
    {
        RegisterTool(new ExecutePythonTool());
        RegisterTool(new ExecuteCSharpTool());
        RegisterTool(new AnalyzeResultsTool());
    }
}

3. Agents de longue durée

Agents en arrière-plan qui surveillent et répondent :

public class MonitoringAgent : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var issues = await DetectIssuesAsync();
            
            foreach (var issue in issues)
            {
                await _agent.RunAsync($"Résoudre : {issue}");
            }
            
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

Conclusion

Les agents IA représentent la prochaine évolution dans la construction de systèmes intelligents. En combinant des LLM locaux via Ollama avec des capacités d’appel d’outils, vous pouvez créer des assistants puissants et autonomes qui :

  • Résolvent des problèmes complexes en plusieurs étapes
  • Accèdent et manipulent des données externes
  • Fonctionnent complètement hors ligne et en privé
  • Évoluent sans coûts par token

Points clés à retenir :

  1. Agents = LLM + Outils + Boucle - Le pattern de base est simple mais puissant
  2. Pattern ReAct - Alterner entre raisonnement et action est fondamental
  3. Sécurité d’abord - Implémenter timeouts, sandboxing et workflows d’approbation
  4. Commencer simple - Construire l’appel d’outils de base avant les agents autonomes
  5. Exploiter le RAG - Combiner agents et récupération de connaissances pour l’expertise du domaine

Prochaines étapes :

  • Expérimenter avec les exemples de code sur GitHub
  • Essayer différents modèles—certains excellent en raisonnement (llama3.3, qwen2.5), d’autres en codage (deepseek-coder)
  • Construire des agents spécifiques à votre domaine pour vos cas d’usage
  • Explorer les systèmes multi-agents pour des workflows complexes

L’avenir du développement IA est agentique, autonome et local. Commencez à construire dès aujourd’hui !

Ressources


Cet article a été créé avec l’assistance de l’IA.


Suggestions de lecture :