My App
Intelligence

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

CoucheChoix
ProviderVoyage AI voyage-3.5-lite (recommandé Anthropic)
Dimensions1 024
Prix$0.02/MTok — mêmes tarifs qu'OpenAI small, qualité Claude-optimisée
Free tier200M tokens gratuits (suffisant pour ~1 000 hôtels seedés)
Stockagepgvector (extension PostgreSQL)
IndexHNSW cosine distance
Packagepackages/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

ProviderDimsPrixVerdict
Voyage voyage-3.5-lite1024$0.02/MTokRetenu par défaut — recommandé Anthropic, 200M tokens free, qualité supérieure
Voyage voyage-3.51024$0.06/MTok🔶 Réservé tier Enterprise (qualité +)
Voyage voyage-3-large1024$0.18/MTok❌ Overkill pour nos chunks courts
OpenAI text-embedding-3-small1536$0.02/MTok🔶 Fallback disponible via EMBEDDING_PROVIDER=openai (re-migration schema requise)
OpenAI text-embedding-3-large3072$0.13/MTok❌ 6× plus cher pour un gain marginal
Ollama self-hostedvariablesgratuit❌ 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, fallback
  • OllamaEmbeddingProvider — self-hosted via OLLAMA_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 :

  1. Implémenter EmbeddingProvider (name, model, dimensions, embed())
  2. L'enregistrer dans createEmbeddingProvider()
  3. Ajouter la valeur à l'enum EMBEDDING_PROVIDER dans packages/env/src/server.ts
  4. 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).
  • pgvector cosine distance (<=> operator) sur article_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érationVolume 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.ts
  • packages/api/src/services/ai/knowledge/ (chunking + ingest + retrieval)
  • packages/db/src/schema/knowledge-base.ts

On this page