Voltar ao blog

Como implementei Q&A «zero alucinações» no leitor

Notas de engenharia sobre Q&A zero alucinações num leitor com IA—respostas baseadas no livro aberto, com citações num clique até à passagem exacta.

Capa: Q&A zero alucinações

Este artigo partilha a implementação de engenharia de Q&A zero alucinações no nosso leitor com IA: as respostas baseiam-se estritamente no texto do livro aberto e as afirmações-chave podem ser rastreadas num clique até à passagem exacta. Se está a desenvolver leitura com IA, Q&A de documentos ou aplicações tipo RAG, esperamos que três iterações de lições e a arquitectura final sejam úteis.


I. Evolução em três fases

O Q&A zero alucinações não foi desenhado de forma perfeita no primeiro dia. Evoluiu sob tensão entre custo, latência e precisão. Segue-se uma visão cronológica das três fases—contexto útil para perceber porque a arquitectura actual tem esta forma.

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

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

Abordagem: Quando o utilizador abre um livro e faz uma pergunta, colocar todo o texto extraído no System Prompt ou na mensagem do utilizador e deixar o modelo de conversa responder. Se o livro exceder cerca de 400 mil caracteres, aplicar truncagem rígida—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é ao primeiro token e a latência total crescem com o tamanho do livro;
  • Custo elevado de tokens: Paga-se o input do livro inteiro em cada pergunta;
  • Livros longos distorcem-se 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 truncagem;
  • Granularidade de recuperação zero: O modelo tem de «procurar uma agulha num palheiro» em centenas de milhares de caracteres—fácil perder detalhes e mais fácil produzir resumos plausíveis sem base—exactamente o que apps de leitura devem evitar.

A fase 1 serve para um MVP, não para um produto de qualidade.

Fase 2: LLM mais leve para extrair frases-chave (comprimir contexto—mas demasiado)

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 [fFicheiro-início-fim], depois concatenar excertos num contexto mais curto para o Q&A posterior.

Pipeline típico: Extract → Cache → Chat. Extrair uma vez (offline ou sob pedido), 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 pedido desce face à fase 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 seleccionadas pelo modelo; qualificadores, contra-exemplos e cadeias argumentativas são muitas vezes descartados—as respostas ficam «correctas 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 extracção pode falhar; o Q&A pode mal interpretar excertos—os erros acumulam-se;
  • Contexto estático: Quer o utilizador pergunte sobre um capítulo ou sobre a estrutura do livro, o modelo recebe sempre 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 pedido e se podemos voltar ao texto fonte.

Fase 3: Índice de segmentos + Tool retrieval sob pedido + texto fonte de volta (actual)

Abordagem: Inspirado em PageIndex. Face à fase 2, três mudanças centrais:

  1. O pré-processamento produz um índice estruturado (resumos ao nível do índice + spans exactos de caracteres), não excertos usados directamente como contexto de Q&A;
  2. Cada pergunta usa Tool Calling para recuperar sob pedido, 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 realce no leitor.

Comparação das três fases:

DimensãoFase 1 (texto integral)Fase 2 (frases-chave)Fase 3 (actual)
Contexto por perguntaLivro inteiro (ou metade frontal truncada)Frases-chave pré-extraídasApenas excertos de texto fonte relevantes para a pergunta
Precisão em livros longosColapsa após ~400k charsDepende da extracção; perde detalheRecuperação por TOC/span; sem truncagem rígida 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

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

Segue-se o detalhe da fase 3.


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

Os utilizadores perdoam erros ocasionais num 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 partilhas;
  • Sem fontes, não podem verificar—a confiança é difícil de construir.

Assim, «zero alucinações» 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 da fase 3 e como estas regras se implementam.


III. Arquitectura: 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 utilizador]
        D[Input do utilizador] --> E{Existe cache Segment?}
        E -->|Não| F[Extrair texto integral / pedir pré-processamento]
        F --> prep
        E -->|Sim| G[Registar 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 -->|Factos / 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 mostrar]
        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 realçar]
    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 num índice de segmentos pesquisável

Se cada pergunta ainda usasse contexto de livro integral da fase 1, livros longos rebentam o orçamento de tokens e a recuperação é demasiado grossa. Fase 3: no primeiro chat de IA para um livro, correr 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 ficheiro Spine (PDF: um ficheiro 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ções em livros longos.


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

Zero alucinações 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] = ficheiro Spine 5 (base 0), caracteres 123–165.

5.1 Como as marcas entram no corpo do texto

A camada de extracçã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 excertos 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 standard: Deve usar [f_fileIndex-startChar-endChar]; as três partes numéricas são obrigatórias;
  2. Copiar só das fontes actuais: 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'), registamos 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 utilizador 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: pedido LLM separado escolhe IDs de segmentos relevantes (máx. 5) de { id, title, summary }, JSON como {"Thinking":"...","answer":["1","3"]};
  5. Para segmentos seleccionados, puxar texto fonte com marcas do Spine—não resumos—como resultado Tool.

Desenho-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 / utilizador 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 corre o ciclo de tools: tool_calls → executar → acrescentar role: tool → continuar até texto final. Com tools activas, o canal thinking desliga-se para evitar conflitos de protocolo.


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

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 Interacção por clique

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

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


VIII. Casos limite e degradação honesta

Zero alucinações ≠ «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 tornar-se deep links do leitor para partilha/arquivo

Exportação de conversa


IX. Compromisso de desenho: Porque não «vector RAG»?

Colegas em Q&A de documentos perguntam frequentemente: se fazem retrieval-augmented generation, porque não Embedding + vector DB Top-K?

Estamos a fazer RAG—recuperar antes de gerar. A diferença: «RAG» na comunidade implica muitas vezes similaridade vectorial; a nossa fase 3 é índice de segmentos + Tool com pull de fonte sob pedidosem camada vectorial por desenho. Seguem razões arquitectónicas, sem negar o valor do vector RAG.

Âmbito: não «sem recuperação», mas «sem recuperação vectorial»

  • RAG amplo: recuperar → gerar → fazemos isto;
  • 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 vectorial.


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

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

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

  • Além do modelo de chat, costuma precisar de modelo de embedding (outro nome, por 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, selecção de segmentos e resposta partilham 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—mudar de provider é caro

Em vector RAG, vectores não são formato intermédio 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.

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


Razão 3: Encaminhamento estruturado chega muitas vezes 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ções ancora-se em spans de caracteres.

Vectores ajudam em semântica difusa, multilingue, 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 vectorial (embedding só para Top-N capítulos candidatos), terminando sempre em escolher segmento → fonte → rastreio clicável—regras zero alucinações 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; mudar 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[fFicheiro-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 realçarUtilizador verifica evidência
Sem recuperação vectorialProvider único; trocar modelo de chat sem re-indexarMenor custo de integração e migração

«Zero alucinações» 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 desenvolve leitura com IA ou Q&A de documentos, esperamos que o caminho texto integral → frases-chave → Tool-first sob pedido, 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 descarregamento.