Volver al blog

Cómo implementé preguntas y respuestas sin alucinaciones en nuestro lector

[object Object]

Portada: Q&A sin alucinaciones

Este artículo comparte la implementación técnica de Q&A sin alucinaciones en nuestro lector con IA: las respuestas se basan estrictamente en el texto del libro abierto y las afirmaciones clave pueden rastrearse con un clic hasta el pasaje exacto. Si desarrollas lectura con IA, Q&A documental o aplicaciones tipo RAG, esperamos que tres iteraciones y la arquitectura final te resulten útiles.


I. Evolución en tres etapas

El Q&A sin alucinaciones no se diseñó perfecto desde el primer día. Evolucionó bajo la tensión entre coste, latencia y precisión. A continuación, las tres etapas en orden cronológico—contexto para entender por qué la arquitectura actual tiene esta forma.

mermaid
flowchart LR
    P1[Etapa 1: Texto completo] --> P2[Etapa 2: LLM frases clave]
    P2 --> P3[Etapa 3: Índice segmentos + Tool]
    P1 -.->|Lento, caro, impreciso en libros largos| X1[Descartado]
    P2 -.->|Pérdida de detalle, aún lento| X2[Descartado]
    P3 -->|Actual| OK[Cero alucinaciones + trazable]

Etapa 1: Volcar todo el libro en el contexto (lo más simple—y lo primero que falla)

Enfoque: Cuando el usuario abre un libro y pregunta, meter todo el cuerpo de texto extraído en el system prompt o el mensaje de usuario y dejar que el modelo de chat responda. Si el libro supera unos 400.000 caracteres, truncado duro—solo queda el inicio; los capítulos posteriores son invisibles para el modelo.

Ventajas:

  • Coste de implementación muy bajo, casi sin preprocesado;
  • Funciona razonablemente en libros cortos y documentos simples—el modelo realmente «vio todo el libro»;
  • UX simple: preguntar y obtener respuesta, sin estado «espere mientras analizamos».

Inconvenientes (pronto inaceptables):

  • Respuestas lentas: Cada pregunta reenvía una carga enorme; el tiempo hasta el primer token y la latencia total crecen con la longitud del libro;
  • Coste de tokens alto: Se paga la entrada del libro completo en cada pregunta;
  • Libros largos muy distorsionados: Tras 400k caracteres, la segunda mitad, anexos y conclusiones casi no existen—y la UI no suele indicar claramente el truncado;
  • Granularidad de búsqueda cero: El modelo debe «encontrar una aguja en un pajar» entre cientos de miles de caracteres—fácil omitir detalles y producir resúmenes plausibles sin base—exactamente lo que las apps de lectura deben evitar.

La etapa 1 sirve para un MVP, no para una solución de producto.

Etapa 2: Un LLM ligero extrae frases clave (comprimir contexto—demasiado agresivo)

Enfoque: Antes del Q&A (o al abrir por primera vez), ejecutar un modelo más barato sobre el cuerpo: dividir por capítulo spine (o trocear el libro), extraer frases clave, conservar etiquetas de posición como [fArchivo-inicio-fin], luego concatenar extractos en un contexto más corto para el Q&A posterior.

Pipeline típico: Extract → Cache → Chat. Extraer una vez (offline o bajo demanda), guardar un «paquete de frases clave», reutilizarlo en cada pregunta—como muchos prototipos de Q&A documental: comprimir primero, responder después.

Ventajas:

  • Cada pregunta envía mucho menos texto; el consumo de tokens por petición baja respecto a la etapa 1;
  • El preprocesado puede cachearse; sin reextracción por pregunta en el mismo libro;
  • Las etiquetas de posición sientan las bases de las citas.

Inconvenientes (siguen fallando en libros largos):

  • Pérdida fuerte de detalle: Las «frases clave» las elige el modelo; calificadores, contraejemplos y cadenas argumentales se pierden a menudo—respuestas «correctas pero parciales»;
  • Contexto aún grande en obras largas: Incluso los paquetes de frases clave son considerables—latencia y coste se alivian, no se resuelven;
  • Doble error de LLM: La extracción puede omitir; el Q&A puede leer mal los extractos—los errores se acumulan;
  • Contexto estático: Pregunte el usuario por un capítulo o la estructura global, el modelo recibe siempre el mismo blob preextraído—sin estrechamiento dinámico según la pregunta.

La lección: el problema no es «si comprimimos», sino «si la compresión es bajo demanda y si podemos volver al texto fuente».

Etapa 3: Índice de segmentos + Tool bajo demanda + devolución del texto fuente (actual)

Enfoque: Inspirado en PageIndex. Frente a la etapa 2, tres cambios centrales:

  1. El preprocesado produce un índice estructurado (resúmenes a nivel TOC + spans de caracteres exactos), no extractos usados directamente como contexto Q&A;
  2. Cada pregunta usa Tool Calling para buscar bajo demanda, luego obtiene texto fuente con etiquetas de posición para responder;
  3. System prompt + frontend imponen el formato de cita y permiten clic → salto → resaltado en el lector.

Comparación de las tres etapas:

DimensiónEtapa 1 (texto completo)Etapa 2 (frases clave)Etapa 3 (actual)
Contexto por preguntaLibro entero (o primera mitad truncada)Frases clave preextraídasSolo fragmentos de fuente relevantes
Precisión en libros largosColapso tras ~400k caracteresDepende de la extracción; pierde detalleBúsqueda por TOC/span; sin truncado duro del libro entero
Velocidad de respuestaLentaAlgo mejor; libros largos aún lentosBúsqueda + contexto corto—notablemente más rápido
Coste de tokensMuy altoMedio-altoPreprocesado amortizado + pago bajo demanda
TrazabilidadDébil (citas difíciles)Hay etiquetas pero contenido filtradoNotas al pie → spans fuente reales
Complejidad técnicaBajaMediaAlta

Por qué nos quedamos en la etapa 3: En lectura, cero alucinaciones no es «mostrar al modelo el máximo texto», sino «antes de responder, obtener pruebas fuente para la pregunta». Las etapas 1–2 luchaban con el tamaño del contexto; la etapa 3 divide el pipeline en índice (preprocesado) → búsqueda (Tool) → prueba (fuente) → respuesta (generación restringida)—equilibrio entre precisión, coste y trazabilidad.

A continuación el detalle de la etapa 3.


II. Definición del problema: En Q&A de libros, la alucinación duele más que en un chat genérico

Los usuarios perdonan errores ocasionales en un chatbot general. En Q&A de libros, el coste es mayor:

  • Preguntan qué dice este libro—no lo que vive en la memoria paramétrica del modelo;
  • Una «opinión del libro» plausible puede inducir a error en notas, citas y reenvíos;
  • Sin fuentes, no hay verificación—la confianza cuesta de construir.

«Cero alucinaciones» se traduce en tres reglas exigibles:

  1. Las preguntas sobre el libro deben consultar el libro primero: Todo lo que pueda concernir al libro abierto pasa por la búsqueda (Tool) antes de responder;
  2. Las respuestas deben ser trazables: Las afirmaciones clave llevan etiquetas de posición que la UI puede parsear y saltar;
  3. Decir cuando no se encuentra: Si el libro no lo contiene, decirlo—no disfrazar conocimiento general como «lo que dice el libro».

El resto sigue el flujo de datos de la etapa 3 y cómo se aplican estas reglas.


III. Arquitectura: Preprocesado → Tool → Generación restringida → Citas clicables

mermaid
flowchart TB
    subgraph prep [Offline / primer preprocesado]
        A[Dividir libro por TOC o longitud] --> B[Resúmenes de segmentos LLM]
        B --> C[Persistir caché Segment localmente]
    end

    subgraph ask [Pregunta del usuario]
        D[Entrada del usuario] --> E{¿Existe caché Segment?}
        E -->|No| F[Extraer texto completo / ofrecer preprocesado]
        F --> prep
        E -->|Sí| G[Registrar Tool Calling]
    end

    subgraph retrieve [Búsqueda Tool]
        G --> H{Tipo de pregunta}
        H -->|Visión global / reseña| I[get_full_book_segment_summaries]
        H -->|Hechos / personas / capítulo| J[get_related_segment_summaries]
        J --> K[LLM elige IDs de segmento del catálogo]
        K --> L[Obtener fuente por span + etiquetas de posición]
        I --> M[Concatenar todos los resúmenes de segmentos]
    end

    subgraph answer [Generar y mostrar]
        L --> N[Resultados Tool al modelo]
        M --> N
        N --> O[Reglas de cita del system prompt]
        O --> P[Respuesta en streaming + notas de posición]
        P --> Q[Renderizar notas clicables]
        Q --> R[Clic → vista previa → salto y resaltado]
    end

Idea central: no dejar que el modelo «responda de memoria»—obligarlo a «reunir pruebas, responder y marcar fuentes».


IV. Preprocesado: Convertir el libro en un índice de segmentos buscable

Si cada pregunta siguiera usando el contexto etapa 1 del libro entero, los libros largos revientan el presupuesto de tokens y la búsqueda es demasiado gruesa. Etapa 3: en el primer chat IA de un libro, ejecutar en segundo plano un job de resumen de segmentos—división por TOC o longitud de texto en Segments, resumir cada uno, persistir en IndexedDB local.

Cada Segment contiene resumen más posición física en el cuerpo:

CampoSignificado
startFileIndex / endFileIndexÍndice de archivo spine (PDF: un archivo por página)
startOffset / endOffsetInicio/fin en caracteres
sequenceOrden de lectura lineal
titleTítulo TOC

La división equilibra precisión y coste: nodo TOC bajo ~20 KB → resumir solo ese nodo; nodos hermanos fusionados en lotes (15–20 KB) antes del LLM; bloques largos no estructurados en rangos ~30–40k caracteres.

El system prompt de resumen exige conservar etiquetas de posición inline ([fNúmero-Número-Número]) para que la fuente obtenida por Tool se alinee con offsets spine. Restricción central:

If summary content relates to a passage, keep the trailing position tag [fNumber-Number-Number] (e.g. [f1-90-109]).
Tags are atomic—do not alter, merge, or omit any character or digit.

Tras el preprocesado, el Q&A depende de un índice de segmentos estructurado, no del contexto del libro entero—requisito técnico del cero alucinaciones en libros largos.


V. Sistema de etiquetas de posición: Codificar el «de dónde» en el texto

Cero alucinaciones exige contenido de la fuente y procedencia analizable por máquina y alcanzable en la UI. Usamos etiquetas inline:

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

Ejemplo: [f5-123-165] = archivo spine 5 (base 0), caracteres 123–165.

5.1 Cómo se escriben las etiquetas en el cuerpo

La capa de extracción añade [f{fileIndex}-{start}-{end}] al final de cada segmento:

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

Resúmenes de preprocesado o extractos Tool: las posiciones se alinean con offsets de caracteres spine—no números de página estimados por el modelo.

5.2 Restricciones sobre la salida del modelo

El system prompt incluye Position Citation Rules—cinco puntos esenciales:

  1. Formato estándar: Debe usar [f_fileIndex-startChar-endChar]; las tres partes numéricas obligatorias;
  2. Copiar solo de fuentes actuales: Notas verbatim de mensajes system/user o retornos Tool de esta ronda;
  3. Sin fabricación: No calcular, editar ni inventar posiciones;
  4. Preferir omisión: Sin etiqueta válida en el contexto → responder con normalidad—no emitir etiquetas de posición;
  5. Inline con las afirmaciones: Etiquetas tras la frase relevante; sin listas de citas al final.

La UI también filtra etiquetas bipartitas inválidas ocasionales (p. ej. [f1-293]) antes del render.

Ventana de trazado de citas


VI. Tool Calling: Primero buscar, luego responder

Cuando el chat está vinculado a un libro (resourceId presente, chatType === 'chat'), registramos dos Tools con executors antes de cada generación—bucle function calling compatible con OpenAI.

Para: conceptos, personajes, trama, detalles de capítulo—intención de búsqueda clara.

Flujo:

  1. El modelo reformula la pregunta en términos probables en el libro («Optimize Search Queries» en el system prompt);
  2. Llamada Tool con question;
  3. Agrupar todos los resúmenes de segmentos por presupuesto de tokens (~30k tokens por lote, máx. 5 lotes);
  4. Por lote: petición LLM separada elige IDs relevantes (máx. 5) de { id, title, summary }, JSON como {"Thinking":"...","answer":["1","3"]};
  5. Para segmentos elegidos, extraer texto fuente etiquetado del spine—not resúmenes—como resultado Tool.

Diseño clave: el Tool devuelve fuente, no resúmenes. El modelo responde desde párrafos reales con [f…] inline, evitando la deriva «resumen → re-resumen».

6.2 get_full_book_segment_summaries — Visión global del libro

Para: «resumir el libro», «reseñar este libro», «estructura/temas globales»—vista global.

Concatenar todos los campos summary de segmentos en orden de lectura—evitar perder capítulos clave solo por relevancia por trozo.

6.3 System prompt: Libro primero, herramientas primero

Con libro vinculado, aplica Core Principles for Reading Assistant:

1. Book First, Tool First
   - Any question possibly about the book must call tools first;
   - Answers must rely mainly on retrieval—never invent “book content” without retrieval.

2. General Knowledge as Fallback Only
   - Only for: casual chat / user explicitly skips the book / tools return nothing;
   - If the book lacks it, say “not mentioned in this book” before general knowledge.

3. Direct Style
   - Get to the point—avoid “based on the provided materials…” and similar filler.

La generación ejecuta el bucle Tool: tool_calls → ejecutar → añadir role: tool → continuar hasta el texto final. Con tools activos, el canal thinking está desactivado para evitar conflictos de protocolo.


VII. Trazabilidad en frontend: De la nota al resaltado

La salida [f5-123-165] del modelo no se muestra en bruto; la capa de render convierte etiquetas en citas clicables.

7.1 Render de notas

Normalizar etiquetas a enlaces Markdown como [1]([f5-123-165]), mostrar como notas numeradas; deduplicar la misma posición.

7.2 Interacción al clic

  1. Primer clic: Parsear [f…] → fileIndex + offsets → extraer texto spine → vista previa (título TOC opcional);
  2. Misma nota otra vez: Cerrar vista previa;
  3. Confirmar salto: Abrir vista de lectura, resaltar rango de caracteres.

Del etiqueta copiada del modelo al texto fuente visible para el usuario, la cadena nunca pasa por otra llamada LLM—determinista y reproducible.


VIII. Casos límite y degradación honesta

Cero alucinaciones ≠ «siempre hay respuesta»—es sin prueba, sin invención:

EscenarioComportamiento
Resúmenes de segmentos no listosExtraer primero texto completo y resumir
Tool no encuentra nadaDevolver (No relevant segment excerpts found…); el modelo debe decir «no está en el libro»
Etiquetas bipartitas inválidas del modeloFiltrado en frontend; sin notas rotas
Charla informalEl system prompt permite conocimiento general fuera del libro
Exportar chatLas notas pueden ser deep links del lector para compartir/archivar

Exportación de chat


IX. Compromiso de diseño: ¿Por qué no «RAG vectorial»?

Colegas en Q&A documental suelen preguntar: si haces generación aumentada por recuperación, ¿por qué no Embedding + base vectorial Top-K?

Sí hacemos RAG—buscar antes de generar. La diferencia: «RAG» en el discurso comunitario suele implicar similaridad vectorial; nuestra etapa 3 es índice de segmentos + Tool con fuente bajo demandasin capa vectorial por diseño. Abajo: razones arquitectónicas, sin negar el valor del RAG vectorial.

Alcance: no «sin búsqueda», sino «sin búsqueda vectorial»

  • RAG amplio: buscar → generar → lo hacemos;
  • RAG vectorial: recuperación por similitud de embedding → no en esta versión.

El preprocesado construye un índice de resúmenes de segmentos; el modelo elige segmentos vía Tools y obtiene texto fuente. Hay búsqueda sin modelo de embedding separado ni mantenimiento de índice vectorial.


Razón 1: Proveedores LLM personalizados—superficie de integración pequeña

Los usuarios pueden conectar sus propias API keys, base URL personalizadas u Ollama local—el modelo de chat es su elección; coste y ruta de datos bajo control.

El RAG vectorial típico amplía la integración:

  • Además del modelo de chat, suele hacer falta un modelo de embedding (otro nombre, a veces otro endpoint);
  • Ollama local necesita modelo de embedding aparte más compatibilidad de dimensión/API;
  • Más modos de fallo: chat OK pero búsqueda vacía—embedding, índice o dimensión; más difícil depurar que un proveedor de extremo a extremo.

Aquí, elección de segmentos y respuesta comparten una config de proveedor—no «chat en A, índice en B». Para apps LLM enchufables, eso suele pesar más que unos puntos de recall.

Proveedores de IA personalizados


Razón 2: Los embeddings atan el índice—cambiar de proveedor cuesta caro

En RAG vectorial, los vectores no son un formato intermedio universal—son coordenadas bajo un modelo de embedding. Índice con A, consulta con B: la similitud suele ser incomparable—a menudo re-embedding completo, y dimensiones (768 / 1024 / 1536 …) fijan el esquema de almacenamiento.

La etapa 3 persiste resúmenes estructurados + spans de caracteres, no vectores; cambiar modelo de chat no reconstruye el índice; la cadena de prueba (posiciones fuente) se mantiene—alineado con «probar distintos LLM en cualquier momento».


Razón 3: El enrutamiento estructurado suele bastar en documentos largos con TOC

E-books y PDF suelen tener estructura de capítulos; el preprocesado da títulos de segmento + resúmenes. Para «qué dice el capítulo X» o «cómo define el libro Y», elegir segmentos del catálogo y tirar de la fuente funciona bien en la práctica; el Tool devuelve fuente con [f…], el cero alucinaciones sigue anclado en spans de caracteres.

Los vectores ayudan en semántica difusa, multilingüe, coincidencia literal larga; en lectores TOC + preprocesado + trazabilidad fuerte, invertir en Tool + devolución de fuente + reglas de cita suele tener mejor ROI.


Futuro: Recuperación híbrida, no reescritura

Podríamos añadir recuperación vectorial gruesa (embedding solo para Top-N capítulos candidatos), terminando siempre en elegir segmento → fuente → traza clicable—reglas de cero alucinaciones sin cambio. Si se añade: embedding opcional, avisos explícitos de reindexar al cambiar modelo—evitar recuperación errónea silenciosa.

Hasta entonces: cualquier API chat compatible OpenAI funciona; cambiar modelo de chat no reconstruye el índice local.


X. Resumen

PasoMétodoRol
PreprocesadoDivisión TOC/longitud + caché de segmentosLibros largos buscables y localizables
Etiquetas de posición[fArchivo-inicio-fin] en la fuenteProcedencia analizable por máquina
ToolSegmentos / resúmenes de libro por pregunta, devolver fuenteForzar pruebas antes de responder
System promptLibro primero, sin etiquetas falsas, decir cuando faltaRestringir generación
FrontendNota → vista previa → salto y resaltadoEl usuario verifica pruebas
Sin búsqueda vectorialUn proveedor; cambiar modelo chat sin reindexarMenor coste de integración y migración

«Cero alucinaciones» no significa que el modelo nunca se equivoque—significa que la ingeniería ata la salida a una cadena de pruebas: sin búsqueda → no fingir contenido del libro; con búsqueda → posiciones fuente verificables.

Si desarrollas lectura con IA o Q&A documental, esperamos que el camino texto completo → frases clave → Tool-first bajo demanda, más etiquetas de posición inline + devolución de fuente, sea una implementación de referencia útil.

Estas son lecciones del lector IA Foxycape—solo como referencia. Prueba el lector en la página de descarga.