Documentation technique : RAG pour développeurs
Déployer un assistant RAG sur votre documentation technique : API docs, guides développeur, READMEs et wikis techniques.
Documentation technique : RAG pour développeurs
La documentation technique est le nerf de la guerre pour les équipes de développement. APIs, guides d'intégration, architecture, runbooks, ADRs... Ces ressources critiques sont souvent dispersées, difficiles à naviguer et rapidement obsolètes. Un assistant RAG transforme cette documentation en copilote intelligent capable de répondre aux questions des développeurs en temps réel, réduisant drastiquement le temps perdu à chercher des informations.
Le défi de la documentation technique
Les symptômes d'une documentation dysfonctionnelle
Chaque équipe technique connaît ces frustrations quotidiennes :
- "La doc est obsolète, on ne sait plus ce qui est à jour"
- "Je ne sais pas par où commencer pour intégrer cette API"
- "Le README ne répond pas à ma question spécifique"
- "Les nouveaux développeurs sont perdus pendant l'onboarding"
- "On a documenté ça quelque part, mais impossible de retrouver où"
Le coût caché de la recherche d'information
Une étude McKinsey révèle que les développeurs passent en moyenne 20% de leur temps à chercher des informations. Pour une équipe de 10 développeurs à 70k€/an :
20% × 10 × 70 000€ = 140 000€/an perdus en recherche
Et ce chiffre n'inclut pas le coût des erreurs dues à une documentation mal comprise ou obsolète.
Les spécificités de la documentation technique
| Type de documentation | Défi principal | Fréquence d'accès |
|---|---|---|
| API Reference | Trouver le bon endpoint parmi des centaines | Très haute |
| Guides d'intégration | Suivre les étapes dans le bon ordre | Haute |
| Architecture docs | Comprendre les dépendances système | Moyenne |
| Runbooks | Accéder rapidement aux procédures d'urgence | Critique |
| ADRs (Architecture Decision Records) | Comprendre le "pourquoi" des décisions | Moyenne |
| Code comments/docstrings | Trouver l'usage d'une fonction | Très haute |
Architecture d'un RAG Documentation Technique
┌─────────────────────────────────────────────────────────────────┐
│ Sources de Documentation │
├─────────────────────────────────────────────────────────────────┤
│ OpenAPI Markdown Code ADRs Wiki │
│ Specs Docs Comments Records Pages │
│ │ │ │ │ │ │
│ └───────────┼────────────┼───────────┼───────────┘ │
│ ▼ │
│ ┌───────────────┐ │
│ │ Doc Parser │ ← Parseurs spécialisés par format │
│ └───────┬───────┘ │
└────────────────┼────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Pipeline d'Indexation │
├─────────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ Chunking │ │ Metadata │ │ Embedding │ │
│ │ Sémantique │──│ Extraction │──│ (BGE-M3) │ │
│ │ │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └─────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Vector Store │ │
│ │ (Qdrant) │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Interfaces d'Accès │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │
│ │ Web │ │ Slack │ │ IDE │ │ CLI │ │
│ │ Chat │ │ Bot │ │ Plugin │ │ (ailog ask) │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Indexation Multi-Source
Connecteur de Documentation Technique
DEVELOPERpythonfrom pathlib import Path from typing import List, Dict, Optional import yaml import re from dataclasses import dataclass @dataclass class TechDocument: """Document technique indexable.""" id: str title: str content: str doc_type: str source_path: str metadata: Dict class TechDocIndexer: """Indexeur multi-source pour documentation technique.""" def __init__(self, vector_db, embedding_model): self.vector_db = vector_db self.embedding_model = embedding_model self.parsers = { "openapi": self._parse_openapi, "markdown": self._parse_markdown, "python": self._parse_python_docstrings, "adr": self._parse_adr, } async def index_all_sources(self, config: Dict) -> int: """Indexe toutes les sources de documentation configurées.""" documents = [] # API Documentation (OpenAPI/Swagger) if "openapi" in config: for spec_path in config["openapi"]: api_docs = await self._parse_openapi(spec_path) documents.extend(api_docs) print(f"Indexed {len(api_docs)} endpoints from {spec_path}") # Markdown Documentation if "markdown_dirs" in config: for doc_dir in config["markdown_dirs"]: md_docs = await self._parse_markdown_folder(doc_dir) documents.extend(md_docs) print(f"Indexed {len(md_docs)} markdown files from {doc_dir}") # Code Documentation (Docstrings) if "code_dirs" in config: for code_dir in config["code_dirs"]: code_docs = await self._parse_code_folder(code_dir) documents.extend(code_docs) print(f"Indexed {len(code_docs)} code docs from {code_dir}") # Architecture Decision Records if "adr_dir" in config: adr_docs = await self._parse_adr_folder(config["adr_dir"]) documents.extend(adr_docs) print(f"Indexed {len(adr_docs)} ADRs") # Génération des embeddings et stockage await self._store_documents(documents) return len(documents) async def _parse_openapi(self, spec_path: str) -> List[TechDocument]: """Parse une spécification OpenAPI/Swagger.""" with open(spec_path) as f: spec = yaml.safe_load(f) documents = [] base_url = spec.get("servers", [{}])[0].get("url", "") for path, methods in spec.get("paths", {}).items(): for method, details in methods.items(): if method in ["get", "post", "put", "patch", "delete"]: # Construire le contenu enrichi content = self._format_endpoint_doc( method=method, path=path, details=details, base_url=base_url ) documents.append(TechDocument( id=f"api_{method}_{path.replace('/', '_')}", title=f"{method.upper()} {path}", content=content, doc_type="api_endpoint", source_path=spec_path, metadata={ "method": method.upper(), "path": path, "tags": details.get("tags", []), "deprecated": details.get("deprecated", False), "auth_required": self._requires_auth(details), } )) return documents def _format_endpoint_doc( self, method: str, path: str, details: Dict, base_url: str ) -> str: """Formate la documentation d'un endpoint pour l'indexation.""" sections = [] # En-tête sections.append(f"# {method.upper()} {path}") sections.append(f"URL complète: {base_url}{path}") # Description if details.get("summary"): sections.append(f"\n## Résumé\n{details['summary']}") if details.get("description"): sections.append(f"\n## Description\n{details['description']}") # Paramètres params = details.get("parameters", []) if params: sections.append("\n## Paramètres") for param in params: required = "obligatoire" if param.get("required") else "optionnel" param_type = param.get("schema", {}).get("type", "any") sections.append( f"- **{param['name']}** ({param['in']}, {param_type}, {required}): " f"{param.get('description', 'Non documenté')}" ) # Request Body request_body = details.get("requestBody", {}) if request_body: sections.append("\n## Corps de la requête") content = request_body.get("content", {}) for content_type, schema in content.items(): sections.append(f"Content-Type: {content_type}") if "example" in schema: sections.append(f"Exemple:\n```json\n{schema['example']}\n```") # Réponses responses = details.get("responses", {}) if responses: sections.append("\n## Réponses") for code, response in responses.items(): sections.append(f"- **{code}**: {response.get('description', '')}") return "\n".join(sections) def _requires_auth(self, details: Dict) -> bool: """Vérifie si l'endpoint nécessite une authentification.""" return bool(details.get("security", [])) async def _parse_markdown_folder(self, folder: str) -> List[TechDocument]: """Parse tous les fichiers Markdown d'un dossier.""" documents = [] folder_path = Path(folder) for md_file in folder_path.rglob("*.md"): with open(md_file, encoding="utf-8") as f: content = f.read() # Extraire les métadonnées YAML si présentes metadata = {} if content.startswith("---"): try: _, yaml_content, content = content.split("---", 2) metadata = yaml.safe_load(yaml_content) except: pass # Extraire le titre title_match = re.search(r"^#\s+(.+)$", content, re.MULTILINE) title = title_match.group(1) if title_match else md_file.stem # Déterminer la catégorie depuis le chemin relative_path = md_file.relative_to(folder_path) category = relative_path.parent.name if relative_path.parent.name != "." else "general" documents.append(TechDocument( id=f"doc_{md_file.stem}_{hash(str(md_file))}", title=title, content=content, doc_type="markdown", source_path=str(md_file), metadata={ "category": category, "file_name": md_file.name, **metadata } )) return documents async def _parse_code_folder(self, folder: str) -> List[TechDocument]: """Extrait les docstrings des fichiers Python.""" import ast documents = [] folder_path = Path(folder) for py_file in folder_path.rglob("*.py"): try: with open(py_file, encoding="utf-8") as f: source = f.read() tree = ast.parse(source) for node in ast.walk(tree): # Classes if isinstance(node, ast.ClassDef): docstring = ast.get_docstring(node) if docstring: documents.append(TechDocument( id=f"class_{py_file.stem}_{node.name}", title=f"Classe {node.name}", content=f"# Classe {node.name}\n\n{docstring}", doc_type="code_class", source_path=str(py_file), metadata={ "class_name": node.name, "line_number": node.lineno } )) # Fonctions elif isinstance(node, ast.FunctionDef): docstring = ast.get_docstring(node) if docstring and not node.name.startswith("_"): # Extraire la signature args = [arg.arg for arg in node.args.args] signature = f"{node.name}({', '.join(args)})" documents.append(TechDocument( id=f"func_{py_file.stem}_{node.name}", title=f"Fonction {node.name}", content=f"# {signature}\n\n{docstring}", doc_type="code_function", source_path=str(py_file), metadata={ "function_name": node.name, "signature": signature, "line_number": node.lineno } )) except SyntaxError: continue # Ignorer les fichiers avec erreurs de syntaxe return documents async def _parse_adr_folder(self, folder: str) -> List[TechDocument]: """Parse les Architecture Decision Records.""" documents = [] folder_path = Path(folder) for adr_file in sorted(folder_path.glob("*.md")): with open(adr_file, encoding="utf-8") as f: content = f.read() # Extraire le numéro et le titre de l'ADR adr_match = re.match(r"(\d+)-(.+)\.md", adr_file.name) if adr_match: adr_number = adr_match.group(1) adr_slug = adr_match.group(2) else: adr_number = "0" adr_slug = adr_file.stem # Extraire le statut (Accepted, Deprecated, Superseded, etc.) status_match = re.search(r"Status:\s*(\w+)", content, re.IGNORECASE) status = status_match.group(1) if status_match else "Unknown" # Extraire le titre title_match = re.search(r"^#\s+(.+)$", content, re.MULTILINE) title = title_match.group(1) if title_match else adr_slug.replace("-", " ") documents.append(TechDocument( id=f"adr_{adr_number}_{adr_slug}", title=f"ADR-{adr_number}: {title}", content=content, doc_type="adr", source_path=str(adr_file), metadata={ "adr_number": int(adr_number), "status": status, "slug": adr_slug } )) return documents async def _store_documents(self, documents: List[TechDocument]): """Génère les embeddings et stocke dans la base vectorielle.""" for doc in documents: embedding = await self.embedding_model.embed(doc.content) await self.vector_db.upsert( collection="tech_docs", points=[{ "id": doc.id, "vector": embedding, "payload": { "title": doc.title, "content": doc.content, "doc_type": doc.doc_type, "source_path": doc.source_path, **doc.metadata } }] )
Prompt Système pour Développeurs
DEVELOPERpythonTECH_DOC_SYSTEM_PROMPT = """Tu es un assistant technique expert pour l'équipe de développement. Tu aides les développeurs à trouver rapidement des informations dans la documentation technique. ## Tes compétences - API Reference : endpoints, paramètres, exemples de requêtes - Guides d'intégration : procédures étape par étape - Architecture : composants, dépendances, flux de données - Code : fonctions, classes, patterns d'utilisation - ADRs : contexte et justification des décisions techniques ## Règles de réponse 1. **Précision technique** : Sois exact, cite les versions et paramètres spécifiques 2. **Exemples de code** : Inclus toujours un exemple fonctionnel quand pertinent 3. **Sources** : Cite systématiquement le fichier ou l'endpoint source 4. **Concision** : Va droit au but, les développeurs ont peu de temps 5. **Honnêteté** : Si l'info n'existe pas dans la doc, dis-le clairement ## Format de réponse ### Pour les questions API
Endpoint: METHOD /path Auth: Requis/Non requis
Exemple: curl -X METHOD 'url' -H 'Header: value'
Réponse: {json_example}
📄 Source: openapi.yaml
### Pour les questions d'intégration
Étapes:
- Première étape
- Deuxième étape
Code: code_example
📄 Source: docs/integration.md
## Documentation disponible
{context}
## Question
{query}
"""
Cas d'Usage Concrets
Questions API
Développeur : "Comment créer un utilisateur via l'API ?"
Assistant :
**Endpoint**: POST /api/v1/users
**Auth**: Bearer token requis
**Corps de la requête**:
{
"email": "[email protected]",
"name": "John Doe",
"role": "member"
}
**Exemple curl**:
curl -X POST 'https://api.example.com/api/v1/users' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"email": "[email protected]", "name": "John Doe", "role": "member"}'
**Réponses**:
- 201: Utilisateur créé avec succès
- 400: Données invalides
- 409: Email déjà utilisé
📄 Source: openapi.yaml (POST /api/v1/users)
Debug et Erreurs
Développeur : "Que signifie l'erreur AUTH_TOKEN_EXPIRED ?"
Assistant :
**Erreur AUTH_TOKEN_EXPIRED**
Cette erreur survient quand le token JWT a dépassé sa durée de validité (24h par défaut).
**Solutions**:
1. Rafraîchir le token via POST /api/v1/auth/refresh
2. Redemander un nouveau token via POST /api/v1/auth/login
**Code de gestion**:
async function apiCall(endpoint) {
try {
return await fetch(endpoint, { headers: getAuthHeaders() });
} catch (error) {
if (error.code === 'AUTH_TOKEN_EXPIRED') {
await refreshToken();
return apiCall(endpoint); // Retry
}
throw error;
}
}
📄 Sources: docs/authentication.md, errors/auth-errors.md
Architecture
Développeur : "Comment le service de paiement communique avec le service commandes ?"
Assistant :
**Communication Payment Service ↔ Order Service**
Le Payment Service communique avec l'Order Service via un pattern Event-Driven :
┌─────────────┐ payment.completed ┌─────────────┐
│ Payment │ ────────────────────→ │ Order │
│ Service │ │ Service │
└─────────────┘ └─────────────┘
│ │
│ RabbitMQ Exchange │
└───────────────┬───────────────────────┘
│
payment.events
**Événements émis**:
- payment.initiated : Paiement démarré
- payment.completed : Paiement réussi → déclenche order.confirm
- payment.failed : Échec → déclenche order.cancel
**Pourquoi ce choix ?** (ADR-015)
Découplage des services pour permettre le scaling indépendant et la résilience aux pannes.
📄 Sources: ADR-015, docs/architecture/event-flows.md
Intégration IDE
Extension VS Code
DEVELOPERtypescriptimport * as vscode from 'vscode'; interface AilogResponse { answer: string; sources: Array<{ path: string; title: string }>; confidence: number; } export function activate(context: vscode.ExtensionContext) { // Commande principale : poser une question const askCommand = vscode.commands.registerCommand( 'ailog.askDoc', async () => { const question = await vscode.window.showInputBox({ prompt: 'Posez votre question sur la documentation', placeHolder: 'Comment authentifier une requête API ?' }); if (!question) return; // Récupérer le contexte actuel const editor = vscode.window.activeTextEditor; const context = { currentFile: editor?.document.fileName, selectedCode: editor?.document.getText(editor.selection), language: editor?.document.languageId }; // Afficher le panneau de réponse const panel = vscode.window.createWebviewPanel( 'ailogResponse', 'Ailog - Documentation', vscode.ViewColumn.Beside, { enableScripts: true } ); panel.webview.html = getLoadingHtml(); try { const response = await askAilog(question, context); panel.webview.html = formatResponseHtml(response); } catch (error) { panel.webview.html = getErrorHtml(error); } } ); // Commande contextuelle : expliquer le code sélectionné const explainCommand = vscode.commands.registerCommand( 'ailog.explainCode', async () => { const editor = vscode.window.activeTextEditor; if (!editor) return; const selection = editor.selection; const selectedCode = editor.document.getText(selection); if (!selectedCode) { vscode.window.showWarningMessage('Sélectionnez du code à expliquer'); return; } const question = `Explique ce code et son usage dans notre codebase:\n\`\`\`\n${selectedCode}\n\`\`\``; const response = await askAilog(question, { currentFile: editor.document.fileName, language: editor.document.languageId }); // Afficher en hover ou panneau showInlineResponse(editor, selection, response); } ); context.subscriptions.push(askCommand, explainCommand); } async function askAilog( question: string, context: object ): Promise<AilogResponse> { const config = vscode.workspace.getConfiguration('ailog'); const apiKey = config.get<string>('apiKey'); const endpoint = config.get<string>('endpoint') || 'https://api.ailog.fr'; const response = await fetch(`${endpoint}/v1/ask`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ question, context }) }); if (!response.ok) { throw new Error(`API Error: ${response.status}`); } return response.json(); }
Plugin JetBrains (IntelliJ, PyCharm)
DEVELOPERkotlin// AilogToolWindowFactory.kt class AilogToolWindowFactory : ToolWindowFactory { override fun createToolWindowContent( project: Project, toolWindow: ToolWindow ) { val panel = AilogPanel(project) val content = ContentFactory.getInstance() .createContent(panel, "Ailog Docs", false) toolWindow.contentManager.addContent(content) } } class AilogPanel(private val project: Project) : JPanel() { private val questionField = JTextField() private val responseArea = JEditorPane("text/html", "") init { layout = BorderLayout() // Barre de question val inputPanel = JPanel(BorderLayout()) inputPanel.add(JLabel("Question: "), BorderLayout.WEST) inputPanel.add(questionField, BorderLayout.CENTER) val askButton = JButton("Ask") askButton.addActionListener { askQuestion() } inputPanel.add(askButton, BorderLayout.EAST) add(inputPanel, BorderLayout.NORTH) add(JScrollPane(responseArea), BorderLayout.CENTER) // Raccourci clavier questionField.addActionListener { askQuestion() } } private fun askQuestion() { val question = questionField.text if (question.isBlank()) return // Contexte du fichier actif val editor = FileEditorManager.getInstance(project).selectedTextEditor val context = mapOf( "file" to editor?.virtualFile?.path, "selection" to editor?.selectionModel?.selectedText ) // Appel asynchrone ApplicationManager.getApplication().executeOnPooledThread { val response = AilogClient.ask(question, context) SwingUtilities.invokeLater { responseArea.text = formatResponse(response) } } } }
Synchronisation Continue
Webhook Git pour Mise à Jour Automatique
DEVELOPERpythonfrom fastapi import FastAPI, Request, HTTPException import hmac import hashlib app = FastAPI() @app.post("/webhook/github") async def github_webhook(request: Request): """Webhook GitHub pour synchroniser la documentation.""" # Vérifier la signature signature = request.headers.get("X-Hub-Signature-256") body = await request.body() if not verify_signature(body, signature): raise HTTPException(status_code=401, detail="Invalid signature") payload = await request.json() event = request.headers.get("X-GitHub-Event") if event == "push": # Vérifier si des fichiers de doc ont changé changed_files = [] for commit in payload.get("commits", []): changed_files.extend(commit.get("added", [])) changed_files.extend(commit.get("modified", [])) doc_files = [f for f in changed_files if is_doc_file(f)] if doc_files: # Déclencher la réindexation await trigger_reindex(doc_files) return {"status": "reindex_triggered", "files": doc_files} return {"status": "ignored"} def is_doc_file(path: str) -> bool: """Vérifie si le fichier est de la documentation.""" doc_patterns = [ "docs/", "README", ".md", "openapi", "swagger", "adr/" ] return any(pattern in path for pattern in doc_patterns) async def trigger_reindex(files: list): """Déclenche la réindexation des fichiers modifiés.""" indexer = TechDocIndexer(vector_db, embedding_model) for file_path in files: if file_path.endswith(".md"): doc = await indexer._parse_markdown_file(file_path) await indexer._store_documents([doc]) elif "openapi" in file_path or "swagger" in file_path: docs = await indexer._parse_openapi(file_path) await indexer._store_documents(docs) print(f"Reindexed {len(files)} documentation files")
Mesurer l'Efficacité
Métriques Clés
| Métrique | Objectif | Comment mesurer |
|---|---|---|
| Temps de résolution | < 30 secondes | Temps entre question et réponse satisfaisante |
| Précision des réponses | > 90% | Feedback utilisateur (pouce haut/bas) |
| Réduction tickets support interne | > 50% | Comparaison avant/après déploiement |
| Adoption | > 80% de l'équipe | Utilisateurs actifs / équipe totale |
| Satisfaction développeurs | > 4/5 | Survey NPS trimestriel |
Dashboard de Suivi
DEVELOPERpythonclass DocRAGAnalytics: """Analytics pour mesurer l'efficacité du RAG documentation.""" def get_weekly_report(self) -> Dict: """Génère le rapport hebdomadaire.""" return { "total_queries": self.count_queries(days=7), "unique_users": self.count_unique_users(days=7), "avg_response_time_ms": self.avg_response_time(days=7), "satisfaction_rate": self.satisfaction_rate(days=7), "top_queries": self.top_queries(days=7, limit=10), "unanswered_queries": self.unanswered_queries(days=7), "most_used_docs": self.most_cited_sources(days=7, limit=10) }
Ressources Complémentaires
- Base de connaissances entreprise - Guide pilier KB
- Confluence + RAG - Wiki d'équipe
- Notion + RAG - Documentation structurée
- Introduction au RAG - Fondamentaux
FAQ
Documentation Intelligente avec Ailog
Rendez votre documentation technique accessible instantanément. Ailog propose :
- Indexation multi-source : OpenAPI, Markdown, code comments, ADRs
- Intégration IDE : Extensions VS Code et JetBrains
- Sync automatique : Webhooks Git pour mise à jour en temps réel
- Réponses avec code : Snippets et exemples dans les réponses
- Hébergement France : Conformité RGPD native
Vos développeurs perdent des heures chaque semaine à chercher dans la doc. Donnez-leur un assistant qui trouve instantanément.
Tags
Articles connexes
Base de connaissances intelligente : Centraliser le savoir d'entreprise
Créez une base de connaissances IA pour votre entreprise : documentation technique, onboarding, expertise métier accessibles instantanément.
Confluence : Base de connaissances IA pour equipes
Guide complet pour deployer un assistant RAG sur Confluence. Transformez votre documentation Atlassian en base de connaissances interrogeable par IA.
Slack Bot RAG : Recherche intelligente dans vos conversations
Guide complet pour deployer un bot Slack RAG. Transformez l'historique de vos canaux en base de connaissances interrogeable par IA.