Compter sur un seul fournisseur de recherche pour votre LLM local est un point de défaillance unique. Lorsque ce fournisseur tombe en panne ou atteint ses limites de débit, votre agent perd complètement l'accès au web. Ce tutoriel construit une pile de repli de recherche qui essaie d'abord Scavio ($0.005/requête), puis Brave Search ($0.005/requête), et enfin Tavily ($0.008/crédit) si les deux sont indisponibles. La pile normalise les réponses dans un format commun afin que votre LLM voie des données cohérentes, quel que soit le fournisseur ayant répondu.
Prérequis
- Python 3.9+ installé
- bibliothèque requests installée
- Clés API pour Scavio, Brave Search et Tavily
- Un environnement LLM local (Ollama, llama.cpp, ou vLLM)
Parcours
Étape 1: Définir l'interface du fournisseur
Créez une interface commune que chaque fournisseur de recherche implémente. Tous les fournisseurs renvoient le même format de résultat normalisé.
import os, requests, time
from typing import Optional
SCAVIO_KEY = os.environ.get('SCAVIO_API_KEY', '')
BRAVE_KEY = os.environ.get('BRAVE_API_KEY', '')
TAVILY_KEY = os.environ.get('TAVILY_API_KEY', '')
def normalize_result(title: str, url: str, snippet: str, source: str) -> dict:
return {'title': title, 'url': url, 'snippet': snippet, 'source': source}Étape 2: Implémenter chaque fournisseur de recherche
Écrivez une fonction de recherche pour chaque fournisseur qui renvoie des résultats normalisés. Chaque fonction gère ses propres erreurs et renvoie None en cas d'échec.
def search_scavio(query: str, num: int = 5) -> Optional[list]:
try:
resp = requests.post('https://api.scavio.dev/api/v1/search',
headers={'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json'},
json={'query': query, 'country_code': 'us', 'num_results': num}, timeout=10)
resp.raise_for_status()
return [normalize_result(r['title'], r['link'], r.get('snippet', ''), 'scavio')
for r in resp.json().get('organic_results', [])]
except Exception as e:
print(f'Scavio failed: {e}')
return None
def search_brave(query: str, num: int = 5) -> Optional[list]:
try:
resp = requests.get('https://api.search.brave.com/res/v1/web/search',
headers={'X-Subscription-Token': BRAVE_KEY, 'Accept': 'application/json'},
params={'q': query, 'count': num}, timeout=10)
resp.raise_for_status()
return [normalize_result(r['title'], r['url'], r.get('description', ''), 'brave')
for r in resp.json().get('web', {}).get('results', [])]
except Exception as e:
print(f'Brave failed: {e}')
return None
def search_tavily(query: str, num: int = 5) -> Optional[list]:
try:
resp = requests.post('https://api.tavily.com/search',
json={'api_key': TAVILY_KEY, 'query': query, 'max_results': num}, timeout=10)
resp.raise_for_status()
return [normalize_result(r['title'], r['url'], r.get('content', ''), 'tavily')
for r in resp.json().get('results', [])]
except Exception as e:
print(f'Tavily failed: {e}')
return NoneÉtape 3: Construire la pile de repli
Enchaînez les fournisseurs par ordre de priorité. Le premier fournisseur à renvoyer des résultats gagne. Enregistrez quel fournisseur a servi la requête.
PROVIDERS = [
('scavio', search_scavio, 0.005),
('brave', search_brave, 0.005),
('tavily', search_tavily, 0.008),
]
def search_with_fallback(query: str, num: int = 5) -> dict:
for name, fn, cost in PROVIDERS:
results = fn(query, num)
if results is not None:
return {'results': results, 'provider': name, 'cost': cost, 'query': query}
return {'results': [], 'provider': 'none', 'cost': 0, 'query': query}
# Test the fallback
result = search_with_fallback('best python web frameworks 2026')
print(f'Provider: {result["provider"]} (${result["cost"]}/query)')
for r in result['results'][:3]:
print(f' {r["title"]}')
print(f' {r["url"]}')Étape 4: Intégrer avec votre LLM local
Utilisez la recherche de repli comme outil pour votre LLM local. Le LLM n'a jamais besoin de savoir quel fournisseur a répondu.
def format_for_llm(search_result: dict) -> str:
lines = [f'Web search results for: {search_result["query"]}\n']
for i, r in enumerate(search_result['results'], 1):
lines.append(f'[{i}] {r["title"]}')
lines.append(f' URL: {r["url"]}')
lines.append(f' {r["snippet"]}')
lines.append('')
return '\n'.join(lines)
result = search_with_fallback('latest AI frameworks 2026')
formatted = format_for_llm(result)
print(formatted)
print(f'\n--- Served by: {result["provider"]} at ${result["cost"]}/query ---')Exemple Python
import os, requests
from typing import Optional
SCAVIO_KEY = os.environ.get('SCAVIO_API_KEY', '')
BRAVE_KEY = os.environ.get('BRAVE_API_KEY', '')
def search_scavio(query, num=5):
try:
resp = requests.post('https://api.scavio.dev/api/v1/search',
headers={'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json'},
json={'query': query, 'country_code': 'us', 'num_results': num}, timeout=10)
resp.raise_for_status()
return [{'title': r['title'], 'url': r['link'], 'snippet': r.get('snippet', '')}
for r in resp.json().get('organic_results', [])]
except: return None
def search_brave(query, num=5):
try:
resp = requests.get('https://api.search.brave.com/res/v1/web/search',
headers={'X-Subscription-Token': BRAVE_KEY},
params={'q': query, 'count': num}, timeout=10)
resp.raise_for_status()
return [{'title': r['title'], 'url': r['url'], 'snippet': r.get('description', '')}
for r in resp.json().get('web', {}).get('results', [])]
except: return None
def search(query):
for name, fn in [('scavio', search_scavio), ('brave', search_brave)]:
results = fn(query)
if results:
print(f'Served by {name}')
return results
return []
for r in search('python web frameworks 2026')[:3]:
print(f' {r["title"]}')Exemple JavaScript
const SCAVIO_KEY = process.env.SCAVIO_API_KEY;
const BRAVE_KEY = process.env.BRAVE_API_KEY;
async function searchScavio(query) {
try {
const resp = await fetch('https://api.scavio.dev/api/v1/search', {
method: 'POST',
headers: { 'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, country_code: 'us', num_results: 5 })
});
const data = await resp.json();
return (data.organic_results || []).map(r => ({ title: r.title, url: r.link, snippet: r.snippet || '' }));
} catch { return null; }
}
async function searchBrave(query) {
try {
const resp = await fetch(`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=5`, {
headers: { 'X-Subscription-Token': BRAVE_KEY, Accept: 'application/json' }
});
const data = await resp.json();
return (data.web?.results || []).map(r => ({ title: r.title, url: r.url, snippet: r.description || '' }));
} catch { return null; }
}
async function search(query) {
for (const [name, fn] of [['scavio', searchScavio], ['brave', searchBrave]]) {
const results = await fn(query);
if (results) { console.log(`Served by ${name}`); return results; }
}
return [];
}
search('AI frameworks 2026').then(r => r.slice(0, 3).forEach(x => console.log(x.title)));Sortie attendue
Provider: scavio ($0.005/query)
FastAPI 1.0: The Modern Python Web Framework
https://fastapi.tiangolo.com/release-notes/
Django 6.0 Features and Migration Guide
https://docs.djangoproject.com/en/6.0/
--- Served by: scavio at $0.005/query ---