aissia/docs/architecture/intelligent-document-retrieval.md
StillHammer f6f4813d8f Remove build artifacts from tracking and update retrieval doc
- Add .gitignore for build artifacts
- Update intelligent-document-retrieval.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 21:40:08 +08:00

68 KiB

Intelligent Document Retrieval - Architecture Technique

Vue d'Ensemble

Ce document décrit l'architecture pour la récupération intelligente de documents dans AISSIA. Le système permet à l'AIAssistantModule de sélectionner et lire des documents de manière efficace sans saturer le context window du LLM.

Problème Adressé

  • Context window limité : Les LLMs ont une limite de tokens (~200K pour Claude)
  • Volume de documents variable : 10 à 1000+ documents selon l'utilisateur
  • Coût API : Charger tous les documents = coût prohibitif et latence élevée
  • Pertinence : Tous les documents ne sont pas utiles pour chaque requête

Solution : Retrieval Agentique

Le LLM décide dynamiquement quels documents lire via un système de tools, reproduisant le comportement de Claude Code.

Architecture Système

Flux de Données

┌─────────────────────────────────────────────────────────────────────────┐
│                         DOCUMENT STORE                                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  documents/                      index.json (léger, toujours en RAM)    │
│  ├── doc1.md (5KB)              ┌────────────────────────────────────┐  │
│  ├── doc2.md (12KB)      ──►    │ [{id, title, summary, tags, size}] │  │
│  ├── doc3.md (3KB)              │ [{id, title, summary, tags, size}] │  │
│  └── ...                        │ [...]                              │  │
│                                 └────────────────────────────────────┘  │
│                                          ~100 bytes par document        │
└─────────────────────────────────────────────────────────────────────────┘
                                           │
                                           ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                         AGENTIC LOOP                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  1. User Query ─────────────────────────────────────────────────────►   │
│                                                                         │
│  2. LLM analyse la requête                                              │
│     → Décide d'utiliser un TOOL                                         │
│                                                                         │
│  3. TOOL CALL: list_documents() ou search_documents("query")            │
│     ← Retourne: metadata uniquement (pas le contenu)                    │
│                                                                         │
│  4. LLM analyse les résultats                                           │
│     → Sélectionne les documents pertinents                              │
│                                                                         │
│  5. TOOL CALL: read_document("doc_id", max_chars=8000)                  │
│     ← Retourne: contenu (tronqué si nécessaire)                         │
│                                                                         │
│  6. LLM décide: suffisant? ou besoin de plus?                           │
│     → Continue ou génère la réponse finale                              │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Composants Principaux

modules/ai-assistant/
├── CMakeLists.txt
├── CLAUDE.md
├── src/
│   └── AIAssistantModule.cpp    # Logique agentique (~200 lignes)
├── shared/
│   ├── DocumentStore.hpp        # Gestion index et lecture
│   ├── DocumentIndex.hpp        # Structure index en mémoire
│   ├── ILLMProvider.hpp         # Interface LLM agnostique
│   └── providers/
│       ├── ClaudeProvider.hpp   # Implémentation Anthropic
│       ├── OpenAIProvider.hpp   # Implémentation OpenAI
│       └── OllamaProvider.hpp   # Implémentation locale
└── tests/
    └── ai_assistant_test.cpp

Spécifications Techniques

1. Document Index

Structure légère maintenue en RAM pour accès rapide.

struct DocumentMetadata {
    std::string id;              // Identifiant unique (filename sans extension)
    std::string title;           // Extrait du premier # header
    std::string path;            // Chemin absolu vers le fichier
    std::string summary;         // Premières lignes non-vides (~300 chars)
    std::vector<std::string> tags;  // Headers ## extraits
    size_t size_bytes;           // Taille fichier
    std::string last_modified;   // Timestamp ISO 8601
};

Estimation mémoire :

  • 100 documents : ~10KB en RAM
  • 1000 documents : ~100KB en RAM

2. Tools Disponibles

Le LLM dispose des tools suivants pour naviguer dans les documents :

list_documents

{
  "name": "list_documents",
  "description": "Liste tous les documents disponibles avec titre, résumé court et taille. Utiliser pour avoir une vue d'ensemble.",
  "input_schema": {
    "type": "object",
    "properties": {}
  }
}

Output : Array de metadata (id, title, summary tronqué, size_kb)

search_documents

{
  "name": "search_documents",
  "description": "Recherche des documents par mots-clés. Retourne les plus pertinents.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": {"type": "string", "description": "Mots-clés de recherche"},
      "max_results": {"type": "integer", "default": 5}
    },
    "required": ["query"]
  }
}

Output : Array de metadata avec score de pertinence

read_document

{
  "name": "read_document",
  "description": "Lit le contenu d'un document spécifique. Pour les gros documents, limiter avec max_chars.",
  "input_schema": {
    "type": "object",
    "properties": {
      "document_id": {"type": "string"},
      "max_chars": {"type": "integer", "default": 8000}
    },
    "required": ["document_id"]
  }
}

Output : Contenu du document (tronqué si > max_chars)

read_section

{
  "name": "read_section",
  "description": "Lit une section spécifique d'un document identifiée par son header ##.",
  "input_schema": {
    "type": "object",
    "properties": {
      "document_id": {"type": "string"},
      "section_name": {"type": "string"}
    },
    "required": ["document_id", "section_name"]
  }
}

Output : Contenu de la section uniquement

3. Stratégies de Recherche

Recherche par correspondance de mots-clés dans titre + summary + tags.

int calculateScore(const DocumentMetadata& doc, const std::vector<std::string>& keywords) {
    int score = 0;
    std::string searchable = toLower(doc.title + " " + doc.summary);
    for (const auto& tag : doc.tags) {
        searchable += " " + toLower(tag);
    }

    for (const auto& keyword : keywords) {
        if (searchable.find(toLower(keyword)) != std::string::npos) {
            score++;
        }
        // Bonus si match dans le titre
        if (toLower(doc.title).find(toLower(keyword)) != std::string::npos) {
            score += 2;
        }
    }
    return score;
}

Avantages : Simple, rapide, aucune dépendance externe Limitations : Ne comprend pas la sémantique ("focus" != "concentration")

Phase 2 (Optionnel) : Semantic Search avec Embeddings

Utilisation d'embeddings pour recherche sémantique.

Document ──► Embedding API ──► Vecteur [1536 dimensions]
                                      │
                                      ▼
                               Vector Store (SQLite + extension)
                                      │
Query ──► Embedding API ──────────────┼──► Cosine Similarity
                                      │
                               Top K documents

Implémentation suggérée :

  • Embeddings : API Anthropic (voyage-3) ou OpenAI (text-embedding-3-small)
  • Storage : SQLite avec extension sqlite-vss ou fichier binaire simple
  • Coût : ~$0.0001 par embedding (négligeable)

Avantages : Comprend la sémantique, trouve des documents connexes Limitations : Nécessite pré-calcul, dépendance API externe

Combinaison keyword + semantic pour meilleurs résultats.

std::vector<SearchResult> hybridSearch(const std::string& query) {
    auto keywordResults = keywordSearch(query, 20);
    auto semanticResults = semanticSearch(query, 20);

    // Fusion avec pondération
    std::map<std::string, float> scores;
    for (const auto& r : keywordResults) {
        scores[r.id] += r.score * 0.3;  // 30% keyword
    }
    for (const auto& r : semanticResults) {
        scores[r.id] += r.score * 0.7;  // 70% semantic
    }

    // Trier et retourner top K
    return sortAndLimit(scores, 10);
}

4. Gestion du Context Window

Budget Tokens

Context Window Total : 200,000 tokens
├── System Prompt    :   2,000 tokens (fixe)
├── Conversation     :  20,000 tokens (historique)
├── Tools Definition :   1,000 tokens (fixe)
├── Documents        :  50,000 tokens (budget retrieval)
└── Réponse          :   4,000 tokens (output)
    Reserve          : 123,000 tokens (marge sécurité)

Stratégie de Troncature

std::string smartTruncate(const std::string& content, size_t maxChars) {
    if (content.size() <= maxChars) {
        return content;
    }

    // Option 1: Début + fin (préserve intro et conclusion)
    size_t headSize = maxChars * 0.7;
    size_t tailSize = maxChars * 0.2;

    return content.substr(0, headSize)
         + "\n\n[... contenu tronqué ...]\n\n"
         + content.substr(content.size() - tailSize);
}

Monitoring Usage

class ContextBudget {
    const size_t MAX_DOCUMENT_TOKENS = 50000;
    size_t usedTokens = 0;

public:
    bool canFit(size_t documentSize) {
        size_t estimatedTokens = documentSize / 4;  // ~4 chars/token
        return (usedTokens + estimatedTokens) <= MAX_DOCUMENT_TOKENS;
    }

    void recordUsage(size_t documentSize) {
        usedTokens += documentSize / 4;
    }

    size_t remainingBudget() {
        return MAX_DOCUMENT_TOKENS - usedTokens;
    }
};

Architecture LLM Agnostique

Le système est conçu pour supporter plusieurs providers LLM via une interface commune. Le pattern "tool use" est standardisé chez la plupart des providers modernes.

Compatibilité Providers

Provider Tool Use Format Qualité Tool Use Notes
Claude (Anthropic) Natif tool_use blocks Excellent Meilleur raisonnement
OpenAI Natif tool_calls array Très bon GPT-5, GPT-4.1, o-series
Gemini (Google) Natif function_call Bon Gemini 3, 2.5, Flash
DeepSeek Natif Compatible OpenAI Très bon V3, R1, ultra low-cost
Kimi (Moonshot) Natif Compatible OpenAI Bon K2, 1T params, 128K context
Qwen (Alibaba) Natif Compatible OpenAI Bon Qwen-Max, Turbo (1M context)
Mistral Natif Compatible OpenAI Bon EU-based, Medium 3, Large
Llama Natif Via Ollama Variable Open source (Meta)
Local (Ollama) Partiel Dépend du modèle Variable 100% local

Sources : Anthropic Docs, OpenAI Models, Gemini API, DeepSeek Pricing, Moonshot Platform, Alibaba Model Studio, Mistral Pricing

Interface ILLMProvider

// ILLMProvider.hpp - Interface commune pour tous les providers

struct ToolCall {
    std::string id;      // Identifiant unique du call
    std::string name;    // Nom du tool appelé
    json input;          // Arguments passés au tool
};

struct ToolResult {
    std::string tool_call_id;  // Référence au ToolCall
    std::string content;       // Résultat de l'exécution
    bool is_error = false;     // Indique si erreur
};

class ILLMProvider {
public:
    virtual ~ILLMProvider() = default;

    // Envoie messages + tools, retourne réponse brute du provider
    virtual json chat(const std::string& systemPrompt,
                      const json& messages,
                      const json& tools) = 0;

    // Parse la réponse pour extraire les tool calls (format unifié)
    virtual std::vector<ToolCall> parseToolCalls(const json& response) = 0;

    // Formate les résultats des tools pour le prochain message
    virtual json formatToolResults(const std::vector<ToolResult>& results) = 0;

    // Ajoute la réponse assistant à l'historique
    virtual void appendAssistantMessage(json& messages, const json& response) = 0;

    // Check si la réponse est finale ou demande des tools
    virtual bool isEndTurn(const json& response) = 0;

    // Extrait le texte final de la réponse
    virtual std::string extractText(const json& response) = 0;

    // Retourne le nom du provider pour logging
    virtual std::string getProviderName() const = 0;
};

Implémentation Claude

// ClaudeProvider.hpp

class ClaudeProvider : public ILLMProvider {
    std::string apiKey;
    std::string model;
    std::string baseUrl = "https://api.anthropic.com/v1";

public:
    ClaudeProvider(const json& config)
        : apiKey(getEnvVar(config.value("api_key_env", "ANTHROPIC_API_KEY")))
        , model(config.value("model", "claude-sonnet-4-20250514"))
        , baseUrl(config.value("base_url", baseUrl)) {}

    json chat(const std::string& systemPrompt,
              const json& messages,
              const json& tools) override {
        json request = {
            {"model", model},
            {"max_tokens", 4096},
            {"system", systemPrompt},
            {"messages", messages},
            {"tools", tools}
        };
        return httpPost(baseUrl + "/messages", request, {
            {"x-api-key", apiKey},
            {"anthropic-version", "2023-06-01"}
        });
    }

    std::vector<ToolCall> parseToolCalls(const json& response) override {
        std::vector<ToolCall> calls;
        for (const auto& block : response["content"]) {
            if (block["type"] == "tool_use") {
                calls.push_back({
                    block["id"],
                    block["name"],
                    block["input"]
                });
            }
        }
        return calls;
    }

    json formatToolResults(const std::vector<ToolResult>& results) override {
        // Claude: array de tool_result dans un message user
        json content = json::array();
        for (const auto& r : results) {
            content.push_back({
                {"type", "tool_result"},
                {"tool_use_id", r.tool_call_id},
                {"content", r.content},
                {"is_error", r.is_error}
            });
        }
        return {{"role", "user"}, {"content", content}};
    }

    void appendAssistantMessage(json& messages, const json& response) override {
        messages.push_back({
            {"role", "assistant"},
            {"content", response["content"]}
        });
    }

    bool isEndTurn(const json& response) override {
        return response["stop_reason"] == "end_turn";
    }

    std::string extractText(const json& response) override {
        for (const auto& block : response["content"]) {
            if (block["type"] == "text") {
                return block["text"];
            }
        }
        return "";
    }

    std::string getProviderName() const override { return "claude"; }
};

Implémentation OpenAI

// OpenAIProvider.hpp

class OpenAIProvider : public ILLMProvider {
    std::string apiKey;
    std::string model;
    std::string baseUrl = "https://api.openai.com/v1";

public:
    OpenAIProvider(const json& config)
        : apiKey(getEnvVar(config.value("api_key_env", "OPENAI_API_KEY")))
        , model(config.value("model", "gpt-4o"))
        , baseUrl(config.value("base_url", baseUrl)) {}

    json chat(const std::string& systemPrompt,
              const json& messages,
              const json& tools) override {
        // Convertir tools au format OpenAI
        json openaiTools = json::array();
        for (const auto& tool : tools) {
            openaiTools.push_back({
                {"type", "function"},
                {"function", {
                    {"name", tool["name"]},
                    {"description", tool["description"]},
                    {"parameters", tool["input_schema"]}
                }}
            });
        }

        // Préparer messages avec system prompt
        json allMessages = json::array();
        allMessages.push_back({{"role", "system"}, {"content", systemPrompt}});
        for (const auto& msg : messages) {
            allMessages.push_back(msg);
        }

        json request = {
            {"model", model},
            {"messages", allMessages},
            {"tools", openaiTools}
        };
        return httpPost(baseUrl + "/chat/completions", request, {
            {"Authorization", "Bearer " + apiKey}
        });
    }

    std::vector<ToolCall> parseToolCalls(const json& response) override {
        std::vector<ToolCall> calls;
        auto& message = response["choices"][0]["message"];
        if (message.contains("tool_calls")) {
            for (const auto& tc : message["tool_calls"]) {
                calls.push_back({
                    tc["id"],
                    tc["function"]["name"],
                    json::parse(tc["function"]["arguments"].get<std::string>())
                });
            }
        }
        return calls;
    }

    json formatToolResults(const std::vector<ToolResult>& results) override {
        // OpenAI: messages séparés avec role "tool"
        json messages = json::array();
        for (const auto& r : results) {
            messages.push_back({
                {"role", "tool"},
                {"tool_call_id", r.tool_call_id},
                {"content", r.content}
            });
        }
        return messages;  // Note: retourne array, pas un seul message
    }

    void appendAssistantMessage(json& messages, const json& response) override {
        messages.push_back(response["choices"][0]["message"]);
    }

    bool isEndTurn(const json& response) override {
        auto& message = response["choices"][0]["message"];
        return !message.contains("tool_calls") || message["tool_calls"].empty();
    }

    std::string extractText(const json& response) override {
        return response["choices"][0]["message"]["content"].get<std::string>();
    }

    std::string getProviderName() const override { return "openai"; }
};

Implémentation Ollama (Local)

// OllamaProvider.hpp

class OllamaProvider : public ILLMProvider {
    std::string model;
    std::string baseUrl = "http://localhost:11434/api";

public:
    OllamaProvider(const json& config)
        : model(config.value("model", "llama3.1:70b"))
        , baseUrl(config.value("base_url", baseUrl)) {}

    json chat(const std::string& systemPrompt,
              const json& messages,
              const json& tools) override {
        // Format Ollama avec tools (Llama 3.1+)
        json request = {
            {"model", model},
            {"messages", buildMessages(systemPrompt, messages)},
            {"tools", convertToolsFormat(tools)},
            {"stream", false}
        };
        return httpPost(baseUrl + "/chat", request, {});
    }

    // ... autres méthodes similaires à OpenAI

    std::string getProviderName() const override { return "ollama"; }

private:
    json buildMessages(const std::string& systemPrompt, const json& messages) {
        json result = json::array();
        result.push_back({{"role", "system"}, {"content", systemPrompt}});
        for (const auto& msg : messages) {
            result.push_back(msg);
        }
        return result;
    }
};

Factory Pattern

// LLMProviderFactory.hpp

class LLMProviderFactory {
public:
    static std::unique_ptr<ILLMProvider> create(const json& config) {
        std::string provider = config.value("provider", "claude");

        if (provider == "claude") {
            return std::make_unique<ClaudeProvider>(config["providers"]["claude"]);
        }
        if (provider == "openai") {
            return std::make_unique<OpenAIProvider>(config["providers"]["openai"]);
        }
        if (provider == "ollama") {
            return std::make_unique<OllamaProvider>(config["providers"]["ollama"]);
        }

        throw std::runtime_error("Unknown LLM provider: " + provider);
    }
};

Boucle Agentique - Fonctionnement Détaillé

Principe fondamental

Le LLM ne fait PAS les appels API lui-même. C'est ton code qui :

  1. Reçoit la demande de tool du LLM
  2. Exécute l'appel (API externe, lecture fichier, etc.)
  3. Renvoie le résultat au LLM
  4. Relance un appel LLM avec le contexte enrichi

Flux visuel annoté

┌─────────────────────────────────────────────────────────────────────────┐
│  EXEMPLE: User demande "Quelle météo à Paris ?"                         │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ ÉTAPE 1: Initialisation                                         │    │
│  │                                                                 │    │
│  │ messages = [                                                    │    │
│  │   {role: "user", content: "Quelle météo à Paris ?"}             │    │
│  │ ]                                                               │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              │                                          │
│                              ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ ÉTAPE 2: Premier appel LLM                                      │    │
│  │                                                                 │    │
│  │ response = llm.chat(messages, tools)                            │    │
│  │                                                                 │    │
│  │ LLM analyse et décide: "J'ai besoin de get_weather"             │    │
│  │                                                                 │    │
│  │ Réponse LLM:                                                    │    │
│  │ {                                                               │    │
│  │   stop_reason: "tool_use",     ← PAS "end_turn", on continue!   │    │
│  │   content: [{                                                   │    │
│  │     type: "tool_use",                                           │    │
│  │     id: "call_123",                                             │    │
│  │     name: "get_weather",                                        │    │
│  │     input: {city: "Paris"}                                      │    │
│  │   }]                                                            │    │
│  │ }                                                               │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              │                                          │
│                              ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ ÉTAPE 3: TON CODE exécute le tool                               │    │
│  │                                                                 │    │
│  │ // C'est ICI que tu fais l'appel API externe                    │    │
│  │ result = executeTool("get_weather", {city: "Paris"})            │    │
│  │                                                                 │    │
│  │ // Ton implémentation:                                          │    │
│  │ json executeTool(name, input) {                                 │    │
│  │   if (name == "get_weather") {                                  │    │
│  │     return httpGet("https://api.weather.com", input);  ← API!   │    │
│  │   }                                                             │    │
│  │ }                                                               │    │
│  │                                                                 │    │
│  │ Résultat: {temp: 12, condition: "nuageux"}                      │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              │                                          │
│                              ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ ÉTAPE 4: Enrichir le contexte                                   │    │
│  │                                                                 │    │
│  │ messages = [                                                    │    │
│  │   {role: "user", content: "Quelle météo à Paris ?"},            │    │
│  │   {role: "assistant", content: [{tool_use...}]},     ← AJOUTÉ   │    │
│  │   {role: "user", content: [{                         ← AJOUTÉ   │    │
│  │     type: "tool_result",                                        │    │
│  │     tool_use_id: "call_123",                                    │    │
│  │     content: "{temp: 12, condition: nuageux}"                   │    │
│  │   }]}                                                           │    │
│  │ ]                                                               │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              │                                          │
│                              ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ ÉTAPE 5: DEUXIÈME appel LLM (avec contexte enrichi)             │    │
│  │                                                                 │    │
│  │ response = llm.chat(messages, tools)  ← MÊME array enrichi      │    │
│  │                                                                 │    │
│  │ LLM voit le résultat du tool et génère la réponse finale        │    │
│  │                                                                 │    │
│  │ Réponse LLM:                                                    │    │
│  │ {                                                               │    │
│  │   stop_reason: "end_turn",     ← MAINTENANT c'est fini!         │    │
│  │   content: [{                                                   │    │
│  │     type: "text",                                               │    │
│  │     text: "Il fait 12°C à Paris avec un temps nuageux."         │    │
│  │   }]                                                            │    │
│  │ }                                                               │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              │                                          │
│                              ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ ÉTAPE 6: Retourner au user                                      │    │
│  │                                                                 │    │
│  │ return "Il fait 12°C à Paris avec un temps nuageux."            │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Code annoté

json AIAssistantModule::agenticLoop(const std::string& userQuery) {

    // ═══════════════════════════════════════════════════════════════════
    // ÉTAPE 1: Initialiser l'historique avec la question du user
    // ═══════════════════════════════════════════════════════════════════
    json messages = json::array();
    messages.push_back({{"role", "user"}, {"content", userQuery}});

    int iterations = 0;
    const int MAX_ITERATIONS = config["max_iterations"].get<int>();

    // ═══════════════════════════════════════════════════════════════════
    // BOUCLE PRINCIPALE - Continue tant que le LLM demande des tools
    // Chaque tour = 1 appel API LLM payant
    // ═══════════════════════════════════════════════════════════════════
    while (iterations++ < MAX_ITERATIONS) {

        // ───────────────────────────────────────────────────────────────
        // ÉTAPE 2/5/8...: Appel LLM avec tout le contexte accumulé
        // Le LLM voit: question initiale + tous les tools calls + résultats
        // ───────────────────────────────────────────────────────────────
        auto response = provider->chat(systemPrompt, messages, tools);

        // ───────────────────────────────────────────────────────────────
        // CHECK: Est-ce que le LLM a fini ou veut encore des tools ?
        // stop_reason == "end_turn" → Réponse finale, on sort
        // stop_reason == "tool_use" → Il veut des données, on continue
        // ───────────────────────────────────────────────────────────────
        if (provider->isEndTurn(response)) {
            // ÉTAPE 6: C'est fini! Extraire et retourner la réponse
            return {
                {"response", provider->extractText(response)},
                {"iterations", iterations},
                {"provider", provider->getProviderName()}
            };
        }

        // ───────────────────────────────────────────────────────────────
        // ÉTAPE 3: Le LLM veut des tools - Parser sa demande
        // Il peut demander PLUSIEURS tools en une fois (parallèle)
        // ───────────────────────────────────────────────────────────────
        auto toolCalls = provider->parseToolCalls(response);
        if (toolCalls.empty()) {
            return {{"error", "unexpected_state"}};
        }

        // ───────────────────────────────────────────────────────────────
        // ÉTAPE 3 (suite): EXÉCUTER les tools
        // C'est ICI que TON CODE fait le travail:
        // - Appels API externes (météo, recherche web, etc.)
        // - Lecture de fichiers
        // - Requêtes base de données
        // - N'importe quelle opération
        // ───────────────────────────────────────────────────────────────
        std::vector<ToolResult> results;
        for (const auto& call : toolCalls) {
            // executeTool() = TA fonction qui fait le vrai travail
            auto result = executeTool(call.name, call.input);
            results.push_back({call.id, result.dump(), false});
        }

        // ───────────────────────────────────────────────────────────────
        // ÉTAPE 4: Enrichir l'historique pour le prochain appel LLM
        // On ajoute:
        // 1. Ce que le LLM a demandé (assistant message avec tool_use)
        // 2. Les résultats qu'on a obtenus (user message avec tool_result)
        // ───────────────────────────────────────────────────────────────
        provider->appendAssistantMessage(messages, response);

        // Formater les résultats selon le provider (Claude vs OpenAI)
        auto toolResultsMsg = provider->formatToolResults(results);
        if (toolResultsMsg.is_array()) {
            // OpenAI: chaque tool_result = un message séparé
            for (const auto& msg : toolResultsMsg) {
                messages.push_back(msg);
            }
        } else {
            // Claude: tous les tool_results dans un seul message
            messages.push_back(toolResultsMsg);
        }

        // → RETOUR AU DÉBUT DE LA BOUCLE
        // Le prochain llm.chat() verra tout l'historique enrichi
    }

    return {{"error", "max_iterations_reached"}};
}

Coût par scénario

Scénario Tours de boucle Appels LLM Coût (~Sonnet)
Question simple (pas de tool) 1 1 ~$0.01
1 tool call 2 2 ~$0.02
2 tools en parallèle 2 2 ~$0.02
2 tools séquentiels 3 3 ~$0.03
Search docs → Read doc → Réponse 3 3 ~$0.03
Recherche complexe (5 tools) 4-6 4-6 ~$0.05

Tools avec Appels API Externes

Architecture des Tools

// ═══════════════════════════════════════════════════════════════════════
// Interface pour tous les tools (locaux ou API)
// ═══════════════════════════════════════════════════════════════════════

class IToolExecutor {
public:
    virtual ~IToolExecutor() = default;

    // Exécute le tool et retourne le résultat (JSON)
    virtual json execute(const json& input) = 0;

    // Définition du tool pour le LLM (nom, description, paramètres)
    virtual json getToolDefinition() = 0;

    // Timeout spécifique (certaines API sont lentes)
    virtual std::chrono::milliseconds getTimeout() {
        return std::chrono::milliseconds(30000);  // 30s par défaut
    }
};

Exemple: Tool avec API externe (météo)

// ═══════════════════════════════════════════════════════════════════════
// WeatherTool - Appelle une API météo externe
// ═══════════════════════════════════════════════════════════════════════

class WeatherTool : public IToolExecutor {
    std::string apiKey;

public:
    WeatherTool() : apiKey(getEnvVar("WEATHER_API_KEY")) {}

    // ───────────────────────────────────────────────────────────────────
    // Définition envoyée au LLM pour qu'il sache comment utiliser ce tool
    // ───────────────────────────────────────────────────────────────────
    json getToolDefinition() override {
        return {
            {"name", "get_weather"},
            {"description", "Obtient la météo actuelle pour une ville donnée."},
            {"input_schema", {
                {"type", "object"},
                {"properties", {
                    {"city", {
                        {"type", "string"},
                        {"description", "Nom de la ville (ex: Paris, Tokyo)"}
                    }}
                }},
                {"required", {"city"}}
            }}
        };
    }

    // ───────────────────────────────────────────────────────────────────
    // Exécution: C'est ICI que l'appel API externe se fait
    // Le LLM ne fait JAMAIS cet appel - c'est ton code
    // ───────────────────────────────────────────────────────────────────
    json execute(const json& input) override {
        std::string city = input["city"];

        // ┌─────────────────────────────────────────────────────────────┐
        // │ APPEL API EXTERNE - Ton code HTTP                          │
        // └─────────────────────────────────────────────────────────────┘
        auto response = httpGet(
            "https://api.weatherapi.com/v1/current.json",
            {
                {"key", apiKey},
                {"q", city}
            }
        );

        // Gestion erreur
        if (response.status != 200) {
            return {
                {"error", "weather_api_failed"},
                {"status", response.status}
            };
        }

        // ┌─────────────────────────────────────────────────────────────┐
        // │ FORMATER le résultat pour le LLM                           │
        // │ Pas besoin de tout renvoyer, juste l'essentiel             │
        // └─────────────────────────────────────────────────────────────┘
        return {
            {"city", city},
            {"temp_c", response.body["current"]["temp_c"]},
            {"condition", response.body["current"]["condition"]["text"]},
            {"humidity", response.body["current"]["humidity"]}
        };
    }
};

Exemple: Tool de recherche web

class WebSearchTool : public IToolExecutor {
    std::string apiKey;

public:
    WebSearchTool() : apiKey(getEnvVar("SERPER_API_KEY")) {}

    json getToolDefinition() override {
        return {
            {"name", "web_search"},
            {"description", "Recherche sur le web. Pour informations récentes ou externes."},
            {"input_schema", {
                {"type", "object"},
                {"properties", {
                    {"query", {{"type", "string"}, {"description", "Requête de recherche"}}},
                    {"num_results", {{"type", "integer"}, {"default", 5}}}
                }},
                {"required", {"query"}}
            }}
        };
    }

    json execute(const json& input) override {
        std::string query = input["query"];
        int numResults = input.value("num_results", 5);

        // Appel API Serper (moteur de recherche)
        auto response = httpPost(
            "https://google.serper.dev/search",
            {{"q", query}, {"num", numResults}},
            {{"X-API-KEY", apiKey}}
        );

        if (response.status != 200) {
            return {{"error", "search_failed"}};
        }

        // Extraire les résultats pertinents
        json results = json::array();
        for (const auto& item : response.body["organic"]) {
            results.push_back({
                {"title", item["title"]},
                {"snippet", item["snippet"]},
                {"url", item["link"]}
            });
        }

        return {{"results", results}, {"query", query}};
    }

    // Timeout plus long pour recherche web
    std::chrono::milliseconds getTimeout() override {
        return std::chrono::milliseconds(15000);  // 15s
    }
};

Registry: Gérer tous les tools

// ═══════════════════════════════════════════════════════════════════════
// ToolRegistry - Centralise tous les tools disponibles
// ═══════════════════════════════════════════════════════════════════════

class ToolRegistry {
    std::map<std::string, std::unique_ptr<IToolExecutor>> tools;

public:
    // Enregistrer un tool
    void registerTool(std::unique_ptr<IToolExecutor> tool) {
        auto def = tool->getToolDefinition();
        std::string name = def["name"];
        tools[name] = std::move(tool);
    }

    // Générer les définitions pour l'API LLM
    json getToolDefinitions() {
        json defs = json::array();
        for (const auto& [name, tool] : tools) {
            defs.push_back(tool->getToolDefinition());
        }
        return defs;
    }

    // Exécuter un tool par nom (appelé dans la boucle agentique)
    json execute(const std::string& name, const json& input) {
        if (tools.find(name) == tools.end()) {
            return {{"error", "unknown_tool"}, {"name", name}};
        }

        try {
            return tools[name]->execute(input);
        } catch (const std::exception& e) {
            return {{"error", "execution_failed"}, {"message", e.what()}};
        }
    }
};

// ═══════════════════════════════════════════════════════════════════════
// Utilisation dans AIAssistantModule
// ═══════════════════════════════════════════════════════════════════════

class AIAssistantModule {
    ToolRegistry toolRegistry;

public:
    AIAssistantModule() {
        // Enregistrer tous les tools disponibles
        toolRegistry.registerTool(std::make_unique<ListDocumentsTool>());
        toolRegistry.registerTool(std::make_unique<ReadDocumentTool>());
        toolRegistry.registerTool(std::make_unique<SearchDocumentsTool>());
        toolRegistry.registerTool(std::make_unique<WeatherTool>());
        toolRegistry.registerTool(std::make_unique<WebSearchTool>());
        // ... autres tools
    }

    json executeTool(const std::string& name, const json& input) {
        return toolRegistry.execute(name, input);
    }
};

Qui fait quoi - Récapitulatif

Action LLM Ton Code
Analyser la question du user
Décider quel(s) tool(s) appeler
Générer les arguments du tool
Exécuter l'appel HTTP/API
Gérer les erreurs réseau
Gérer les timeouts
Interpréter le résultat du tool
Décider si besoin d'autres tools
Générer la réponse finale

Gestion Erreurs API

json ILLMProvider::chatWithRetry(const std::string& systemPrompt,
                                  const json& messages,
                                  const json& tools,
                                  int maxRetries) {
    int retries = 0;

    while (retries < maxRetries) {
        try {
            auto response = chat(systemPrompt, messages, tools);

            // Vérifier si réponse valide
            if (isValidResponse(response)) {
                return response;
            }

            // Rate limit (429)
            if (isRateLimited(response)) {
                std::this_thread::sleep_for(
                    std::chrono::seconds(1 << retries)  // Exponential backoff
                );
                retries++;
                continue;
            }

            // Server error (5xx)
            if (isServerError(response)) {
                retries++;
                continue;
            }

            // Client error (4xx) - ne pas retry
            return {{"error", getErrorMessage(response)}};

        } catch (const std::exception& e) {
            retries++;
        }
    }

    return {{"error", "max_retries_exceeded"}};
}

Estimation Coûts

Coût par Requête par Provider

Anthropic Claude (Novembre 2025)

Modèle API ID Input/1M Output/1M Context Notes
Claude Sonnet 4.5 claude-sonnet-4-5-20250929 $3.00 $15.00 200K (1M beta) Frontier, meilleur coding
Claude Haiku 4.5 claude-haiku-4-5 $1.00 $5.00 200K Rapide, 1/3 coût Sonnet 4
Claude Opus 4.1 claude-opus-4-1 $15.00 $75.00 200K Deep reasoning
Claude Sonnet 4 claude-sonnet-4-20250514 $3.00 $15.00 200K Stable

OpenAI (Novembre 2025)

Modèle API ID Input/1M Output/1M Context Notes
GPT-5 gpt-5 ~$5.00 ~$15.00 256K Multimodal, SOTA
GPT-5 Mini gpt-5-mini ~$0.50 ~$1.50 128K Léger
GPT-5 Nano gpt-5-nano ~$0.10 ~$0.30 32K Ultra-léger
GPT-4.1 gpt-4.1 $2.00 $8.00 1M Long context
GPT-4.1 Mini gpt-4.1-mini $0.40 $1.60 1M Budget-friendly
GPT-4.1 Nano gpt-4.1-nano $0.10 $0.40 1M Ultra low-cost
GPT-4o gpt-4o $2.50 $10.00 128K Legacy multimodal
o3 o3 Variable Variable - Reasoning

Google Gemini (Novembre 2025)

Modèle API ID Input/1M Output/1M Context Notes
Gemini 3 Pro gemini-3.0-pro $2.00 $12.00 200K Newest, best reasoning
Gemini 2.5 Pro gemini-2.5-pro $4.00 $20.00 1M Thinking model
Gemini 2.5 Flash gemini-2.5-flash $0.15 $0.60 1M Fast, thinking enabled
Gemini 2.5 Flash-Lite gemini-2.5-flash-lite $0.02 $0.08 - Ultra low-cost
Gemini 1.5 Pro gemini-1.5-pro $1.25 $5.00 2M Stable
Gemini 1.5 Flash gemini-1.5-flash $0.075 $0.30 1M Fast

DeepSeek (Septembre 2025)

Modèle API ID Input/1M Output/1M Context Notes
DeepSeek V3 deepseek-chat $0.07 (hit) / $0.56 $1.68 128K Généraliste
DeepSeek R1 deepseek-reasoner $0.07 (hit) / $0.56 $1.68 64K output Chain-of-thought
DeepSeek V3.2-Exp deepseek-v3.2-exp $0.028 ~$0.84 128K 50% moins cher, MIT license

Kimi / Moonshot (Novembre 2025)

Modèle API ID Input/1M Output/1M Context Notes
Kimi K2 kimi-k2 $0.15 (hit) $2.50 128K 1T params MoE, 32B active
Kimi K2 Thinking kimi-k2-thinking ~$0.30 ~$3.00 128K Agentic, reasoning
Moonshot v1 128K moonshot-v1-128k $0.82 $0.82 128K Legacy

Alibaba Qwen (2025)

Modèle API ID Input/1M Output/1M Context Notes
Qwen-Max qwen-max ~$1.60 ~$6.40 32K Flagship
Qwen-Plus qwen-plus ~$0.80 ~$3.20 128K Balanced
Qwen-Turbo qwen-turbo ~$0.20 ~$0.80 1M Fast, long context
Qwen3-Coder qwen3-coder-plus Variable Variable - Coding specialized

Mistral (Mai 2025)

Modèle API ID Input/1M Output/1M Context Notes
Mistral Medium 3 mistral-medium-3 $0.40 $2.00 - Nouveau, compétitif
Mistral Large mistral-large-latest $2.00 $6.00 128K Flagship
Mistral Small mistral-small-latest $0.20 $0.60 32K Budget

Local (Ollama)

Modèle Coût Notes
Llama 3.1 70B Gratuit Requires ~40GB VRAM
Llama 3.1 8B Gratuit Runs on consumer GPU
Qwen 2.5 72B Gratuit Good multilingual
DeepSeek V3 Gratuit MIT license, self-host

Sources : Anthropic Models, OpenAI GPT-5, OpenAI GPT-4.1, Gemini Pricing, DeepSeek Pricing, Moonshot Pricing, Mistral Pricing

Détail par Opération (Claude Sonnet)

Opération Tokens (input) Tokens (output) Coût
list_documents (100 docs) ~1,500 ~50 $0.005
search_documents ~500 ~50 $0.002
read_document (8KB) ~2,500 ~50 $0.008
Réponse finale ~500 ~500 $0.005
Total typique ~5,000 ~650 ~$0.02

Comparaison Stratégies

Stratégie Tokens/requête Coût/requête Latence
Charger tous docs ~125,000 ~$0.40 ~10s
Retrieval agentique ~5,000 ~$0.02 ~3s
Économie 96% 95% 70%

Plan d'Implémentation

Phase 1 : MVP (Priorité Haute)

Objectif : Système fonctionnel avec keyword search et architecture LLM agnostique

  1. ILLMProvider + ClaudeProvider (~150 lignes)

    • Interface ILLMProvider
    • Implémentation ClaudeProvider
    • Gestion retry et rate limiting
    • Parsing réponses tool_use
  2. DocumentStore (~150 lignes)

    • Scan directory et extraction metadata
    • Index en mémoire (JSON)
    • Keyword search basique
    • Lecture avec troncature
  3. AIAssistantModule (~200 lignes)

    • Boucle agentique unifiée
    • Définition tools
    • Exécution tools
    • Extraction réponse finale
  4. Tests

    • Test unitaire DocumentStore
    • Test unitaire ILLMProvider (mock)
    • Test intégration avec API réelle

Phase 2 : Multi-Provider (Priorité Moyenne)

  1. Providers Additionnels

    • OpenAIProvider (~100 lignes)
    • OllamaProvider (~100 lignes)
    • LLMProviderFactory
  2. Index Persistant

    • Sauvegarder index.json sur disque
    • Rebuild incrémental (hash fichiers)
    • File watcher pour auto-update
  3. Métriques et Logging

    • Logging requêtes/réponses par provider
    • Tracking usage tokens
    • Comparaison performance providers

Phase 3 : Optimisations (Priorité Moyenne)

  1. Cache Réponses

    • Cache LRU pour documents fréquents
    • Cache embeddings (si Phase 4)
    • TTL configurable
  2. Fallback Automatique

    • Basculement sur provider secondaire si erreur
    • Configuration priorité providers
    • Health check providers
  3. Provider Selection Dynamique

    • Sélection basée sur type de requête
    • Budget-aware routing
    • Latency-aware routing

Phase 4 : Semantic Search (Priorité Basse)

  1. Embeddings Pipeline

    • Interface IEmbeddingProvider
    • Implémentations (OpenAI, Voyage, local)
    • Génération embeddings au build index
    • Storage SQLite avec sqlite-vss
  2. Hybrid Search

    • Fusion keyword + semantic
    • Tuning pondération
    • A/B testing efficacité
  3. Advanced Features

    • Clustering documents similaires
    • Suggestions proactives
    • Résumés automatiques gros documents

Configuration

config/ai-assistant.json

{
  "documents_path": "./data/documents",
  "index_path": "./data/index.json",

  "retrieval": {
    "strategy": "keyword",
    "max_results": 10,
    "default_max_chars": 8000
  },

  "context_budget": {
    "max_document_tokens": 50000,
    "truncation_strategy": "head_tail"
  },

  "llm": {
    "provider": "claude",
    "max_iterations": 10,
    "retry_attempts": 3,

    "providers": {
      "_comment": "=== ANTHROPIC CLAUDE ===",
      "claude_sonnet_4_5": {
        "api_key_env": "ANTHROPIC_API_KEY",
        "model": "claude-sonnet-4-5-20250929",
        "max_tokens": 8192,
        "base_url": "https://api.anthropic.com/v1"
      },
      "claude_haiku_4_5": {
        "api_key_env": "ANTHROPIC_API_KEY",
        "model": "claude-haiku-4-5",
        "max_tokens": 8192,
        "base_url": "https://api.anthropic.com/v1"
      },
      "claude_opus_4_1": {
        "api_key_env": "ANTHROPIC_API_KEY",
        "model": "claude-opus-4-1",
        "max_tokens": 8192,
        "base_url": "https://api.anthropic.com/v1"
      },
      "claude_sonnet_4": {
        "api_key_env": "ANTHROPIC_API_KEY",
        "model": "claude-sonnet-4-20250514",
        "max_tokens": 8192,
        "base_url": "https://api.anthropic.com/v1"
      },

      "_comment2": "=== OPENAI ===",
      "gpt5": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-5",
        "max_tokens": 16384,
        "base_url": "https://api.openai.com/v1"
      },
      "gpt5_mini": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-5-mini",
        "max_tokens": 16384,
        "base_url": "https://api.openai.com/v1"
      },
      "gpt5_nano": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-5-nano",
        "max_tokens": 8192,
        "base_url": "https://api.openai.com/v1"
      },
      "gpt4_1": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-4.1",
        "max_tokens": 16384,
        "base_url": "https://api.openai.com/v1"
      },
      "gpt4_1_mini": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-4.1-mini",
        "max_tokens": 16384,
        "base_url": "https://api.openai.com/v1"
      },
      "gpt4_1_nano": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-4.1-nano",
        "max_tokens": 8192,
        "base_url": "https://api.openai.com/v1"
      },
      "gpt4o": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-4o",
        "max_tokens": 4096,
        "base_url": "https://api.openai.com/v1"
      },
      "o3": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "o3",
        "max_tokens": 32768,
        "base_url": "https://api.openai.com/v1"
      },

      "_comment3": "=== GOOGLE GEMINI ===",
      "gemini_3_pro": {
        "api_key_env": "GOOGLE_API_KEY",
        "model": "gemini-3.0-pro",
        "max_tokens": 8192,
        "base_url": "https://generativelanguage.googleapis.com/v1beta"
      },
      "gemini_2_5_pro": {
        "api_key_env": "GOOGLE_API_KEY",
        "model": "gemini-2.5-pro",
        "max_tokens": 8192,
        "base_url": "https://generativelanguage.googleapis.com/v1beta"
      },
      "gemini_2_5_flash": {
        "api_key_env": "GOOGLE_API_KEY",
        "model": "gemini-2.5-flash",
        "max_tokens": 8192,
        "base_url": "https://generativelanguage.googleapis.com/v1beta"
      },
      "gemini_1_5_pro": {
        "api_key_env": "GOOGLE_API_KEY",
        "model": "gemini-1.5-pro",
        "max_tokens": 8192,
        "base_url": "https://generativelanguage.googleapis.com/v1beta"
      },
      "gemini_1_5_flash": {
        "api_key_env": "GOOGLE_API_KEY",
        "model": "gemini-1.5-flash",
        "max_tokens": 8192,
        "base_url": "https://generativelanguage.googleapis.com/v1beta"
      },

      "_comment4": "=== DEEPSEEK ===",
      "deepseek": {
        "api_key_env": "DEEPSEEK_API_KEY",
        "model": "deepseek-chat",
        "max_tokens": 8192,
        "base_url": "https://api.deepseek.com/v1"
      },
      "deepseek_reasoner": {
        "api_key_env": "DEEPSEEK_API_KEY",
        "model": "deepseek-reasoner",
        "max_tokens": 65536,
        "base_url": "https://api.deepseek.com/v1"
      },

      "_comment5": "=== KIMI / MOONSHOT ===",
      "kimi_k2": {
        "api_key_env": "MOONSHOT_API_KEY",
        "model": "kimi-k2",
        "max_tokens": 8192,
        "base_url": "https://api.moonshot.cn/v1"
      },
      "kimi_k2_thinking": {
        "api_key_env": "MOONSHOT_API_KEY",
        "model": "kimi-k2-thinking",
        "max_tokens": 8192,
        "base_url": "https://api.moonshot.cn/v1"
      },

      "_comment6": "=== ALIBABA QWEN ===",
      "qwen_max": {
        "api_key_env": "DASHSCOPE_API_KEY",
        "model": "qwen-max",
        "max_tokens": 8192,
        "base_url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
      },
      "qwen_plus": {
        "api_key_env": "DASHSCOPE_API_KEY",
        "model": "qwen-plus",
        "max_tokens": 8192,
        "base_url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
      },
      "qwen_turbo": {
        "api_key_env": "DASHSCOPE_API_KEY",
        "model": "qwen-turbo",
        "max_tokens": 8192,
        "base_url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
      },

      "_comment7": "=== MISTRAL ===",
      "mistral_medium": {
        "api_key_env": "MISTRAL_API_KEY",
        "model": "mistral-medium-3",
        "max_tokens": 8192,
        "base_url": "https://api.mistral.ai/v1"
      },
      "mistral_large": {
        "api_key_env": "MISTRAL_API_KEY",
        "model": "mistral-large-latest",
        "max_tokens": 8192,
        "base_url": "https://api.mistral.ai/v1"
      },
      "mistral_small": {
        "api_key_env": "MISTRAL_API_KEY",
        "model": "mistral-small-latest",
        "max_tokens": 8192,
        "base_url": "https://api.mistral.ai/v1"
      },

      "_comment8": "=== LOCAL (OLLAMA) ===",
      "ollama_llama": {
        "model": "llama3.1:70b",
        "base_url": "http://localhost:11434/api"
      },
      "ollama_llama_small": {
        "model": "llama3.1:8b",
        "base_url": "http://localhost:11434/api"
      },
      "ollama_qwen": {
        "model": "qwen2.5:72b",
        "base_url": "http://localhost:11434/api"
      },
      "ollama_deepseek": {
        "model": "deepseek-v3",
        "base_url": "http://localhost:11434/api"
      }
    },

    "fallback": {
      "enabled": true,
      "order": ["claude", "openai", "ollama"]
    }
  },

  "embeddings": {
    "enabled": false,
    "provider": "openai",
    "providers": {
      "openai": {
        "api_key_env": "OPENAI_API_KEY",
        "model": "text-embedding-3-small"
      },
      "voyage": {
        "api_key_env": "VOYAGE_API_KEY",
        "model": "voyage-3"
      }
    },
    "cache_path": "./data/embeddings.db"
  }
}

Variables d'Environnement

# === ANTHROPIC ===
export ANTHROPIC_API_KEY="sk-ant-..."

# === OPENAI ===
export OPENAI_API_KEY="sk-..."

# === GOOGLE ===
export GOOGLE_API_KEY="..."

# === DEEPSEEK ===
export DEEPSEEK_API_KEY="sk-..."

# === MOONSHOT (Kimi) ===
export MOONSHOT_API_KEY="sk-..."

# === ALIBABA (Qwen) ===
export DASHSCOPE_API_KEY="sk-..."

# === MISTRAL ===
export MISTRAL_API_KEY="..."

# === EMBEDDINGS (Phase 4) ===
export VOYAGE_API_KEY="..."

Changement de Provider à Runtime

Le provider peut être changé via la configuration sans recompilation :

{
  "llm": {
    "provider": "ollama"  // Switch de "claude" à "ollama"
  }
}

Ou programmatiquement :

// Changement dynamique
auto newProvider = LLMProviderFactory::create(newConfig);
aiAssistant->setProvider(std::move(newProvider));

Considérations Multi-Provider

Différences de Comportement

Aspect Claude OpenAI Gemini DeepSeek Kimi Qwen Mistral Ollama
Qualité tool use Excellent Très bon Bon Très bon Bon Bon Bon Variable
Respect instructions Excellent Très bon Bon Bon Bon Bon Bon Variable
Vitesse ~2-3s ~1-2s ~1-2s ~2-3s ~2-3s ~2-3s ~1-2s Hardware
Coût $ $ $ $ Gratuit
Privacy Cloud US Cloud US Cloud US Cloud CN Cloud CN Cloud CN Cloud EU 100% local
Long context 1M beta 256K 2M 128K 128K 1M 128K Dépend modèle

Recommandations d'Usage

Cas d'usage Provider recommandé Raison
Production critique Claude Sonnet 4.5 Meilleur raisonnement tool use
Production budget DeepSeek V3 95% moins cher, qualité comparable
Développement/Tests Ollama Gratuit, pas de rate limit
Long context (>200K) Gemini 1.5 Pro / Qwen Turbo 2M / 1M tokens
Ultra low-cost DeepSeek / Gemini Flash-Lite < $0.10/1M tokens
Privacy EU Mistral Serveurs EU, GDPR compliant
Privacy totale Ollama 100% local, zero data leak
Reasoning complex Claude Opus 4.1 / o3 Deep thinking
Coding Claude Sonnet 4.5 / DeepSeek SOTA sur SWE-bench
Multilingue (CN/EN) Qwen / Kimi Optimisés bilingue

Tests Cross-Provider

// Exemple de test comparatif
void testProviderConsistency() {
    std::vector<std::string> providers = {"claude", "openai", "ollama"};
    std::string testQuery = "Liste les documents sur le planning";

    for (const auto& p : providers) {
        config["llm"]["provider"] = p;
        auto provider = LLMProviderFactory::create(config);
        auto result = aiAssistant->query(testQuery);

        // Vérifier que tous trouvent les mêmes documents pertinents
        ASSERT_TRUE(result.contains("planning"));
    }
}

Références