Inicio / Artículos / La ventana de 1M de Gemini sustituyó mi vector store, pero la latencia y la procedencia se opusieron

La ventana de 1M de Gemini sustituyó mi vector store, pero la latencia y la procedencia se opusieron

Contexto completo frente a RAG en 500 preguntas de soporte: precisión, latencia y coste, por qué importa la procedencia de cada respuesta y la configuración híbrida que conservó lo mejor de ambos.

836 palabras

Adiós a la tabla de vectores

Imagine un asistente de soporte que usan nueve mil empleados de una empresa. Cualquier respuesta que da puede acabar reenviada a un cliente por una persona, así que una respuesta errónea o imposible de explicar sale cara.

Por debajo hay una capa de recuperación formada por un chunker, un trabajo de embeddings, un vector store y, al final de la cadena, un reranker que se ha ajustado tantas veces que ya nadie sabe justificar sus valores.

user ─► embed ─► vector db ─► rerank ─► 4k ctx ─► model ─► answer
                    ▲
                 chunker
                    ▲
              nightly reindex

Mantenerlo implica cuatro servicios y dos alertas de guardia, además de un bug recurrente en el que el pie de página de un PDF corrompe en silencio la mitad de los chunks. La ruta de consulta en sí es minúscula:

q = embed(text)
hits = db.search(q, k=8)
ctx = "\n".join(h.doc for h in hits)
ans = llm(f"{ctx}\n\nQ: {text}")

Todo el sufrimiento está en lo que rodea a esas cuatro líneas.

Entonces llega una observación tentadora. Gemini acepta un millón de tokens con una memoria casi perfecta, y el corpus completo ocupa solo 640.000 tokens. Cabe. Así que un martes por la noche alguien elimina una tabla de 2,3 millones de embeddings y sustituye el pipeline por esto:

docs = load_all()
ans = gem(f"{docs}\n\nQ: {text}")

Sin chunker, sin trabajo de reindexación, sin un reranker con una constante que nadie puede justificar. Funciona, y durante unas seis horas da la sensación de haber copiado en un examen sin que te pillen.

Lo que mostró la mañana siguiente

La latencia crece más o menos en proporción al tamaño de la entrada. No es ningún secreto, pero no se nota de verdad hasta que el equipo de soporte empieza a quejarse. Para comparar, se pasaron 500 preguntas reales por cada configuración. La precisión la medía un segundo modelo que contrastaba cada respuesta con una referencia escrita a mano, y sesenta de esas evaluaciones se revisaron manualmente para comprobar que el juez no era demasiado indulgente.

setup               p50      p95     cost/query   accuracy
-----------------------------------------------------------
RAG, 4k ctx         1.9s     3.4s      $0.004       71%
full stuff, 640k   28.7s    41.2s      $0.81        74%
cached, 640k        9.4s    15.1s      $0.11        74%

Una mediana de 28 segundos es mucha espera para una ventana de chat. El caché de prompts reduce ese tiempo y conviene usarlo, aunque tiene sus límites: un tamaño mínimo de prompt, una vida útil del caché por la que se paga para mantenerlo activo y una debilidad que en este caso dolió de verdad. El corpus cambia unas cuarenta veces al día, cada edición invalida el caché y se fue una tarde entera pagando por reconstruir 640.000 tokens.

La pregunta que importaba más

La precisión subió tres puntos, y eso casi ocultó el problema real. Una responsable de soporte hizo una pregunta sencilla: ¿de dónde ha salido esa respuesta?

La recuperación da una respuesta concreta, como el chunk 4.112 de la página 9, editado por última vez en marzo. Con la ventana completa, el modelo devuelve un párrafo muy seguro armado a partir de algún punto de esos 640.000 tokens, y no hay nada que enseñarle. Llamémoslo la prueba de procedencia. La cuestión no es si el modelo puede responder, sino si usted puede demostrar de dónde salió la respuesta. Es un problema de producto y no del modelo, de los que acaban con carreras y no con sprints.

¿No era simplemente que el reranker era malo?

Alguien objetará enseguida: ajusta el reranker, elige un plan más barato, divide mejor los chunks. Es justo, y el reranker era realmente mediocre. Pero incluso uno bien ajustado pasa ocho fragmentos y confía en que salga bien, y ningún ajuste produce una cita a partir de una ventana que se lo tragó todo de golpe.

La configuración que se quedó

La base de datos vectorial volvió, y el reranker no.

ids = db.search(embed(q), k=40)
docs = fetch(ids)
ans = gem(f"{docs}\n\nQ: {q}", cache=c)

La recuperación ya no encuentra la respuesta. Su trabajo ahora es acotar el espacio, y la ventana larga se encarga de leer.

user ─► embed ─► vector db (k=40, no rerank)
                       │
                       ▼
             ~90k ctx + cached policy docs
                       │
                       ▼
                    model ─► answer + doc ids

Las mismas 500 preguntas pasaron por el mismo juez:

hybrid, 90k ctx     3.1s     5.2s      $0.021       88%

Cuarenta chunks sin filtrar demasiado le dejan al modelo espacio para razonar, mientras que un reranker tendría que adivinar los ocho perfectos. Y cada respuesta lleva los identificadores de lo que utilizó.

Las ventanas grandes no acabaron con la recuperación, sino con la recuperación de precisión. La tarea antigua era implacable: encontrar los ocho chunks perfectos, porque el presupuesto solo daba para ocho. Esa tarea se ha terminado y nadie debería echarla de menos.