# 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. ```cpp 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 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 ```json { "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 ```json { "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 ```json { "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 ```json { "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 #### Phase 1 (MVP) : Keyword Search Recherche par correspondance de mots-clés dans titre + summary + tags. ```cpp int calculateScore(const DocumentMetadata& doc, const std::vector& 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 #### Phase 3 (Optionnel) : Hybrid Search Combinaison keyword + semantic pour meilleurs résultats. ```cpp std::vector hybridSearch(const std::string& query) { auto keywordResults = keywordSearch(query, 20); auto semanticResults = semanticSearch(query, 20); // Fusion avec pondération std::map 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 ```cpp 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 ```cpp 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](https://docs.anthropic.com/en/docs/about-claude/models/overview), [OpenAI Models](https://platform.openai.com/docs/models), [Gemini API](https://ai.google.dev/gemini-api/docs/models), [DeepSeek Pricing](https://api-docs.deepseek.com/quick_start/pricing), [Moonshot Platform](https://platform.moonshot.ai/), [Alibaba Model Studio](https://www.alibabacloud.com/help/en/model-studio/models), [Mistral Pricing](https://docs.mistral.ai/deployment/laplateforme/pricing/) ### Interface ILLMProvider ```cpp // 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 parseToolCalls(const json& response) = 0; // Formate les résultats des tools pour le prochain message virtual json formatToolResults(const std::vector& 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 ```cpp // 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 parseToolCalls(const json& response) override { std::vector 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& 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 ```cpp // 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 parseToolCalls(const json& response) override { std::vector 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()) }); } } return calls; } json formatToolResults(const std::vector& 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 getProviderName() const override { return "openai"; } }; ``` ### Implémentation Ollama (Local) ```cpp // 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 ```cpp // LLMProviderFactory.hpp class LLMProviderFactory { public: static std::unique_ptr create(const json& config) { std::string provider = config.value("provider", "claude"); if (provider == "claude") { return std::make_unique(config["providers"]["claude"]); } if (provider == "openai") { return std::make_unique(config["providers"]["openai"]); } if (provider == "ollama") { return std::make_unique(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é ```cpp 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(); // ═══════════════════════════════════════════════════════════════════ // 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 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 ```cpp // ═══════════════════════════════════════════════════════════════════════ // 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) ```cpp // ═══════════════════════════════════════════════════════════════════════ // 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 ```cpp 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 ```cpp // ═══════════════════════════════════════════════════════════════════════ // ToolRegistry - Centralise tous les tools disponibles // ═══════════════════════════════════════════════════════════════════════ class ToolRegistry { std::map> tools; public: // Enregistrer un tool void registerTool(std::unique_ptr 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()); toolRegistry.registerTool(std::make_unique()); toolRegistry.registerTool(std::make_unique()); toolRegistry.registerTool(std::make_unique()); toolRegistry.registerTool(std::make_unique()); // ... 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 ```cpp 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](https://docs.anthropic.com/en/docs/about-claude/models/overview), [OpenAI GPT-5](https://openai.com/index/introducing-gpt-5/), [OpenAI GPT-4.1](https://openai.com/index/gpt-4-1/), [Gemini Pricing](https://ai.google.dev/gemini-api/docs/pricing), [DeepSeek Pricing](https://api-docs.deepseek.com/quick_start/pricing), [Moonshot Pricing](https://platform.moonshot.ai/docs/pricing/chat), [Mistral Pricing](https://docs.mistral.ai/deployment/laplateforme/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 ```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 ```bash # === 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 : ```json { "llm": { "provider": "ollama" // Switch de "claude" à "ollama" } } ``` Ou programmatiquement : ```cpp // 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 ```cpp // Exemple de test comparatif void testProviderConsistency() { std::vector 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 - [Anthropic API Documentation](https://docs.anthropic.com/) - [Anthropic Tool Use Guide](https://docs.anthropic.com/en/docs/build-with-claude/tool-use) - [OpenAI Function Calling](https://platform.openai.com/docs/guides/function-calling) - [Ollama API](https://github.com/ollama/ollama/blob/main/docs/api.md) - [architecture-technique.md](./architecture-technique.md) - Architecture système AISSIA - [claude-code-integration.md](./claude-code-integration.md) - Patterns développement Claude Code