Back

Como construir um assistente de código com IA local usando Ollama, RAG e sua própria base de código

Você acabou de pedir pro ChatGPT ajudar a debugar uma função no código da empresa. Ele mandou com toda confiança um método que não existe, referenciou um endpoint que o time aposentou há seis meses e sugeriu importar de um pacote que ninguém nunca usou. Resultado: 20 minutos jogados fora até perceber que tava tudo errado.

E não, não é azar. É assim que funciona quando você usa assistente de IA genérico pra código privado. Eles não conhecem seu código — e não têm como conhecer. Seus repos nunca entraram nos dados de treinamento, e mesmo colando trechos no chat, o contexto some depois de uns milhares de tokens.

Agora imagina construir um assistente que realmente entende sua base de código. Rodando 100% na sua máquina, sem API key, sem dados saindo da rede, sem cobrança por token. Um bicho que sabe do seu ORM custom, das convenções do time e até daquele workaround maluco no utils/legacy-parser.ts que ninguém documentou.

É exatamente isso que a gente vai montar aqui. Usando Ollama pra inferência, ChromaDB pro armazenamento vetorial e um pipeline RAG (Retrieval-Augmented Generation) que indexa todo o código e usa como contexto em cada consulta. Bora.

No final, você vai ter um sistema funcional capaz de responder perguntas como:

  • "Como nosso middleware de autenticação lida com a renovação de tokens?"
  • "Quais serviços dependem da classe PaymentGateway?"
  • "Escreve um teste unitário pra calculateShippingCost usando nossos padrões de teste existentes."

Bora começar.

Visão geral da arquitetura

Antes de escrever código, vamos entender o sistema que vamos construir:

┌─────────────────────────────────────────────────────┐
│                 Sua base de código                   │
│  (arquivos .ts, .py, .go, .md)                      │
└──────────────┬──────────────────────────────────────┘
               │  1. Parse e Chunking
               ▼
┌─────────────────────────────────────────────────────┐
│            Motor de Chunking de Código               │
│  (Divisão por AST: função/classe/módulo)            │
└──────────────┬──────────────────────────────────────┘
               │  2. Embedding
               ▼
┌─────────────────────────────────────────────────────┐
│          Modelo de Embedding (Ollama)                │
│  nomic-embed-text / bge-m3               │
└──────────────┬──────────────────────────────────────┘
               │  3. Armazenamento
               ▼
┌─────────────────────────────────────────────────────┐
│        Banco de dados vetorial (ChromaDB)            │
│  Armazenamento local persistente + filtro metadata  │
└──────────────┬──────────────────────────────────────┘
               │  4. Consulta (no momento da inferência)
               ▼
┌─────────────────────────────────────────────────────┐
│              Pipeline RAG                            │
│  Pergunta → Buscar chunks relevantes → Enriquecer   │
└──────────────┬──────────────────────────────────────┘
               │  5. Geração
               ▼
┌─────────────────────────────────────────────────────┐
│        LLM (Ollama: Qwen 3.5 / Llama 4 / DeepSeek)   │
│  Inferência local, nada sai da sua máquina          │
└─────────────────────────────────────────────────────┘

A sacada aqui é a divisão de papéis: o modelo de embedding (leve e rápido) transforma seu código em vetores pesquisáveis, enquanto o LLM (pesado e poderoso) só processa o pedaço específico do código que tem a ver com a pergunta.

Passo 1: Configurando o Ollama

Ollama é o runtime que torna a inferência local de LLMs algo tranquilo. Se você ainda não instalou:

# macOS / Linux curl -fsSL https://ollama.com/install.sh | sh # Verificar instalação ollama --version

Agora baixe os modelos que vamos precisar — um pra embeddings, outro pra geração de código:

# Modelo de embedding (768 dimensões, muito rápido) ollama pull nomic-embed-text # LLM focado em código — escolha conforme seu hardware: # 8GB RAM mínimo: ollama pull qwen3.5:8b # 16GB RAM recomendado: ollama pull qwen3.5:14b # 32GB+ RAM pra melhor qualidade: ollama pull deepseek-coder-v2:33b

Por que esses modelos?

nomic-embed-text é um dos melhores modelos leves de embedding local pra código. Produz vetores de 768 dimensões (ajustável até 64 via Matryoshka Representation Learning), lida bem com sintaxe de código e roda rápido mesmo em CPU. Alternativas como bge-m3 (excelente pra busca híbrida) ou snowflake-arctic-embed também funcionam bem — BGE-M3 em particular se destaca em recuperação multilíngue e de contexto longo. Pra maioria dos setups locais, nomic oferece a melhor relação velocidade-qualidade no seu tamanho compacto (~137M parâmetros).

Qwen 3.5 (8B/14B) é o melhor custo-benefício pra geração local de código em 2026. Lançado em fevereiro de 2026, supera modelos anteriores em tamanhos equivalentes nos benchmarks de codificação (HumanEval+, MBPP+), suporta 262K tokens de contexto nativamente, inclui suporte multimodal nativo, e o "modo de raciocínio híbrido" com chain-of-thought melhora drasticamente a qualidade do código. DeepSeek Coder V2 a 33B continua sendo uma alternativa sólida pra qualidade pura de geração de código se você tiver VRAM suficiente.

Verifique se tá tudo funcionando:

# Teste de embedding curl http://localhost:11434/api/embed -d '{ "model": "nomic-embed-text", "input": "function calculateTotal(items) { return items.reduce((sum, i) => sum + i.price, 0); }" }' # Teste de geração ollama run qwen3.5:8b "Explique o que é um pipeline RAG em 2 frases."

Passo 2: Chunking inteligente da base de código

Aqui é onde a maioria dos tutoriais de RAG quebra. O povo manda dividir o texto em chunks fixos de 500 tokens. Pra código, isso é desastre. Uma função cortada no meio vira lixo na busca. Definição de classe sem métodos não serve pra nada.

O que a gente precisa é de chunking baseado em AST — dividir o código em fronteiras lógicas (funções, classes, módulos) em vez de contar caracteres.

// src/chunker.ts import * as fs from 'fs'; import * as path from 'path'; import { glob } from 'glob'; interface CodeChunk { id: string; content: string; filePath: string; language: string; type: 'function' | 'class' | 'module' | 'documentation' | 'config'; name: string; startLine: number; endLine: number; dependencies: string[]; tokenEstimate: number; } const LANGUAGE_EXTENSIONS: Record<string, string> = { '.ts': 'typescript', '.tsx': 'typescript', '.js': 'javascript', '.jsx': 'javascript', '.py': 'python', '.go': 'go', '.rs': 'rust', '.md': 'markdown', '.yaml': 'config', '.yml': 'config', '.json': 'config', }; const IGNORE_PATTERNS = [ 'node_modules/**', 'dist/**', 'build/**', '.git/**', '*.lock', '*.min.js', '*.map', 'coverage/**', '__pycache__/**', '.venv/**', 'vendor/**', ]; export async function chunkCodebase(rootDir: string): Promise<CodeChunk[]> { const extensions = Object.keys(LANGUAGE_EXTENSIONS).map(ext => `**/*${ext}`); const files = await glob(extensions, { cwd: rootDir, ignore: IGNORE_PATTERNS, absolute: true, }); const chunks: CodeChunk[] = []; for (const filePath of files) { const content = fs.readFileSync(filePath, 'utf-8'); const ext = path.extname(filePath); const language = LANGUAGE_EXTENSIONS[ext] || 'unknown'; if (content.length < 50) continue; if (content.length > 100_000) continue; const fileChunks = splitByLogicalBoundaries(content, language, filePath); chunks.push(...fileChunks); } console.log(`${files.length} arquivos divididos em ${chunks.length} chunks`); return chunks; } function splitByLogicalBoundaries( content: string, language: string, filePath: string ): CodeChunk[] { const lines = content.split('\n'); const chunks: CodeChunk[] = []; if (language === 'markdown' || language === 'config') { return [createWholeFileChunk(content, filePath, language)]; } const boundaries = detectBoundaries(lines, language); if (boundaries.length === 0) { return [createWholeFileChunk(content, filePath, language)]; } for (let i = 0; i < boundaries.length; i++) { const start = boundaries[i]; const end = i + 1 < boundaries.length ? boundaries[i + 1].line - 1 : lines.length - 1; const chunkLines = lines.slice(start.line, end + 1); const chunkContent = chunkLines.join('\n').trim(); if (chunkContent.length < 30) continue; const importLines = extractImports(lines, language); const contextualContent = importLines ? `// File: ${path.basename(filePath)}\n${importLines}\n\n${chunkContent}` : `// File: ${path.basename(filePath)}\n${chunkContent}`; chunks.push({ id: `${filePath}:${start.line}-${end}`, content: contextualContent, filePath: path.relative(process.cwd(), filePath), language, type: start.type, name: start.name, startLine: start.line + 1, endLine: end + 1, dependencies: extractDependencies(chunkContent, language), tokenEstimate: Math.ceil(contextualContent.length / 4), }); } return chunks.length > 0 ? chunks : [createWholeFileChunk(content, filePath, language)]; } interface Boundary { line: number; type: CodeChunk['type']; name: string; } function detectBoundaries(lines: string[], language: string): Boundary[] { const boundaries: Boundary[] = []; const patterns = getBoundaryPatterns(language); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); for (const pattern of patterns) { const match = line.match(pattern.regex); if (match) { boundaries.push({ line: i, type: pattern.type, name: match[1] || `anonymous_${i}`, }); break; } } } return boundaries; } function getBoundaryPatterns(language: string) { const tsPatterns = [ { regex: /^(?:export\s+)?class\s+(\w+)/, type: 'class' as const }, { regex: /^(?:export\s+)?(?:async\s+)?function\s+(\w+)/, type: 'function' as const }, { regex: /^(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(/, type: 'function' as const }, { regex: /^(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*\{/, type: 'module' as const }, ]; const pyPatterns = [ { regex: /^class\s+(\w+)/, type: 'class' as const }, { regex: /^(?:async\s+)?def\s+(\w+)/, type: 'function' as const }, ]; switch (language) { case 'typescript': case 'javascript': return tsPatterns; case 'python': return pyPatterns; default: return tsPatterns; } } function extractImports(lines: string[], language: string): string { const importLines = lines.filter(line => { const trimmed = line.trim(); if (language === 'python') { return trimmed.startsWith('import ') || trimmed.startsWith('from '); } return trimmed.startsWith('import ') || trimmed.startsWith('require('); }); return importLines.slice(0, 10).join('\n'); } function extractDependencies(content: string, language: string): string[] { const deps: string[] = []; const importRegex = language === 'python' ? /(?:from|import)\s+([\w.]+)/g : /(?:from|require\()\s*['"]([^'"]+)['"]/g; let match; while ((match = importRegex.exec(content)) !== null) { deps.push(match[1]); } return [...new Set(deps)]; } function createWholeFileChunk( content: string, filePath: string, language: string ): CodeChunk { const lines = content.split('\n'); return { id: `${filePath}:0-${lines.length}`, content: `// File: ${path.basename(filePath)}\n${content}`, filePath: path.relative(process.cwd(), filePath), language, type: 'module', name: path.basename(filePath, path.extname(filePath)), startLine: 1, endLine: lines.length, dependencies: extractDependencies(content, language), tokenEstimate: Math.ceil(content.length / 4), }; }

Por que o chunking baseado em AST importa

Considere esse cenário real. Você tem uma classe UserService:

export class UserService { async createUser(data: CreateUserDTO): Promise<User> { // ... 40 linhas de validação, hashing, inserção no BD } async getUserById(id: string): Promise<User | null> { // ... 15 linhas de recuperação com cache } async deleteUser(id: string): Promise<void> { // ... 25 linhas de lógica de deleção em cascata } }

O chunking fixo de 500 tokens corta no meio de uma função. O chunk 1 tem a declaração da classe e metade do createUser. O chunk 2 tem a outra metade do createUser e todo o getUserById. Nenhum chunk é útil sozinho.

O chunking baseado em AST produz três chunks: um por método, cada um com o nome da classe e os imports do arquivo como prefixo. Agora quando você pergunta "como funciona a exclusão de usuários?", o retriever encontra o chunk do deleteUser com contexto completo.

Passo 3: Embedding e armazenamento no ChromaDB

ChromaDB é o banco de dados vetorial mais fácil de rodar localmente. Zero configuração, armazenamento persistente e filtro de metadados integrado.

pip install chromadb
// src/embedder.ts import { ChromaClient, Collection } from 'chromadb'; interface EmbeddingConfig { ollamaUrl: string; embeddingModel: string; chromaPath: string; collectionName: string; } export class CodebaseEmbedder { private chroma: ChromaClient; private collection: Collection | null = null; private config: EmbeddingConfig; constructor(config: EmbeddingConfig) { this.config = config; this.chroma = new ChromaClient({ path: config.chromaPath }); } async initialize(): Promise<void> { this.collection = await this.chroma.getOrCreateCollection({ name: this.config.collectionName, metadata: { 'hnsw:space': 'cosine' }, }); } async embedChunks(chunks: CodeChunk[]): Promise<void> { if (!this.collection) throw new Error('Não inicializado'); const BATCH_SIZE = 50; const totalBatches = Math.ceil(chunks.length / BATCH_SIZE); for (let i = 0; i < chunks.length; i += BATCH_SIZE) { const batch = chunks.slice(i, i + BATCH_SIZE); const batchNum = Math.floor(i / BATCH_SIZE) + 1; console.log(`Gerando embeddings lote ${batchNum}/${totalBatches}...`); const embeddings = await Promise.all( batch.map(chunk => this.getEmbedding(chunk.content)) ); await this.collection.upsert({ ids: batch.map(c => c.id), embeddings, documents: batch.map(c => c.content), metadatas: batch.map(c => ({ filePath: c.filePath, language: c.language, type: c.type, name: c.name, startLine: c.startLine, endLine: c.endLine, dependencies: JSON.stringify(c.dependencies), tokenEstimate: c.tokenEstimate, })), }); } console.log(`${chunks.length} chunks embebidos no ChromaDB`); } async query( queryText: string, options: { nResults?: number; filterLanguage?: string; filterType?: string; } = {} ): Promise<QueryResult[]> { if (!this.collection) throw new Error('Não inicializado'); const queryEmbedding = await this.getEmbedding(queryText); const where: Record<string, any> = {}; if (options.filterLanguage) where.language = options.filterLanguage; if (options.filterType) where.type = options.filterType; const results = await this.collection.query({ queryEmbeddings: [queryEmbedding], nResults: options.nResults || 10, where: Object.keys(where).length > 0 ? where : undefined, }); return (results.documents?.[0] || []).map((doc, i) => ({ content: doc || '', metadata: results.metadatas?.[0]?.[i] || {}, distance: results.distances?.[0]?.[i] || 1, })); } private async getEmbedding(text: string): Promise<number[]> { const response = await fetch(`${this.config.ollamaUrl}/api/embed`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: this.config.embeddingModel, input: text, }), }); const data = await response.json(); return data.embeddings[0]; } } interface QueryResult { content: string; metadata: Record<string, any>; distance: number; }

Indexando sua base de código

// src/index-codebase.ts import { chunkCodebase } from './chunker'; import { CodebaseEmbedder } from './embedder'; async function indexCodebase(targetDir: string) { const startTime = Date.now(); console.log(`Dividindo a base de código em: ${targetDir}`); const chunks = await chunkCodebase(targetDir); console.log(`${chunks.length} chunks gerados`); const embedder = new CodebaseEmbedder({ ollamaUrl: 'http://localhost:11434', embeddingModel: 'nomic-embed-text', chromaPath: './.codebase-index', collectionName: 'codebase', }); await embedder.initialize(); await embedder.embedChunks(chunks); const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); console.log(`Indexação completa em ${elapsed}s`); const byLanguage = chunks.reduce((acc, c) => { acc[c.language] = (acc[c.language] || 0) + 1; return acc; }, {} as Record<string, number>); console.log('\nChunks por linguagem:'); Object.entries(byLanguage) .sort(([, a], [, b]) => b - a) .forEach(([lang, count]) => console.log(` ${lang}: ${count}`)); } indexCodebase(process.argv[2] || '.');

Performance típica de indexação num Mac com chip M:

Tamanho do codebaseArquivosChunksTempo
Pequeno (10K LOC)~50~200~30s
Médio (50K LOC)~300~1.200~3min
Grande (200K LOC)~1.500~6.000~15min
Monorepo (1M LOC)~8.000~30.000~1hr

Passo 4: O pipeline RAG

Esse é o loop principal: pega a pergunta do dev, encontra os chunks de código relevantes e passa pro LLM como contexto.

// src/assistant.ts import { CodebaseEmbedder } from './embedder'; import * as readline from 'readline'; interface AssistantConfig { ollamaUrl: string; generationModel: string; embeddingModel: string; chromaPath: string; maxContextChunks: number; maxContextTokens: number; } export class CodingAssistant { private embedder: CodebaseEmbedder; private config: AssistantConfig; private conversationHistory: Array<{ role: string; content: string }> = []; constructor(config: AssistantConfig) { this.config = config; this.embedder = new CodebaseEmbedder({ ollamaUrl: config.ollamaUrl, embeddingModel: config.embeddingModel, chromaPath: config.chromaPath, collectionName: 'codebase', }); } async initialize(): Promise<void> { await this.embedder.initialize(); console.log('Assistente de código pronto. Conectado ao índice da base de código.'); } async ask(question: string): Promise<string> { // Passo 1: Recuperar chunks relevantes const relevantChunks = await this.embedder.query(question, { nResults: this.config.maxContextChunks, }); // Passo 2: Filtrar e ordenar por relevância const filteredChunks = relevantChunks .filter(chunk => chunk.distance < 0.7) .slice(0, this.config.maxContextChunks); // Passo 3: Montar o prompt enriquecido const contextBlock = filteredChunks .map((chunk, i) => { const meta = chunk.metadata; return `--- Chunk ${i + 1} [${meta.filePath}:${meta.startLine}-${meta.endLine}] (${meta.type}: ${meta.name}) ---\n${chunk.content}`; }) .join('\n\n'); const systemPrompt = `You are a senior software engineer with deep knowledge of the codebase described below. Answer questions accurately based on the actual code provided. If the code context doesn't contain enough information to answer, say so explicitly rather than guessing. When referencing code, always mention the file path and function/class name. When suggesting changes, show the exact code that should be modified. IMPORTANT: Base your answers on the code chunks provided below. Do not invent functions, classes, or APIs that are not shown in the context.`; const userPrompt = `## Relevant Codebase Context ${contextBlock} ## Question ${question}`; // Passo 4: Gerar resposta via Ollama const response = await this.generate(systemPrompt, userPrompt); // Passo 5: Rastrear conversa this.conversationHistory.push( { role: 'user', content: question }, { role: 'assistant', content: response } ); return response; } private async generate( systemPrompt: string, userPrompt: string ): Promise<string> { const messages = [ { role: 'system', content: systemPrompt }, ...this.conversationHistory.slice(-6), { role: 'user', content: userPrompt }, ]; const response = await fetch(`${this.config.ollamaUrl}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: this.config.generationModel, messages, stream: false, options: { temperature: 0.1, num_ctx: 32768, top_p: 0.9, }, }), }); const data = await response.json(); return data.message?.content || 'Não foi possível gerar uma resposta.'; } clearHistory(): void { this.conversationHistory = []; } } // CLI interativo async function main() { const assistant = new CodingAssistant({ ollamaUrl: 'http://localhost:11434', generationModel: 'qwen3.5:14b', embeddingModel: 'nomic-embed-text', chromaPath: './.codebase-index', maxContextChunks: 8, maxContextTokens: 12000, }); await assistant.initialize(); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); console.log('\n🤖 Assistente de código local pronto'); console.log('Pergunte qualquer coisa sobre seu codebase. Digite "exit" pra sair.\n'); const askQuestion = () => { rl.question('Você: ', async (input) => { const trimmed = input.trim(); if (trimmed.toLowerCase() === 'exit') { console.log('Até mais!'); rl.close(); return; } if (trimmed.toLowerCase() === 'clear') { assistant.clearHistory(); console.log('Conversa limpa.\n'); askQuestion(); return; } try { const response = await assistant.ask(trimmed); console.log(`\nAssistente: ${response}\n`); } catch (error) { console.error('Erro:', error); } askQuestion(); }); }; askQuestion(); } main().catch(console.error);

Passo 5: Otimizações avançadas

O pipeline básico funciona, mas uso em produção exige várias otimizações.

5.1 Re-ranking pra mais precisão

A busca por similaridade vetorial retorna resultados "semanticamente similares", mas similar nem sempre quer dizer relevante. Um passo de re-ranking usa o LLM pra filtrar falsos positivos:

async function rerankChunks( query: string, chunks: QueryResult[], llm: OllamaClient ): Promise<QueryResult[]> { const prompt = `Given the developer's question: "${query}" Rate each code chunk's relevance from 0-10 (10 = directly answers the question, 0 = completely irrelevant): ${chunks.map((c, i) => `[Chunk ${i}] ${c.metadata.filePath} (${c.metadata.name})\n${c.content.slice(0, 300)}...`).join('\n\n')} Return ONLY a JSON array of objects: [{"index": 0, "score": 8, "reason": "..."}, ...]`; const response = await llm.generate(prompt); const scores = JSON.parse(response); return chunks .map((chunk, i) => ({ ...chunk, relevanceScore: scores.find((s: any) => s.index === i)?.score || 0, })) .filter(c => c.relevanceScore >= 5) .sort((a, b) => b.relevanceScore - a.relevanceScore); }

5.2 Indexação incremental

Re-indexar toda a base de código a cada mudança é desperdício. Use os tempos de modificação dos arquivos pra embedar só os que mudaram:

import * as fs from 'fs'; interface IndexManifest { files: Record<string, { mtime: number; chunkIds: string[] }>; lastFullIndex: number; } async function incrementalIndex( rootDir: string, embedder: CodebaseEmbedder, manifestPath: string ): Promise<{ added: number; updated: number; removed: number }> { const manifest: IndexManifest = fs.existsSync(manifestPath) ? JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) : { files: {}, lastFullIndex: 0 }; const currentFiles = await glob('**/*.{ts,js,py,go,md}', { cwd: rootDir, ignore: IGNORE_PATTERNS, absolute: true, }); let added = 0, updated = 0, removed = 0; const filesToProcess: string[] = []; for (const filePath of currentFiles) { const stat = fs.statSync(filePath); const existing = manifest.files[filePath]; if (!existing || stat.mtimeMs > existing.mtime) { filesToProcess.push(filePath); if (existing) { await embedder.deleteChunks(existing.chunkIds); updated++; } else { added++; } } } for (const [filePath, data] of Object.entries(manifest.files)) { if (!currentFiles.includes(filePath)) { await embedder.deleteChunks(data.chunkIds); delete manifest.files[filePath]; removed++; } } if (filesToProcess.length > 0) { const chunks = await chunkFiles(filesToProcess); await embedder.embedChunks(chunks); for (const filePath of filesToProcess) { const stat = fs.statSync(filePath); const fileChunks = chunks.filter(c => c.filePath === path.relative(process.cwd(), filePath) ); manifest.files[filePath] = { mtime: stat.mtimeMs, chunkIds: fileChunks.map(c => c.id), }; } } fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); return { added, updated, removed }; }

5.3 Recuperação multi-consulta

Às vezes uma única busca de embedding não encontra todo o contexto relevante. Gere múltiplas queries a partir da pergunta original:

async function multiQueryRetrieval( question: string, embedder: CodebaseEmbedder, llm: OllamaClient ): Promise<QueryResult[]> { const alternativeQueries = await llm.generate(` Given this developer question: "${question}" Generate 3 alternative search queries that might find relevant code. Focus on different aspects: function names, class names, file patterns, error messages. Return as JSON array of strings. `); const queries = [question, ...JSON.parse(alternativeQueries)]; const allResults = await Promise.all( queries.map(q => embedder.query(q, { nResults: 5 })) ); const seen = new Map<string, QueryResult>(); for (const results of allResults) { for (const result of results) { const id = result.metadata.filePath + ':' + result.metadata.startLine; const existing = seen.get(id); if (!existing || result.distance < existing.distance) { seen.set(id, result); } } } return [...seen.values()].sort((a, b) => a.distance - b.distance); }

5.4 Monitorando mudanças de arquivos

Pra uma experiência suave pro dev, monitore o sistema de arquivos e re-indexe automaticamente:

import { watch } from 'chokidar'; function watchAndReindex(rootDir: string, embedder: CodebaseEmbedder) { const watcher = watch(rootDir, { ignored: IGNORE_PATTERNS, persistent: true, ignoreInitial: true, }); let debounceTimer: NodeJS.Timeout; const scheduleReindex = () => { clearTimeout(debounceTimer); debounceTimer = setTimeout(async () => { console.log('Arquivos modificados, re-indexando...'); const stats = await incrementalIndex(rootDir, embedder, '.index-manifest.json'); console.log(`Re-indexação completa: +${stats.added} ~${stats.updated} -${stats.removed}`); }, 2000); }; watcher.on('change', scheduleReindex); watcher.on('add', scheduleReindex); watcher.on('unlink', scheduleReindex); console.log(`Monitorando ${rootDir} pra detectar mudanças...`); }

Benchmarks de performance

Aqui tá o que você pode esperar em hardware real (testado em abril de 2026):

Velocidade de indexação (nomic-embed-text)

Hardware1K Chunks5K Chunks10K Chunks
M3 MacBook Pro (36GB)18s85s170s
M2 MacBook Air (16GB)32s155s310s
RTX 4090 (24GB VRAM)8s38s75s
Só CPU (AMD 7950X)45s220s440s

Latência de geração (tempo pro primeiro token)

ModeloM3 Pro (36GB)RTX 4090CPU (7950X)
Qwen 3.5 8B0.8s0.3s3.2s
Qwen 3.5 14B1.5s0.5s8.1s
DeepSeek Coder V2 33B3.2s0.9sN/A (OOM)

Qualidade da recuperação (num monorepo TypeScript de 50K LOC)

MétricaChunking fixoChunking por AST
Relevância Top-142%71%
Recall Top-561%89%
Com Re-ranking68%94%

A combinação de chunking por AST + re-ranking praticamente dobra a precisão comparado com abordagens ingênuas.

Erros comuns e como evitar

Erro 1: Modelo de embedding inconsistente

Se você embebeu com nomic-embed-text mas consulta com mxbai-embed-large, os resultados vão ser lixo. O modelo de embedding tem que ser idêntico pra indexação e consultas. Parece óbvio, mas é o erro #1 quando você troca de modelo durante o desenvolvimento.

Erro 2: Extremos no tamanho dos chunks

Chunks pequenos demais (linhas individuais) perdem contexto. Grandes demais (arquivos inteiros) diluem o sinal semântico. O ponto ideal pra código é 50–300 linhas por chunk, correspondendo a funções individuais ou classes pequenas.

Erro 3: Ignorar o filtro de metadados

Sem filtro de metadados, uma consulta sobre código de autenticação em Python pode retornar utilitários de teste em TypeScript que por acaso mencionam "auth". Sempre armazene e use metadados (linguagem, tipo de arquivo, nome do módulo) pra refinar a busca.

Erro 4: Índice desatualizado

Sua base de código muda todo dia, mas seu índice não atualiza. Configure indexação incremental (Seção 5.2) ou, no mínimo, re-indexe a cada git pull com um hook post-merge:

# .git/hooks/post-merge #!/bin/sh npx tsx src/index-codebase.ts . & echo "Re-indexando a base de código em segundo plano..."

Erro 5: Overflow da janela de contexto

Mesmo com RAG, você pode estourar a janela de contexto recuperando chunks demais. Um modelo de 14B com janela de 32K tokens consegue lidar confortavelmente com ~20K tokens de contexto de código + 4K pro system prompt + 4K pro histórico + 4K pra resposta. Isso dá uns 8–10 chunks de código. Passar disso degrada a qualidade.

Quando usar isso vs. IA na nuvem

Essa abordagem local nem sempre é melhor que APIs na nuvem. Aqui vai a comparação honesta:

FatorLocal (Ollama + RAG)Nuvem (GPT-4.1 / Claude Opus 4.6)
Privacidade✅ Nada sai da sua máquina❌ Código enviado pra servidores externos
Custo✅ Grátis depois do hardware❌ $2–15/M tokens
Qualidade do código⚠️ Boa (14B) a Muito boa (33B+)✅ A melhor disponível
Setup❌ ~2 horas de configuração inicial✅ API key e pronto
Latência⚠️ 1–3s em bom hardware✅ <1s (streaming)
Compreensão do código✅ Profunda (RAG em todo o repo)⚠️ Limitada à janela de contexto
Offline✅ Funciona sem internet❌ Precisa de internet

Use local quando: sua base de código é proprietária, você trabalha em setores regulados (saúde, finanças, defesa), tem um monorepo grande, ou quer custo recorrente zero.

Use nuvem quando: qualidade do código é o mais importante (GPT-4.1 e Claude Opus 4.6 ainda superam qualquer modelo local de 14B), tempo de setup importa, ou seu time já tem orçamento pra APIs.

A abordagem híbrida: Use RAG local pra recuperação de código, mas roteie a geração final pra uma API na nuvem pra máxima qualidade. Seu contexto de código fica local; só o prompt montado (com os snippets relevantes) vai pra nuvem.

Próximos passos

Este guia te dá uma base sólida. Pra ir além:

  1. Adicione parsing com tree-sitter pra chunking preciso por AST em todas as linguagens, substituindo a abordagem baseada em regex.
  2. Integre com seu editor — construa uma extensão de VS Code ou plugin de Neovim que consulte o assistente inline.
  3. Adicione contexto de git — inclua diffs recentes, informações de blame e descrições de PR nos metadados de recuperação.
  4. Implemente loops agênticos — deixe o assistente executar buscas, ler arquivos e rodar testes autonomamente usando function calling.
  5. Ajuste fino do modelo de embedding na sua base de código usando contrastive learning pra melhorar a precisão de recuperação em 15–20%.

As ferramentas tão aí. Os modelos são bons o suficiente. A infraestrutura roda num laptop só. A única coisa entre você e um assistente de código privado e consciente do contexto é um fim de semana de configuração.

AIOllamaRAGLLMIA localassistente de códigoChromaDBembeddingsprivacidadeferramentas de desenvolvimento

Explore ferramentas relacionadas

Experimente estas ferramentas gratuitas do Pockit