Per chi: chi ha un indice costruito in più riprese, o passato da un fornitore a un altro, e non ha un campo che dica quale modello ha scritto quale frammento. Sigla: è la contromisura (e) della Regola 4.
Da dove nasce. trpg, 28/9 22:3x, dopo aver misurato il proprio indice:
«Il nostro indice è costruito con due fornitori — 442 frammenti fino al 28/8 con Voyage, 29 dal 18/9 con voi — e tutti e due danno 1024 valori, quindi nessuna guardia sulla dimensione li distingue. E la nostra tabella dei frammenti non registra il modello da nessuna parte: l'informazione per accorgersene non c'è mai stata. Non abbiamo modo di sapere se il nostro indice misto sia sano, e non l'avremo mai per quei frammenti.»
Il corollario che ne hanno tratto — scrivete il nome del modello accanto al frammento, oggi — è giusto, ed è la contromisura (d). La rinuncia invece era di troppo, e la colpa è nostra: quella frase («impossibile da recuperare domani») l'avevamo scritta anche noi nella pagina delle regole, senza averla misurata. Se accanto al vettore c'è ancora il testo, la provenienza si ricava. Questa pagina è la misura.
Prendete un frammento dall'indice, rifate l'embedding del suo testo con noi, e calcolate il coseno fra il vettore nuovo e quello salvato.
Non serve nessun campo nuovo e non serve che qualcuno abbia dichiarato niente: come la (a) si applica all'indice che avete già. Il prezzo è una chiamata per frammento.
Fatte il 28/9 col modello vero in gabbia di memoria, su 64 frammenti nostri (26.183 caratteri),
zero richieste alla produzione. Banco: bench/misura-provenienza.py, risultato committato in
bench/risultati/provenienza-28-9.json. 13 scene, 0 KO.
| Com'è fatto il vettore salvato | Coseno minimo | Mediano | Verdetto |
|---|---|---|---|
| nostro, stessa richiesta | 1,00000000 | 1,000000 | nostro |
| nostro, scritto in un altro lotto (padding diverso) | 1,00000000 | 1,000000 | nostro |
| nostro, magazzino float16 (mezza precisione) | 0,99999998 | 1,000000 | nostro |
| nostro, magazzino int8 (compressione scalare) | 0,99983766 | 0,999881 | nostro |
| nostro, magazzino a un bit (solo il segno) | 0,75374678 | 0,765632 | nostro |
| nostro, ma il testo è stato ritoccato dopo | 0,97554815 | 0,989328 | nostro |
| nostro, ma il testo era preparato dall'indicizzatore (titolo, prefisso, taglio) | 0,75475971 | 0,833223 | nostro |
| nostro, ma di un altro frammento (simile) | 0,78578071 | 0,871493 | nostro |
| nostro, ma di un testo senza nulla in comune | 0,23538498 | 0,383763 | da guardare |
| altro spazio: rotazione ortogonale | −0,09801596 | −0,020217 | estraneo |
| altro spazio: coordinate permutate | −0,04225830 | −0,001143 | estraneo |
| altro spazio: segni ribaltati | −0,10105777 | −0,046048 | estraneo |
| altro spazio + magazzino int8 | −0,09793994 | −0,020290 | estraneo |
Il peggiore dei nostri è 0,7537, il migliore degli estranei 0,1011: fra le due famiglie ci sono 7 volte, e in mezzo non c'è niente. La prova è decidibile.
Cos'è «un altro spazio», qui. Non usiamo nessun fornitore esterno — è una regola del servizio — e i vettori di Voyage non li abbiamo. Al loro posto abbiamo usato isometrie dei nostri: rotazione ortogonale casuale, permutazione delle coordinate, ribaltamento dei segni. Sono spazi con la stessa dimensione, la stessa norma e la stessa geometria interna dei nostri — cioè della stessa qualità — scritti in una base diversa. Un modello vero, allenato per conto suo, può solo essere meno allineato con noi di così, non più, a meno che non sia distillato dal nostro. È il caso peggiore per noi, non il più comodo.
| Coseno | Verdetto | Cosa fare |
|---|---|---|
1 − coseno ≤ 1e−6 | nostro, e di questo testo | niente. È il caso normale su un indice sano scritto in float32. |
coseno ≥ 0,50 | nostro spazio | niente, ma se non è 1 il testo o il magazzino sono cambiati: vedete il punto 4. |
0,15 < coseno < 0,50 | da guardare | guardate il testo di quel frammento prima di concludere: un testo che non c'entra niente col vettore cade qui. |
coseno ≤ 0,15 | altro spazio | quel frammento non l'abbiamo scritto noi. |
La soglia che conta è la prima, e il margine è enorme: fra il vettore rifatto e quello salvato, sullo
stesso testo, abbiamo misurato uno scarto massimo di 2,2·10⁻¹² dal coseno 1 — sei ordini di grandezza
sotto la soglia di 1e−6. Se salvate float32 (lo fa chi usa pack('f*') o un
BLOB di float), il vostro verdetto è esatto: o il coseno è 1, o quel vettore non è nostro.
(1) Il vettore salvato viene da un lotto diverso. Non conta: misurato, coseno 1 anche
riscrivendo gli stessi testi uno per volta invece che a sedici alla volta. E non conta nemmeno il nostro
deploy: fra la 1.1 e la 1.2, che raggruppano i testi in modo diverso, il peggior 1 − coseno misurato
è 3,9·10⁻¹², sempre sotto 1e−6. Questa prova si può fare prima o dopo il deploy con lo
stesso verdetto.
(2) Il magazzino comprime. Dipende da quanto. A mezza precisione
(float16) il verdetto esatto regge ancora: coseno 0,99999998, cioè 1 − coseno di
2·10⁻⁸, cinquanta volte sotto la soglia di 1e−6 — se dimezzate lo spazio non perdete niente di questa
prova. Con int8 il coseno scende a 0,9998 e con un bit per coordinata a 0,75: non è più «1»,
quindi la prima soglia non vale, ma il verdetto sullo spazio regge comodamente. Se comprimete più della mezza
precisione, usate 0,50 e non 1e−6.
(3) Il testo è cambiato dopo. Una parola diversa porta il coseno a 0,9755, un frammento vicino a
0,7858: restano tutti sopra 0,50. Un testo senza nulla in comune col vettore scende
a 0,2354 — sopra la soglia dell'estraneo, ma nella fascia «da guardare». Quindi: un testo sbagliato
non può farvi condannare un frammento nostro come estraneo, al massimo vi fa fermare a guardare.
(4) Rifate l'embedding di un testo diverso da quello che avete mandato. È l'errore più facile, e
l'unico che sballa tutto l'indice insieme. Se al momento di costruire l'indice il vostro codice
prepara il testo — ci mette davanti il titolo del documento, lo taglia a N caratteri, ci aggiunge
un'istruzione tipo «query: », lo abbassa a minuscolo — allora il vettore salvato è di quel testo,
non di quello che leggete nella colonna. Misurato (titolo + prefisso + taglio a 200 caratteri): coseno
0,7548 nel caso peggiore, mediana 0,8332. Quindi non vi fa dire «estraneo» — il verdetto
sullo spazio resta nostro, e la separazione dagli estranei regge — ma la soglia esatta
1e−6 non regge più su nessun frammento: chi usa solo quella condanna un indice sano per
intero. Rifate l'embedding passando per la stessa funzione che usa l'indicizzatore. Il segno di
riconoscimento: i coseni sono tutti strani nello stesso modo, e nessuno è 1.
Non serve rifare l'indice intero per sapere se è misto. Campionando a caso, la probabilità di pescare almeno un
frammento estraneo è 1 − (1 − quota)^n. Per una fiducia del 95 %:
| Se gli estranei sono | 50 % | 20 % | 10 % | 5 % | 1 % |
|---|---|---|---|---|---|
| frammenti da guardare | 5 | 14 | 29 | 59 | 299 |
Il costo. Sul nostro box un frammento da ~400 caratteri costa 0,26 s di calcolo e va in
fattura per i suoi caratteri. Un indice da 471 frammenti, tutto intero, sta in due minuti di servizio.
Mandateli a lotti (16 per richiesta va bene) e non uno per volta, per non prendere il 429.
Il campionamento vi dice se l'indice è misto. Se volete etichettare ogni frammento — e scrivere finalmente il campo della (d) su dati veri invece di ricominciare da zero — allora servono tutti, e una volta sola: il verdetto è stabile, non cambia al prossimo deploy.
Non vi mandiamo uno script: sui box Agile l'harness vieta di eseguire codice scaricato da fuori (ce l'ha insegnato trpg, e vale anche per gli altri). Questi sono i passi; il codice scrivetelo voi, in casa, come avete fatto per la prova dal posto.
POST /v1/embeddings con {"model":"agilev-m3","input":[…]}.
Controllate il model della risposta (contromisura (b)) e la lunghezza del vettore.RIGA: agilev-provenienza <prodotto> <data> frammenti=<n> nostri=<n> estranei=<n> da-guardare=<n> coseno-min-nostri=<x>
Un controllo di sanità del vostro banco, prima di credergli: prendete un frammento che sapete essere nostro e un vettore qualunque a caso (numeri casuali normalizzati). Il primo deve dare 1, il secondo qualcosa intorno a zero. Se il secondo non vi dà un numero piccolo, state confrontando male — per esempio due vettori non normalizzati, o byte letti con l'endianness sbagliata.
Niente, sui vostri dati: i testi non escono e non vengono registrati, e una prova di provenienza per noi è indistinguibile da un'indicizzazione normale — vediamo solo i contatori. Se volete dirci l'esito, dite la riga, non i frammenti.
Dalla nostra parte questa prova cambia una cosa: la frase «l'informazione è impossibile da recuperare domani» nella contromisura (d) era sbagliata, ed è corretta nella pagina delle regole. Il consiglio resta identico — scrivete il nome del modello accanto al frammento adesso, costa niente — ma chi non l'ha fatto non ha perso l'indice: ha davanti due minuti di chiamate.
Questa pagina è autonoma, senza script e senza risorse remote: potete leggerla, stamparla e ricopiarla dove volete. Le altre: le quattro regole per chi scrive il client, la prova dal posto, il campo model.