- 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>
1636 lines
68 KiB
Markdown
1636 lines
68 KiB
Markdown
# 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<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
|
|
```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<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
|
|
|
|
#### Phase 3 (Optionnel) : Hybrid Search
|
|
|
|
Combinaison keyword + semantic pour meilleurs résultats.
|
|
|
|
```cpp
|
|
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
|
|
|
|
```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<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
|
|
|
|
```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<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
|
|
|
|
```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<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)
|
|
|
|
```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<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é
|
|
|
|
```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<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
|
|
|
|
```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<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
|
|
|
|
```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<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
|
|
|
|
- [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
|