Embeddings & RAG
Stack embeddings OpenAI, chunking sémantique, retrieval pgvector — pipeline complet de la knowledge base hôtelière.
Chaque hôtel a sa propre KB (politiques, horaires, amenities, recommandations, procédures). Claude répond sur la base de ces docs grâce à un RAG : chunking sémantique + embeddings + retrieval cosine via pgvector. Le staff uploade en HTML riche via TipTap, le pipeline chunke/embed automatiquement.
Stack embeddings
| Couche | Choix |
|---|---|
| Provider | Voyage AI voyage-3.5-lite (recommandé Anthropic) |
| Dimensions | 1 024 |
| Prix | $0.02/MTok — mêmes tarifs qu'OpenAI small, qualité Claude-optimisée |
| Free tier | 200M tokens gratuits (suffisant pour ~1 000 hôtels seedés) |
| Stockage | pgvector (extension PostgreSQL) |
| Index | HNSW cosine distance |
| Package | packages/api/src/services/ai/embedding.ts |
Pourquoi Voyage ?
Anthropic n'offre pas d'API embeddings à ce jour (2026), mais recommande officiellement Voyage AI comme partenaire pour le RAG en contexte Claude. Voyage entraîne ses modèles spécifiquement pour maximiser la qualité de retrieval quand le LLM cible est Claude : gain mesurable sur le multilingue (FR/EN typique hôtellerie), les queries courtes matched sur des docs longs, et le jargon métier.
Comparatif des providers
| Provider | Dims | Prix | Verdict |
|---|---|---|---|
Voyage voyage-3.5-lite ⭐ | 1024 | $0.02/MTok | ✅ Retenu par défaut — recommandé Anthropic, 200M tokens free, qualité supérieure |
Voyage voyage-3.5 | 1024 | $0.06/MTok | 🔶 Réservé tier Enterprise (qualité +) |
Voyage voyage-3-large | 1024 | $0.18/MTok | ❌ Overkill pour nos chunks courts |
OpenAI text-embedding-3-small | 1536 | $0.02/MTok | 🔶 Fallback disponible via EMBEDDING_PROVIDER=openai (re-migration schema requise) |
OpenAI text-embedding-3-large | 3072 | $0.13/MTok | ❌ 6× plus cher pour un gain marginal |
| Ollama self-hosted | variables | gratuit | ❌ Qualité inférieure sur multilingue hôtelier |
Switcher d'un provider à l'autre
On garde OpenAI actif en fallback pour pouvoir revenir rapidement si Voyage a un incident ou si on veut benchmark. Les étapes pour switcher :
# 1. Modifier .env
EMBEDDING_PROVIDER=openai
AI_MODEL_EMBEDDINGS=text-embedding-3-small
# 2. Migrer schema (1024 → 1536) — voir migrate-embeddings-to-voyage.ts
# (adapter pour aller dans l'autre sens)
# 3. Re-ingest toutes les KB
bun scripts/seed-dev-guest.ts # (en dev)
# En prod : loop sur les articles publiés et re-call ingestArticle(id)
# 4. Restart le server (le provider est cached en mémoire)Provider interface
packages/api/src/services/ai/embedding.ts expose une abstraction EmbeddingProvider avec 4 implémentations :
VoyageEmbeddingProvider— production (default)OpenAIEmbeddingProvider— alternative, fallbackOllamaEmbeddingProvider— self-hosted viaOLLAMA_URL(tests de coûts zéro)MockEmbeddingProvider— fallback déterministe pour CI sans clé API
Le switch se fait via EMBEDDING_PROVIDER=voyage|openai|ollama|mock. Ajouter un nouveau provider :
- Implémenter
EmbeddingProvider(name, model, dimensions,embed()) - L'enregistrer dans
createEmbeddingProvider() - Ajouter la valeur à l'enum
EMBEDDING_PROVIDERdanspackages/env/src/server.ts - Documenter les dimensions et le format API
Le provider est cached en singleton après première invocation — un restart du server est requis après changement d'env.
Pipeline RAG
1. Chunking (chunking.ts)
Découpage sémantique de l'article HTML :
- Parse par headings
h1/h2/h3→ sections. - Découpage de chaque section en sous-chunks de ~400 tokens avec overlap 50 tokens.
- Backtracking sur la dernière fin de phrase (
.) pour éviter de couper au milieu. - Chaque chunk hérite du titre de son heading pour que Claude voie la hiérarchie.
const { chunks, bodyPlain } = chunkArticle(article.bodyHtml, article.title);
// chunks: Array<{ position, title, content, tokens }>2. Ingestion (ingest.ts)
await ingestArticle(articleId);
// → wipe existing chunks
// → chunk HTML
// → batch embed via OpenAI (1 call pour tous les chunks)
// → INSERT into article_chunk avec embedding vector(1536)Idempotent : re-run efface et recrée les chunks.
3. Retrieval (retrieval.ts)
await retrieveRelevantChunks({
organizationId, // scope par hôtel
query: "à quelle heure le petit déj ?",
topK: 5, // récupère 10 puis rerank
minScore: 0.25, // seuil cosine similarity
});- Embed query (1 call OpenAI, ~20 tokens).
pgvectorcosine distance (<=>operator) surarticle_chunk.embedding.- Over-fetch top-K × 2 pour permettre un reranking.
- Diversification : max 2 chunks du même article (évite les 5 meilleurs chunks d'UN SEUL article qui écraseraient un autre article pertinent).
4. Injection dans Claude
Les chunks retrievés sont injectés dans le bloc dynamic du system prompt avec une section dédiée :
## Knowledge base context
Les extraits ci-dessous viennent de la documentation de l'hôtel.
Cite la source entre crochets quand tu t'appuies dessus.
[Source 1 — "Breakfast hours" (Restaurant)]
Breakfast buffet on ground floor.
Weekdays : 06:30 → 10:30
Weekends : 07:00 → 11:00
...Claude cite les sources ([Source 1]) → côté UI on peut highlighter l'article dans le transcript.
Schema DB
// packages/db/src/schema/knowledge-base.ts
knowledge_article {
id, organizationId, title, category,
locales jsonb, // ["fr", "en"]
status enum, // draft | published | archived
bodyHtml, bodyPlain,
version, parentArticleId, // versioning pour audit
publishedAt, authorUserId,
createdAt, updatedAt
}
article_chunk {
id, articleId, organizationId,
position, // ordre dans l'article
title, content,
tokens, // estimation pour budget
embedding vector(1024), // ← voyage-3.5-lite, index HNSW cosine
createdAt
}L'index HNSW sur article_chunk.embedding rend le retrieval O(log N). Avec 10k chunks, une recherche prend ~5ms.
Coûts embeddings
Négligeables à l'échelle Bell :
| Opération | Volume annuel estimé | Coût |
|---|---|---|
| Ingestion KB par hôtel | ~100 articles × 2 000 tokens = 200k tokens/hôtel | $0.004/hôtel |
| Re-ingestion (update article) | ~30% des articles/an = 60k tokens/hôtel | $0.0012/hôtel |
| Queries retrieval (1 embed par chat message) | 7 200 msgs × 20 tokens = 144k tokens/hôtel/mois | $0.003/hôtel/mois |
Total ~$0.05/hôtel/an pour les embeddings → on peut oublier cette ligne au budget.
Tuning & debug
Paramètres clés
// retrieval.ts defaults
topK = 5 // nb de chunks retournés
minScore = 0.25 // seuil cosine similarity
// (text-embedding-3-small typiquement 0.2-0.5 pour du pertinent)minScore=0.25 a été calibré empiriquement sur notre contenu FR/EN. Avec Voyage AI (distributions plus hautes), il faudrait remonter à ~0.5.
Vérifier qu'une query retrieve bien
const chunks = await retrieveRelevantChunks({
organizationId: "your-org-id",
query: "petit déjeuner",
topK: 5,
});
console.log(chunks.map(c => ({ article: c.articleTitle, score: c.score })));Re-ingest tout une KB
Via le script dev : bun scripts/seed-dev-guest.ts (wipe + re-ingest des 3 articles seed).
En prod : le bouton "Republish" d'un article dans le dashboard staff déclenche ingestArticle(id) automatiquement.
Références
- OpenAI embeddings pricing
- pgvector docs
packages/api/src/services/ai/embedding.tspackages/api/src/services/ai/knowledge/(chunking + ingest + retrieval)packages/db/src/schema/knowledge-base.ts