# rag-vs-okf — RAG, OKF y OKF+RAG sobre el mismo corpus Monté este repo para responder a una pregunta concreta: **cuando un asistente se equivoca contestando sobre la documentación de una empresa, ¿el problema es el modelo o es cómo le damos el conocimiento?** Aquí hay una empresa ficticia (Acme Shop) con dos versiones del mismo conocimiento, y tres formas de consultarlo. Las tres usan el mismo modelo, el mismo hardware y las mismas siete preguntas. Todo corre en local con Ollama, sin API de pago ni clave de nadie. --- ## Las tres capas | Capa | Qué hace | Fichero | |---|---|---| | **RAG** | Trocea `docs-acme/`, lo embebe en Chroma y recupera los `k` chunks más cercanos | `rag.py` | | **OKF** | Un agente con una herramienta (`leer_concepto`) que navega `okf-bundle/`: parte del índice, sigue enlaces y lee solo los conceptos que necesita | `okf.py` + `agente.py` | | **OKF+RAG** | El mismo agente, con las dos herramientas: conceptos curados para definiciones, búsqueda semántica para incidencias | `agente.py` | La diferencia de fondo no es el retriever: es que `docs-acme/` es documentación real —desordenada, con documentos deprecados que nadie borró, tablas homónimas y tickets en lenguaje coloquial— mientras que `okf-bundle/` son nueve conceptos curados, enlazados entre sí y con vigencia explícita. --- ## Las siete preguntas Cada pregunta está elegida para disparar un fallo distinto. Están en `preguntas.py`, con la respuesta correcta al lado. | # | Pregunta | Fallo que dispara | |---|---|---| | 1 | ¿Cómo calculamos los ingresos? | Documento deprecado que sigue ganando por similitud | | 2 | ¿De qué tabla saco los pedidos del informe mensual? | Entidades homónimas (`pedidos`, `_staging`, `_v2`, `_legacy_shopify`) | | 3 | ¿Tiene la tabla pedidos un campo de canal? | La tabla se parte al trocear y la columna cae en otro chunk | | 4 | ¿Cuál es la definición completa del MRR? | Composición: la respuesta vive repartida en cuatro documentos | | 5 | ¿Cuál de las definiciones de ingresos está vigente? | Vigencia: dos versiones coexisten y nada dice cuál manda | | 6 | ¿Cuál es nuestra política de precios B2B? | Ausencia: no existe, y hay que decirlo en vez de inventarlo | | 7 | ¿Hubo incidencias con pedidos duplicados en marzo? | Cola larga: aquí el corpus operativo sí es la fuente buena | La 6 y la 7 están puestas a mala idea: la 6 para ver quién se inventa una política que no existe, y la 7 porque es la que **el OKF solo no acierta**. Una comparativa donde gana siempre lo mismo no vale para nada. --- ## Montaje Necesitas [uv](https://docs.astral.sh/uv/) y [Ollama](https://ollama.com) corriendo en local. ```bash uv sync ollama pull nomic-embed-text ollama pull qwen3:8b ollama serve # en otra terminal, si no lo tienes ya levantado ``` Indexado del corpus (una vez, ~1 min). Recrea la colección cada vez, así que se puede repetir sin borrar nada a mano: ```bash uv run rag.py # Listo: 85 chunks de 60 ficheros ``` ## Uso ```bash uv run comparar.py # las tres capas, las siete preguntas uv run comparar.py --solo-rag # solo la línea base uv run comparar.py -p 1 4 7 # solo las preguntas que quieras uv run comparar.py -p 3 -k 3 # cambiando el top-k del RAG uv run comparar.py | tee resultados.txt # todo, guardado ``` Escenarios sueltos, uno por fallo, con salida más legible que la tabla completa: ```bash uv run escenarios.py # lista los que hay uv run escenarios.py chunk-partido # dónde se corta la tabla de pedidos uv run escenarios.py mrr-rag mrr-okf # el fallo de composición, lado a lado ``` Y desde el REPL, para trastear: ```python >>> import rag >>> r = rag.preguntar_rag("¿Cómo calculamos los ingresos?") >>> print(r["respuesta"]) >>> for f, d in zip(r["fuentes"], r["distancias"]): ... print(f"{d:.3f} {f}") ``` Esas distancias son lo interesante: el documento de 2023 gana al vigente por coseno, y el modelo no tiene forma de saber cuál de los dos manda. --- ## Resultados Ejecución completa con `qwen3:8b`, `nomic-embed-text` y `k=5`. La salida en crudo está en `resultados.txt`; los aciertos los revisé uno a uno. | # | Fallo | RAG | OKF | OKF+RAG | |---|---|---|---|---| | 1 | documento deprecado | no | sí | sí | | 2 | entidades homónimas | a medias | sí | sí | | 3 | tabla partida | no | sí | no | | 4 | composición | no | sí | sí | | 5 | vigencia | no | sí | sí | | 6 | ausencia | sí | sí | sí | | 7 | cola larga | sí | no | sí | | | **Total** | **2 / 7** | **6 / 7** | **6 / 7** | En la 2 el RAG contesta bien y a continuación añade "NO LO SÉ" en la misma respuesta, así que no la cuento entera. Coste en tokens de prompt, mismo orden: | # | RAG | OKF | OKF+RAG | |---|---|---|---| | 1 | 850 | 1645 | 1819 | | 2 | 869 | 566 | 653 | | 3 | 973 | 2011 | 649 | | 4 | 945 | 1601 | 1775 | | 5 | 965 | 564 | 651 | | 6 | 671 | 562 | 649 | | 7 | 1068 | 1676 | 2239 | Tres cosas que salen de aquí: 1. **El RAG no falla por recuperar poco, falla por recuperar lo que no toca.** En la 1 y la 5 el documento deprecado tiene mejor distancia que el vigente. Subir `k` no lo arregla: mete más ruido del mismo sitio. 2. **El OKF sale más barato cuando la respuesta está donde tiene que estar.** En las preguntas 2, 5 y 6 gasta la mitad de tokens que el RAG, porque lee un concepto en vez de arrastrar cinco chunks de contexto. 3. **La capa combinada no es la suma de las dos.** Gana la 7, que el OKF solo no acierta, pero pierde la 3, que el OKF solo sí acierta: con dos herramientas sobre la mesa, el agente a veces elige mal. Lo dejo en el repo tal cual salió. --- ## Qué hay en el repo ``` rag.py troceado, embeddings, Chroma y la consulta RAG okf.py lectura de conceptos del bundle agente.py bucle de agente con herramientas (OKF y OKF+RAG) preguntas.py las siete preguntas con su respuesta correcta comparar.py ejecuta todo y saca la tabla escenarios.py escenarios sueltos, uno por fallo docs-acme/ la documentación "real": wiki, esquemas, decisiones, tickets okf-bundle/ los nueve conceptos curados, con índice y log de cambios resultados.txt salida en crudo de la última ejecución completa ``` `docs-acme/` y `okf-bundle/` contienen el **mismo conocimiento**. Lo que cambia es la forma. Ese es todo el experimento. --- ## IA para Desarrolladores Soy Joaquín Ruiz y esto sale de mi canal de YouTube, **IA para Desarrolladores**, donde subo IA aplicada al desarrollo real: siempre con el código delante, ejecutándose y con los fallos incluidos. **[youtube.com/@jokioki](https://youtube.com/@jokioki)** · más cosas en **[jokiruiz.com](https://jokiruiz.com)** El vídeo con esta comparativa está en camino. Suscríbete al canal y te salta cuando salga. ## Mis libros Los cuatro están en Amazon, en papel y en Kindle: - 📗 **[Del vibe coding al Spec-Driven Development](https://amzn.eu/d/02csLpKC)** - 📙 **[El motor de la Inteligencia Artificial](https://amzn.eu/d/083CTN3U)** - 📘 **[Programar con Inteligencia Artificial](https://amzn.eu/d/eK4f73N)** - 📙 **[Explora la Inteligencia Artificial](https://amzn.eu/d/dSwYhue)** Si te sirve alguno, una reseña en Amazon ayuda más de lo que parece. --- ## Notas - Los datos de Acme Shop son inventados. Cualquier parecido con tu empresa es que este problema lo tenemos todos. - Todo corre en local: si cambias de modelo en `rag.py` y `agente.py`, los números de la tabla cambian. Los fallos, por experiencia, no.