aissia/docs/architecture/intelligent-document-retrieval.md
StillHammer 80f26aea54 Add infrastructure foundation and intelligent document retrieval
- CMakeLists.txt: build configuration
- src/: initial infrastructure structure
- config/: application configuration
- external/: third-party dependencies
- docs/GROVEENGINE_GUIDE.md: GroveEngine reference guide
- docs/architecture/intelligent-document-retrieval.md: agentic retrieval for AIAssistantModule

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

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

44 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 Unifiée

json AIAssistantModule::agenticLoop(const std::string& userQuery) {
    json messages = json::array();
    messages.push_back({{"role", "user"}, {"content", userQuery}});

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

    while (iterations++ < MAX_ITERATIONS) {
        auto response = provider->chat(systemPrompt, messages, tools);

        if (provider->isEndTurn(response)) {
            return {
                {"response", provider->extractText(response)},
                {"iterations", iterations},
                {"provider", provider->getProviderName()}
            };
        }

        auto toolCalls = provider->parseToolCalls(response);
        if (toolCalls.empty()) {
            return {{"error", "unexpected_state"}};
        }

        // Exécuter les tools (logique commune à tous les providers)
        std::vector<ToolResult> results;
        for (const auto& call : toolCalls) {
            auto result = executeTool(call.name, call.input);
            results.push_back({call.id, result.dump(), false});
        }

        // Ajouter à l'historique (format dépend du provider)
        provider->appendAssistantMessage(messages, response);

        // Gérer la différence de format pour tool results
        auto toolResultsMsg = provider->formatToolResults(results);
        if (toolResultsMsg.is_array()) {
            // OpenAI: plusieurs messages
            for (const auto& msg : toolResultsMsg) {
                messages.push_back(msg);
            }
        } else {
            // Claude: un seul message
            messages.push_back(toolResultsMsg);
        }
    }

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

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