Voltar ao blog

Como implementei Q&A «zero alucinação» no leitor

Notas de engenharia sobre Q&A zero alucinação em um leitor com IA—respostas baseadas no livro aberto, com citações em um clique até o trecho exato.

Capa: Q&A zero alucinação

Este artigo compartilha a implementação de engenharia de Q&A zero alucinação no nosso leitor com IA: as respostas se baseiam estritamente no texto do livro aberto e as afirmações-chave podem ser rastreadas em um clique até a passagem exata. Se você está desenvolvendo leitura com IA, Q&A de documentos ou apps no estilo RAG, esperamos que três iterações de lições e a arquitetura final sejam úteis.


I. Evolução em três estágios

O Q&A zero alucinação não foi projetado de forma perfeita no primeiro dia. Evoluiu sob tensão entre custo, latência e precisão. Abaixo está uma visão cronológica dos três estágios—contexto útil para entender por que a arquitetura atual tem esta forma.

mermaid
flowchart LR
    P1[Estágio 1: Texto integral no contexto] --> P2[Estágio 2: LLM extrai frases-chave]
    P2 --> P3[Estágio 3: Índice de segmentos + Tool retrieval]
    P1 -.->|Lento, caro, impreciso em livros longos| X1[Descontinuado]
    P2 -.->|Perde detalhe, ainda lento| X2[Descontinuado]
    P3 -->|Atual| OK[Zero alucinação + rastreável]

Estágio 1: Inserir o livro inteiro no contexto (o mais simples—e o primeiro a falhar)

Abordagem: Quando o usuário abre um livro e faz uma pergunta, colocar todo o texto extraído no System Prompt ou na mensagem do usuário e deixar o modelo de conversa responder. Se o livro exceder cerca de 400 mil caracteres, aplicar truncamento rígido—só se mantém o início; os capítulos posteriores ficam invisíveis para o modelo.

Vantagens:

  • Custo de implementação muito baixo; quase sem pré-processamento;
  • Funciona razoavelmente em livros curtos e documentos simples—o modelo realmente «viu o livro inteiro»;
  • UX simples: pergunta e resposta, sem estado «aguarde enquanto analisamos».

Desvantagens (rapidamente inaceitáveis):

  • Respostas lentas: Cada pergunta reenvia um payload enorme; o tempo até o primeiro token e a latência total crescem com o tamanho do livro;
  • Custo alto de tokens: Você paga o input do livro inteiro em cada pergunta;
  • Livros longos distorcem muito: Após 400 mil caracteres, a segunda metade, apêndices e conclusões podem não existir para o modelo—e a UI muitas vezes não indica claramente que houve truncamento;
  • Granularidade de recuperação zero: O modelo precisa «procurar uma agulha no palheiro» em centenas de milhares de caracteres—fácil perder detalhes e mais fácil produzir resumos plausíveis sem base—exatamente o que apps de leitura devem evitar.

O estágio 1 serve para um MVP, não para um produto de qualidade.

Estágio 2: LLM mais leve para extrair frases-chave (comprimir contexto—mas demais)

Abordagem: Antes do Q&A (ou na primeira abertura do livro), usar um modelo mais barato sobre o corpo: dividir por capítulo do Spine (ou segmentar o livro), extrair frases-chave, manter marcas de posição como [fArquivo-início-fim], depois concatenar trechos em um contexto mais curto para o Q&A posterior.

Pipeline típico: Extract → Cache → Chat. Extrair uma vez (offline ou sob demanda), guardar um «conjunto de frases-chave», reutilizar em cada pergunta—a mesma ideia de muitos protótipos de Q&A de documentos que comprimem primeiro e depois respondem.

Vantagens:

  • Cada pergunta envia muito menos texto; o consumo de tokens por requisição cai em relação ao estágio 1;
  • O pré-processamento pode ser em cache; sem re-extrair por pergunta no mesmo livro;
  • As marcas de posição lançam bases para citações.

Desvantagens (ainda falha em livros longos):

  • Perda massiva de detalhe: As «frases-chave» são selecionadas pelo modelo; qualificadores, contraexemplos e cadeias argumentativas são frequentemente descartados—as respostas ficam «corretas mas parciais»;
  • Contexto ainda grande em livros longos: Mesmo bundles de frases-chave em obras grandes são consideráveis—latência e custo são aliviados, não resolvidos;
  • Erro duplo de LLM: A extração pode falhar; o Q&A pode interpretar mal trechos—os erros se acumulam;
  • Contexto estático: Quer o usuário pergunte sobre um capítulo ou sobre a estrutura do livro, o modelo sempre recebe o mesmo blob pré-extraído—sem estreitamento dinâmico pela pergunta.

Lição: o problema não é «se comprimimos», mas se a compressão é sob demanda e se podemos voltar ao texto fonte.

Estágio 3: Índice de segmentos + Tool retrieval sob demanda + texto fonte de volta (atual)

Abordagem: Inspirado em PageIndex. Em relação ao estágio 2, três mudanças centrais:

  1. O pré-processamento produz um índice estruturado (resumos no nível do índice + spans exatos de caracteres), não trechos usados diretamente como contexto de Q&A;
  2. Cada pergunta usa Tool Calling para recuperar sob demanda, depois puxa texto fonte com marcas de posição para responder;
  3. System Prompt + frontend impõem formato de citação e suportam saltos com clique e destaque no leitor.

Comparação dos três estágios:

DimensãoEstágio 1 (texto integral)Estágio 2 (frases-chave)Estágio 3 (atual)
Contexto por perguntaLivro inteiro (ou metade frontal truncada)Frases-chave pré-extraídasApenas trechos de texto fonte relevantes para a pergunta
Precisão em livros longosColapsa após ~400k charsDepende da extração; perde detalheRecuperação por TOC/span; sem truncamento rígido do livro inteiro
Velocidade de respostaLentaUm pouco melhor; livros longos ainda lentosRecuperação + contexto curto—visivelmente mais rápido
Custo de tokensMuito altoMédio-altoPré-processamento amortizado + pagar sob necessidade
RastreabilidadeFraca (difícil citar)Tags existem mas conteúdo já filtradoNotas de rodapé mapeiam spans reais de fonte
Complexidade de engenhariaBaixaMédiaAlta

Por que paramos no estágio 3: Para leitura, zero alucinação não é «mostrar ao modelo o máximo de texto possível», mas «antes de responder, obter evidência fonte para a pergunta». Os estágios 1–2 lutaram o tamanho do contexto; o estágio 3 divide o pipeline em índice (pré-processar) → recuperar (Tool) → evidência (fonte) → responder (geração restrita)—equilibrando precisão, custo e rastreabilidade.

A seguir o detalhe do estágio 3.


II. Definição do problema: Em Q&A de livros, alucinação dói mais que no chat genérico

Os usuários perdoam erros ocasionais em um chatbot geral. Em Q&A de livros, o custo é maior:

  • Perguntam o que este livro diz—não o que vive na memória paramétrica do modelo;
  • Uma «opinião do livro» plausível pode enganar notas, citações e compartilhamentos;
  • Sem fontes, não podem verificar—a confiança é difícil de construir.

Assim, «zero alucinação» torna-se três regras executáveis:

  1. Perguntas sobre o livro devem consultar o livro primeiro: Tudo o que possa ser sobre o livro aberto deve passar por recuperação (Tool) antes da resposta;
  2. Respostas devem ser rastreáveis: Afirmações-chave com marcas de posição que a UI possa analisar e saltar;
  3. Dizer quando não encontrar: Se o livro não contém, dizer—não disfarçar conhecimento geral como «o que o livro diz».

O resto segue o fluxo de dados do estágio 3 e como essas regras se implementam.


III. Arquitetura: Pré-processar → Tool retrieval → Geração restrita → Citações clicáveis

mermaid
flowchart TB
    subgraph prep [Pré-processamento offline / primeira vez]
        A[Dividir livro por TOC ou comprimento] --> B[Resumos de segmentos com LLM]
        B --> C[Persistir cache Segment localmente]
    end

    subgraph ask [Pergunta do usuário]
        D[Input do usuário] --> E{Existe cache Segment?}
        E -->|Não| F[Extrair texto integral / pedir pré-processamento]
        F --> prep
        E -->|Sim| G[Registrar Tool Calling]
    end

    subgraph retrieve [Recuperação por Tool]
        G --> H{Tipo de pergunta}
        H -->|Visão geral / crítica| I[get_full_book_segment_summaries]
        H -->|Fatos / pessoas / capítulo| J[get_related_segment_summaries]
        J --> K[LLM escolhe IDs de segmentos do catálogo de resumos]
        K --> L[Obter fonte por span + marcas de posição]
        I --> M[Concatenar todos os resumos de segmentos]
    end

    subgraph answer [Gerar e exibir]
        L --> N[Resultados Tool de volta ao modelo]
        M --> N
        N --> O[Regras de citação no System Prompt]
        O --> P[Resposta em streaming + notas de posição]
        P --> Q[Renderizar notas clicáveis]
        Q --> R[Clique → pré-visualização → saltar e destacar]
    end

Ideia central: não deixar o modelo «responder de memória»—obrigá-lo a «reunir evidência, responder e marcar fontes».


IV. Pré-processamento: Transformar o livro inteiro em um índice de segmentos pesquisável

Se cada pergunta ainda usasse contexto de livro integral do estágio 1, livros longos estouram o orçamento de tokens e a recuperação é grossa demais. Estágio 3: no primeiro chat de IA para um livro, rodar em segundo plano uma tarefa de resumo de segmentos—dividir por TOC ou comprimento de texto em Segments, resumir cada um, persistir em IndexedDB local.

Cada Segment contém resumo mais posição física no corpo:

CampoSignificado
startFileIndex / endFileIndexÍndice de arquivo Spine (PDF: um arquivo por página)
startOffset / endOffsetInício/fim em caracteres
sequenceOrdem linear de leitura
titleTítulo do TOC

A divisão equilibra precisão e custo: se o corpo de um nó TOC tiver menos de ~20KB, resumir só esse nó; nós irmãos podem fundir-se em lotes (15–20KB) antes de chamadas LLM; blocos longos sem estrutura dividem-se em intervalos de ~30–40 mil caracteres.

O System Prompt de resumo exige manter marcas de posição inline ([fNúmero-Número-Número]) para o texto fonte obtido por Tool alinhar com offsets do Spine. Restrição central:

Se o conteúdo do resumo se relacionar com uma passagem, manter a marca de posição no final [fNúmero-Número-Número] (ex. [f1-90-109]).
As marcas são atômicas—não alterar, fundir ou omitir qualquer caractere ou dígito.

Após o pré-processamento, o Q&A depende de um índice estruturado de segmentos, não do contexto do livro inteiro—pré-requisito de engenharia para zero alucinação em livros longos.


V. Sistema de marcas de posição: Codificar «onde» no texto

Zero alucinação exige conteúdo da fonte e proveniência analisável por máquina e saltável na UI. Usamos marcas inline:

[f{fileIndex}-{startChar}-{endChar}]

Exemplo: [f5-123-165] = arquivo Spine 5 (base 0), caracteres 123–165.

5.1 Como as marcas entram no corpo do texto

A camada de extração acrescenta [f{fileIndex}-{start}-{end}] no fim dos segmentos:

const position = `[f${fileIndex}-${absOffset}-${absOffset + segment.length}]`;
fileLines.push(segment.text.trim() + position);

Quer resumos de pré-processamento quer trechos de Tool, as posições alinham com offsets de caracteres do Spine—não números de página estimados pelo modelo.

5.2 Restrições na saída do modelo

O System Prompt inclui Position Citation Rules—cinco pontos centrais:

  1. Formato padrão: Deve usar [f_fileIndex-startChar-endChar]; as três partes numéricas são obrigatórias;
  2. Copiar só das fontes atuais: Notas de rodapé verbatim das mensagens system/user ou retornos Tool deste turno;
  3. Sem fabricação: Não calcular, editar ou inventar posições;
  4. Preferir omissão: Se não houver marca válida no contexto, responder normalmente—não emitir marcas de posição;
  5. Inline com afirmações: Marcas seguem a frase relevante; sem listas de citações no final.

A UI também filtra marcas inválidas de duas partes ocasionais (ex. [f1-293]) antes de renderizar.

Popup de rastreio de citação


VI. Tool Calling: Recuperar primeiro, responder depois

Quando o chat está ligado a um livro (resourceId presente, chatType === 'chat'), registramos duas Tools com executors antes de cada geração—ciclo function calling compatível com OpenAI.

Para: conceitos, personagens, enredo, detalhes de capítulo—intenção clara de recuperação.

Fluxo:

  1. O modelo reescreve a formulação do usuário em termos prováveis no livro («Optimize Search Queries» no System Prompt);
  2. Chamar Tool com question;
  3. Agrupar todos os resumos de segmentos por orçamento de tokens (~30k tokens por lote, máx. 5 lotes);
  4. Cada lote: requisição LLM separada escolhe IDs de segmentos relevantes (máx. 5) de { id, title, summary }, JSON como {"Thinking":"...","answer":["1","3"]};
  5. Para segmentos selecionados, puxar texto fonte com marcas do Spine—não resumos—como resultado Tool.

Design-chave: Tool devolve fonte, não resumos. O modelo responde a partir de parágrafos reais com [f…] inline, evitando deriva «resumo → re-resumo».

6.2 get_full_book_segment_summaries — Visão geral do livro

Para: «resumir o livro», «avaliar este livro», «estrutura/temas globais»—visão global.

Concatenar todos os campos summary dos segmentos por ordem de leitura—evitar perder capítulos-chave só com relevância por bloco.

6.3 System Prompt: Livro primeiro, tools primeiro

Com livro ligado, aplica-se Core Principles for Reading Assistant:

1. Book First, Tool First
   - Qualquer pergunta possivelmente sobre o livro deve chamar tools primeiro;
   - Respostas devem basear-se sobretudo na recuperação—nunca inventar «conteúdo do livro» sem recuperação.

2. General Knowledge as Fallback Only
   - Só para: conversa casual / usuário pede explicitamente ignorar o livro / tools sem resultado;
   - Se o livro não tem, dizer «não mencionado neste livro» antes de conhecimento geral.

3. Direct Style
   - Ir ao assunto—evitar «com base nos materiais fornecidos…» e frases vazias semelhantes.

A geração roda o ciclo de tools: tool_calls → executar → acrescentar role: tool → continuar até texto final. Com tools ativas, o canal thinking desliga-se para evitar conflitos de protocolo.


VII. Rastreabilidade no frontend: Da nota de rodapé ao destaque

A saída [f5-123-165] do modelo não se mostra em bruto; a camada de render transforma em citações clicáveis.

7.1 Renderização de notas

Normalizar marcas para links Markdown como [1]([f5-123-165]), renderizar como notas numeradas; deduplicar a mesma posição para evitar sobrecarga na UI.

7.2 Interação por clique

  1. Primeiro clique: Analisar [f…] → fileIndex + offsets → extrair texto do Spine → pré-visualização (título TOC opcional);
  2. Mesma nota de novo: Fechar pré-visualização;
  3. Confirmar salto: Abrir vista de leitura, destacar intervalo de caracteres.

Da marca copiada do modelo ao texto visível pelo usuário, a cadeia nunca passa por outra chamada LLM—determinística e reproduzível.


VIII. Casos limite e degradação honesta

Zero alucinação ≠ «sempre há resposta»—significa sem evidência, sem fabricação:

CenárioComportamento
Resumos de segmentos ainda não prontosExtrair texto integral e resumir primeiro
Tool não encontra nadaDevolver (No relevant segment excerpts found…); o modelo deve dizer que não está no livro
Marcas inválidas de duas partes do modeloFrontend filtra; sem notas quebradas
Conversa casualSystem Prompt permite conhecimento geral fora do livro
Exportar conversaNotas podem virar deep links do leitor para compartilhar/arquivar

Exportação de conversa


IX. Compromisso de design: Por que não «vector RAG»?

Colegas em Q&A de documentos perguntam com frequência: se fazem retrieval-augmented generation, por que não Embedding + vector DB Top-K?

Estamos fazendo RAG—recuperar antes de gerar. A diferença: «RAG» na comunidade implica muitas vezes similaridade vetorial; nosso estágio 3 é índice de segmentos + Tool com pull de fonte sob demandasem camada vetorial por design. Seguem razões arquiteturais, sem negar o valor do vector RAG.

Escopo: não «sem recuperação», mas «sem recuperação vetorial»

  • RAG amplo: recuperar → gerar → fazemos isso;
  • Vector RAG: recall via similaridade de embedding → não nesta versão.

O pré-processamento constrói um índice de resumos de segmentos; o modelo escolhe segmentos via Tools e obtém texto fonte. A recuperação existe sem modelo de embedding separado nem manutenção de índice vetorial.


Razão 1: Providers LLM personalizados—manter a superfície de integração pequena

Os usuários podem usar suas API keys, URLs base personalizadas ou Ollama local—o modelo de chat é escolha deles; custo e caminho de dados sob controle.

O vector RAG típico alarga a integração:

  • Além do modelo de chat, costuma precisar de modelo de embedding (outro nome, às vezes outro endpoint);
  • Ollama local precisa de modelo de embedding à parte e compatibilidade de dimensão/API;
  • Mais modos de falha: chat OK mas recuperação vazia—embedding, índice ou dimensão inconsistentes; mais difícil depurar que um provider de ponta a ponta.

Aqui, seleção de segmentos e resposta compartilham a mesma configuração de provider—sem «chat em A, índice em B». Para apps com LLM plugável, isso muitas vezes supera alguns pontos de recall.

Providers de IA personalizados


Razão 2: Embeddings ligam-se ao índice—trocar de provider é caro

Em vector RAG, vetores não são formato intermediário universal—são coordenadas sob um modelo de embedding. Índice com A, consulta com B: similaridade normalmente incomparável—muitas vezes re-embedding completo, e dimensões (768 / 1024 / 1536 …) fixam o schema de armazenamento.

O estágio 3 persiste resumos estruturados + spans de caracteres, não vetores; trocar modelo de chat não reconstrói o índice; a cadeia de evidência (posições fonte) se mantém—alinhado com «experimentar LLMs diferentes a qualquer momento».


Razão 3: Roteamento estruturado costuma bastar para documentos longos com TOC

E-books e PDF têm normalmente estrutura de capítulos; o pré-processamento produz títulos + resumos de segmentos. Para «o que diz o capítulo X» ou «como o livro define Y», escolher segmentos no catálogo e puxar fonte funciona bem na prática; a Tool devolve fonte com [f…], zero alucinação ancora-se em spans de caracteres.

Vetores ajudam em semântica difusa, multilíngue, desajuste literal em passagens longas; para leitores com TOC + pré-processamento + rastreabilidade forte, investir em Tool + devolução de fonte + regras de citação costuma ter melhor ROI.


Futuro: Recall híbrido, não reescrita

Podemos acrescentar recall grosso vetorial (embedding só para Top-N capítulos candidatos), terminando sempre em escolher segmento → fonte → rastreio clicável—regras zero alucinação inalteradas. Se adicionado: embedding opcional, avisos explícitos de re-indexar ao mudar modelos—evitar recuperação errada silenciosa.

Até lá: qualquer API de chat compatível com OpenAI funciona; trocar modelo de chat não reconstrói o índice local.


X. Resumo

EtapaMétodoPapel
Pré-processarDividir por TOC/comprimento + cache de resumosLivros longos pesquisáveis e localizáveis
Marcas de posição[fArquivo-início-fim] na fonteProveniência analisável
Tool retrievalSegmentos por pergunta / resumos do livro, devolver fonteForçar evidência antes de responder
System PromptLivro primeiro, sem marcas falsas, dizer quando faltaRestringir geração
FrontendNota → pré-visualização → saltar e destacarUsuário verifica evidência
Sem recuperação vetorialProvider único; trocar modelo de chat sem re-indexarMenor custo de integração e migração

«Zero alucinação» não significa que o modelo nunca erra—significa engenharia que prende a saída a uma cadeia de evidência: sem recuperação → não fingir conteúdo do livro; com recuperação → dar posições fonte verificáveis.

Se você desenvolve leitura com IA ou Q&A de documentos, esperamos que o caminho texto integral → frases-chave → Tool-first sob demanda, mais marcas inline + devolução de fonte, seja uma implementação de referência útil.

Estas são lições do desenvolvimento do leitor com IA Foxycape—apenas para referência. Experimente o leitor na página de download.