O que é Reescrita de Consulta?
Reescrita de Consulta (Query Rewriting) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Contexto e conhecimento. Transformar a pergunta do usuário em uma ou mais buscas melhores.
Query Rewriting
Reescrita de Consulta (Query Rewriting) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Contexto e conhecimento. Transformar a pergunta do usuário em uma ou mais buscas melhores.
Transformar a pergunta do usuário numa ou mais buscas melhores antes de consultar o índice. Serve para sistemas de busca e RAG em que as perguntas chegam vagas, dependentes da conversa, com siglas ou misturando vários assuntos.
O balconista que ouve “aquele remédio pra pressão que minha mãe toma” e procura pelo nome certo.
Rodrigo cuida do chat interno de uma empresa de energia. As perguntas raramente chegam prontas para busca. “E para quem é PJ?” só faz sentido depois da pergunta anterior, sobre reembolso de despesas. “Qual a regra do VR no home?” usa duas siglas e uma abreviação que nenhum documento usa. “Como funciona férias e o que acontece se eu pedir demissão no meio” tem dois assuntos que estão em documentos diferentes.
Mandar essas frases direto para o índice traz resultados ruins. A primeira não tem assunto; a segunda não tem as palavras que os documentos usam; a terceira acha um dos dois temas e ignora o outro.
A busca híbrida ajuda quando a consulta já está bem formada. Ela não inventa o assunto que a mensagem escondeu, nem separa duas perguntas coladas.
A pergunta que este padrão responde: como converter o jeito que as pessoas perguntam no jeito que os documentos estão escritos?
Antes da busca, um passo curto reescreve a pergunta com o contexto disponível:
O resultado é estruturado (uma lista de buscas, cada uma com filtros opcionais) e vai para a busca. A pergunta original continua sendo a que o modelo principal responde.
flowchart LR
Q(["1. Pergunta e histórico"]):::io --> RW(["2. Reescrever em buscas"]):::llm
G[("Glossário interno")]:::store --> RW
RW --> S1["3. Busca 1"]:::tool
RW --> S2["3. Busca 2"]:::tool
S1 --> U["4. Unir e deduplicar"]:::tool
S2 --> U
U --> M(["5. Responder a pergunta original"]):::llm
Histórico recente: “Como funciona o reembolso de despesas de viagem?”. Pergunta nova: “e para quem é PJ?”.
Sem reescrita, a busca híbrida procura PJ e devolve páginas sobre contratação de pessoa jurídica, nada de reembolso. Com o passo de reescrita e o glossário {PJ: prestador de serviço pessoa jurídica}, a saída estruturada fica:
{
"queries": [
{ "text": "reembolso de despesas de viagem para prestador PJ", "area": "financeiro" }
],
"needs_clarification": false
}
A busca agora acha a política certa. O modelo principal ainda vê a pergunta original (“e para quem é PJ?”) mais os trechos, então responde no tom da conversa, não com um parágrafo solto sobre a política.
Segundo caso, dois assuntos: “Como funciona férias e o que acontece se eu pedir demissão no meio”. A reescrita devolve duas buscas, férias CLT e demissão durante férias. Sem decompor, a híbrida tende a ficar só com o documento de férias, que é o mais parecido com a primeira metade.
schema Rewrite { queries: list[{ text: string, filter: { area: string? } }], needs_clarification: bool }
function retrieve(question, history):
glossary = glossary_for(question) # siglas encontradas
rw = llm(REWRITE_PROMPT, question, history[-6:], glossary, model="small") -> Rewrite # 2
if rw.needs_clarification:
return ask_user("Você quer saber sobre X ou Y?")
parallel { # 3
results = [hybrid_search(q.text, filters=q.filter, k=20) for q in rw.queries]
}
trace.span("rewrite", original=question, queries=rw.queries)
return dedupe(flatten(results)) # 4
No RAG Agêntico, a reescrita deixa de ser um passo fixo: o agente decide se busca de novo. O padrão desta página é o passo fixo, barato, antes de uma busca só.
O glossário começa pequeno: VR, PJ, home, N2, o apelido do sistema de RH. Cada pergunta sem resultado depois da reescrita é candidata a uma entrada nova, com revisão de quem conhece o domínio.
needs_clarification para ambiguidade de verdade.max_price_cents: 400 em vez de 40000. Confira unidades no eval.| Aspecto | O que acontece | Como aparece no trace |
|---|---|---|
| Chamadas | Uma chamada pequena por pergunta, mais uma busca por consulta gerada | span rewrite com as consultas |
| Latência | Soma a reescrita; buscas em paralelo | duração de rewrite |
| Custo | Modelo pequeno, entrada curta | custo de rewrite por pergunta |
| Falha: reescrita muda o sentido | A busca procura outra coisa | consultas sem relação com a pergunta original |
| Falha: buscas demais | Cinco variações para uma pergunta simples | número médio de consultas alto |
| Falha: histórico errado | Assunto antigo contamina a pergunta nova | reescrita com termos de uma conversa já encerrada |
{ "history": ["Como funciona o reembolso de despesas de viagem?"], "question": "e para quem é PJ?", "expected_queries_contain": ["reembolso", "prestador PJ"] }
Inclua casos em que a reescrita deve recusar: “isso” sem histórico, ou duas leituras igualmente possíveis. needs_clarification=true é sucesso nesses casos, não falha.
rewrite com a pergunta original, as consultas e os filtros.Se a consulta gerada não contém nenhum termo da original nem do glossário, trate como anomalia. É o sinal mais barato de distorção de sentido.
O histórico da conversa pode trazer dados pessoais (nome, matrícula, valor de reembolso). A reescrita lê esse histórico. Mascare o que a busca não precisa antes de mandar ao modelo pequeno, e não grave o histórico cru no span se o trace for amplo demais.
A saída da reescrita vai para o índice, não para o usuário, mas um filtro inventado (area: rh quando a pergunta era financeira) muda quem pode ver o trecho se você usa o mesmo campo para permissão. Não reutilize filtro extraído como controle de acesso.
| Prós | Contras |
|---|---|
| Perguntas em conversa passam a funcionar | Uma chamada a mais por pergunta |
| Vocabulário interno vira o dos documentos | Reescrita pode distorcer a intenção |
| Perguntas com vários assuntos acham os dois | Mais buscas, mais custo de índice |
| Filtros implícitos viram filtros reais | Glossário precisa de manutenção |
As duas abas reescrevem a pergunta com o histórico recente e um glossário, e rodam as buscas geradas em paralelo.
Python. Use quando o RAG roda em Python e você quer a reescrita com saída estruturada.
import asyncio
import anthropic
from pydantic import BaseModel
client = anthropic.AsyncAnthropic()
GLOSSARY = {"VR": "vale-refeição", "home": "trabalho remoto", "PJ": "prestador de serviço pessoa jurídica"}
class Query(BaseModel):
text: str
area: str | None = None
class Rewrite(BaseModel):
queries: list[Query]
needs_clarification: bool
async def rewrite(question: str, history: list[str]) -> Rewrite:
terms = {k: v for k, v in GLOSSARY.items() if k.lower() in question.lower()}
reply = await client.messages.parse( # 2
model="claude-haiku-4-5", max_tokens=500,
system="Reescreva a pergunta em até 3 buscas autocontidas para a base de RH. "
"Resolva referências à conversa, use os termos do glossário e separe assuntos diferentes.",
messages=[{"role": "user", "content": f"Conversa recente: {history[-6:]}\nGlossário: {terms}\nPergunta: {question}"}],
output_format=Rewrite,
)
return reply.parsed_output
async def retrieve(question: str, history: list[str]) -> list[dict]:
rw = await rewrite(question, history)
if rw.needs_clarification:
return [] # o chamador pergunta ao usuário
results = await asyncio.gather(*(search(q.text, area=q.area, k=20) for q in rw.queries)) # 3
seen, merged = set(), []
for doc in (d for r in results for d in r): # 4
if doc["id"] not in seen:
seen.add(doc["id"])
merged.append(doc)
return mergedO que você ganhou: buscas autocontidas a partir de perguntas de conversa. O que você perdeu: uma chamada a mais em toda pergunta; pule a reescrita quando a pergunta já for autocontida e sem siglas.
TypeScript. Use quando a busca é de catálogo e a reescrita extrai filtros da frase.
import Anthropic from "@anthropic-ai/sdk";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";
const client = new Anthropic();
const ProductQuery = z.object({
text: z.string(),
category: z.enum(["tenis", "roupa", "acessorio", "outro"]).nullable(),
max_price_cents: z.number().int().nullable(),
attributes: z.array(z.string()),
});
export async function rewriteSearch(userText: string) {
const { parsed_output } = await client.messages.parse({
model: "claude-haiku-4-5",
max_tokens: 300,
system:
"Converta a busca do cliente em consulta de catálogo. Extraia categoria, preço máximo em centavos e atributos " +
"(ex.: impermeável, amortecimento). O campo text deve usar termos do catálogo, sem gíria.",
messages: [{ role: "user", content: userText }],
output_config: { format: zodOutputFormat(ProductQuery) },
});
if (!parsed_output) return { text: userText, category: null, max_price_cents: null, attributes: [] };
return parsed_output; // "tênis pra correr na chuva até 400" -> { text: "tênis de corrida", category: "tenis", max_price_cents: 40000, attributes: ["impermeável"] }
}O que você ganhou: filtros exatos aplicados pelo índice, em vez de depender do vetor para entender preço. O que você perdeu: em caso de falha, a busca cai para o texto original, sem filtros.
Reescrita de Consulta (Query Rewriting) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Contexto e conhecimento. Transformar a pergunta do usuário em uma ou mais buscas melhores.
Nos cenários reais Chat com a base de conhecimento interna, Busca semântica em e-commerce e Bot de suporte com escalonamento humano.