
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.
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:
- 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;
- Cada pergunta usa Tool Calling para recuperar sob pedido, depois puxa texto fonte com marcas de posição para responder;
- 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ão | Fase 1 (texto integral) | Fase 2 (frases-chave) | Fase 3 (actual) |
|---|---|---|---|
| Contexto por pergunta | Livro inteiro (ou metade frontal truncada) | Frases-chave pré-extraídas | Apenas excertos de texto fonte relevantes para a pergunta |
| Precisão em livros longos | Colapsa após ~400k chars | Depende da extracção; perde detalhe | Recuperação por TOC/span; sem truncagem rígida do livro inteiro |
| Velocidade de resposta | Lenta | Um pouco melhor; livros longos ainda lentos | Recuperação + contexto curto—visivelmente mais rápido |
| Custo de tokens | Muito alto | Médio-alto | Pré-processamento amortizado + pagar sob necessidade |
| Rastreabilidade | Fraca (difícil citar) | Tags existem mas conteúdo já filtrado | Notas de rodapé mapeiam spans reais de fonte |
| Complexidade de engenharia | Baixa | Média | Alta |
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:
- 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;
- Respostas devem ser rastreáveis: Afirmações-chave com marcas de posição que a UI possa analisar e saltar;
- 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
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:
| Campo | Significado |
|---|---|
startFileIndex / endFileIndex | Índice de ficheiro Spine (PDF: um ficheiro por página) |
startOffset / endOffset | Início/fim em caracteres |
sequence | Ordem linear de leitura |
title | Tí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:
- Formato standard: Deve usar
[f_fileIndex-startChar-endChar]; as três partes numéricas são obrigatórias; - Copiar só das fontes actuais: Notas de rodapé verbatim das mensagens system/user ou retornos Tool deste turno;
- Sem fabricação: Não calcular, editar ou inventar posições;
- Preferir omissão: Se não houver marca válida no contexto, responder normalmente—não emitir marcas de posição;
- 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.

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.
6.1 get_related_segment_summaries — Pesquisa segmentada dirigida
Para: conceitos, personagens, enredo, detalhes de capítulo—intenção clara de recuperação.
Fluxo:
- O modelo reescreve a formulação do utilizador em termos prováveis no livro («Optimize Search Queries» no System Prompt);
- Chamar Tool com
question; - Agrupar todos os resumos de segmentos por orçamento de tokens (~30k tokens por lote, máx. 5 lotes);
- Cada lote: pedido LLM separado escolhe IDs de segmentos relevantes (máx. 5) de
{ id, title, summary }, JSON como{"Thinking":"...","answer":["1","3"]}; - 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
- Primeiro clique: Analisar
[f…]→ fileIndex + offsets → extrair texto do Spine → pré-visualização (título TOC opcional); - Mesma nota outra vez: Fechar pré-visualização;
- 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ário | Comportamento |
|---|---|
| Resumos de segmentos ainda não prontos | Extrair texto integral e resumir primeiro |
| Tool não encontra nada | Devolver (No relevant segment excerpts found…); o modelo deve dizer que não está no livro |
| Marcas inválidas de duas partes do modelo | Frontend filtra; sem notas quebradas |
| Conversa casual | System Prompt permite conhecimento geral fora do livro |
| Exportar conversa | Notas podem tornar-se deep links do leitor para partilha/arquivo |

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 pedido—sem 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.

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
| Etapa | Método | Papel |
|---|---|---|
| Pré-processar | Dividir por TOC/comprimento + cache de resumos | Livros longos pesquisáveis e localizáveis |
| Marcas de posição | [fFicheiro-início-fim] na fonte | Proveniência analisável |
| Tool retrieval | Segmentos por pergunta / resumos do livro, devolver fonte | Forçar evidência antes de responder |
| System Prompt | Livro primeiro, sem marcas falsas, dizer quando falta | Restringir geração |
| Frontend | Nota → pré-visualização → saltar e realçar | Utilizador verifica evidência |
| Sem recuperação vectorial | Provider único; trocar modelo de chat sem re-indexar | Menor 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.
