Nous avons construit une couche de routage pour réduire nos coûts d'IA. Cela a cassé le produit.
Une équipe a construit une couche de routage IA personnalisée pour réduire les coûts d'API, mais cela a introduit de la latence, des erreurs et un comportement imprévisible qui ont dégradé l'expérience utilisateur, finissant par casser le produit.
Tags
Résumé rapide
Une équipe a construit une couche de routage IA personnalisée pour réduire les coûts d'API, mais cela a introduit de la latence, des erreurs et un comportement imprévisible qui ont dégradé l'expérience utilisateur, finissant par casser le produit.
Nous avons construit une couche de routage pour réduire nos coûts d'IA. Elle a cassé le produit.
L'optimisation des coûts en IA est une arme à double tranchant. Chaque équipe d'ingénierie que je connais a fait face à la même pression : réduire les factures d'API sans sacrifier la qualité. Nous pensions avoir trouvé la solution parfaite — une couche de routage intelligente qui choisissait dynamiquement le modèle le moins cher pour chaque requête. Cela fonctionnait brillamment en test. Puis ça a cassé le produit.
Voici l'histoire de ce qui a mal tourné, les détails techniques de la couche de routage que nous avons construite, et les dures leçons que nous avons apprises sur les coûts cachés de l'optimisation.
Le problème que nous essayions de résoudre
Notre produit basé sur l'IA reposait sur plusieurs grands modèles de langage (LLM) de fournisseurs comme OpenAI et Google. Nous utilisions GPT-4 pour le raisonnement complexe, GPT-3.5 pour les tâches simples, et PaLM 2 de Google pour des requêtes spécifiques à un domaine. Notre facture mensuelle approchait les six chiffres.
La solution évidente : router chaque requête vers le modèle le moins cher capable de la traiter. Nous voulions un système qui analyserait chaque prompt, estimerait sa complexité, et l'enverrait au modèle le plus rentable. Si un utilisateur demandait "Quel temps fait-il ?", nous routions vers GPT-3.5-mini. S'il demandait "Rédigez une analyse juridique détaillée", nous routions vers GPT-4.
Exigences
Avant de construire, nous avons défini ce que la couche de routage devait faire :
- **Optimisation des coûts** : Réduire le coût moyen par requête d'au moins 40 %.
- **Contrôle de la latence** : Maintenir les temps de réponse sous 2 secondes pour 95 % des requêtes.
- **Préservation de la qualité** : Maintenir les scores de satisfaction utilisateur à moins de 5 % de la référence.
- **Logique de repli** : Escalader automatiquement vers des modèles plus capables si le modèle bon marché échoue.
- **Observabilité** : Enregistrer chaque décision de routage avec le modèle, le coût et la latence.
Installation étape par étape
Nous avons construit la couche de routage comme un service middleware Python utilisant FastAPI et Redis. Voici comment nous l'avons configurée.
1. Installer les dépendances
D'abord, créez un environnement virtuel et installez les paquets requis.
python -m venv routing-env
source routing-env/bin/activate
pip install fastapi uvicorn redis openai google-generativeai pydantic python-dotenv2. Configuration de l'environnement
Créez un fichier `.env` avec vos clés API et les prix des modèles.
# .env
OPENAI_API_KEY=sk-votre-clé-ici
GOOGLE_API_KEY=votre-clé-google
REDIS_URL=redis://localhost:6379
# Prix des modèles par 1K tokens (USD)
GPT4_PRICE=0.03
GPT35_PRICE=0.0015
PALM2_PRICE=0.00053. Logique de routage principale
Le cœur du système est un estimateur de complexité qui note les prompts sur une échelle de 1 à 10.
# router.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import openai
import google.generativeai as palm
import redis
import os
from dotenv import load_dotenv
load_dotenv()
app = FastAPI()
r = redis.from_url(os.getenv("REDIS_URL"))
class Request(BaseModel):
prompt: str
user_id: str
class Response(BaseModel):
model: str
content: str
cost: float
latency_ms: int
def estimate_complexity(prompt: str) -> int:
"""Note la complexité du prompt de 1 à 10 selon la longueur, les mots-clés et la structure."""
score = 1
if len(prompt) > 200:
score += 2
if len(prompt) > 500:
score += 2
if any(word in prompt.lower() for word in ["juridique", "médical", "code", "math"]):
score += 3
if "?" not in prompt:
score += 1
return min(score, 10)
def select_model(complexity: int) -> str:
"""Route vers le modèle le moins cher capable de gérer la complexité."""
if complexity <= 3:
return "palm-2" # $0.0005
elif complexity <= 6:
return "gpt-3.5-turbo" # $0.0015
else:
return "gpt-4" # $0.03
@app.post("/route")
async def route_request(request: Request):
complexity = estimate_complexity(request.prompt)
model = select_model(complexity)
# Enregistrer la décision dans Redis pour le monitoring
r.rpush("routing_log", f"{model}:{complexity}:{request.user_id}")
# Appeler le modèle sélectionné
import time
start = time.time()
try:
if model == "gpt-4" or model == "gpt-3.5-turbo":
openai.api_key = os.getenv("OPENAI_API_KEY")
response = openai.ChatCompletion.create(
model=model,
messages=[{"role": "user", "content": request.prompt}]
)
content = response.choices[0].message.content
elif model == "palm-2":
palm.configure(api_key=os.getenv("GOOGLE_API_KEY"))
response = palm.generate_text(prompt=request.prompt)
content = response.result
latency = int((time.time() - start) * 1000)
cost = calculate_cost(model, len(request.prompt), len(content))
return Response(model=model, content=content, cost=cost, latency_ms=latency)
except Exception as e:
# Repli vers GPT-4 en cas d'échec
return await fallback(request.prompt)
def calculate_cost(model: str, input_tokens: int, output_tokens: int) -> float:
prices = {
"gpt-4": 0.03,
"gpt-3.5-turbo": 0.0015,
"palm-2": 0.0005
}
return prices[model] * (input_tokens + output_tokens) / 10004. Démarrer le service
Exécutez la couche de routage sur le port 8000.
uvicorn router:app --host 0.0.0.0 --port 8000 --reloadExemples d'utilisation
Une fois le service en cours d'exécution, vous pouvez le tester avec curl.
Requête simple (Route vers PaLM 2)
curl -X POST http://localhost:8000/route \
-H "Content-Type: application/json" \
-d '{"prompt": "Quelle est la capitale de la France ?", "user_id": "test1"}'Réponse attendue :
{
"model": "palm-2",
"content": "La capitale de la France est Paris.",
"cost": 0.0005,
"latency_ms": 340
}Requête complexe (Route vers GPT-4)
curl -X POST http://localhost:8000/route \
-H "Content-Type: application/json" \
-d '{"prompt": "Rédigez une analyse juridique détaillée des implications du Quatrième Amendement concernant la localisation des téléphones portables sans mandat par les forces de l'ordre, y compris les précédents pertinents de la Cour suprême et les opinions dissidentes.", "user_id": "test2"}'Réponse attendue :
{
"model": "gpt-4",
"content": "...",
"cost": 0.12,
"latency_ms": 2100
}Ce qui a cassé le produit
La couche de routage fonctionnait parfaitement en isolation. Notre coût par requête a chuté de 52 % dès la première semaine. Mais les utilisateurs ont commencé à se plaindre d'une qualité incohérente, d'échecs aléatoires et de comportements étranges.
La fragilité de l'estimation de complexité
Notre fonction `estimate_complexity` était trop simpliste. Un utilisateur demandant "Quelle est la différence entre un chat et un chien ?" obtenait un score de 1 (simple) et était routé vers PaLM 2. PaLM 2 renvoyait une réponse courte et techniquement correcte. Mais l'utilisateur s'attendait à une réponse détaillée et conversationnelle comme GPT-4 l'aurait donnée. La couche de routage optimisait pour le coût, pas pour l'expérience utilisateur.
Pire encore, des prompts ambigus comme "Aide-moi à comprendre ce code" obtenaient un score élevé à cause du mot "code" et étaient routés inutilement vers GPT-4. Nous brûlions de l'argent sur des requêtes simples.
Le cauchemar du repli
Quand PaLM 2 échouait sur une requête (ce qui arrivait environ 8 % du temps à cause des limites de débit), notre repli vers GPT-4 fonctionnait — mais il doublait la latence. Les utilisateurs voyaient des indicateurs de chargement tourner pendant plus de 4 secondes. Notre latence au 95e percentile est passée de 1,2 s à 4,8 s.
Divergence de comportement des modèles
Chaque modèle a une personnalité différente. GPT-4 est verbeux et prudent. GPT-3.5 est concis et direct. PaLM 2 a tendance à être plus créatif. Les utilisateurs ont remarqué que le même prompt renvoyait des tons radicalement différents selon la décision de routage. Notre produit a perdu sa voix cohérente.
Ce que nous avons appris
1. L'optimisation des coûts doit inclure l'expérience utilisateur
Nous mesurions le coût par requête mais ignorions le coût par utilisateur satisfait. Une réponse bon marché qui fait fuir un utilisateur est infiniment plus chère qu'une réponse premium qui le retient.
2. Le routage nécessite une personnalisation par utilisateur
Certains utilisateurs préfèrent des réponses courtes. D'autres veulent une analyse approfondie. Notre couche de routage aurait dû apprendre les préférences des utilisateurs et ajuster les seuils en conséquence.
3. Les replis ont besoin de budgets de temps
Au lieu de basculer aveuglément vers GPT-4, nous aurions dû fixer un budget de latence maximum. Si le modèle bon marché prend plus de 500 ms, escaladez immédiatement — n'attendez pas qu'il échoue.
4. Surveillez la qualité, pas seulement le coût
Nous suivions le coût par requête et la latence. Nous aurions dû suivre :
- Les scores de satisfaction utilisateur par modèle
- Les taux d'achèvement des tâches par modèle
- Les taux d'abandon de session
La couche de routage corrigée
Après l'incident, nous avons reconstruit la couche de routage en tenant compte de ces leçons. Voici la version améliorée.
# improved_router.py
import asyncio
from typing import Optional
class AdaptiveRouter:
def __init__(self, user_preferences: dict = None):
self.user_prefs = user_preferences or {}
self.latency_budget = 2000 # max 2 secondes au total
async def route_with_timeout(self, prompt: str, user_id: str):
complexity = self.estimate_complexity(prompt)
preferred_style = self.user_prefs.get(user_id, "équilibré")
# Ajuster le seuil selon la préférence utilisateur
if preferred_style == "concis":
threshold = 4
elif preferred_style == "détaillé":
threshold = 6
else:
threshold = 5
# Essayer d'abord le modèle le moins cher, avec timeout
model = self.select_model(complexity, threshold)
try:
result = await asyncio.wait_for(
self.call_model(model, prompt),
timeout=self.latency_budget / 1000
)
return result
except asyncio.TimeoutError:
# Escalader immédiatement si le modèle bon marché est lent
result = await self.call_model("gpt-4", prompt)
return resultConclusion
Construire une couche de routage pour réduire les coûts d'IA est tentant. Les calculs sont séduisants sur le papier : router 70 % des requêtes vers des modèles bon marché, économiser 50 % sur les factures d'API. Mais les coûts cachés — expérience utilisateur incohérente, latence accrue et dégradation de la qualité — peuvent détruire votre produit.
Notre expérience nous a appris que l'optimisation des coûts en IA ne consiste pas seulement à choisir le modèle le moins cher. Il s'agit de comprendre vos utilisateurs, de mesurer les bonnes métriques et de construire des systèmes adaptatifs qui équilibrent coût et qualité. La meilleure couche de routage est invisible pour les utilisateurs. La nôtre était tout sauf cela.
Si vous construisez un système similaire, commencez par une règle simple : ne laissez jamais un utilisateur voir que vous avez changé de modèle. S'il le remarque, vous avez déjà cassé le produit.
Sources
FAQ
De quoi parle cet article ?
Cet article traite de « Nous avons construit une couche de routage pour réduire nos coûts d'IA. Cela a cassé le produit. » dans la catégorie Outils IA. Une équipe a construit une couche de routage IA personnalisée pour réduire les coûts d'API, mais cela a introduit de la latence, des erreurs et un comportement imprévisible qui ont dégradé l'expérience utilisateur, finissant par casser le produit.
À qui cet article est-il utile ?
Il est utile aux lecteurs qui veulent comprendre les outils et usages de l’IA de façon pratique.
Que faire ensuite ?
Lisez l’article, vérifiez les sources indiquées, puis testez les idées pertinentes pour votre contexte.



