RAG, ou Retrieval-Augmented Generation, virou uma das formas mais pragmáticas de levar IA generativa para aplicações reais sem depender apenas da memória estatística de um modelo. Em vez de pedir para o LLM “saber tudo”, a aplicação busca documentos relevantes em uma base controlada, monta um contexto e só então chama o modelo para responder. Para times que já usam Kotlin no backend, o Spring AI torna esse fluxo mais familiar, porque encaixa embeddings, vector stores, prompts e clientes de modelo dentro do ecossistema Spring Boot.
Esse assunto interessa especialmente a quem trabalha com produto interno, suporte técnico, base de conhecimento, documentação, atendimento, busca semântica ou automação corporativa. Kotlin entra como uma ótima escolha quando o sistema de IA precisa ficar perto de regras de domínio, APIs existentes, autenticação, filas, bancos relacionais e observabilidade. Se você ainda está escolhendo a base do backend, leia também nosso guia de Kotlin para backend, o artigo sobre Kotlin com Spring Boot e a introdução a Kotlin e IA/Machine Learning.
O que RAG resolve na prática
Um chatbot simples, sem recuperação de contexto, depende do conhecimento geral do modelo e do texto enviado diretamente no prompt. Isso funciona para perguntas genéricas, mas falha quando a resposta precisa refletir políticas internas, documentação privada, contratos, changelogs, manuais, catálogo de produtos ou estado atualizado do negócio.
RAG adiciona uma etapa intermediária. Quando o usuário pergunta algo, a aplicação transforma a pergunta em embedding, busca trechos semanticamente próximos em um vector store, seleciona os melhores resultados e injeta esses trechos no prompt. O modelo continua gerando linguagem natural, mas agora responde com base em material que a aplicação escolheu.
Esse padrão reduz alucinações, facilita atualização de conhecimento sem retreinar modelo e melhora rastreabilidade. Você consegue guardar quais documentos foram usados, mostrar fontes na resposta e auditar por que o sistema chegou a determinada conclusão. Não é mágica: se os documentos são ruins, desatualizados ou mal segmentados, a resposta também sofre. Mas o controle operacional é muito maior do que em um prompt solto.
Arquitetura básica com Kotlin e Spring AI
Uma aplicação RAG em Kotlin costuma ter cinco peças:
- Pipeline de ingestão: lê documentos, quebra em pedaços menores e gera embeddings.
- Vector store: armazena embeddings e metadados para busca semântica.
- Serviço de pergunta: recebe a consulta do usuário e busca trechos relevantes.
- Prompt template: combina pergunta, contexto recuperado e instruções de resposta.
- Camada de resposta: devolve texto, fontes, confiança operacional e logs.
Em Spring AI, essas responsabilidades aparecem como beans e serviços comuns de Spring Boot. O ponto importante é não colocar tudo dentro do controller. O controller deve validar entrada e chamar um serviço; o serviço coordena busca, prompt e modelo; a infraestrutura fica isolada atrás de interfaces. Essa separação conversa bem com Clean Architecture em Kotlin e facilita testes.
Um serviço simplificado pode ter esta cara:
@Service
class PerguntasRagService(
private val chatClient: ChatClient,
private val vectorStore: VectorStore,
) {
fun responder(pergunta: String): RespostaRag {
val documentos = vectorStore.similaritySearch(
SearchRequest.query(pergunta).withTopK(5)
)
val contexto = documentos.joinToString("\n\n") { documento ->
"Fonte: ${documento.metadata["fonte"]}\n${documento.content}"
}
val resposta = chatClient.prompt()
.system("Responda em português brasileiro, usando apenas o contexto fornecido.")
.user("""
Contexto:
$contexto
Pergunta:
$pergunta
""".trimIndent())
.call()
.content()
return RespostaRag(
texto = resposta.orEmpty(),
fontes = documentos.mapNotNull { it.metadata["fonte"]?.toString() }
)
}
}
O exemplo é curto de propósito. Em produção, você vai querer tratamento de erro, timeout, limites de tamanho, logging estruturado, cache quando fizer sentido e validação de permissões por documento. Ainda assim, a ideia central já aparece: o modelo não responde sozinho; ele responde com base no que a busca recuperou.
Ingestão: onde muitos projetos erram
A parte mais subestimada de RAG não é chamar o LLM. É preparar os documentos. Um pipeline ruim cria chunks grandes demais, pequenos demais, sem metadados, sem versão e sem relação clara com a fonte original. Depois, quando a resposta sai incompleta, o time tenta “melhorar o prompt”, mas o problema estava na recuperação.
Para Kotlin com Spring AI, pense na ingestão como um processo explícito:
@Component
class IngestaoDocumentos(
private val vectorStore: VectorStore,
private val embeddingModel: EmbeddingModel,
) {
fun indexar(documentos: List<DocumentoFonte>) {
val chunks = documentos.flatMap { documento ->
documento.texto.chunked(1_500).mapIndexed { indice, trecho ->
Document(
trecho,
mapOf(
"fonte" to documento.url,
"titulo" to documento.titulo,
"chunk" to indice,
"versao" to documento.versao,
)
)
}
}
vectorStore.add(chunks)
}
}
Na prática, chunked(1_500) é simplista. Um pipeline melhor respeita parágrafos, títulos, seções, tabelas e blocos de código. Documentação técnica, por exemplo, não deveria cortar uma função no meio. Políticas internas não deveriam separar exceção e regra principal em chunks desconectados. Se o conteúdo tem hierarquia, preserve essa hierarquia nos metadados.
Também vale manter versionamento. Quando um documento muda, você precisa saber se o vector store contém a versão nova ou uma versão antiga. Para bases pequenas, reindexar tudo pode ser aceitável. Para bases maiores, um processo incremental por hash de conteúdo evita custo desnecessário.
Prompt: seja explícito sobre limites
Um bom prompt de RAG não precisa ser poético. Ele precisa ser operacional. Diga ao modelo para responder em português brasileiro, usar apenas o contexto, admitir quando não houver informação suficiente e listar fontes quando possível. Isso reduz respostas inventadas e cria uma experiência mais confiável para o usuário.
Exemplo de instrução de sistema:
Você é um assistente técnico. Responda em português brasileiro.
Use apenas o contexto fornecido pela aplicação.
Se o contexto não contiver a resposta, diga que não há informação suficiente.
Não invente números, políticas, links ou nomes de responsáveis.
Quando usar uma fonte, mencione o título ou URL fornecido nos metadados.
Essa instrução não elimina alucinação por completo, mas estabelece um contrato. O contrato também deve ser reforçado na aplicação: limite temperatura quando o caso exigir precisão, recuse perguntas fora de escopo e registre quais fontes entraram no prompt. Se o sistema responde sobre documentos internos, autorização é parte do RAG. Não basta recuperar o documento semanticamente correto; o usuário precisa ter permissão para vê-lo.
Testes para RAG em Kotlin
Testar RAG exige uma mentalidade diferente de testes unitários tradicionais. Você não deve comparar a resposta inteira caractere por caractere, porque modelos podem variar. Em vez disso, teste contratos observáveis.
Alguns testes úteis:
- quando a pergunta menciona um tema existente, a busca recupera documentos esperados;
- quando não há contexto suficiente, o serviço não inventa resposta;
- fontes usadas na resposta aparecem no objeto retornado;
- documentos sem permissão não entram no contexto;
- prompts respeitam limite máximo de tokens ou caracteres;
- erros do provedor de IA viram respostas controladas, não stack traces para o usuário.
Você pode combinar testes unitários com mocks para o VectorStore e o ChatClient, além de testes de integração com uma base pequena e determinística. O artigo sobre JUnit 5 e MockK em Kotlin complementa bem essa etapa, e o guia de CI/CD para Kotlin mostra como levar essas verificações para o pipeline.
Observabilidade e custo
RAG coloca uma dependência cara e variável no caminho da requisição. Cada pergunta pode consumir embeddings, busca vetorial, tokens de prompt, tokens de resposta e chamadas externas. Por isso, observe desde o início: latência da busca, tamanho do contexto, documentos recuperados, provedor chamado, custo estimado, taxa de erro e perguntas sem resposta.
Se o time já usa OpenTelemetry, trace a operação inteira: controller, busca no vector store, chamada ao modelo e pós-processamento. Em aplicações com agentes ou fluxos mais complexos, vale ler também o artigo sobre Tracy para observabilidade de IA em Kotlin. A regra é simples: se você não consegue explicar por que a resposta ficou lenta, cara ou errada, ainda não tem uma operação madura.
Quando Spring AI faz sentido
Spring AI é uma boa escolha quando sua aplicação já está no mundo Spring Boot ou quando o time quer padronizar integração com modelos, embeddings e vector stores sem criar uma camada própria do zero. Ele combina especialmente com backend Kotlin, APIs REST, autenticação via Spring Security, jobs de ingestão, mensageria e bancos já existentes.
Se o projeto é um protótipo rápido de ciência de dados, Python ainda pode ser mais direto. O ecossistema Python domina notebooks, avaliação de modelos e experimentação com IA. Mas quando a IA precisa virar produto backend integrado a sistemas JVM, Kotlin ganha força. Para serviços pequenos e binários simples, Go também é uma alternativa forte; para componentes de performance crítica, Rust pode complementar a arquitetura.
Checklist para colocar em produção
Antes de publicar um recurso RAG para usuários reais, revise este checklist:
- documentos têm fonte, versão, data e permissões;
- chunks preservam sentido e não cortam trechos críticos;
- busca vetorial foi avaliada com perguntas reais;
- prompt manda admitir falta de contexto;
- resposta retorna fontes quando possível;
- logs não vazam dados sensíveis;
- custos e latência estão monitorados;
- testes cobrem recuperação, autorização e fallback;
- existe processo para reindexar conteúdo atualizado.
RAG com Kotlin e Spring AI não é apenas “colocar IA no backend”. É um projeto de engenharia de informação. O valor aparece quando documentos bons, busca semântica, prompts objetivos, testes e observabilidade trabalham juntos. Para times Kotlin, essa combinação permite entregar IA generativa com mais controle, mantendo a aplicação próxima do ecossistema JVM que já sustenta APIs, jobs, regras de negócio e integrações críticas.