Proyecto #14 · IA generativa y texto
Asistente RAG sobre regulación financiera
Recuperación de artículos de ley (LTOSF, CONDUSEF, datos personales) con BM25, embeddings e híbridos, y respuesta con cita.
Resumen ejecutivo
- Corpus: 274 artículos vigentes de 3 leyes (LTOSF, LPDUSF/CONDUSEF y LFPDPPP), extraídos de los PDF oficiales y separados por artículo. Prueba: 33 preguntas en lenguaje de cliente con su artículo correcto.
- Mejor recuperación: Denso: E5-large multilingüe — el artículo correcto aparece en el top 3 en 82% de las preguntas (MRR 0.76); BM25 55%, MiniLM 42%, híbrido BM25 + MiniLM 67%.
- Lección: con un modelo de embeddings débil, mezclarlo con BM25 ayuda; con uno fuerte (E5-large, entrenado para búsqueda con prefijos query/passage) el léxico mete ruido y el híbrido empeora. La arquitectura correcta depende del modelo, y solo se sabe midiendo.
- Incertidumbre: con 33 preguntas el IC 95 % de Recall@3 del mejor es 0.67–0.94. La ventaja pareada sobre BM25 (+0.06 a +0.48) excluye el cero; frente al segundo mejor (Híbrido RRF: BM25 + E5-large, -0.03 a +0.30) la diferencia no es concluyente. El conjunto de prueba también se usó para elegir, así que la cifra es algo optimista.
- Generación con LLM openai/gpt-oss-120b vía Groq: cita el artículo correcto en 100% de las preguntas donde fue recuperado; cuando no lo fue, se abstiene en 33% de los casos y en los demás responde apoyándose en otro artículo recuperado (a veces defendible, a veces no). Riesgo a controlar: en producción se exige cita y un umbral de similitud mínimo antes de responder.
Resultados
Recall@k: ¿aparece el artículo correcto entre los primeros k?
- BM25 (léxico)
- Denso: MiniLM multilingüe
- Híbrido RRF: BM25 + MiniLM
- Denso: E5-large multilingüe
- Híbrido RRF: BM25 + E5-large
MRR por sistema de recuperación
Recall@3 por ley (Denso: E5-large multilingüe)
Respuesta extractiva: lo que el asistente recupera
Para cada pregunta, los 3 artículos que el mejor sistema entrega al LLM. ✔ marca el artículo correcto.
¿Me pueden cobrar los intereses de un préstamo por adelantado? ✔ encontrado
No, las entidades financieras no pueden cobrar los intereses de un préstamo por adelantado; sólo pueden exigirlos por períodos vencidos, tal como lo establece el artículo 10 de la Ley de Transparencia y Ordenamiento del Sistema Financiero.
Artículo 10. En los créditos, préstamos o financiamientos que las Entidades otorguen, el pago de los intereses no podrá ser exigido por adelantado, sino únicamente por períodos vencidos, sin perjuicio de ajustarse a la legislación mercantil aplicable. El Banco de México mediante disposiciones de carácter general determinará los montos y tipos de los créditos, préstamos y financ iamientos a los que les será aplicable este artículo, de los cuales las Entidades estarán obligadas a informar a sus clientes al momento de pactar los términos del crédito.…
Artículo 18 Bis 6. En los créditos, préstamos o financiamientos revolventes asociados a una tarjeta que las Entidades otorguen, sólo podrá cobrarse intereses sobre los saldos diarios insolutos comprendidos dentro del período de cálculo de intereses del estado de cuenta de que se trate. Artículo adicionado DOF 25-06-2009…
Artículo 23. En todas las operaciones y servicios que las Entidades Financieras celebren por medio de Contratos de Adhesión masivamente celebradas y hasta por los montos máximos que establezca la Comisión Nacional para la Protección y Defensa de los Usuarios de Servicios Financieros en disposiciones de carácter general, aquéllas deberán proporcionarle a sus Clientes la asistencia, acceso y facilidades necesarias para atender las aclaraciones relacionadas con dichas operaciones y servicios. Párrafo reformado DOF 25-06-2009 Al efecto, sin perjuicio de los demás procedimientos y requisitos que impongan otras autoridades financieras facultadas para ello en relación con operaciones materia de su …
¿Quién define cómo se calcula el CAT de un crédito? ✔ encontrado
El Banco de México es quien establece, a través de disposiciones de carácter general, la fórmula, los componentes y la metodología de cálculo del CAT para los créditos, préstamos o financiamientos que otorguen las Entidades.
Artículo 8. El Banco de México establecerá a través de disposiciones de carácter general, la fórmula, los componentes y la metodología de cálculo del CAT, en relación con los créditos, préstamos o financiamientos que otorguen las Entidades. En dichas disposiciones el Ba nco de México establecerá los tipos y montos de los créditos, préstamos o financiamientos a los que será aplicable el CAT. El Banco de México establecerá a través de disposiciones de carácter general, la fórmula, los componentes y la metodología de cálcul o de la GAT. En dichas disposiciones el Banco de México establecerá los tipos y montos de las operaciones a los que será aplicable la GAT. Párrafo adicionado DOF 25-05-2010…
Artículo 15. Tratándose del otorgamiento de créditos, préstamos o financiamientos que otorgu en las Entidades a los que les sea aplicable el CAT de acuerdo con las disposiciones a las que se refiere el artículo 8 de esta Ley, la publicidad y los Contratos de Adhesión deberán contener dicho CAT, cuando así lo establezcan las disposiciones de caráct er general que conforme a lo previsto en los artículos 11 y 12 de esta Ley, emitan la Procuraduría Federal del Consumidor y la Comisión Nacional para la Protección y Defensa de los Usuarios de Servicios Financieros, en el ámbito de sus respectivas competen cias. Párrafo reformado DOF 25-06-2009 La información a que se refiere este artículo deberá r…
Artículo 44.- La Procuraduría Federal del Consumidor sancionará con multa de doscientos a dos mil días de salario, a las Enti dades Comerciales que infrinjan cualquier disposición de esta Ley cuya conducta no competa sancionar a otra de las Autoridades y que no correspondan a las conductas infractoras señaladas en el párrafo siguiente, así como cuando infrinjan las disposiciones de carácter general que la propia Procuraduría expida en términos de esta Ley. Asimismo, la Procuraduría Federal del Consumidor sancionará, en el ámbito de su competencia, con multa de: I. Dos mil a cinco mil días de salario, a las Entidades Comerciales que: a) No cuenten en sus sucursales, establecimientos o en su p…
¿El banco me puede obligar a contratar otro producto para darme el crédito? ✔ encontrado
No, el banco no puede obligarte a contratar otro producto como condición para otorgarte el crédito. La normativa prohíbe que las entidades financieras condicionen la contratación de operaciones o servicios financieros a la contratación de otro producto, salvo que cuenten con tu consentimiento expreso y que se informe de forma clara en el contrato y la publicidad correspondiente.
Artículo 23 Bis. - A las Entidades les estará prohibido condicionar la contratación de operaciones o servicios financieros a la contratación de otra operación o servicio. Sin perjuicio de lo anterior, las Entidades podrán ofrecer productos y servicios financieros de otras Entidades que estén ligados a los productos y servicios ofrecidos al Cliente, cuando se cuente con el consentimiento expreso de éste para contratar los productos o servicios adicionales o ligados con la operación o servicio solicitado, bajo la premisa de que es un derecho innegable del Cliente contratar éstos a través de un tercero independiente. Lo previsto en el párrafo anterio r, deberá informarse a los Clientes a través…
Artículo 18 Bis 5. Si en los contratos de créditos, préstamos o financiamientos revolventes asociados a una tarjeta, así como de créditos personales de liquidez sin garantía real, al momento de la cont ratación se ofrece al Cliente un seguro con cargo a éste, de los no previstos como obligatorios por virtud de la normatividad que resulte aplicable o que no se establezcan como requisito de contratación por la Entidad, el costo y consentimiento para contra tar dicho seguro deberá constar expresamente y deberá señalarse en la misma sección en la que habrá que recabarse el consentimiento del Cliente, que la contratación de dicho seguro es opcional y que la cancelación del seguro no implica la ca…
Artículo 10 Bis 1 .- En los créditos al consumo otorgados por Entidades, la terminación del contrato podrá hacerse en cualquier momento por parte del Cliente acreditado, en cuyo caso la relación jurídica derivada de los recursos previamente dispuestos, solo continuará en vigor para efectos del pago del principal con los intereses y accesorios que correspondan al crédito otorgado, procediendo a la cancelación del Medio de Disposición, en su caso. En caso de que un crédito al consu mo se pague mediante la contratación de uno nuevo con otra Entidad, los Clientes podrán convenir con esta última que realice los trámites necesarios para dar por terminado dicho crédito ante la Entidad original. Ser…
Firmé un contrato y me arrepentí, ¿cuántos días tengo para cancelarlo sin costo? ✔ encontrado
Tienes un período de gracia de diez días hábiles posteriores a la firma del contrato para cancelarlo sin que la entidad cobre comisión alguna, siempre que no hayas utilizado ni operado los productos o servicios financieros contratados.
Artículo 10. El responsable procurará que los datos personales contenidos en las bases de datos sean exactos, completos, correctos y actualizados para los fines para los cuales fueron recabados. Cuando los datos personales hayan dejado de ser necesarios para el cumplimiento de las finalidades previstas en el aviso de privacidad y que motivaron su tratamiento conforme a las disposiciones legales aplicables, deberán ser suprimidos previo bloqueo, en su caso, y una vez que conc luya el plazo de conservación de los mismos. El responsable estará obligado a eliminar los datos relativos al incumplimiento de obligaciones contractuales, una vez que transcurra un plazo de setenta y dos meses, contado …
Artículo 11 Bis 1. Los Clientes contarán con u n período de gracia de diez días hábiles posteriores a la firma de un contrato de adhesión que documenten operaciones masivas establecidas por las disposiciones de carácter general a que se refiere el artículo 11 de la presente Ley, con excepción de los créditos con garantía hipotecaria, para cancelarlo, en cuyo caso, las Entidades no podrán cobrar Comisión alguna, regresando las cosas al estado en el que se encontraban antes de su firma, sin responsabilidad alguna para el Cliente. Lo anterior, siempre y cu ando el Cliente no haya utilizado u operado los productos o servicios financieros contratados. Artículo adicionado DOF 25-06-2009…
Artículo 18 Bis 5. Si en los contratos de créditos, préstamos o financiamientos revolventes asociados a una tarjeta, así como de créditos personales de liquidez sin garantía real, al momento de la cont ratación se ofrece al Cliente un seguro con cargo a éste, de los no previstos como obligatorios por virtud de la normatividad que resulte aplicable o que no se establezcan como requisito de contratación por la Entidad, el costo y consentimiento para contra tar dicho seguro deberá constar expresamente y deberá señalarse en la misma sección en la que habrá que recabarse el consentimiento del Cliente, que la contratación de dicho seguro es opcional y que la cancelación del seguro no implica la ca…
¿Es legal que me cobren comisión por pasarme del límite de mi tarjeta? ✔ encontrado
No, no es legal que le cobren una comisión por exceder el límite de su tarjeta. El artículo 18 Bis 8 prohíbe cobrar cualquier comisión por concepto de sobregiro o intento de sobregiro en créditos, préstamos o financiamientos revolventes asociados a una tarjeta, y el artículo 43 fracción XI establece sanciones a las entidades que lo hagan.
Artículo 18 Bis 8. Queda prohibido cobrar cualquier Comisión por concepto de sobregiro o intento de sobregiro en créditos, préstamos o financiamientos revolventes asociado s a una tarjeta, así como de créditos personales de liquidez sin garantía real. Artículo adicionado DOF 25-06-2009 Capítulo IV.- Disposiciones Comunes…
Artículo 5. Las Entidades que operen cajeros automáticos deberán informar en las pantallas de éstos las Comisiones que cobran por su u so, así como obtener el previo consentimiento de los Clientes para el cobro de tales Comisiones.…
Artículo 43. - La Comisión Nacional para la Protección y Defensa de los Usuarios de Servicios Financieros sancionará con multa de cuatro m il a ciento cincuenta mil Unidades de Medida y Actualización, a las Entidades Financieras que: Párrafo reformado DOF 25-06-2009, 09-03-2018 I. Realicen cargos adicionales a sus Clientes por la terminación de los contratos que tengan celebrados, en contravención a lo señalado en el segundo párrafo del artículo 7 de esta Ley. II. No modifiquen los Contratos de Adhesión conforme a lo ordenado por la Comisión Nacional para la Protección y Defensa de los Usuarios de Servicios Financieros, en términos del artí culo 11 de esta Ley. Fracción reformada DOF 25-06-2…
¿Quién decide cuánto es el pago mínimo de la tarjeta de crédito? ✔ encontrado
El Banco de México es quien, mediante disposiciones de carácter general, determina los montos de pago mínimo que deben cobrar las Entidades Financieras en los créditos, préstamos o financiamientos revolventes asociados a una tarjeta.
Artículo 18 Bis 7. El Banco de México mediante disposiciones de carácter general determinará los montos de pago mínimo que deberán cobrar las En tidades Financieras, en los créditos, préstamos o financiamientos revolventes asociados a una tarjeta. El Banco de México al emitir las citadas disposiciones deberá prever que no existan amortizaciones negativas en el crédito conferido y se procure que las deudas sean cubiertas en un periodo razonable. Artículo adicionado DOF 25-06-2009…
Artículo 18 Bis 2. En los créditos, préstamos o financiamientos revolventes asociados a una tarjeta que otorguen las Entidades, se deberá pactar una sola tasa de interés ordinaria máxima y, en su caso, una sola tasa de interés moratoria máxima. Adicionalmente, las Entidades podrán otorgar tasas de interés promocionales, las cual es en todo caso deberán ser inferiores a la tasa de interés ordinaria máxima, siempre y cuando sus términos y condiciones estén claramente estipulados. Artículo adicionado DOF 25-06-2009…
Artículo 18 Bis 1. Las Entidades sólo otorgarán créditos, préstamos o financiamientos revolventes asociados a una tarjeta previa estimación de la viabilidad de pago por parte de l os solicitantes valiéndose para ello de un análisis a partir de información cuantitativa y cualitativa que permita establecer su solvencia crediticia y capacidad de pago. Las Entidades sólo podrán elevar el límite de crédito en créditos, préstamos o finan ciamientos revolventes asociados a una tarjeta, a Clientes que tengan un comportamiento de cumplimiento en sus compromisos crediticios, en cuyo caso la Entidad deberá formularle una oferta para elevar el límite de su crédito, préstamo o financiamiento, la cual deb…
Metodología y validación
Recuperación BM25, densa (MiniLM y E5-large multilingües, ONNX local) e híbrida por Reciprocal Rank Fusion sobre artículos de ley; generación con LLM condicionada a los artículos recuperados.
✔ Cómo se validó
- 33 preguntas de prueba escritas a mano con su artículo correcto, en lenguaje de cliente.
- Recall@1/3/5 y MRR; desglose por ley y detalle de cada pregunta.
⚙ Supuestos
- La respuesta correcta está en un solo artículo (algunas preguntas tienen respuestas repartidas).
⚠ Limitaciones
- El conjunto de prueba es pequeño y lo escribió el autor: hay que ampliarlo con preguntas reales de clientes.
- Las leyes cambian: el índice debe regenerarse con cada reforma publicada en el DOF.
Comparación de sistemas de recuperación
| Sistema | Recall@1 | Recall@3 | Recall@5 | MRR | IC 95 % Recall@3 |
|---|---|---|---|---|---|
| BM25 (léxico) | 0.364 | 0.545 | 0.636 | 0.47 | 0.36–0.70 |
| Denso: MiniLM multilingüe | 0.273 | 0.424 | 0.606 | 0.395 | 0.27–0.61 |
| Híbrido RRF: BM25 + MiniLM | 0.364 | 0.667 | 0.758 | 0.519 | 0.52–0.82 |
| Denso: E5-large multilingüe | 0.697 | 0.818 | 0.879 | 0.763 | 0.67–0.94 |
| Híbrido RRF: BM25 + E5-large | 0.515 | 0.697 | 0.758 | 0.612 | 0.52–0.85 |
Diferencias pareadas de Recall@3 contra Denso: E5-large multilingüe (bootstrap por pregunta)
| Comparación | Δ Recall@3 | IC 95 % | P(Δ ≤ 0) |
|---|---|---|---|
| Denso: E5-large multilingüe − BM25 (léxico) | 0.273 | +0.06 a +0.48 | 0.006 |
| Denso: E5-large multilingüe − Denso: MiniLM multilingüe | 0.394 | +0.24 a +0.58 | 0 |
| Denso: E5-large multilingüe − Híbrido RRF: BM25 + MiniLM | 0.152 | -0.03 a +0.36 | 0.087 |
| Denso: E5-large multilingüe − Híbrido RRF: BM25 + E5-large | 0.121 | -0.03 a +0.30 | 0.106 |
Detalle por pregunta (mejor sistema)
| Pregunta | Artículo correcto | Top 3 recuperados | ¿Acierto top 3? |
|---|---|---|---|
| ¿Me pueden cobrar los intereses de un préstamo por adelantado? | LTOSF 10 | LTOSF 10 · LTOSF 18 Bis 6 · LTOSF 23 | Sí |
| ¿Quién define cómo se calcula el CAT de un crédito? | LTOSF 8 | LTOSF 8 · LTOSF 15 · LTOSF 44 | Sí |
| ¿El banco me puede obligar a contratar otro producto para darme el crédito? | LTOSF 23 Bis | LTOSF 23 Bis · LTOSF 18 Bis 5 · LTOSF 10 Bis 1 | Sí |
| Firmé un contrato y me arrepentí, ¿cuántos días tengo para cancelarlo sin costo? | LTOSF 11 Bis 1 | LFPDPPP 10 · LTOSF 11 Bis 1 · LTOSF 18 Bis 5 | Sí |
| ¿Es legal que me cobren comisión por pasarme del límite de mi tarjeta? | LTOSF 18 Bis 8 | LTOSF 18 Bis 8 · LTOSF 5 · LTOSF 43 | Sí |
| ¿Quién decide cuánto es el pago mínimo de la tarjeta de crédito? | LTOSF 18 Bis 7 | LTOSF 18 Bis 7 · LTOSF 18 Bis 2 · LTOSF 18 Bis 1 | Sí |
| ¿El cajero automático tiene que avisarme cuánto me va a cobrar? | LTOSF 5 | LTOSF 5 · LTOSF 23 · LTOSF 7 | Sí |
| Quiero que mi sueldo se deposite en otro banco, ¿el banco de nómina me lo debe permitir? | LTOSF 18 | LTOSF 18 · LTOSF 23 · LTOSF 4 Bis | Sí |
| ¿Cómo sé qué despacho de cobranza tiene mi deuda y cómo localizarlo? | LTOSF 17 Bis 1 | LTOSF 17 Bis 3 · LTOSF 17 Bis 4 · LTOSF 17 Bis 1 | Sí |
| ¿Qué cuenta como discriminación por parte de una entidad financiera? | LTOSF 17 | LTOSF 17 · LTOSF 43 · LTOSF 44 | Sí |
| ¿En cuántos años prescribe la facultad de la autoridad para multar a un banco? | LTOSF 24 | LPDUSF 96 · LPDUSF 94 · LTOSF 41 | No |
| Si le di una tarjeta adicional a mi hijo, ¿él responde por mi deuda? | LTOSF 11 Bis | LTOSF 11 Bis · LTOSF 18 Bis 7 · LTOSF 18 Bis 6 | Sí |
Pruébalo: Python en tu navegador
Una versión compacta del análisis, ejecutada con Pyodide (CPython en WebAssembly). Edita el código y vuelve a ejecutarlo; si defines la variable grafica, se dibuja abajo.
Código completo
El script exacto que generó los resultados de esta página (python run_all.py).
"""#14 Asistente RAG sobre regulación financiera mexicana (recuperación con citas).
Corpus: tres leyes públicas de la Cámara de Diputados — Ley para la Transparencia y Ordenamiento de los
Servicios Financieros (LTOSF), Ley de Protección y Defensa al Usuario de Servicios Financieros (LPDUSF,
CONDUSEF) y Ley Federal de Protección de Datos Personales en Posesión de los Particulares (LFPDPPP).
La parte crítica de un RAG es la RECUPERACIÓN: si no encuentra el artículo correcto, el LLM inventa. Se evalúa
con 33 preguntas escritas en lenguaje de cliente (sin copiar la redacción legal), cada una con su artículo
correcto. La generación de la respuesta (con cita) corre en `llm_rag.py` cuando hay API key; sin ella, el sitio
muestra la respuesta extractiva: los artículos recuperados.
"""
import json
import re
import unicodedata
import numpy as np
import pandas as pd
from pypdf import PdfReader
from rank_bm25 import BM25Okapi
from common import RAW, SEED, SITE_DATA, bars, download, export, line, table
from validacion import model_card
LEYES = {"LTOSF": "Ley de Transparencia y Ordenamiento de los Servicios Financieros",
"LPDUSF": "Ley de Protección y Defensa al Usuario de Servicios Financieros",
"LFPDPPP": "Ley Federal de Protección de Datos Personales en Posesión de los Particulares"}
URL = "https://www.diputados.gob.mx/LeyesBiblio/pdf/{}.pdf"
MODELOS_EMB = { # nombre corto → (modelo, prefijo de consulta, prefijo de documento)
"MiniLM multilingüe": ("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", "", ""),
"E5-large multilingüe": ("intfloat/multilingual-e5-large", "query: ", "passage: "),
}
# Preguntas de prueba (lenguaje de cliente) → artículo que las responde
PREGUNTAS = [
("¿Me pueden cobrar los intereses de un préstamo por adelantado?", "LTOSF 10"),
("¿Quién define cómo se calcula el CAT de un crédito?", "LTOSF 8"),
("¿El banco me puede obligar a contratar otro producto para darme el crédito?", "LTOSF 23 Bis"),
("Firmé un contrato y me arrepentí, ¿cuántos días tengo para cancelarlo sin costo?", "LTOSF 11 Bis 1"),
("¿Es legal que me cobren comisión por pasarme del límite de mi tarjeta?", "LTOSF 18 Bis 8"),
("¿Quién decide cuánto es el pago mínimo de la tarjeta de crédito?", "LTOSF 18 Bis 7"),
("¿El cajero automático tiene que avisarme cuánto me va a cobrar?", "LTOSF 5"),
("Quiero que mi sueldo se deposite en otro banco, ¿el banco de nómina me lo debe permitir?", "LTOSF 18"),
("¿Cómo sé qué despacho de cobranza tiene mi deuda y cómo localizarlo?", "LTOSF 17 Bis 1"),
("¿Qué cuenta como discriminación por parte de una entidad financiera?", "LTOSF 17"),
("¿En cuántos años prescribe la facultad de la autoridad para multar a un banco?", "LTOSF 24"),
("Si le di una tarjeta adicional a mi hijo, ¿él responde por mi deuda?", "LTOSF 11 Bis"),
("¿Puedo liquidar y terminar mi crédito de consumo cuando yo quiera?", "LTOSF 10 Bis 1"),
("¿Pueden llamarme a mi trabajo para ofrecerme créditos?", "LTOSF 10 Bis 2"),
("¿Sobre qué saldo me calculan los intereses de la tarjeta?", "LTOSF 18 Bis 6"),
("¿El banco debe revisar si puedo pagar antes de darme una tarjeta de crédito?", "LTOSF 18 Bis 1"),
("¿Para qué existe la CONDUSEF?", "LPDUSF 5o"),
("¿Qué es el Buró de Entidades Financieras?", "LPDUSF 8o Bis"),
("¿Las financieras deben tener un área para atender mis quejas?", "LPDUSF 50 Bis"),
("¿Hasta cuándo puedo presentar una queja ante la CONDUSEF?", "LPDUSF 65"),
("¿La CONDUSEF me ayuda si me suben la tasa de interés que acordé?", "LPDUSF 61"),
("Si presento mi reclamación, ¿se detiene el plazo para demandar?", "LPDUSF 66"),
("¿La CONDUSEF me puede dar un abogado gratis?", "LPDUSF 85"),
("Si gano el arbitraje, ¿en cuánto tiempo me tiene que pagar la institución?", "LPDUSF 81"),
("¿Qué pasa si no me presento a la audiencia de conciliación?", "LPDUSF 69"),
("¿Cada cuánto se reúne la Junta de Gobierno de la CONDUSEF?", "LPDUSF 19"),
("¿Qué principios debe seguir una empresa que usa mis datos personales?", "LFPDPPP 5"),
("Para usar mis datos de salud, ¿qué tipo de permiso necesitan?", "LFPDPPP 8"),
("¿Qué debe decir un aviso de privacidad?", "LFPDPPP 15"),
("¿En qué casos pueden usar mis datos sin pedirme permiso?", "LFPDPPP 9"),
("¿Qué medidas deben tomar las empresas para proteger mis datos?", "LFPDPPP 18"),
("Hubo un hackeo y se filtraron mis datos, ¿la empresa debe avisarme?", "LFPDPPP 19"),
("¿Quién puede pedir acceso, corrección o cancelación de mis datos?", "LFPDPPP 21"),
]
STOP = set("de la el los las un una y o a en por para con que se su sus del al lo es mi me mis le les como "
"qué cuál cuáles cuándo cuánto cuántos quién puedo pueden puede debe deben tiene tienen si no".split())
def normalizar(t: str) -> str:
t = unicodedata.normalize("NFKD", t.lower())
return "".join(c for c in t if not unicodedata.combining(c))
def tokens(t: str) -> list[str]:
return [w for w in re.findall(r"[a-z0-9]+", normalizar(t)) if w not in STOP and len(w) > 2]
def articulos() -> pd.DataFrame:
filas = []
patron = r"Art[íi]culo\s+(\d+)\s*([oº°]?)\.?\s*((?:Bis(?:\s*\d+)?|Ter|Qu[áa]ter|Quinquies)?)"
for ley in LEYES:
texto = "\n".join(p.extract_text() for p in PdfReader(download(URL.format(ley), f"ley_{ley}.pdf")).pages)
# Quitar encabezados y pies de página del PDF de la Cámara de Diputados
for patron_ruido in [r"LEY (PARA LA|DE PROTECCI|FEDERAL DE PROTECCI)[^\n]*\n?",
r"CÁMARA DE DIPUTADOS DEL H\.\s*CONGRESO DE LA UNIÓN", r"Secretaría General",
r"Secretaría de Servicios Parlamentarios", r"Últimas? [Rr]eformas? (publicadas? )?DOF[^\n]*",
r"Nueva Ley (publicada|DOF)[^\n]*", r"\n\s*\d+\s+de\s+\d+\s*\n"]:
texto = re.sub(patron_ruido, "\n", texto)
for bloque in re.split(r"\n\s*(?=Art[íi]culo\s+\d+)", texto)[1:]:
m = re.match(patron, bloque)
if not m:
continue
num = m.group(1) + m.group(2).replace("º", "o").replace("°", "o")
sufijo = re.sub(r"\s+", " ", m.group(3)).strip().replace("Quáter", "Quater")
art = f"{ley} {num}{(' ' + sufijo) if sufijo else ''}"
limpio = re.sub(r"\s+", " ", bloque).strip()
if "Se deroga" in limpio[:60]:
continue
filas.append({"id": art, "ley": ley, "texto": limpio[:4000]})
return pd.DataFrame(filas).drop_duplicates("id")
def embeddings(textos: list[str], modelo: str, nombre: str) -> np.ndarray:
from fastembed import TextEmbedding
cache = RAW / f"rag_emb_{modelo.split('/')[-1]}_{nombre}_{len(textos)}.npy"
if cache.exists():
return np.load(cache)
E = np.array(list(TextEmbedding(modelo).embed(textos, batch_size=16)))
E /= np.linalg.norm(E, axis=1, keepdims=True)
np.save(cache, E)
return E
def metricas(rankings: list[list[str]], oro: list[str]) -> dict:
pos = [r.index(o) + 1 if o in r else None for r, o in zip(rankings, oro)]
return {"Recall@1": np.mean([p == 1 for p in pos]), "Recall@3": np.mean([p is not None and p <= 3 for p in pos]),
"Recall@5": np.mean([p is not None and p <= 5 for p in pos]),
"MRR": np.mean([1 / p if p else 0 for p in pos])}
def puntajes_densos(arts: pd.DataFrame, qs: list[str], corto: str) -> np.ndarray:
"""Similitud pregunta × artículo con embeddings. Se indexa por fragmentos de ~700 caracteres y cada artículo
toma el puntaje de su mejor fragmento (los artículos largos no se diluyen). La usa también `llm_rag.py`."""
frag_txt, frag_art = [], []
for a, t in zip(arts.id, arts.texto):
for i in range(0, len(t), 600):
frag_txt.append(t[i:i + 700])
frag_art.append(a)
frag_art = np.array(frag_art)
modelo, pq, pd_ = MODELOS_EMB[corto]
sim = embeddings([pq + q for q in qs], modelo, "preg") @ embeddings([pd_ + t for t in frag_txt], modelo, "frag").T
return np.array([[sim[k][frag_art == a].max() for a in arts.id] for k in range(len(qs))])
def aciertos_top3(S: np.ndarray, ids: list[str], oro: list[str]) -> np.ndarray:
return np.array([o in [ids[j] for j in np.argsort(-fila)[:3]] for fila, o in zip(S, oro)], dtype=float)
def run():
arts = articulos()
ids = arts.id.tolist()
faltan = [o for _, o in PREGUNTAS if o not in ids]
assert not faltan, f"Artículos de prueba no encontrados en el corpus: {faltan}"
qs, oro = [q for q, _ in PREGUNTAS], [o for _, o in PREGUNTAS]
# 1) BM25 (léxico): coincidencia de palabras, el estándar de los buscadores
bm25 = BM25Okapi([tokens(t) for t in arts.texto])
s_bm = np.array([bm25.get_scores(tokens(q)) for q in qs])
# 2) Denso: embeddings multilingües (capturan sinónimos: "hackeo" ≈ "vulneración de seguridad").
densos = {corto: puntajes_densos(arts, qs, corto) for corto in MODELOS_EMB}
# 3) Híbridos con Reciprocal Rank Fusion (no requiere calibrar escalas entre puntajes)
def rangos(S):
return np.argsort(np.argsort(-S, axis=1), axis=1) + 1
rrf = lambda A, B: 1 / (60 + rangos(A)) + 1 / (60 + rangos(B))
def top(S, k=10):
return [[ids[j] for j in np.argsort(-fila)[:k]] for fila in S]
sistemas = {"BM25 (léxico)": s_bm, "Denso: MiniLM multilingüe": densos["MiniLM multilingüe"],
"Híbrido RRF: BM25 + MiniLM": rrf(s_bm, densos["MiniLM multilingüe"]),
"Denso: E5-large multilingüe": densos["E5-large multilingüe"],
"Híbrido RRF: BM25 + E5-large": rrf(s_bm, densos["E5-large multilingüe"])}
res = pd.DataFrame([{"Sistema": k, **{m: round(v, 3) for m, v in metricas(top(S), oro).items()}}
for k, S in sistemas.items()])
# Con 33 preguntas la incertidumbre es grande: IC bootstrap de Recall@3 y diferencias PAREADAS
# (mismas preguntas remuestreadas para ambos sistemas), que son mucho más precisas que comparar IC sueltos.
rng = np.random.default_rng(SEED)
idx = rng.integers(0, len(qs), (2000, len(qs)))
hits = {k: aciertos_top3(S, ids, oro) for k, S in sistemas.items()}
res["IC 95 % Recall@3"] = [f"{np.percentile(h[idx].mean(1), 2.5):.2f}–{np.percentile(h[idx].mean(1), 97.5):.2f}"
for h in hits.values()]
mejor = res.loc[res.MRR.idxmax(), "Sistema"]
pareadas = []
for otro in sistemas:
if otro == mejor:
continue
d = (hits[mejor] - hits[otro])[idx].mean(1)
pareadas.append({"Comparación": f"{mejor} − {otro}", "Δ Recall@3": round(float((hits[mejor] - hits[otro]).mean()), 3),
"IC 95 %": f"{np.percentile(d, 2.5):+.2f} a {np.percentile(d, 97.5):+.2f}",
"P(Δ ≤ 0)": round(float((d <= 0).mean()), 3)})
pareadas = pd.DataFrame(pareadas)
S_best = sistemas[mejor]
rk = top(S_best)
detalle = pd.DataFrame([{"Pregunta": q, "Artículo correcto": o, "Top 3 recuperados": " · ".join(r[:3]),
"¿Acierto top 3?": "Sí" if o in r[:3] else "No"} for q, o, r in zip(qs, oro, rk)])
# Curva Recall@k
curva = {k: [[kk, round(float(np.mean([o in r[:kk] for o, r in zip(oro, top(S, 10))])) * 100, 1)]
for kk in range(1, 11)] for k, S in sistemas.items()}
por_ley = detalle.assign(ley=detalle["Artículo correcto"].str.split().str[0]).groupby("ley")["¿Acierto top 3?"] \
.apply(lambda s: (s == "Sí").mean() * 100)
ejemplos = [{"pregunta": q, "oro": o, "recuperados": [{"id": a, "texto": arts.set_index("id").loc[a, "texto"][:700]}
for a in r[:3]]}
for q, o, r in list(zip(qs, oro, rk))[:6]]
llm_path = SITE_DATA / "llm" / "p14-rag-regulatorio.json"
llm = json.loads(llm_path.read_text(encoding="utf-8")) if llm_path.exists() else None
if llm: # la respuesta generada (con citas) se muestra junto a lo recuperado
resp = {f["pregunta"]: f for f in llm["respuestas"]}
for e in ejemplos:
if e["pregunta"] in resp:
e["respuesta"], e["citados"] = resp[e["pregunta"]]["respuesta"], resp[e["pregunta"]]["citados"]
r = res.set_index("Sistema")
vs_bm = pareadas.set_index("Comparación").loc[f"{mejor} − BM25 (léxico)"]
seg = pareadas.loc[pareadas["Δ Recall@3"].idxmin()]
result = {
"title": "Asistente RAG sobre regulación financiera",
"summary": [
f"Corpus: {len(arts)} artículos vigentes de 3 leyes (LTOSF, LPDUSF/CONDUSEF y LFPDPPP), extraídos de los PDF "
f"oficiales y separados por artículo. Prueba: {len(qs)} preguntas en lenguaje de cliente con su artículo correcto.",
f"Mejor recuperación: {mejor} — el artículo correcto aparece en el top 3 en {r.loc[mejor, 'Recall@3']:.0%} de las "
f"preguntas (MRR {r.loc[mejor, 'MRR']:.2f}); BM25 {r.loc['BM25 (léxico)', 'Recall@3']:.0%}, MiniLM "
f"{r.loc['Denso: MiniLM multilingüe', 'Recall@3']:.0%}, híbrido BM25 + MiniLM {r.loc['Híbrido RRF: BM25 + MiniLM', 'Recall@3']:.0%}.",
"Lección: con un modelo de embeddings débil, mezclarlo con BM25 ayuda; con uno fuerte (E5-large, entrenado para "
"búsqueda con prefijos query/passage) el léxico mete ruido y el híbrido empeora. La arquitectura correcta depende "
"del modelo, y solo se sabe midiendo.",
f"Incertidumbre: con {len(qs)} preguntas el IC 95 % de Recall@3 del mejor es {r.loc[mejor, 'IC 95 % Recall@3']}. "
f"La ventaja pareada sobre BM25 ({vs_bm['IC 95 %']}) "
+ ("excluye el cero" if vs_bm['P(Δ ≤ 0)'] < 0.025 else "no excluye el cero")
+ f"; frente al segundo mejor ({seg['Comparación'].split(' − ')[1]}, {seg['IC 95 %']}) "
+ ("también" if seg['P(Δ ≤ 0)'] < 0.025 else "la diferencia no es concluyente") + ". El conjunto de prueba también se usó para "
"elegir, así que la cifra es algo optimista.",
(f"Generación con {llm['modelo']}: cita el artículo correcto en {llm['cita_correcta'] or 0:.0%} de las preguntas "
f"donde fue recuperado; cuando no lo fue, se abstiene en {llm['abstencion_correcta'] or 0:.0%} de los casos y en "
"los demás responde apoyándose en otro artículo recuperado (a veces defendible, a veces no). Riesgo a "
"controlar: en producción se exige cita y un umbral de similitud mínimo antes de responder."
if llm else
"Generación con LLM y citas: lista en `llm_rag.py`; corre en cuanto haya una API key de LLM (Groq, Gemini, "
"OpenRouter, Hugging Face o Anthropic; ver `llm_cliente.py`). Regla de "
"cumplimiento: el LLM solo puede responder con los artículos recuperados y debe citarlos; si no están, "
"debe abstenerse."),
],
"metrics": [
{"label": "Recall@3 (mejor)", "value": f"{r.loc[mejor, 'Recall@3']:.0%}"},
{"label": "MRR (mejor)", "value": f"{r.loc[mejor, 'MRR']:.2f}"},
{"label": "Artículos indexados", "value": f"{len(arts)}"},
{"label": "Preguntas de prueba", "value": f"{len(qs)}"},
],
"charts": [
line("recall", "Recall@k: ¿aparece el artículo correcto entre los primeros k?", curva,
x_label="k (artículos recuperados)", y_label="% de preguntas", fmt="pct"),
bars("mrr", "MRR por sistema de recuperación", res.Sistema, res.MRR.values, fmt="dec"),
bars("ley", f"Recall@3 por ley ({mejor})", por_ley.index, por_ley.values.round(1), fmt="pct"),
],
"tables": [table("Comparación de sistemas de recuperación", res),
table(f"Diferencias pareadas de Recall@3 contra {mejor} (bootstrap por pregunta)", pareadas),
table("Detalle por pregunta (mejor sistema)", detalle)],
"ejemplos": ejemplos,
"card": model_card(
"Recuperación BM25, densa (MiniLM y E5-large multilingües, ONNX local) e híbrida por Reciprocal Rank Fusion sobre "
"artículos de ley; generación con LLM condicionada a los artículos recuperados.",
[f"{len(qs)} preguntas de prueba escritas a mano con su artículo correcto, en lenguaje de cliente.",
"Recall@1/3/5 y MRR; desglose por ley y detalle de cada pregunta."],
["La respuesta correcta está en un solo artículo (algunas preguntas tienen respuestas repartidas)."],
["El conjunto de prueba es pequeño y lo escribió el autor: hay que ampliarlo con preguntas reales de clientes.",
"Las leyes cambian: el índice debe regenerarse con cada reforma publicada en el DOF."]),
}
corpus = arts.assign(texto=arts.texto.str.slice(0, 1200))
export("p14-rag-regulatorio", result, corpus)
return result
if __name__ == "__main__":
run()