| name | rag-implementation-patterns |
| description | RAG (Retrieval-Augmented Generation) implementation with embeddings, vector search, and anti-hallucination strategies. Use when implementing document ingestion, chunking, vector stores, semantic search, or preventing LLM hallucinations. |
| license | MIT |
| metadata | {"author":"camaral-team","version":"1.0.0","techniques":"embeddings, vector-search, cosine-similarity"} |
RAG Implementation Patterns
Retrieval-Augmented Generation patterns for building reliable, contextual chatbots with embeddings and vector search.
When to Apply
Use this skill when:
- Implementing document ingestion and chunking
- Setting up vector stores (SQLite, in-memory, Pinecone, etc.)
- Implementing semantic search with embeddings
- Preventing LLM hallucinations through context grounding
- Optimizing retrieval performance and accuracy
- Building knowledge-based chatbots
Key Patterns
1. Semantic Chunking Strategy (CRITICAL)
Pattern: Chunk by semantic boundaries with overlap for context preservation
import { v4 as uuidv4 } from 'uuid'
export interface ChunkMetadata {
section?: string
title?: string
wordCount?: number
}
export interface Chunk {
id: string
text: string
source_file: string
metadata: ChunkMetadata
}
export function chunkDocument(
content: string,
filename: string,
options = {
chunkSize: 500, // words per chunk
overlapSize: 50, // overlapping words
splitByHeaders: true // use ## headers as boundaries
}
): Chunk[] {
const chunks: Chunk[] = []
if (options.splitByHeaders) {
const sections = content.split(/^#{2,3}\s+(.+)$/m)
for (let i = 0; i < sections.length; i += 2) {
const title = sections[i]?.trim() || 'Introduction'
const sectionContent = sections[i + 1]?.trim() || ''
if (!sectionContent) continue
const sectionChunks = chunkText(sectionContent, {
chunkSize: options.chunkSize,
overlapSize: options.overlapSize
})
sectionChunks.forEach(text => {
chunks.push({
id: uuidv4(),
text,
source_file: filename,
metadata: {
section: title,
wordCount: text.split(/\s+/).length
}
})
})
}
} else {
const allChunks = chunkText(content, options)
allChunks.forEach(text => {
chunks.push({
id: uuidv4(),
text,
source_file: filename,
metadata: { wordCount: text.split(/\s+/).length }
})
})
}
return chunks
}
function chunkText(
text: string,
options: { chunkSize: number; overlapSize: number }
): string[] {
const words = text.split(/\s+/).filter(Boolean)
const chunks: string[] = []
for (let i = 0; i < words.length; i += options.chunkSize - options.overlapSize) {
const chunk = words.slice(i, i + options.chunkSize).join(' ')
if (chunk.trim()) {
chunks.push(chunk)
}
}
return chunks
}
2. Embedding Generation (CRITICAL)
Pattern: Batch embeddings with OpenAI for efficiency and cost optimization
import { OpenAI } from 'openai'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
})
const EMBEDDING_MODEL = 'text-embedding-3-small'
const BATCH_SIZE = 100
export async function generateEmbeddings(
texts: string[]
): Promise<number[][]> {
const embeddings: number[][] = []
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
const batch = texts.slice(i, i + BATCH_SIZE)
console.log(`Generating embeddings for batch ${i / BATCH_SIZE + 1}...`)
try {
const response = await openai..({
: ,
: batch
})
batchEmbeddings = response.
.( a. - b.)
.( item.)
embeddings.(...batchEmbeddings)
(i + < texts.) {
( (resolve, ))
}
} (error) {
.(, error)
error
}
}
embeddings
}
(): <[]> {
response = openai..({
: ,
: text
})
response.[].
}
3. Vector Store (SQLite) (HIGH)
Pattern: Local SQLite database with JSON storage for embeddings
import Database from 'better-sqlite3'
import path from 'path'
import fs from 'fs'
const DB_PATH = path.join(process.cwd(), 'data', 'vector_store.db')
export interface StoredChunk {
id: string
text: string
source_file: string
embedding: number[]
metadata: string
}
export function initDB(): Database.Database {
const dataDir = path.dirname(DB_PATH)
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true })
}
const db = new Database(DB_PATH)
db.()
db.()
db
}
(): {
stmt = db.()
stmt.(
chunk.,
chunk.,
chunk.,
.(embedding),
.(chunk.)
)
}
(): [] {
stmt = db.()
rows = stmt.() []
rows.( ({
: row.,
: row.,
: row.,
: .(row.),
: row.
}))
}
(): {
db.()
}
(): {
result = db.().()
result.
}
4. Semantic Search (CRITICAL)
Pattern: Cosine similarity with top-K retrieval
export interface RetrievalResult {
text: string
source_file: string
similarity: number
metadata?: any
}
export function cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) {
throw new Error('Vectors must have same length')
}
let dotProduct = 0
let normA = 0
let normB = 0
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i]
normA += a[i] * a[i]
normB += b[i] * b[i]
}
const denominator = Math.sqrt(normA) * Math.sqrt(normB)
if (denominator === 0) return 0
return dotProduct / denominator
}
export (): <[]> {
chunks = (db)
scored = chunks.( ({
: chunk.,
: chunk.,
: (queryEmbedding, chunk.),
: chunk.
}))
results = scored
.( result. >= minSimilarity)
.( b. - a.)
.(, topK)
results
}
(): <[]> {
db = ()
{
queryEmbedding = (query)
results = (db, queryEmbedding, topK)
results
} {
db.()
}
}
5. Anti-Hallucination System Prompt (CRITICAL)
Pattern: Strict constraints with explicit "I don't know" instructions
export const SYSTEM_PROMPT = `
Eres un asistente experto en Camaral, plataforma de humanos digitales y avatares de IA.
REGLAS CRÍTICAS - DEBES SEGUIR ESTAS REGLAS SIEMPRE:
1. CONTEXTO ES TU ÚNICA FUENTE DE VERDAD
- SOLO responde basándote en el CONTEXTO proporcionado
- NO uses conocimiento general o información externa
- Si algo no está en el contexto, di "No tengo información sobre eso"
2. PROHIBIDO INVENTAR
- NO inventes precios, costos o planes de suscripción
- NO menciones clientes que no estén en el contexto
- NO inventes métricas, estadísticas o números
- NO inventes integraciones o características técnicas
- NO prometas funcionalidades no mencionadas
3. TRANSPARENCIA CUANDO NO SABES
- Si no tienes información suficiente, dilo explícitamente
- Ejemplo: "No cuento con información sobre [tema] en mi base de conocimiento"
- Sugiere dónde pueden obtener más información (página web, contacto)
4. CITAS Y ATRIBUCIÓN
- Cuando sea posible, menciona la fuente: "Según [nombre del documento]..."
- Esto genera confianza y permite verificación
5. TONO Y ESTILO
- Profesional, claro y confiable
- Prioriza claridad sobre longitud
- Respuestas concisas pero completas
- Lenguaje accesible, no técnico-comercial excesivo
6. REDIRECCIÓN APROPIADA
- Si preguntan fuera del contexto de Camaral, redirige amablemente
- Ejemplo: "Soy un asistente especializado en Camaral. Para esa pregunta..."
RECUERDA: Es mejor decir "No sé" que inventar información incorrecta.
`.trim()
export function buildPromptWithContext(
chunks: RetrievalResult[],
question: string
): string {
const context = chunks
.map((chunk, i) => `
[Fuente ${i + 1}: ${chunk.source_file}]
${chunk.text}
`.trim())
.()
}
6. Complete RAG Pipeline (HIGH)
Pattern: End-to-end retrieval-augmented generation
import { NextRequest, NextResponse } from 'next/server'
import { retrieve } from '@/lib/rag/search'
import { buildPromptWithContext } from '@/lib/llm/prompts'
import { OpenAI } from 'openai'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
})
export async function POST(req: NextRequest) {
try {
const { message, history } = await req.json()
const chunks = await retrieve(message, 3)
console.log(`Retrieved ${chunks.length} chunks with similarities:`,
chunks.map(c => c.similarity.toFixed(3))
)
const systemPrompt = (chunks, message)
completion = openai...({
: ,
: [
{ : , : systemPrompt },
...history.(-),
{ : , : message }
],
: ,
:
})
response = completion.[]..
sources = [... (chunks.( c.))]
.({
response,
sources,
: {
: ,
: chunks.,
: chunks.( sum + c., ) / chunks.
}
})
} (error) {
.(, error)
.(
{ : },
{ : }
)
}
}
Anti-Patterns
❌ Don't: Use keyword search instead of semantic search
const relevantChunks = allChunks.filter(chunk =>
chunk.text.toLowerCase().includes(query.toLowerCase())
)
✅ Do: Use semantic embeddings
const queryEmbedding = await generateEmbedding(query)
const relevantChunks = await searchSimilar(db, queryEmbedding, topK)
❌ Don't: Send all documents as context
const allDocs = readAllMarkdownFiles()
const prompt = `Context: ${allDocs.join('\n\n')}\nQuestion: ${query}`
✅ Do: Retrieve only relevant chunks
const relevantChunks = await retrieve(query, 3)
const prompt = buildPromptWithContext(relevantChunks, query)
Performance Tips
- Batch embeddings - Process 100 texts per API call
- Use smaller model - text-embedding-3-small is cheap and effective
- Cache query embeddings - Same queries → reuse embeddings
- Limit topK - 3-5 chunks usually sufficient
- Add similarity threshold - Filter out low-relevance chunks (< 0.5)
- Index frequently - Re-ingest when knowledge base changes
- Monitor costs - Log embedding API calls
Testing
describe('chunkDocument', () => {
it('should split by headers', () => {
const content = '## Section 1\nContent...\n## Section 2\nMore...'
const chunks = chunkDocument(content, 'test.md')
expect(chunks.length).toBeGreaterThan(0)
expect(chunks[0].metadata.section).toBe('Section 1')
})
})
describe('cosineSimilarity', () => {
it('should return 1 for identical vectors', () => {
const v = [1, 2, 3]
expect(cosineSimilarity(v, v)).toBeCloseTo(1)
})
it('should return 0 for orthogonal vectors', () => {
expect(cosineSimilarity([1, 0], [0, 1])).toBeCloseTo(0)
})
})
References