[{"data":1,"prerenderedAt":2425},["ShallowReactive",2],{"article-\u002Fia\u002Frag-com-postgresql-pgvector":3},{"id":4,"title":5,"author":6,"body":7,"category":2412,"date":2413,"description":2414,"extension":2415,"keywords":2416,"meta":2419,"navigation":700,"path":2420,"seo":2421,"slug":2422,"stem":2423,"updated":2413,"__hash__":2424},"content\u002Fia\u002Frag-com-postgresql-pgvector.md","RAG com PostgreSQL e pgvector: IA com seus dados","Inicialize Tec",{"type":8,"value":9,"toc":2369},"minimark",[10,15,98,102,120,129,147,153,164,168,178,188,193,303,309,313,316,320,330,335,338,342,345,366,369,410,420,424,428,504,511,515,518,527,531,589,592,606,610,613,743,750,754,758,907,918,922,932,940,943,947,1048,1052,1055,1241,1247,1251,1264,1268,1271,1384,1391,1395,1398,1446,1450,1453,1746,1757,1761,1765,1772,1827,1836,1855,1869,1873,1880,1940,1943,1964,2013,2017,2021,2024,2035,2038,2106,2110,2113,2151,2158,2162,2165,2169,2172,2176,2187,2261,2271,2275,2278,2282,2351,2354,2365],[11,12,14],"h2",{"id":13},"índice","Índice",[16,17,18,26,32,38,44,50,56,62,68,74,80,86,92],"ol",{},[19,20,21],"li",{},[22,23,25],"a",{"href":24},"#o-que-e-rag","O que é RAG e por que você precisa",[19,27,28],{},[22,29,31],{"href":30},"#por-que-pgvector","Por que PostgreSQL e pgvector",[19,33,34],{},[22,35,37],{"href":36},"#conceitos","Conceitos: embeddings e similaridade",[19,39,40],{},[22,41,43],{"href":42},"#instalando","Instalando o pgvector",[19,45,46],{},[22,47,49],{"href":48},"#esquema","Esquema do banco para RAG",[19,51,52],{},[22,53,55],{"href":54},"#gerando","Gerando e armazenando embeddings",[19,57,58],{},[22,59,61],{"href":60},"#buscando","Buscando contexto: a mágica do RAG",[19,63,64],{},[22,65,67],{"href":66},"#prompt","Prompt e geração de resposta",[19,69,70],{},[22,71,73],{"href":72},"#pipeline","Pipeline completo em Python",[19,75,76],{},[22,77,79],{"href":78},"#otimizando","Otimizando: índices e chunking",[19,81,82],{},[22,83,85],{"href":84},"#avancado","RAG avançado: reranking e filtros",[19,87,88],{},[22,89,91],{"href":90},"#avaliando","Avaliando qualidade do RAG",[19,93,94],{},[22,95,97],{"href":96},"#erros","Erros comuns",[11,99,101],{"id":100},"o-que-é-rag-e-por-que-você-precisa-o-que-e-rag","O que é RAG e por que você precisa {#o-que-e-rag}",[103,104,105,106,110,111,115,116,119],"p",{},"Um ",[107,108,109],"strong",{},"LLM"," (",[112,113,114],"em",{},"Large Language Model",") é um modelo de IA treinado para entender\ne gerar texto — o ChatGPT é o exemplo mais conhecido. O problema do LLM\npuro: ele só sabe o que aprendeu no treino. Pergunte ao GPT-4 qual a política\nde reembolso da ",[107,117,118],{},"sua"," empresa e ele vai inventar algo plausível — porque\nnão tem como saber.",[103,121,122,110,125,128],{},[107,123,124],{},"RAG",[112,126,127],{},"Retrieval-Augmented Generation",", algo como \"geração aumentada por\nrecuperação\") resolve isso. A ideia é simples:",[16,130,131,134,137,140],{},[19,132,133],{},"Você guarda seus documentos em um banco preparado para busca semântica",[19,135,136],{},"Quando o usuário pergunta algo, o sistema busca os trechos relevantes",[19,138,139],{},"Esses trechos são injetados no prompt enviado ao LLM, como \"contexto\"",[19,141,142,143,146],{},"O LLM responde ",[107,144,145],{},"olhando"," para o contexto, em vez de adivinhar",[103,148,149,152],{},[107,150,151],{},"Analogia",": imagina que o LLM é um médico generalista muito bem formado.\nSem RAG, ele atende no escuro — só com o que lembra da faculdade. Com RAG,\nvocê entrega o prontuário do paciente antes da consulta. Ele continua sendo\no mesmo médico, mas agora responde com base naquele caso específico.",[103,154,155,156,159,160,163],{},"RAG é o que transforma um LLM genérico em um assistente que entende do\n",[107,157,158],{},"seu"," negócio. E a boa notícia: você não precisa de um banco de dados\nnovo. Se já usa PostgreSQL, basta instalar a extensão ",[107,161,162],{},"pgvector",".",[11,165,167],{"id":166},"por-que-postgresql-e-pgvector-por-que-pgvector","Por que PostgreSQL e pgvector {#por-que-pgvector}",[103,169,105,170,173,174,177],{},[107,171,172],{},"vector database"," (banco vetorial) é um banco otimizado para armazenar\ne buscar vetores numéricos — que é como representamos o \"sentido\" dos\ntextos. Existem bancos dedicados (Pinecone, Qdrant, Weaviate, Milvus), mas\npara a maioria dos projetos, ",[107,175,176],{},"usar o PostgreSQL que você já tem"," é a\nescolha mais inteligente.",[103,179,180,182,183,187],{},[107,181,162],{}," é uma extensão open source do PostgreSQL que adiciona o tipo\n",[184,185,186],"code",{},"vector"," e operações de busca por similaridade. É mantida pela comunidade,\nsuportada por AWS RDS, Google Cloud SQL e Supabase, e roda em qualquer\nPostgres 13+.",[189,190,192],"h3",{"id":191},"vantagens-do-pgvector-sobre-bancos-dedicados","Vantagens do pgvector sobre bancos dedicados",[194,195,196,211],"table",{},[197,198,199],"thead",{},[200,201,202,206,208],"tr",{},[203,204,205],"th",{},"Critério",[203,207,162],{},[203,209,210],{},"Banco vetorial dedicado",[212,213,214,226,237,248,259,270,281,292],"tbody",{},[200,215,216,220,223],{},[217,218,219],"td",{},"Infraestrutura nova",[217,221,222],{},"Não (reaproveita PG)",[217,224,225],{},"Sim",[200,227,228,231,234],{},[217,229,230],{},"Transações ACID",[217,232,233],{},"Sim (nativo do PG)",[217,235,236],{},"Geralmente não",[200,238,239,242,245],{},[217,240,241],{},"Join com dados relacionais",[217,243,244],{},"Sim, na mesma query",[217,246,247],{},"Não, precisa sincronizar",[200,249,250,253,256],{},[217,251,252],{},"Backup e replicação",[217,254,255],{},"Igual ao seu PG atual",[217,257,258],{},"Configuração separada",[200,260,261,264,267],{},[217,262,263],{},"Custo adicional",[217,265,266],{},"$0 (extensão livre)",[217,268,269],{},"$20–200+\u002Fmês",[200,271,272,275,278],{},[217,273,274],{},"Escala para 100M+ vetores",[217,276,277],{},"So-so, precisa tuning",[217,279,280],{},"Sim, nativo",[200,282,283,286,289],{},[217,284,285],{},"Curva de aprendizado",[217,287,288],{},"Baixa (sabe SQL? sabe usar)",[217,290,291],{},"Nova stack",[200,293,294,297,300],{},[217,295,296],{},"Tipos de índice",[217,298,299],{},"IVFFlat, HNSW",[217,301,302],{},"Vários",[103,304,305,308],{},[107,306,307],{},"Quando NÃO usar pgvector",": se você tem 50M+ vetores e precisa de\nlatência abaixo de 50ms em toda busca, um banco dedicado (Qdrant, Milvus)\ntende a performar melhor. Para 99% dos projetos corporativos, pgvector\nresolve com folga.",[11,310,312],{"id":311},"conceitos-embeddings-e-similaridade-conceitos","Conceitos: embeddings e similaridade {#conceitos}",[103,314,315],{},"Dois conceitos-chave. Vamos com calma.",[189,317,319],{"id":318},"embeddings","Embeddings",[103,321,105,322,325,326,329],{},[107,323,324],{},"embedding"," é uma lista de números (um vetor) que representa o sentido\nde um texto. Textos com significado parecido têm vetores parecidos. É\ngerado por um modelo treinado para isso — não pelo LLM que gera as respostas,\nmas por um modelo especializado em embeddings (ex: ",[184,327,328],{},"text-embedding-3-small","\nda OpenAI).",[103,331,332,334],{},[107,333,151],{},": pense em embeddings como coordenadas GPS do sentido. \"Cachorro\nlate\" e \"O perro ladra\" ficam em pontos próximos do mapa, porque querem dizer\na mesma coisa. \"Cachorro late\" e \"Bicicleta vermelha\" ficam longe um do outro.",[103,336,337],{},"Um embedding típico tem 384, 768, 1536 ou 3072 dimensões (depende do modelo).\nQuanto mais dimensões, mais nuance captura — e mais espaço ocupa.",[189,339,341],{"id":340},"similaridade-distância","Similaridade (distância)",[103,343,344],{},"Para encontrar documentos relevantes, comparamos vetores. As duas métricas\nmais comuns:",[346,347,348,357],"ul",{},[19,349,350,110,353,356],{},[107,351,352],{},"Cosseno",[184,354,355],{},"\u003C=>"," no pgvector): mede o ângulo entre vetores. Ideal para\ntexto, porque ignora o tamanho (quantas palavras) e olha só a direção\n(sentido).",[19,358,359,110,362,365],{},[107,360,361],{},"L2 \u002F Euclidiana",[184,363,364],{},"\u003C->"," no pgvector): mede a distância reta. Funciona,\nmas é menos comum em RAG.",[103,367,368],{},"Valores menores = mais similares (no cosseno, 0 = idêntico, 2 = oposto).",[370,371,376],"pre",{"className":372,"code":373,"language":374,"meta":375,"style":375},"language-sql shiki shiki-themes github-light github-dark","-- Exemplo: buscar os 5 documentos mais parecidos com um vetor de consulta\nSELECT id, conteudo\nFROM documentos\nORDER BY embedding \u003C=> '[0.1, 0.2, ...]'  -- vetor da pergunta\nLIMIT 5;\n","sql","",[184,377,378,386,392,398,404],{"__ignoreMap":375},[379,380,383],"span",{"class":381,"line":382},"line",1,[379,384,385],{},"-- Exemplo: buscar os 5 documentos mais parecidos com um vetor de consulta\n",[379,387,389],{"class":381,"line":388},2,[379,390,391],{},"SELECT id, conteudo\n",[379,393,395],{"class":381,"line":394},3,[379,396,397],{},"FROM documentos\n",[379,399,401],{"class":381,"line":400},4,[379,402,403],{},"ORDER BY embedding \u003C=> '[0.1, 0.2, ...]'  -- vetor da pergunta\n",[379,405,407],{"class":381,"line":406},5,[379,408,409],{},"LIMIT 5;\n",[103,411,412,413,415,416,419],{},"Comentário: o operador ",[184,414,355],{}," calcula a distância-cosseno entre cada\nembedding da tabela e o vetor da pergunta; o ",[184,417,418],{},"ORDER BY ... LIMIT 5"," pega os\n5 mais próximos. Simples assim — é só SQL.",[11,421,423],{"id":422},"instalando-o-pgvector-instalando","Instalando o pgvector {#instalando}",[189,425,427],{"id":426},"em-docker-recomendado-para-desenvolvimento","Em Docker (recomendado para desenvolvimento)",[370,429,433],{"className":430,"code":431,"language":432,"meta":375,"style":375},"language-bash shiki shiki-themes github-light github-dark","# Imagem oficial do Postgres já com pgvector instalado\ndocker run -d \\\n  --name pgvector-db \\\n  -e POSTGRES_PASSWORD=suasenha \\\n  -e POSTGRES_DB=ia \\\n  -p 5432:5432 \\\n  pgvector\u002Fpgvector:pg16\n","bash",[184,434,435,441,458,468,478,487,498],{"__ignoreMap":375},[379,436,437],{"class":381,"line":382},[379,438,440],{"class":439},"sJ8bj","# Imagem oficial do Postgres já com pgvector instalado\n",[379,442,443,447,451,455],{"class":381,"line":388},[379,444,446],{"class":445},"sScJk","docker",[379,448,450],{"class":449},"sZZnC"," run",[379,452,454],{"class":453},"sj4cs"," -d",[379,456,457],{"class":453}," \\\n",[379,459,460,463,466],{"class":381,"line":394},[379,461,462],{"class":453},"  --name",[379,464,465],{"class":449}," pgvector-db",[379,467,457],{"class":453},[379,469,470,473,476],{"class":381,"line":400},[379,471,472],{"class":453},"  -e",[379,474,475],{"class":449}," POSTGRES_PASSWORD=suasenha",[379,477,457],{"class":453},[379,479,480,482,485],{"class":381,"line":406},[379,481,472],{"class":453},[379,483,484],{"class":449}," POSTGRES_DB=ia",[379,486,457],{"class":453},[379,488,490,493,496],{"class":381,"line":489},6,[379,491,492],{"class":453},"  -p",[379,494,495],{"class":449}," 5432:5432",[379,497,457],{"class":453},[379,499,501],{"class":381,"line":500},7,[379,502,503],{"class":449},"  pgvector\u002Fpgvector:pg16\n",[103,505,506,507,510],{},"Comentário: a imagem ",[184,508,509],{},"pgvector\u002Fpgvector:pg16"," é Postgres 16 com a extensão\npré-instalada. Sem compilação, sem dor de cabeça.",[189,512,514],{"id":513},"em-aws-rds","Em AWS RDS",[103,516,517],{},"A partir do Postgres 13, o pgvector já vem pré-instalado em RDS — basta\nativar:",[370,519,521],{"className":372,"code":520,"language":374,"meta":375,"style":375},"CREATE EXTENSION IF NOT EXISTS vector;\n",[184,522,523],{"__ignoreMap":375},[379,524,525],{"class":381,"line":382},[379,526,520],{},[189,528,530],{"id":529},"em-postgres-self-hosted","Em Postgres self-hosted",[370,532,534],{"className":430,"code":533,"language":432,"meta":375,"style":375},"# Compilar e instalar a extensão\ncd \u002Ftmp\ngit clone --branch v0.7.4 https:\u002F\u002Fgithub.com\u002Fpgvector\u002Fpgvector.git\ncd pgvector\nmake\nmake install  # pode precisar de sudo\n",[184,535,536,541,549,566,573,578],{"__ignoreMap":375},[379,537,538],{"class":381,"line":382},[379,539,540],{"class":439},"# Compilar e instalar a extensão\n",[379,542,543,546],{"class":381,"line":388},[379,544,545],{"class":453},"cd",[379,547,548],{"class":449}," \u002Ftmp\n",[379,550,551,554,557,560,563],{"class":381,"line":394},[379,552,553],{"class":445},"git",[379,555,556],{"class":449}," clone",[379,558,559],{"class":453}," --branch",[379,561,562],{"class":449}," v0.7.4",[379,564,565],{"class":449}," https:\u002F\u002Fgithub.com\u002Fpgvector\u002Fpgvector.git\n",[379,567,568,570],{"class":381,"line":400},[379,569,545],{"class":453},[379,571,572],{"class":449}," pgvector\n",[379,574,575],{"class":381,"line":406},[379,576,577],{"class":445},"make\n",[379,579,580,583,586],{"class":381,"line":489},[379,581,582],{"class":445},"make",[379,584,585],{"class":449}," install",[379,587,588],{"class":439},"  # pode precisar de sudo\n",[103,590,591],{},"Depois, no banco:",[370,593,595],{"className":372,"code":594,"language":374,"meta":375,"style":375},"CREATE EXTENSION IF NOT EXISTS vector;\nSELECT * FROM pg_extension WHERE extname = 'vector';\n",[184,596,597,601],{"__ignoreMap":375},[379,598,599],{"class":381,"line":382},[379,600,520],{},[379,602,603],{"class":381,"line":388},[379,604,605],{},"SELECT * FROM pg_extension WHERE extname = 'vector';\n",[11,607,609],{"id":608},"esquema-do-banco-para-rag-esquema","Esquema do banco para RAG {#esquema}",[103,611,612],{},"A estrutura típica de uma tabela de RAG:",[370,614,616],{"className":372,"code":615,"language":374,"meta":375,"style":375},"CREATE TABLE documentos (\n    id          BIGSERIAL PRIMARY KEY,\n    -- Origem do documento: arquivo, URL, sistema\n    fonte       TEXT NOT NULL,\n    -- Metadados para filtro (JSONB é flexível)\n    -- ex: {\"tipo\": \"manual\", \"produto\": \"X100\", \"data\": \"2026-01\"}\n    metadados   JSONB DEFAULT '{}'::jsonb,\n    -- Texto original do chunk (pedaço do documento)\n    conteudo    TEXT NOT NULL,\n    -- Vetor (dimensão depende do modelo de embedding)\n    embedding   vector(1536) NOT NULL,\n    -- Quando foi inserido\u002Fatualizado (para re-indexar só o que mudou)\n    criado_em   TIMESTAMPTZ DEFAULT now()\n);\n\n-- Índice para filtrar por metadados (ex: só documentos de um produto)\nCREATE INDEX idx_documentos_metadados\n    ON documentos USING gin (metadados);\n\n-- Índice vetorial - explicado adiante em \"Otimizando\"\nCREATE INDEX idx_documentos_embedding\n    ON documentos USING hnsw (embedding vector_cosine_ops);\n",[184,617,618,623,628,633,638,643,648,653,659,665,671,677,683,689,695,702,708,714,720,725,731,737],{"__ignoreMap":375},[379,619,620],{"class":381,"line":382},[379,621,622],{},"CREATE TABLE documentos (\n",[379,624,625],{"class":381,"line":388},[379,626,627],{},"    id          BIGSERIAL PRIMARY KEY,\n",[379,629,630],{"class":381,"line":394},[379,631,632],{},"    -- Origem do documento: arquivo, URL, sistema\n",[379,634,635],{"class":381,"line":400},[379,636,637],{},"    fonte       TEXT NOT NULL,\n",[379,639,640],{"class":381,"line":406},[379,641,642],{},"    -- Metadados para filtro (JSONB é flexível)\n",[379,644,645],{"class":381,"line":489},[379,646,647],{},"    -- ex: {\"tipo\": \"manual\", \"produto\": \"X100\", \"data\": \"2026-01\"}\n",[379,649,650],{"class":381,"line":500},[379,651,652],{},"    metadados   JSONB DEFAULT '{}'::jsonb,\n",[379,654,656],{"class":381,"line":655},8,[379,657,658],{},"    -- Texto original do chunk (pedaço do documento)\n",[379,660,662],{"class":381,"line":661},9,[379,663,664],{},"    conteudo    TEXT NOT NULL,\n",[379,666,668],{"class":381,"line":667},10,[379,669,670],{},"    -- Vetor (dimensão depende do modelo de embedding)\n",[379,672,674],{"class":381,"line":673},11,[379,675,676],{},"    embedding   vector(1536) NOT NULL,\n",[379,678,680],{"class":381,"line":679},12,[379,681,682],{},"    -- Quando foi inserido\u002Fatualizado (para re-indexar só o que mudou)\n",[379,684,686],{"class":381,"line":685},13,[379,687,688],{},"    criado_em   TIMESTAMPTZ DEFAULT now()\n",[379,690,692],{"class":381,"line":691},14,[379,693,694],{},");\n",[379,696,698],{"class":381,"line":697},15,[379,699,701],{"emptyLinePlaceholder":700},true,"\n",[379,703,705],{"class":381,"line":704},16,[379,706,707],{},"-- Índice para filtrar por metadados (ex: só documentos de um produto)\n",[379,709,711],{"class":381,"line":710},17,[379,712,713],{},"CREATE INDEX idx_documentos_metadados\n",[379,715,717],{"class":381,"line":716},18,[379,718,719],{},"    ON documentos USING gin (metadados);\n",[379,721,723],{"class":381,"line":722},19,[379,724,701],{"emptyLinePlaceholder":700},[379,726,728],{"class":381,"line":727},20,[379,729,730],{},"-- Índice vetorial - explicado adiante em \"Otimizando\"\n",[379,732,734],{"class":381,"line":733},21,[379,735,736],{},"CREATE INDEX idx_documentos_embedding\n",[379,738,740],{"class":381,"line":739},22,[379,741,742],{},"    ON documentos USING hnsw (embedding vector_cosine_ops);\n",[103,744,745,746,749],{},"Cada linha é um ",[107,747,748],{},"chunk"," (pedaço) do documento original. O motivo de\nquebrar em pedaços é que o LLM tem limite de contexto e fica confuso com\ntextos muito longos. Em vez de armazenar o PDF inteiro, armazenamos\npedaços de ~1000 caracteres cada, com seus embeddings.",[11,751,753],{"id":752},"gerando-e-armazenando-embeddings-gerando","Gerando e armazenando embeddings {#gerando}",[189,755,757],{"id":756},"em-python-com-a-biblioteca-oficial-da-openai","Em Python, com a biblioteca oficial da OpenAI",[370,759,763],{"className":760,"code":761,"language":762,"meta":375,"style":375},"language-python shiki shiki-themes github-light github-dark","# pip install openai psycopg[binary] tiktoken\n\nfrom openai import OpenAI\nimport psycopg\n\nclient = OpenAI()  # lê OPENAI_API_KEY do ambiente\n\ndef gerar_embedding(texto: str) -> list[float]:\n    \"\"\"Gera embedding do texto usando o modelo small da OpenAI.\n    Retorna um vetor de 1536 dimensões.\"\"\"\n    resposta = client.embeddings.create(\n        model=\"text-embedding-3-small\",\n        input=texto,\n    )\n    return resposta.data[0].embedding\n\ndef inserir_documento(conn, fonte: str, conteudo: str, metadados: dict):\n    \"\"\"Insere um chunk no banco com seu embedding.\"\"\"\n    embedding = gerar_embedding(conteudo)\n    with conn.cursor() as cur:\n        cur.execute(\n            \"\"\"\n            INSERT INTO documentos (fonte, metadados, conteudo, embedding)\n            VALUES (%s, %s, %s, %s)\n            \"\"\",\n            (fonte, metadados, conteudo, embedding),\n        )\n    conn.commit()\n","python",[184,764,765,770,774,779,784,788,793,797,802,807,812,817,822,827,832,837,841,846,851,856,861,866,871,877,883,889,895,901],{"__ignoreMap":375},[379,766,767],{"class":381,"line":382},[379,768,769],{},"# pip install openai psycopg[binary] tiktoken\n",[379,771,772],{"class":381,"line":388},[379,773,701],{"emptyLinePlaceholder":700},[379,775,776],{"class":381,"line":394},[379,777,778],{},"from openai import OpenAI\n",[379,780,781],{"class":381,"line":400},[379,782,783],{},"import psycopg\n",[379,785,786],{"class":381,"line":406},[379,787,701],{"emptyLinePlaceholder":700},[379,789,790],{"class":381,"line":489},[379,791,792],{},"client = OpenAI()  # lê OPENAI_API_KEY do ambiente\n",[379,794,795],{"class":381,"line":500},[379,796,701],{"emptyLinePlaceholder":700},[379,798,799],{"class":381,"line":655},[379,800,801],{},"def gerar_embedding(texto: str) -> list[float]:\n",[379,803,804],{"class":381,"line":661},[379,805,806],{},"    \"\"\"Gera embedding do texto usando o modelo small da OpenAI.\n",[379,808,809],{"class":381,"line":667},[379,810,811],{},"    Retorna um vetor de 1536 dimensões.\"\"\"\n",[379,813,814],{"class":381,"line":673},[379,815,816],{},"    resposta = client.embeddings.create(\n",[379,818,819],{"class":381,"line":679},[379,820,821],{},"        model=\"text-embedding-3-small\",\n",[379,823,824],{"class":381,"line":685},[379,825,826],{},"        input=texto,\n",[379,828,829],{"class":381,"line":691},[379,830,831],{},"    )\n",[379,833,834],{"class":381,"line":697},[379,835,836],{},"    return resposta.data[0].embedding\n",[379,838,839],{"class":381,"line":704},[379,840,701],{"emptyLinePlaceholder":700},[379,842,843],{"class":381,"line":710},[379,844,845],{},"def inserir_documento(conn, fonte: str, conteudo: str, metadados: dict):\n",[379,847,848],{"class":381,"line":716},[379,849,850],{},"    \"\"\"Insere um chunk no banco com seu embedding.\"\"\"\n",[379,852,853],{"class":381,"line":722},[379,854,855],{},"    embedding = gerar_embedding(conteudo)\n",[379,857,858],{"class":381,"line":727},[379,859,860],{},"    with conn.cursor() as cur:\n",[379,862,863],{"class":381,"line":733},[379,864,865],{},"        cur.execute(\n",[379,867,868],{"class":381,"line":739},[379,869,870],{},"            \"\"\"\n",[379,872,874],{"class":381,"line":873},23,[379,875,876],{},"            INSERT INTO documentos (fonte, metadados, conteudo, embedding)\n",[379,878,880],{"class":381,"line":879},24,[379,881,882],{},"            VALUES (%s, %s, %s, %s)\n",[379,884,886],{"class":381,"line":885},25,[379,887,888],{},"            \"\"\",\n",[379,890,892],{"class":381,"line":891},26,[379,893,894],{},"            (fonte, metadados, conteudo, embedding),\n",[379,896,898],{"class":381,"line":897},27,[379,899,900],{},"        )\n",[379,902,904],{"class":381,"line":903},28,[379,905,906],{},"    conn.commit()\n",[103,908,909,910,913,914,917],{},"Comentários: ",[184,911,912],{},"gerar_embedding"," chama a API da OpenAI, que devolve o vetor.\n",[184,915,916],{},"inserir_documento"," salva no Postgres junto com metadados — esses metadados\npermitem filtrar a busca depois (ex: só buscar em manuais do produto X).",[189,919,921],{"id":920},"custo-da-geração-de-embeddings","Custo da geração de embeddings",[103,923,924,925,927,928,931],{},"Modelo ",[184,926,328],{},": ",[107,929,930],{},"$0,02 por 1 milhão de tokens",". Para\numa base de 10.000 documentos de ~1.000 tokens cada:",[346,933,934],{},[19,935,936,937],{},"10M tokens × $0,02\u002F1M = ",[107,938,939],{},"$0,20 USD",[103,941,942],{},"Ou seja, indexar uma base inteira custa menos que um café. Por isso, sempre\nuse a OpenAI (ou Cohere) para embeddings, mesmo que seu LLM de resposta seja\nopen source. Não vale a pena hospedar seu próprio modelo de embedding até\nter volume enorme.",[189,944,946],{"id":945},"alternativas-de-modelo-de-embedding","Alternativas de modelo de embedding",[194,948,949,965],{},[197,950,951],{},[200,952,953,956,959,962],{},[203,954,955],{},"Modelo",[203,957,958],{},"Dimensão",[203,960,961],{},"Custo (1M tok)",[203,963,964],{},"Notas",[212,966,967,981,995,1009,1022,1035],{},[200,968,969,972,975,978],{},[217,970,971],{},"OpenAI text-embedding-3-small",[217,973,974],{},"1536",[217,976,977],{},"$0,02",[217,979,980],{},"Padrão da indústria",[200,982,983,986,989,992],{},[217,984,985],{},"OpenAI text-embedding-3-large",[217,987,988],{},"3072",[217,990,991],{},"$0,13",[217,993,994],{},"Mais qualidade",[200,996,997,1000,1003,1006],{},[217,998,999],{},"Cohere embed-multilingual-v3",[217,1001,1002],{},"1024",[217,1004,1005],{},"$0,10",[217,1007,1008],{},"Muito bom em PT-BR",[200,1010,1011,1014,1016,1019],{},[217,1012,1013],{},"Voyage voyage-3",[217,1015,1002],{},[217,1017,1018],{},"$0,12",[217,1020,1021],{},"Estado da arte em RAG",[200,1023,1024,1027,1029,1032],{},[217,1025,1026],{},"BGE-large-zh-v1.5 (open source)",[217,1028,1002],{},[217,1030,1031],{},"$0 (self-host)",[217,1033,1034],{},"Chines\u002Finglês",[200,1036,1037,1040,1043,1045],{},[217,1038,1039],{},"SBERT multilingual (open source)",[217,1041,1042],{},"384",[217,1044,1031],{},[217,1046,1047],{},"Leve, menos preciso",[11,1049,1051],{"id":1050},"buscando-contexto-a-mágica-do-rag-buscando","Buscando contexto: a mágica do RAG {#buscando}",[103,1053,1054],{},"A função central do RAG: dada a pergunta, buscar chunks relevantes.",[370,1056,1058],{"className":760,"code":1057,"language":762,"meta":375,"style":375},"def buscar_contexto(conn, pergunta: str, top_k: int = 4,\n                    filtro_metadados: dict | None = None) -> list[dict]:\n    \"\"\"Busca os top_k chunks mais parecidos com a pergunta.\n    Opcionalmente filtra por metadados (ex: {\"produto\": \"X100\"}).\"\"\"\n\n    # 1. Gerar embedding da pergunta (mesmo modelo dos documentos!)\n    embedding_pergunta = gerar_embedding(pergunta)\n\n    # 2. Construir a query SQL\n    #    O operador \u003C=> é a distância-cosseno (pgvector)\n    #    Quanto menor, mais parecido\n    sql = \"\"\"\n        SELECT id, fonte, conteudo, metadados,\n               embedding \u003C=> %s::vector AS distancia\n        FROM documentos\n    \"\"\"\n    params = [embedding_pergunta]\n\n    # 3. Aplicar filtro de metadados se vier\n    if filtro_metadados:\n        clausulas = []\n        for chave, valor in filtro_metadados.items():\n            clausulas.append(\"metadados @> %s::jsonb\")\n            params.append({chave: valor})\n        sql += \" WHERE \" + \" AND \".join(clausulas)\n\n    # 4. Ordenar por distância (mais parecido primeiro) e limitar\n    sql += \" ORDER BY distancia LIMIT %s\"\n    params.append(top_k)\n\n    with conn.cursor() as cur:\n        cur.execute(sql, params)\n        colunas = [desc[0] for desc in cur.description]\n        resultados = [dict(zip(colunas, row)) for row in cur.fetchall()]\n\n    return resultados\n",[184,1059,1060,1065,1070,1075,1080,1084,1089,1094,1098,1103,1108,1113,1118,1123,1128,1133,1138,1143,1147,1152,1157,1162,1167,1172,1177,1182,1186,1191,1196,1202,1207,1212,1218,1224,1230,1235],{"__ignoreMap":375},[379,1061,1062],{"class":381,"line":382},[379,1063,1064],{},"def buscar_contexto(conn, pergunta: str, top_k: int = 4,\n",[379,1066,1067],{"class":381,"line":388},[379,1068,1069],{},"                    filtro_metadados: dict | None = None) -> list[dict]:\n",[379,1071,1072],{"class":381,"line":394},[379,1073,1074],{},"    \"\"\"Busca os top_k chunks mais parecidos com a pergunta.\n",[379,1076,1077],{"class":381,"line":400},[379,1078,1079],{},"    Opcionalmente filtra por metadados (ex: {\"produto\": \"X100\"}).\"\"\"\n",[379,1081,1082],{"class":381,"line":406},[379,1083,701],{"emptyLinePlaceholder":700},[379,1085,1086],{"class":381,"line":489},[379,1087,1088],{},"    # 1. Gerar embedding da pergunta (mesmo modelo dos documentos!)\n",[379,1090,1091],{"class":381,"line":500},[379,1092,1093],{},"    embedding_pergunta = gerar_embedding(pergunta)\n",[379,1095,1096],{"class":381,"line":655},[379,1097,701],{"emptyLinePlaceholder":700},[379,1099,1100],{"class":381,"line":661},[379,1101,1102],{},"    # 2. Construir a query SQL\n",[379,1104,1105],{"class":381,"line":667},[379,1106,1107],{},"    #    O operador \u003C=> é a distância-cosseno (pgvector)\n",[379,1109,1110],{"class":381,"line":673},[379,1111,1112],{},"    #    Quanto menor, mais parecido\n",[379,1114,1115],{"class":381,"line":679},[379,1116,1117],{},"    sql = \"\"\"\n",[379,1119,1120],{"class":381,"line":685},[379,1121,1122],{},"        SELECT id, fonte, conteudo, metadados,\n",[379,1124,1125],{"class":381,"line":691},[379,1126,1127],{},"               embedding \u003C=> %s::vector AS distancia\n",[379,1129,1130],{"class":381,"line":697},[379,1131,1132],{},"        FROM documentos\n",[379,1134,1135],{"class":381,"line":704},[379,1136,1137],{},"    \"\"\"\n",[379,1139,1140],{"class":381,"line":710},[379,1141,1142],{},"    params = [embedding_pergunta]\n",[379,1144,1145],{"class":381,"line":716},[379,1146,701],{"emptyLinePlaceholder":700},[379,1148,1149],{"class":381,"line":722},[379,1150,1151],{},"    # 3. Aplicar filtro de metadados se vier\n",[379,1153,1154],{"class":381,"line":727},[379,1155,1156],{},"    if filtro_metadados:\n",[379,1158,1159],{"class":381,"line":733},[379,1160,1161],{},"        clausulas = []\n",[379,1163,1164],{"class":381,"line":739},[379,1165,1166],{},"        for chave, valor in filtro_metadados.items():\n",[379,1168,1169],{"class":381,"line":873},[379,1170,1171],{},"            clausulas.append(\"metadados @> %s::jsonb\")\n",[379,1173,1174],{"class":381,"line":879},[379,1175,1176],{},"            params.append({chave: valor})\n",[379,1178,1179],{"class":381,"line":885},[379,1180,1181],{},"        sql += \" WHERE \" + \" AND \".join(clausulas)\n",[379,1183,1184],{"class":381,"line":891},[379,1185,701],{"emptyLinePlaceholder":700},[379,1187,1188],{"class":381,"line":897},[379,1189,1190],{},"    # 4. Ordenar por distância (mais parecido primeiro) e limitar\n",[379,1192,1193],{"class":381,"line":903},[379,1194,1195],{},"    sql += \" ORDER BY distancia LIMIT %s\"\n",[379,1197,1199],{"class":381,"line":1198},29,[379,1200,1201],{},"    params.append(top_k)\n",[379,1203,1205],{"class":381,"line":1204},30,[379,1206,701],{"emptyLinePlaceholder":700},[379,1208,1210],{"class":381,"line":1209},31,[379,1211,860],{},[379,1213,1215],{"class":381,"line":1214},32,[379,1216,1217],{},"        cur.execute(sql, params)\n",[379,1219,1221],{"class":381,"line":1220},33,[379,1222,1223],{},"        colunas = [desc[0] for desc in cur.description]\n",[379,1225,1227],{"class":381,"line":1226},34,[379,1228,1229],{},"        resultados = [dict(zip(colunas, row)) for row in cur.fetchall()]\n",[379,1231,1233],{"class":381,"line":1232},35,[379,1234,701],{"emptyLinePlaceholder":700},[379,1236,1238],{"class":381,"line":1237},36,[379,1239,1240],{},"    return resultados\n",[103,1242,1243,1244,1246],{},"Cada passo numerado explica o que está acontecendo: gera embedding da\npergunta, monta a query, aplica filtro opcional, ordena por distância. O\npulo do gato é o operador ",[184,1245,355],{}," que o pgvector adiciona ao SQL.",[189,1248,1250],{"id":1249},"por-que-o-mesmo-modelo-de-embedding","Por que o mesmo modelo de embedding?",[103,1252,1253,1254,1256,1257,1260,1261],{},"Embeddings só são comparáveis se vierem do mesmo modelo. Se você gerou os\ndocumentos com ",[184,1255,328],{}," e a pergunta com ",[184,1258,1259],{},"text-embedding-3-large",",\na busca fica sem sentido — são vetores em \"mapas\" diferentes. ",[107,1262,1263],{},"Sempre use o\nmesmo modelo para indexar e para consultar.",[11,1265,1267],{"id":1266},"prompt-e-geração-de-resposta-prompt","Prompt e geração de resposta {#prompt}",[103,1269,1270],{},"Agora que temos contexto, montamos o prompt e chamamos o LLM:",[370,1272,1274],{"className":760,"code":1273,"language":762,"meta":375,"style":375},"def responder(pergunta: str, contexto: list[dict]) -> str:\n    \"\"\"Gera resposta do LLM usando o contexto recuperado.\"\"\"\n    contexto_texto = \"\\n\\n---\\n\\n\".join(\n        f\"[Fonte: {c['fonte']}]\\n{c['conteudo']}\" for c in contexto\n    )\n\n    prompt = f\"\"\"Você é um assistente que responde estritamente com base\nno contexto fornecido. Se a informação não estiver no contexto, diga\nexplicitamente \"Não tenho essa informação na base de conhecimento.\"\n\nContexto:\n{contexto_texto}\n\nPergunta: {pergunta}\n\nResposta:\"\"\"\n\n    resposta = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        temperature=0.2,  # baixa: respostas mais determinísticas\n        messages=[{\"role\": \"user\", \"content\": prompt}],\n    )\n    return resposta.choices[0].message.content\n",[184,1275,1276,1281,1286,1291,1296,1300,1304,1309,1314,1319,1323,1328,1333,1337,1342,1346,1351,1355,1360,1365,1370,1375,1379],{"__ignoreMap":375},[379,1277,1278],{"class":381,"line":382},[379,1279,1280],{},"def responder(pergunta: str, contexto: list[dict]) -> str:\n",[379,1282,1283],{"class":381,"line":388},[379,1284,1285],{},"    \"\"\"Gera resposta do LLM usando o contexto recuperado.\"\"\"\n",[379,1287,1288],{"class":381,"line":394},[379,1289,1290],{},"    contexto_texto = \"\\n\\n---\\n\\n\".join(\n",[379,1292,1293],{"class":381,"line":400},[379,1294,1295],{},"        f\"[Fonte: {c['fonte']}]\\n{c['conteudo']}\" for c in contexto\n",[379,1297,1298],{"class":381,"line":406},[379,1299,831],{},[379,1301,1302],{"class":381,"line":489},[379,1303,701],{"emptyLinePlaceholder":700},[379,1305,1306],{"class":381,"line":500},[379,1307,1308],{},"    prompt = f\"\"\"Você é um assistente que responde estritamente com base\n",[379,1310,1311],{"class":381,"line":655},[379,1312,1313],{},"no contexto fornecido. Se a informação não estiver no contexto, diga\n",[379,1315,1316],{"class":381,"line":661},[379,1317,1318],{},"explicitamente \"Não tenho essa informação na base de conhecimento.\"\n",[379,1320,1321],{"class":381,"line":667},[379,1322,701],{"emptyLinePlaceholder":700},[379,1324,1325],{"class":381,"line":673},[379,1326,1327],{},"Contexto:\n",[379,1329,1330],{"class":381,"line":679},[379,1331,1332],{},"{contexto_texto}\n",[379,1334,1335],{"class":381,"line":685},[379,1336,701],{"emptyLinePlaceholder":700},[379,1338,1339],{"class":381,"line":691},[379,1340,1341],{},"Pergunta: {pergunta}\n",[379,1343,1344],{"class":381,"line":697},[379,1345,701],{"emptyLinePlaceholder":700},[379,1347,1348],{"class":381,"line":704},[379,1349,1350],{},"Resposta:\"\"\"\n",[379,1352,1353],{"class":381,"line":710},[379,1354,701],{"emptyLinePlaceholder":700},[379,1356,1357],{"class":381,"line":716},[379,1358,1359],{},"    resposta = client.chat.completions.create(\n",[379,1361,1362],{"class":381,"line":722},[379,1363,1364],{},"        model=\"gpt-4o-mini\",\n",[379,1366,1367],{"class":381,"line":727},[379,1368,1369],{},"        temperature=0.2,  # baixa: respostas mais determinísticas\n",[379,1371,1372],{"class":381,"line":733},[379,1373,1374],{},"        messages=[{\"role\": \"user\", \"content\": prompt}],\n",[379,1376,1377],{"class":381,"line":739},[379,1378,831],{},[379,1380,1381],{"class":381,"line":873},[379,1382,1383],{},"    return resposta.choices[0].message.content\n",[103,1385,1386,1387,1390],{},"Comentários explicam cada parte: montamos o contexto com marcadores de\nfonte (para citar depois), o prompt diz ao LLM o que fazer, e\n",[184,1388,1389],{},"temperature=0.2"," deixa a resposta mais conservadora — em RAG, você quer\nfidelidade ao contexto, não criatividade.",[189,1392,1394],{"id":1393},"template-de-prompt-recomendado","Template de prompt recomendado",[103,1396,1397],{},"Elementos de um bom prompt de RAG:",[16,1399,1400,1406,1412,1418,1428,1434,1440],{},[19,1401,1402,1405],{},[107,1403,1404],{},"Papel",": \"Você é um assistente de atendimento...\"",[19,1407,1408,1411],{},[107,1409,1410],{},"Regra de fonte",": \"Responda só com base no contexto.\"",[19,1413,1414,1417],{},[107,1415,1416],{},"Regra de honestidade",": \"Se não souber, diga que não sabe.\"",[19,1419,1420,1423,1424,1427],{},[107,1421,1422],{},"Citação",": \"Cite a fonte entre colchetes: ",[379,1425,1426],{},"Fonte: ...","\"",[19,1429,1430,1433],{},[107,1431,1432],{},"Contexto",": o que foi recuperado do banco",[19,1435,1436,1439],{},[107,1437,1438],{},"Pergunta",": o que o usuário perguntou",[19,1441,1442,1445],{},[107,1443,1444],{},"Formato",": \"Responda em no máximo 3 parágrafos.\"",[11,1447,1449],{"id":1448},"pipeline-completo-em-python-pipeline","Pipeline completo em Python {#pipeline}",[103,1451,1452],{},"Juntando tudo:",[370,1454,1456],{"className":760,"code":1455,"language":762,"meta":375,"style":375},"# pip install openai psycopg[binary] tiktoken python-dotenv\n\nimport os\nfrom openai import OpenAI\nimport psycopg\nfrom dotenv import load_dotenv\n\nload_dotenv()\nclient = OpenAI()\n\ndef conectar():\n    return psycopg.connect(os.getenv(\"DATABASE_URL\"))\n\ndef gerar_embedding(texto: str) -> list[float]:\n    r = client.embeddings.create(model=\"text-embedding-3-small\", input=texto)\n    return r.data[0].embedding\n\ndef buscar_contexto(conn, pergunta: str, top_k: int = 4) -> list[dict]:\n    emb = gerar_embedding(pergunta)\n    with conn.cursor() as cur:\n        cur.execute(\"\"\"\n            SELECT fonte, conteudo, metadados,\n                   embedding \u003C=> %s::vector AS distancia\n            FROM documentos\n            ORDER BY distancia\n            LIMIT %s\n        \"\"\", (emb, top_k))\n        return [dict(zip([\"fonte\",\"conteudo\",\"metadados\",\"distancia\"], r))\n                for r in cur.fetchall()]\n\ndef responder(pergunta: str, contexto: list[dict]) -> str:\n    ctx = \"\\n\\n\".join(f\"[{c['fonte']}] {c['conteudo']}\" for c in contexto)\n    r = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        temperature=0.2,\n        messages=[{\"role\":\"user\",\"content\":f\"\"\"\nResponda só com base no contexto. Se não souber, diga que não tem informação.\n\nContexto:\n{ctx}\n\nPergunta: {pergunta}\n\"\"\"}])\n    return r.choices[0].message.content\n\ndef rag(pergunta: str) -> str:\n    \"\"\"Pipeline completo: pergunta -> busca -> resposta.\"\"\"\n    with conectar() as conn:\n        contexto = buscar_contexto(conn, pergunta)\n        print(f\"Recuperados {len(contexto)} chunks\")\n        for c in contexto:\n            print(f\"  - {c['fonte']} (distância: {c['distancia']:.3f})\")\n        return responder(pergunta, contexto)\n\n# Uso\nif __name__ == \"__main__\":\n    print(rag(\"Qual a política de reembolso do produto X100?\"))\n",[184,1457,1458,1463,1467,1472,1476,1480,1485,1489,1494,1499,1503,1508,1513,1517,1521,1526,1531,1535,1540,1545,1549,1554,1559,1564,1569,1574,1579,1584,1589,1594,1598,1602,1607,1612,1616,1621,1626,1632,1637,1642,1648,1653,1658,1664,1670,1675,1681,1687,1693,1699,1705,1711,1717,1723,1728,1734,1740],{"__ignoreMap":375},[379,1459,1460],{"class":381,"line":382},[379,1461,1462],{},"# pip install openai psycopg[binary] tiktoken python-dotenv\n",[379,1464,1465],{"class":381,"line":388},[379,1466,701],{"emptyLinePlaceholder":700},[379,1468,1469],{"class":381,"line":394},[379,1470,1471],{},"import os\n",[379,1473,1474],{"class":381,"line":400},[379,1475,778],{},[379,1477,1478],{"class":381,"line":406},[379,1479,783],{},[379,1481,1482],{"class":381,"line":489},[379,1483,1484],{},"from dotenv import load_dotenv\n",[379,1486,1487],{"class":381,"line":500},[379,1488,701],{"emptyLinePlaceholder":700},[379,1490,1491],{"class":381,"line":655},[379,1492,1493],{},"load_dotenv()\n",[379,1495,1496],{"class":381,"line":661},[379,1497,1498],{},"client = OpenAI()\n",[379,1500,1501],{"class":381,"line":667},[379,1502,701],{"emptyLinePlaceholder":700},[379,1504,1505],{"class":381,"line":673},[379,1506,1507],{},"def conectar():\n",[379,1509,1510],{"class":381,"line":679},[379,1511,1512],{},"    return psycopg.connect(os.getenv(\"DATABASE_URL\"))\n",[379,1514,1515],{"class":381,"line":685},[379,1516,701],{"emptyLinePlaceholder":700},[379,1518,1519],{"class":381,"line":691},[379,1520,801],{},[379,1522,1523],{"class":381,"line":697},[379,1524,1525],{},"    r = client.embeddings.create(model=\"text-embedding-3-small\", input=texto)\n",[379,1527,1528],{"class":381,"line":704},[379,1529,1530],{},"    return r.data[0].embedding\n",[379,1532,1533],{"class":381,"line":710},[379,1534,701],{"emptyLinePlaceholder":700},[379,1536,1537],{"class":381,"line":716},[379,1538,1539],{},"def buscar_contexto(conn, pergunta: str, top_k: int = 4) -> list[dict]:\n",[379,1541,1542],{"class":381,"line":722},[379,1543,1544],{},"    emb = gerar_embedding(pergunta)\n",[379,1546,1547],{"class":381,"line":727},[379,1548,860],{},[379,1550,1551],{"class":381,"line":733},[379,1552,1553],{},"        cur.execute(\"\"\"\n",[379,1555,1556],{"class":381,"line":739},[379,1557,1558],{},"            SELECT fonte, conteudo, metadados,\n",[379,1560,1561],{"class":381,"line":873},[379,1562,1563],{},"                   embedding \u003C=> %s::vector AS distancia\n",[379,1565,1566],{"class":381,"line":879},[379,1567,1568],{},"            FROM documentos\n",[379,1570,1571],{"class":381,"line":885},[379,1572,1573],{},"            ORDER BY distancia\n",[379,1575,1576],{"class":381,"line":891},[379,1577,1578],{},"            LIMIT %s\n",[379,1580,1581],{"class":381,"line":897},[379,1582,1583],{},"        \"\"\", (emb, top_k))\n",[379,1585,1586],{"class":381,"line":903},[379,1587,1588],{},"        return [dict(zip([\"fonte\",\"conteudo\",\"metadados\",\"distancia\"], r))\n",[379,1590,1591],{"class":381,"line":1198},[379,1592,1593],{},"                for r in cur.fetchall()]\n",[379,1595,1596],{"class":381,"line":1204},[379,1597,701],{"emptyLinePlaceholder":700},[379,1599,1600],{"class":381,"line":1209},[379,1601,1280],{},[379,1603,1604],{"class":381,"line":1214},[379,1605,1606],{},"    ctx = \"\\n\\n\".join(f\"[{c['fonte']}] {c['conteudo']}\" for c in contexto)\n",[379,1608,1609],{"class":381,"line":1220},[379,1610,1611],{},"    r = client.chat.completions.create(\n",[379,1613,1614],{"class":381,"line":1226},[379,1615,1364],{},[379,1617,1618],{"class":381,"line":1232},[379,1619,1620],{},"        temperature=0.2,\n",[379,1622,1623],{"class":381,"line":1237},[379,1624,1625],{},"        messages=[{\"role\":\"user\",\"content\":f\"\"\"\n",[379,1627,1629],{"class":381,"line":1628},37,[379,1630,1631],{},"Responda só com base no contexto. Se não souber, diga que não tem informação.\n",[379,1633,1635],{"class":381,"line":1634},38,[379,1636,701],{"emptyLinePlaceholder":700},[379,1638,1640],{"class":381,"line":1639},39,[379,1641,1327],{},[379,1643,1645],{"class":381,"line":1644},40,[379,1646,1647],{},"{ctx}\n",[379,1649,1651],{"class":381,"line":1650},41,[379,1652,701],{"emptyLinePlaceholder":700},[379,1654,1656],{"class":381,"line":1655},42,[379,1657,1341],{},[379,1659,1661],{"class":381,"line":1660},43,[379,1662,1663],{},"\"\"\"}])\n",[379,1665,1667],{"class":381,"line":1666},44,[379,1668,1669],{},"    return r.choices[0].message.content\n",[379,1671,1673],{"class":381,"line":1672},45,[379,1674,701],{"emptyLinePlaceholder":700},[379,1676,1678],{"class":381,"line":1677},46,[379,1679,1680],{},"def rag(pergunta: str) -> str:\n",[379,1682,1684],{"class":381,"line":1683},47,[379,1685,1686],{},"    \"\"\"Pipeline completo: pergunta -> busca -> resposta.\"\"\"\n",[379,1688,1690],{"class":381,"line":1689},48,[379,1691,1692],{},"    with conectar() as conn:\n",[379,1694,1696],{"class":381,"line":1695},49,[379,1697,1698],{},"        contexto = buscar_contexto(conn, pergunta)\n",[379,1700,1702],{"class":381,"line":1701},50,[379,1703,1704],{},"        print(f\"Recuperados {len(contexto)} chunks\")\n",[379,1706,1708],{"class":381,"line":1707},51,[379,1709,1710],{},"        for c in contexto:\n",[379,1712,1714],{"class":381,"line":1713},52,[379,1715,1716],{},"            print(f\"  - {c['fonte']} (distância: {c['distancia']:.3f})\")\n",[379,1718,1720],{"class":381,"line":1719},53,[379,1721,1722],{},"        return responder(pergunta, contexto)\n",[379,1724,1726],{"class":381,"line":1725},54,[379,1727,701],{"emptyLinePlaceholder":700},[379,1729,1731],{"class":381,"line":1730},55,[379,1732,1733],{},"# Uso\n",[379,1735,1737],{"class":381,"line":1736},56,[379,1738,1739],{},"if __name__ == \"__main__\":\n",[379,1741,1743],{"class":381,"line":1742},57,[379,1744,1745],{},"    print(rag(\"Qual a política de reembolso do produto X100?\"))\n",[103,1747,1748,1749,1752,1753,1756],{},"Comentários no topo mostram como instalar; cada função tem uma\nresponsabilidade clara. O fluxo ",[184,1750,1751],{},"rag()"," é o ponto de entrada: conecta,\nbusca contexto, gera resposta. Em produção, você troca o ",[184,1754,1755],{},"__main__"," por\num endpoint FastAPI.",[11,1758,1760],{"id":1759},"otimizando-índices-e-chunking-otimizando","Otimizando: índices e chunking {#otimizando}",[189,1762,1764],{"id":1763},"índices-vetoriais","Índices vetoriais",[103,1766,1767,1768,1771],{},"Sem índice, o pgvector calcula a distância da pergunta contra ",[107,1769,1770],{},"todas"," as\nlinhas — funciona para 10k documentos, vira lento em 100k+. Existem dois\níndices principais:",[194,1773,1774,1789],{},[197,1775,1776],{},[200,1777,1778,1780,1783,1786],{},[203,1779,14],{},[203,1781,1782],{},"Como funciona",[203,1784,1785],{},"Pró",[203,1787,1788],{},"Contra",[212,1790,1791,1813],{},[200,1792,1793,1796,1799,1802],{},[217,1794,1795],{},"IVFFlat",[217,1797,1798],{},"Agrupa vetores em listas, busca só nas listas próximas",[217,1800,1801],{},"Rápido, leve",[217,1803,1804,1805,1808,1809,1812],{},"Precisa de tuning (",[184,1806,1807],{},"lists",", ",[184,1810,1811],{},"probes",")",[200,1814,1815,1818,1821,1824],{},[217,1816,1817],{},"HNSW",[217,1819,1820],{},"Grafo hierárquico de vizinhos próximos",[217,1822,1823],{},"Qualidade alta, build rápido",[217,1825,1826],{},"Ocupa mais memória",[103,1828,1829,1832,1833,1835],{},[107,1830,1831],{},"Recomendação",": use ",[107,1834,1817],{}," para a maioria dos casos. Configuração\npadrão já funciona bem:",[370,1837,1839],{"className":372,"code":1838,"language":374,"meta":375,"style":375},"CREATE INDEX idx_documentos_embedding\n    ON documentos USING hnsw (embedding vector_cosine_ops)\n    WITH (m = 16, ef_construction = 64);\n",[184,1840,1841,1845,1850],{"__ignoreMap":375},[379,1842,1843],{"class":381,"line":382},[379,1844,736],{},[379,1846,1847],{"class":381,"line":388},[379,1848,1849],{},"    ON documentos USING hnsw (embedding vector_cosine_ops)\n",[379,1851,1852],{"class":381,"line":394},[379,1853,1854],{},"    WITH (m = 16, ef_construction = 64);\n",[103,1856,1857,1860,1861,1864,1865,1868],{},[184,1858,1859],{},"m = 16"," é o número máximo de conexões por nó (mais = mais qualidade, mais\nmemória); ",[184,1862,1863],{},"ef_construction = 64"," controla a qualidade da construção do\níndice. Para queries, ajuste ",[184,1866,1867],{},"ef_search"," (padrão 40) — mais alto, mais\nprecisão e mais lento.",[189,1870,1872],{"id":1871},"chunking-o-tamanho-do-pedaço","Chunking: o tamanho do pedaço",[103,1874,1875,1876,1879],{},"A decisão mais impactante na qualidade do RAG é ",[107,1877,1878],{},"como cortar os\ndocumentos",":",[194,1881,1882,1896],{},[197,1883,1884],{},[200,1885,1886,1889,1892,1894],{},[203,1887,1888],{},"chunk_size",[203,1890,1891],{},"Quando usar",[203,1893,1785],{},[203,1895,1788],{},[212,1897,1898,1912,1926],{},[200,1899,1900,1903,1906,1909],{},[217,1901,1902],{},"256–512",[217,1904,1905],{},"Documentos curtos, FAQ",[217,1907,1908],{},"Precisão",[217,1910,1911],{},"Perde contexto amplo",[200,1913,1914,1917,1920,1923],{},[217,1915,1916],{},"800–1200",[217,1918,1919],{},"Padrão, maioria dos casos",[217,1921,1922],{},"Equilíbrio",[217,1924,1925],{},"—",[200,1927,1928,1931,1934,1937],{},[217,1929,1930],{},"1500–3000",[217,1932,1933],{},"Documentos longos, técnicos",[217,1935,1936],{},"Contexto amplo",[217,1938,1939],{},"Pode diluir relevância",[103,1941,1942],{},"Regras práticas:",[346,1944,1945,1955,1958,1961],{},[19,1946,1947,1948,1951,1952,1954],{},"Sempre use ",[184,1949,1950],{},"chunk_overlap"," de 10–20% do ",[184,1953,1888],{}," para não cortar\nideias no meio",[19,1956,1957],{},"Para PDFs com seções, quebre por cabeçalho (H1, H2) em vez de tamanho\nfixo — preserve a estrutura",[19,1959,1960],{},"Para código, quebre por função\u002Fclasse",[19,1962,1963],{},"Para manuais, quebre por seção do sumário",[370,1965,1967],{"className":760,"code":1966,"language":762,"meta":375,"style":375},"from langchain_text_splitters import RecursiveCharacterTextSplitter\n\nsplitter = RecursiveCharacterTextSplitter(\n    chunk_size=1000,\n    chunk_overlap=200,\n    separators=[\"\\n## \", \"\\n### \", \"\\n\\n\", \"\\n\", \". \", \" \"],\n)\n# Tenta quebrar primeiro por cabeçalhos H2, depois H3,\n# depois parágrafos, depois linhas, etc. - preserva estrutura.\n",[184,1968,1969,1974,1978,1983,1988,1993,1998,2003,2008],{"__ignoreMap":375},[379,1970,1971],{"class":381,"line":382},[379,1972,1973],{},"from langchain_text_splitters import RecursiveCharacterTextSplitter\n",[379,1975,1976],{"class":381,"line":388},[379,1977,701],{"emptyLinePlaceholder":700},[379,1979,1980],{"class":381,"line":394},[379,1981,1982],{},"splitter = RecursiveCharacterTextSplitter(\n",[379,1984,1985],{"class":381,"line":400},[379,1986,1987],{},"    chunk_size=1000,\n",[379,1989,1990],{"class":381,"line":406},[379,1991,1992],{},"    chunk_overlap=200,\n",[379,1994,1995],{"class":381,"line":489},[379,1996,1997],{},"    separators=[\"\\n## \", \"\\n### \", \"\\n\\n\", \"\\n\", \". \", \" \"],\n",[379,1999,2000],{"class":381,"line":500},[379,2001,2002],{},")\n",[379,2004,2005],{"class":381,"line":655},[379,2006,2007],{},"# Tenta quebrar primeiro por cabeçalhos H2, depois H3,\n",[379,2009,2010],{"class":381,"line":661},[379,2011,2012],{},"# depois parágrafos, depois linhas, etc. - preserva estrutura.\n",[11,2014,2016],{"id":2015},"rag-avançado-reranking-e-filtros-avancado","RAG avançado: reranking e filtros {#avancado}",[189,2018,2020],{"id":2019},"reranking","Reranking",[103,2022,2023],{},"Recuperar por similaridade de embeddings é rápido, mas impreciso — às vezes\nos 4 chunks mais \"parecidos\" não são os mais úteis. Reranking resolve isso:",[16,2025,2026,2029,2032],{},[19,2027,2028],{},"Recupere 20 chunks por similaridade (barato, aproximado)",[19,2030,2031],{},"Use um modelo de reranking (ex: Cohere Rerank, BGE-reranker) para\nreordenar os 20 por relevância real",[19,2033,2034],{},"Use só os top 4 do reranking no prompt",[103,2036,2037],{},"Custo: a chamada de reranking é pequena (20 documentos) e barata. Qualidade\nsobe significativamente — especialmente em bases grandes.",[370,2039,2041],{"className":760,"code":2040,"language":762,"meta":375,"style":375},"# pip install cohere\nimport cohere\nco = cohere.Client(os.getenv(\"COHERE_API_KEY\"))\n\ndef rerank(pergunta: str, documentos: list[dict], top_n: int = 4) -> list[dict]:\n    \"\"\"Reordena documentos por relevância real, não só similaridade vetorial.\"\"\"\n    r = co.rerank(\n        model=\"rerank-multilingual-v3.0\",\n        query=pergunta,\n        documents=[d[\"conteudo\"] for d in documentos],\n        top_n=top_n,\n    )\n    return [documentos[doc.index] for doc in r.results]\n",[184,2042,2043,2048,2053,2058,2062,2067,2072,2077,2082,2087,2092,2097,2101],{"__ignoreMap":375},[379,2044,2045],{"class":381,"line":382},[379,2046,2047],{},"# pip install cohere\n",[379,2049,2050],{"class":381,"line":388},[379,2051,2052],{},"import cohere\n",[379,2054,2055],{"class":381,"line":394},[379,2056,2057],{},"co = cohere.Client(os.getenv(\"COHERE_API_KEY\"))\n",[379,2059,2060],{"class":381,"line":400},[379,2061,701],{"emptyLinePlaceholder":700},[379,2063,2064],{"class":381,"line":406},[379,2065,2066],{},"def rerank(pergunta: str, documentos: list[dict], top_n: int = 4) -> list[dict]:\n",[379,2068,2069],{"class":381,"line":489},[379,2070,2071],{},"    \"\"\"Reordena documentos por relevância real, não só similaridade vetorial.\"\"\"\n",[379,2073,2074],{"class":381,"line":500},[379,2075,2076],{},"    r = co.rerank(\n",[379,2078,2079],{"class":381,"line":655},[379,2080,2081],{},"        model=\"rerank-multilingual-v3.0\",\n",[379,2083,2084],{"class":381,"line":661},[379,2085,2086],{},"        query=pergunta,\n",[379,2088,2089],{"class":381,"line":667},[379,2090,2091],{},"        documents=[d[\"conteudo\"] for d in documentos],\n",[379,2093,2094],{"class":381,"line":673},[379,2095,2096],{},"        top_n=top_n,\n",[379,2098,2099],{"class":381,"line":679},[379,2100,831],{},[379,2102,2103],{"class":381,"line":685},[379,2104,2105],{},"    return [documentos[doc.index] for doc in r.results]\n",[189,2107,2109],{"id":2108},"filtros-por-metadados","Filtros por metadados",[103,2111,2112],{},"Já vimos filtro na query SQL. Para casos comuns:",[346,2114,2115,2124,2133,2142],{},[19,2116,2117,927,2120,2123],{},[107,2118,2119],{},"Por produto",[184,2121,2122],{},"{\"produto\": \"X100\"}"," — só busca no manual daquele\nproduto",[19,2125,2126,927,2129,2132],{},[107,2127,2128],{},"Por tipo",[184,2130,2131],{},"{\"tipo\": \"faq\"}"," — só em FAQs, ignora manuais longos",[19,2134,2135,927,2138,2141],{},[107,2136,2137],{},"Por data",[184,2139,2140],{},"{\"data\": {\"$gte\": \"2026-01-01\"}}"," — só documentos recentes",[19,2143,2144,927,2147,2150],{},[107,2145,2146],{},"Por idioma",[184,2148,2149],{},"{\"idioma\": \"pt-BR\"}"," — só em português",[103,2152,2153,2154,2157],{},"Filtrar ",[107,2155,2156],{},"antes"," da busca vetorial reduz o conjunto e melhora precisão.\nSempre que o usuário puder especificar escopo (produto, período, categoria),\nuse isso como filtro.",[11,2159,2161],{"id":2160},"avaliando-qualidade-do-rag-avaliando","Avaliando qualidade do RAG {#avaliando}",[103,2163,2164],{},"RAG sem avaliação é fé. Você não sabe se está bom ou se só \"parece bom\".\nDuas métricas centrais:",[189,2166,2168],{"id":2167},"_1-recallk","1. Recall@k",[103,2170,2171],{},"Dos chunks que deveriam estar no top-k, quantos aparecem? Para medir, crie\num conjunto de ~50 perguntas com a resposta esperada e os documentos que\ndeveriam ser recuperados. Rode o RAG e compare.",[189,2173,2175],{"id":2174},"_2-fidelidade-da-resposta-faithfulness","2. Fidelidade da resposta (faithfulness)",[103,2177,2178,2179,2182,2183,2186],{},"A resposta do LLM está apoiada no contexto recuperado, ou ele alucinou?\nFerramentas como ",[107,2180,2181],{},"Ragas"," ou ",[107,2184,2185],{},"TruLens"," automatizam essa avaliação:",[370,2188,2190],{"className":760,"code":2189,"language":762,"meta":375,"style":375},"# pip install ragas\nfrom ragas.metrics import faithfulness, answer_relevancy\nfrom ragas import evaluate\nfrom datasets import Dataset\n\n# dados: perguntas, respostas geradas, contextos recuperados, respostas esperadas\ndados = Dataset.from_dict({\n    \"question\": [...],\n    \"answer\": [...],         # o que o LLM respondeu\n    \"contexts\": [...],       # o que foi recuperado\n    \"ground_truth\": [...],   # a resposta correta\n})\nresultado = evaluate(dados, metrics=[faithfulness, answer_relevancy])\nprint(resultado)\n",[184,2191,2192,2197,2202,2207,2212,2216,2221,2226,2231,2236,2241,2246,2251,2256],{"__ignoreMap":375},[379,2193,2194],{"class":381,"line":382},[379,2195,2196],{},"# pip install ragas\n",[379,2198,2199],{"class":381,"line":388},[379,2200,2201],{},"from ragas.metrics import faithfulness, answer_relevancy\n",[379,2203,2204],{"class":381,"line":394},[379,2205,2206],{},"from ragas import evaluate\n",[379,2208,2209],{"class":381,"line":400},[379,2210,2211],{},"from datasets import Dataset\n",[379,2213,2214],{"class":381,"line":406},[379,2215,701],{"emptyLinePlaceholder":700},[379,2217,2218],{"class":381,"line":489},[379,2219,2220],{},"# dados: perguntas, respostas geradas, contextos recuperados, respostas esperadas\n",[379,2222,2223],{"class":381,"line":500},[379,2224,2225],{},"dados = Dataset.from_dict({\n",[379,2227,2228],{"class":381,"line":655},[379,2229,2230],{},"    \"question\": [...],\n",[379,2232,2233],{"class":381,"line":661},[379,2234,2235],{},"    \"answer\": [...],         # o que o LLM respondeu\n",[379,2237,2238],{"class":381,"line":667},[379,2239,2240],{},"    \"contexts\": [...],       # o que foi recuperado\n",[379,2242,2243],{"class":381,"line":673},[379,2244,2245],{},"    \"ground_truth\": [...],   # a resposta correta\n",[379,2247,2248],{"class":381,"line":679},[379,2249,2250],{},"})\n",[379,2252,2253],{"class":381,"line":685},[379,2254,2255],{},"resultado = evaluate(dados, metrics=[faithfulness, answer_relevancy])\n",[379,2257,2258],{"class":381,"line":691},[379,2259,2260],{},"print(resultado)\n",[103,2262,2263,2266,2267,2270],{},[184,2264,2265],{},"faithfulness"," mede o quanto a resposta está apoiada no contexto;\n",[184,2268,2269],{},"answer_relevancy"," mede o quanto ela responde à pergunta. Ideal: ambos\nacima de 0,8.",[189,2272,2274],{"id":2273},"conjunto-de-avaliação","Conjunto de avaliação",[103,2276,2277],{},"Mantenha um conjunto de 30–100 pares pergunta\u002Fresposta esperada. Rode a\navaliação a cada mudança (modelo, prompt, chunking). Sem isso, qualquer\najuste é tiro no escuro.",[11,2279,2281],{"id":2280},"erros-comuns-erros","Erros comuns {#erros}",[16,2283,2284,2290,2296,2302,2308,2314,2323,2329,2339,2345],{},[19,2285,2286,2289],{},[107,2287,2288],{},"Misturar modelos de embedding",": indexar com um modelo, consultar com\noutro. Resultado: busca sem sentido. Sempre use o mesmo modelo.",[19,2291,2292,2295],{},[107,2293,2294],{},"Chunk muito grande ou muito pequeno",": 5000 caracteres dilui relevância;\n50 caracteres perde contexto. Teste 800–1200 como ponto de partida.",[19,2297,2298,2301],{},[107,2299,2300],{},"Não usar índice vetorial",": com 50k documentos, busca sem HNSW vira\nsegundo. Crie o índice assim que a tabela crescer.",[19,2303,2304,2307],{},[107,2305,2306],{},"Esquecer do overlap no chunking",": cortar uma ideia no meio faz o LLM\nperder contexto. Use 10–20% de overlap.",[19,2309,2310,2313],{},[107,2311,2312],{},"Não filtrar por metadados",": deixar o usuário perguntar sobre \"produto\nX\" e o RAG buscar em todos os produtos. Use a intenção do usuário como\nfiltro.",[19,2315,2316,927,2319,2322],{},[107,2317,2318],{},"Temperature alta em RAG",[184,2320,2321],{},"temperature=0.7"," faz o LLM \"criar\" além do\ncontexto. Em RAG, use 0.1–0.3.",[19,2324,2325,2328],{},[107,2326,2327],{},"Não ter avaliação automatizada",": acham que está bom, até cliente\nreclamar. Monte conjunto de teste desde o início.",[19,2330,2331,2334,2335,2338],{},[107,2332,2333],{},"Não atualizar embeddings",": documento mudou, embedding não. Resposta\nfica desatualizada. Use ",[184,2336,2337],{},"updated_at"," para reindexar só o que mudou.",[19,2340,2341,2344],{},[107,2342,2343],{},"Mandar contexto demais",": 20 chunks de 2000 tokens cada = 40k tokens\nde input, caro e confuso. Top-k de 3–6 é o sweet spot.",[19,2346,2347,2350],{},[107,2348,2349],{},"Ignorar a fonte",": RAG sem citação de fonte vira \"diz-que-disse\". Sempre\nmostre de onde veio a informação — aumenta confiança e permite auditoria.",[2352,2353],"hr",{},[103,2355,2356,2359,2360,2364],{},[107,2357,2358],{},"Quer um RAG funcionando com os dados da sua empresa?"," A Inicialize Tec\nimplementa do esquema do banco ao pipeline completo, com avaliação de\nqualidade e monitoramento. ",[22,2361,2363],{"href":2362},"\u002F#top","Entre em contato"," e mostramos como fica\ncom a sua base.",[2366,2367,2368],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":375,"searchDepth":394,"depth":394,"links":2370},[2371,2372,2373,2376,2380,2385,2386,2391,2394,2397,2398,2402,2406,2411],{"id":13,"depth":388,"text":14},{"id":100,"depth":388,"text":101},{"id":166,"depth":388,"text":167,"children":2374},[2375],{"id":191,"depth":394,"text":192},{"id":311,"depth":388,"text":312,"children":2377},[2378,2379],{"id":318,"depth":394,"text":319},{"id":340,"depth":394,"text":341},{"id":422,"depth":388,"text":423,"children":2381},[2382,2383,2384],{"id":426,"depth":394,"text":427},{"id":513,"depth":394,"text":514},{"id":529,"depth":394,"text":530},{"id":608,"depth":388,"text":609},{"id":752,"depth":388,"text":753,"children":2387},[2388,2389,2390],{"id":756,"depth":394,"text":757},{"id":920,"depth":394,"text":921},{"id":945,"depth":394,"text":946},{"id":1050,"depth":388,"text":1051,"children":2392},[2393],{"id":1249,"depth":394,"text":1250},{"id":1266,"depth":388,"text":1267,"children":2395},[2396],{"id":1393,"depth":394,"text":1394},{"id":1448,"depth":388,"text":1449},{"id":1759,"depth":388,"text":1760,"children":2399},[2400,2401],{"id":1763,"depth":394,"text":1764},{"id":1871,"depth":394,"text":1872},{"id":2015,"depth":388,"text":2016,"children":2403},[2404,2405],{"id":2019,"depth":394,"text":2020},{"id":2108,"depth":394,"text":2109},{"id":2160,"depth":388,"text":2161,"children":2407},[2408,2409,2410],{"id":2167,"depth":394,"text":2168},{"id":2174,"depth":394,"text":2175},{"id":2273,"depth":394,"text":2274},{"id":2280,"depth":388,"text":2281},"inteligencia-artificial","2026-08-26","Aprenda a construir um sistema de RAG (Retrieval-Augmented Generation) usando PostgreSQL com pgvector — IA que responde com base nos dados da sua empresa.","md",[124,162,2417,318,109,2418,172],"PostgreSQL","IA com dados privados",{},"\u002Fia\u002Frag-com-postgresql-pgvector",{"title":5,"description":2414},"rag-com-postgresql-pgvector","ia\u002Frag-com-postgresql-pgvector","l_jhoCoEv6kbSvWYB9ftITUR9QCQSY16cK9Q7rSnhZ0",1787796309109]