Un assistente IA sul manuale d'officina della mia Lexus CT200h
Un sistema RAG che risponde alle domande sul manuale d'officina di un'auto citando le pagine: ricerca ibrida, alberi diagnostici, collegamenti elettrici ricostruiti dagli schemi e ricerca web dei difetti noti.
Indice
- Cos'è un RAG, in due righe
- Il materiale
- Perché la ricerca per significato sbaglia i codici
- Riordinare i candidati
- Il manuale non è testo piatto
- Ricostruire i collegamenti dagli schemi
- Far leggere, non solo recuperare
- Il manuale non basta: la ricerca web
- Problemi e soluzioni
- Come verifico le modifiche
- Limiti
- Stack
In breve
- Ho costruito un assistente che risponde a domande sul manuale d'officina della mia Lexus CT200h: circa 13.500 file in inglese, tra pagine HTML, schemi elettrici e PDF.
- Il modello linguistico da solo non basta: bisogna prima trovargli le pagine giuste. La parte difficile è quella, non la generazione del testo.
- I codici (errori, connettori, pin) si cercano per parola esatta, le descrizioni per significato. Servono entrambe le ricerche.
- Negli schemi elettrici i collegamenti non sono scritti da nessuna parte: li ho ricostruiti dalla geometria del disegno.
- Il manuale dice come verificare un guasto, ma non quali guasti capitano davvero: per quello serve la ricerca web, su bollettini tecnici e forum.
- Ogni affermazione della risposta cita la pagina da cui viene, manuale o web, così si può controllare.
Ho il manuale d'officina e gli schemi elettrici della mia Lexus CT200h, una compatta ibrida del marchio di lusso di Toyota. Consultarli a mano è lento, e le domande che mi faccio di solito non sono «cosa c'è a pagina 412» ma «ho il codice P0A80, da dove comincio?» oppure «a cosa è collegato il pin 42 della centralina ABS, e che tensione dovrei misurarci?».
CarManualRAG è il prototipo che ho scritto per rispondere a domande di questo tipo. Gira in locale, in Python. Per la generazione usa un qualsiasi servizio compatibile con le API di OpenAI: un modello locale in LM Studio (un programma per far girare modelli linguistici sul proprio computer) oppure un servizio cloud, senza cambiare nulla nel resto del sistema.
Cos'è un RAG, in due righe
RAG sta per retrieval-augmented generation: prima si cercano i documenti pertinenti, poi si passano al modello linguistico insieme alla domanda, e il modello risponde basandosi su quelli. Un modello linguistico da solo sa molte cose sulle auto in generale, ma non conosce la tensione attesa su un pin specifico della mia centralina ABS. Se non gli si dà la pagina giusta, inventa un valore plausibile.
Un paragone. È la differenza tra chiedere a un meccanico esperto di rispondere a memoria e chiedergli di rispondere con il manuale aperto davanti. Il RAG è il pezzo che apre il manuale alla pagina giusta. Se la pagina è sbagliata, anche il meccanico migliore sbaglia.
Quasi tutto il lavoro di questo progetto sta nella ricerca, non nella generazione.
Il materiale
Il manuale arriva come un sito HTML statico: 5.480 pagine, alcune delle quali contengono schemi elettrici in SVG, più 24 PDF e 8.020 immagini. È tutto in inglese, mentre io faccio le domande in italiano.
Il parser spezza le pagine in blocchi di testo di al massimo 1.800 caratteri (i chunk), e a ognuno assegna un tipo di documento. Alla fine l'indice contiene 28.114 chunk, distribuiti così:
| Tipo di pagina | Chunk | Cosa contiene |
|---|---|---|
| Procedure per codice di errore (DTC) | 10.081 | cosa fare, passo per passo |
| Generali | 4.951 | pagine che non rientrano negli altri tipi |
| Connettori | 4.186 | forma del connettore, numerazione dei pin |
| Riparazione | 3.698 | smontaggio, rimontaggio, coppie di serraggio |
| Ispezione circuiti | 1.233 | verifiche su un singolo circuito |
| Tabelle dei sintomi | 936 | sintomo → aree sospette, in ordine di probabilità |
| Descrizione sistemi | 832 | come funziona un sistema |
| Data list | 682 | valori letti con lo strumento diagnostico |
| Terminali delle centraline (ECU) | 452 | pinout delle centraline con i valori attesi |
| Componenti, tabelle DTC, specifiche, schemi, posizione parti, PDF | 1.063 | il resto |
Il tipo serve a restringere la ricerca: se la domanda riguarda un pin, non ha senso cercare tra le procedure di smontaggio.
Perché la ricerca per significato sbaglia i codici
Il primo tentativo è quello ovvio: calcolare per ogni chunk un embedding, cioè un vettore di numeri che ne rappresenta il significato, e cercare i chunk con il vettore più vicino a quello della domanda. Uso BGE-M3, un modello di embedding multilingue.
Con le domande descrittive («la radio non rileva il freno a mano») funziona bene. Sbaglia proprio dove serve precisione. Per un modello di embedding P0A80 e P0A8F sono quasi la stessa cosa, e un connettore come H164-15 o un segnale come CANH sono poco più che rumore. Ma in un manuale d'officina la differenza tra due codici è tutto.
Per questo la ricerca è ibrida. Accanto al vettore, ogni chunk ha un campo testuale con i codici che contiene: codici di errore nella forma standard della diagnosi di bordo OBD (una lettera tra P, B, C, U seguita da quattro caratteri), connettori con il numero di pin, sigle dei segnali. Li estraggo con regole volutamente conservative: preferisco perdere qualche codice piuttosto che riempire l'indice di falsi positivi. Quel campo viene indicizzato con BM25, l'algoritmo classico dei motori di ricerca testuali, che premia le corrispondenze esatte e le parole rare.
In pratica. La ricerca per significato trova «la pagina che parla di rumori dal tetto» anche se le parole sono diverse. La ricerca per parola esatta trova «la pagina che contiene P0A80» e nessun'altra. Le due liste di risultati vengono unite.
LanceDB fa entrambe le cose nello stesso database su file, senza un server da gestire: vettori da una parte, indice full-text Tantivy dall'altra.
Riordinare i candidati
La ricerca ibrida è veloce ma grossolana. Per ogni domanda restituisce una trentina di candidati, e il primo non è sempre il migliore. Un secondo modello, un cross-encoder (bge-reranker-v2-m3), li rilegge uno per uno insieme alla domanda e assegna a ciascuno un punteggio di pertinenza tra 0 e 1. Ne tengo gli otto migliori.
Il cross-encoder è molto più preciso della ricerca vettoriale perché guarda domanda e documento insieme, invece di confrontare due vettori calcolati separatamente. È anche molto più lento, per questo lo si usa solo su pochi candidati e non su tutto l'indice.
Il manuale non è testo piatto
Le procedure diagnostiche sono alberi. Ogni passo dice cosa misurare e prevede due esiti: OK (il valore è nella norma) e NG (no good, fuori norma). Ciascun esito porta a un passo diverso.
Se si trasforma una procedura in paragrafi e la si spezza in chunk, il modello vede frammenti dei due rami e finisce per mescolarli: consiglia di sostituire il sensore quando la misura aveva detto che il problema era il filo. Il parser invece salva la struttura dell'albero, con i collegamenti OK e NG di ogni passo. Il modello può chiedere la procedura di un codice e poi avanzare un passo alla volta, dicendo quale esito ha ottenuto.
Allo stesso modo, le tabelle dei terminali delle centraline vengono ricomposte: una tabella lunga finisce spezzata anche in una ventina di chunk, e per rispondere su un pin serve la riga giusta con intestazioni e unità di misura.
Ricostruire i collegamenti dagli schemi
Negli schemi elettrici la connettività non è scritta da nessuna parte. Nel file SVG i connettori sono rettangoli, i pin sono coordinate, i fili sono linee spezzate colorate e le etichette sono testi sparsi sul foglio. Una persona guarda il disegno e capisce subito dove va un filo. Un sistema che legge solo il testo non ha modo di rispondere a «a cosa è collegato questo pin?», che però è metà della diagnosi elettrica.
La ricostruzione è puramente geometrica. Per ogni filo il parser prende i due estremi e cerca il connettore più vicino, misurando la distanza dal suo rettangolo. Poi cerca le etichette di testo entro un piccolo raggio da ciascun estremo: il numero del pin, la sigla del colore del filo secondo la convenzione Toyota e il nome del segnale. Il risultato è una netlist, cioè l'elenco dei collegamenti pin per pin.
Funziona con i fili che terminano direttamente su un connettore, che sono tra il 45 e il 60% dei fili di ogni schema. Le giunzioni intermedie, dove un filo si divide in più rami, non le segue ancora.
Il controllo più utile è stato incrociare due fonti indipendenti. La tabella dei terminali della centralina ABS dice che il pin 42 è SKS, l'ingresso del sensore di corsa del pedale freno, con il suo valore atteso. La netlist ricostruita dallo schema dice che il pin 42 va al connettore A29, cioè proprio al sensore di corsa del pedale. Le due informazioni arrivano da pagine diverse e da parser diversi, e coincidono. Quando l'agente le usa insieme può dare un'indicazione misurabile: quale pin, verso quale connettore, di che colore è il filo, che valore aspettarsi.
Far leggere, non solo recuperare
Il RAG classico mette nel prompt i primi otto chunk e chiede al modello di rispondere. Io invece passo al modello una lista di documenti candidati e una serie di strumenti, e lascio che sia lui a decidere cosa leggere.
| Strumento | Cosa fa |
|---|---|
| Ricerca | cerca nell'indice, eventualmente solo in un tipo di pagina |
| Lettura | legge una pagina o uno schema per intero |
| Procedura DTC | restituisce l'albero diagnostico di un codice di errore |
| Passo successivo | avanza nell'albero dato l'esito OK o NG |
| Connettore | restituisce i collegamenti pin per pin di un connettore |
| Terminali ECU | restituisce il valore atteso su un pin di una centralina |
| Sintomo | dalla tabella dei sintomi, le aree sospette in ordine |
| Immagine | mostra lo schema renderizzato ai modelli che vedono le immagini |
| Ricerca web | bollettini tecnici e difetti noti, fuori dal manuale |
Il motivo è semplice: un chunk piccolo è ottimo per trovare la pagina giusta, ma spesso non contiene i prerequisiti di una procedura o le condizioni di misura, che stanno all'inizio della pagina. Leggendo la pagina completa quel contesto torna disponibile.
Ogni affermazione della risposta porta tra parentesi l'identificativo del documento da cui proviene. Nell'interfaccia web, cliccando la citazione si apre la pagina originale accanto alla chat, con il punto citato evidenziato. Le fonti web hanno citazioni dello stesso tipo. È la parte che rende lo strumento usabile: non devo fidarmi della risposta, posso controllarla in due secondi.
L'agente ha un limite di 40 passi per domanda. Ho provato Gemma di Google in locale con LM Studio, e DeepSeek V4 Flash e MiMo v2.5 di Xiaomi tramite le rispettive API. Il backend è lo stesso per tutti; cambiano la qualità del ragionamento, la tendenza a usare gli strumenti e la possibilità di guardare gli schemi come immagini.
Il manuale non basta: la ricerca web
Il manuale d'officina descrive come l'auto dovrebbe funzionare e come si verifica ogni componente. Non dice quali guasti capitano davvero, con che frequenza, e dopo quanti chilometri. Quell'informazione sta altrove: nei bollettini tecnici del costruttore (i TSB), nei forum dei proprietari, nelle discussioni dei meccanici. Per una diagnosi è spesso la parte più utile, perché dice da dove conviene cominciare.
Per questo l'agente ha anche uno strumento di ricerca web. Usa DuckDuckGo, quindi non serve nessuna chiave API, e restituisce i primi cinque risultati. Nel prompt ho diviso i compiti in modo esplicito:
| Fonte | Cosa ci si trova | Esempio di domanda |
|---|---|---|
| Manuale (indice locale) | specifiche, procedure, valori attesi, schemi | «che tensione devo misurare al pin 42?» |
| Web | difetti ricorrenti, bollettini tecnici, esperienze reali | «questo sintomo è un problema noto su questo motore?» |
Le due fonti si completano. Il web suggerisce l'ipotesi più probabile; il manuale dice come verificarla con una misura. Una risposta buona usa entrambe: «è un difetto noto, ecco la procedura del manuale per confermarlo».
L'esempio che mi ha convinto è quello della vibrazione del motore a freddo. Nel manuale ci sono tutte le procedure per controllare accensione, iniezione e supporti motore, ma nessuna pagina dice che su questo motore la guarnizione della testata è un punto debole noto. Sul web invece se ne parla parecchio. Quando il modello ha iniziato a usare la ricerca web è arrivato a quell'ipotesi, e da lì si passa alle verifiche del manuale.
Un paragone. Il manuale è il libretto di istruzioni; il web è il meccanico anziano che dice «su quel modello succede sempre questo». Servono tutti e due: il primo dice come controllare, il secondo da dove partire.
Il web però è una fonte meno affidabile del manuale, quindi deve restare riconoscibile. Il modello deve indicare la provenienza di ogni informazione: manuale, web o esperienza generale. Le fonti web hanno una citazione propria, numerata, come quelle del manuale. Molti siti non si lasciano mostrare dentro un'altra pagina, quindi cliccando la citazione l'interfaccia apre una scheda di anteprima, con titolo, un estratto del testo e il link all'originale.
Anche qui c'è stato un bug istruttivo. La numerazione delle fonti web ripartiva da 1 a ogni messaggio: al secondo turno «web2» indicava una pagina diversa da quella del primo turno, e le citazioni dei messaggi precedenti puntavano alla fonte sbagliata. Ora la numerazione continua per tutta la conversazione.
Problemi e soluzioni
I problemi emersi durante lo sviluppo sono la parte più utile da raccontare, perché si ripresentano in quasi tutti i sistemi RAG.
Il modello che risponde a memoria
Con i modelli piccoli il primo problema era che non usavano gli strumenti. Dai log si vedeva il modello «pianificare» una ricerca nella tabella dei sintomi e poi rispondere a memoria, basandosi sui frammenti dei candidati che aveva già davanti. Su una domanda sulle vibrazioni del motore a freddo non arrivava mai al difetto noto della guarnizione della testata, che invece è ben documentato online.
La correzione è stata doppia. Nel prompt ho descritto cosa c'è nell'indice locale (specifiche e procedure esatte) e cosa si trova sul web (difetti reali e bollettini), perché il modello non sapeva cosa stava interrogando. E se la prima risposta arriva senza nessuna chiamata agli strumenti, viene scartata e il modello viene sollecitato una volta a verificare sulle fonti.
Questa correzione ha introdotto un bug a sua volta. All'inizio il controllo scattava a ogni messaggio, non solo al primo. Così, al secondo o terzo turno, quando il modello rispondeva legittimamente senza strumenti (per esempio per escludere un'ipotesi o fare una domanda), la risposta spariva a metà. Ora vale solo per la prima risposta di una conversazione.
I candidati fuori tema
Il manuale è in inglese. Una domanda vaga in italiano, cercata così com'è, restituiva pagine senza alcun legame con la domanda, per esempio sul servosterzo o sull'immobilizzatore, e i modelli piccoli si lasciavano trascinare. Il reranker però distingueva bene: le pagine fuori tema prendevano punteggi vicini a zero, quelle pertinenti tra 0,5 e 0,99. Adesso i candidati mostrati al modello devono superare una soglia di 0,05. Se nessuno la supera, il modello non riceve nessuna lista e viene invitato a cercare con gli strumenti, invece di partire da una lista fuorviante.
Il tettuccio che diventava il sistema ibrido
Prima della ricerca, una chiamata al modello riscrive la domanda in parole chiave inglesi, lasciando intatti i codici. A un certo punto la domanda «tettuccio in vetro che vibra» restituiva la pagina Loud Rattle del sistema ibrido.
Il reranker non c'entrava: dava 0,90 a una pagina pertinente e 0,00 al rumore. Il problema stava prima. La riscrittura produceva sunroof, una parola che nel manuale non compare (il manuale dice sliding roof), e aggiungeva Lexus CT200h rattle. Siccome cercavo solo con la domanda riscritta, le pagine del tetto non entravano nemmeno tra i candidati, e il reranker non aveva niente da recuperare. La domanda originale in italiano, invece, le trovava al primo posto, perché BGE-M3 è multilingue.
La correzione ha due parti. Ora cerco sia con la domanda riscritta sia con quella originale, unisco i due insiemi di candidati e riordino l'unione. Nel prompt di riscrittura ho inserito il vocabolario del manuale con i sinonimi comuni («tettuccio» diventa sliding roof sunroof moonroof) e ho tolto marca e modello, che in un corpus dedicato a una sola auto non restringono niente.
Una lista fissa di parole da scartare non funziona: «F-Sport» (l'allestimento sportivo) è rumore in una domanda sul tetto, ma è essenziale in una domanda sulle sospensioni; «LED» è rumore nella prima e fondamentale in una domanda sui fari. Ora la pertinenza la decide la riscrittura, domanda per domanda.
La lezione vale anche fuori da questo progetto: se un documento non entra tra i candidati, nessun reranker lo può recuperare. Quando il risultato è sbagliato, conviene controllare il recall (quanti documenti giusti sono arrivati fino al riordino) prima di mettere mano al riordino.
Il prompt che vedeva guarnizioni ovunque
Il caso della vibrazione a freddo, usato come test, era finito nel prompt di sistema come esempio di ragionamento: freddo, quindi guarnizione della testata o condensa. Un esempio così specifico orienta il modello più di qualsiasi istruzione generale: ogni diagnosi tendeva verso il motore, anche quelle sul tettuccio, sui freni o sul climatizzatore.
Ho sostituito l'esempio con un principio generale: lasciare che siano le condizioni (a freddo o a caldo, sulle buche, sotto carico, con la pioggia, graduale o improvviso, con o senza codice di errore) a indicare il tipo di guasto. Poi ho rilanciato domande su sistemi diversi per verificare che ciascuna restasse nel suo ambito.
È un errore facile da fare con i prompt: si corregge un caso e si peggiorano tutti gli altri, senza che niente lo segnali.
Come verifico le modifiche
La verifica si basa su un insieme di domande di riferimento che già funzionavano. Dopo ogni modifica alla ricerca controllo che il documento giusto resti in cima, e con quale punteggio del reranker:
| Domanda di riferimento | Punteggio del primo documento |
|---|---|
| Codice P0A80 | 0,86 |
| Pin 42 della centralina ABS A61 | 0,96 |
| Luci diurne (DRL) | 0,84 |
| La radio non rileva il freno a mano | 0,31 |
L'ultimo punteggio è più basso perché la domanda è descrittiva e la pagina usa parole diverse. Non conta il valore assoluto: conta che questi numeri non peggiorino tra una versione e l'altra. Per le risposte complete dell'agente, invece, il controllo resta manuale: leggo la risposta e apro le citazioni.
Limiti
- La netlist ricostruisce solo una parte dei collegamenti.
- Non c'è una valutazione sistematica: ho domande di regressione, non un benchmark.
- I PDF composti quasi solo da disegni richiedono un modello che veda le immagini.
- Le sessioni restano in memoria sul server; lo storico delle conversazioni è salvato nel browser.
È un prototipo per capire come si cerca in una documentazione tecnica complessa, non uno strumento per diagnosticare un'auto al posto di un'officina. Codice e manuali non sono pubblici.
Stack
| Parte | Scelta |
|---|---|
| Embedding | BGE-M3 con sentence-transformers (MPS su Apple Silicon) |
| Reranking | bge-reranker-v2-m3 (cross-encoder) |
| Indice | LanceDB: vettori + full-text Tantivy, su file |
| Generazione | qualsiasi endpoint compatibile OpenAI (LM Studio o cloud) |
| Parsing | BeautifulSoup, PyMuPDF, cairosvg per il rendering degli SVG |
| Interfaccia | FastAPI + HTML/JS senza build, risposte in streaming (SSE) |