Interrogare, recuperare e generare
Scriverà la pipeline di query che incorpora la domanda dell'utente, recupera i chunk principali, formatta un prompt aumentato, chiama l'LLM e restituisce una risposta con citazioni.
Interrogare, recuperare e generare è una lezione AI Engineering Academy gratuita su CoddyKit. Questa è la lezione 4 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento AI Engineering Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso AI Engineering Academy include 4 lezioni in totale.
La pipeline di query: dall'inizio alla fine
La pipeline di query è la parte online di RAG: il codice che viene eseguito in tempo reale quando un utente pone una domanda. Collega tutti i componenti creati durante l'indicizzazione: il modello di embedding, il vector store, il template del prompt e l'LLM. Una pipeline di query ben implementata si completa in meno di 500 ms per la maggior parte dei carichi di lavoro e produce risposte basate sulle fonti e corredate da citazioni. In questa lezione creeremo ogni passaggio da zero.
Passaggio 1: incorporare la query dell'utente
Il primo passaggio consiste nel convertire la domanda in linguaggio naturale dell'utente in un embedding vettoriale utilizzando lo stesso modello usato durante l'indicizzazione. Questo embedding codifica il significato semantico della domanda e verrà confrontato con gli embedding dei chunk dei documenti nel vector store. Mantenga rapido questo passaggio: utilizzi un modello leggero come text-embedding-3-small e memorizzi nella cache gli embedding per le query identiche ripetute.
from openai import OpenAI
client = OpenAI()
def embed_query(question: str) -> list:
response = client.embeddings.create(
model='text-embedding-3-small',
input=[question]
)
return response.data[0].embedding
user_question = 'What is our remote work policy?'
query_vector = embed_query(user_question)
print(f'Query embedded: {len(query_vector)}-dim vector')Passaggio 2: recuperare i chunk Top-K
Invii il vettore della query al vector store per trovare i K chunk semanticamente più simili. Le corrispondenze restituite sono ordinate in base al punteggio di similarità coseno (in genere da 0,0 a 1,0; più alto è meglio). Il valore ideale di K bilancia la ricchezza del contesto con il costo della context window: K=5 è un punto di partenza comune. Può anche applicare qui i filtri sui metadati per limitare il recupero a un reparto, un tipo di documento o un intervallo di date specifico.
def retrieve_chunks(query_vector, index, top_k=5, filters=None):
query_params = {
'vector': query_vector,
'top_k': top_k,
'include_metadata': True
}
if filters:
query_params['filter'] = filters
results = index.query(**query_params)
chunks = []
for match in results.matches:
chunks.append({
'score': match.score,
'text': match.metadata['text'],
'source': match.metadata.get('source', ''),
'page': match.metadata.get('page', '')
})
return chunksPassaggio 3: filtrare in base alla soglia del punteggio
Non tutti i chunk recuperati sono realmente pertinenti: alcuni possono avere punteggi di similarità bassi, ma classificarsi comunque tra i Top-K perché la query non rientra nella copertura dell'indice. Applichi una soglia minima del punteggio per eliminare le corrispondenze con bassa confidenza. Se tutti i chunk recuperati sono al di sotto della soglia, restituisca una risposta del tipo "nessuna informazione trovata" invece di inviare contesto irrilevante all'LLM, che produrrebbe una risposta peggiore di un rifiuto formulato correttamente.
MIN_SCORE_THRESHOLD = 0.75
def filter_by_score(chunks, threshold=MIN_SCORE_THRESHOLD):
relevant = [c for c in chunks if c['score'] >= threshold]
if not relevant:
print(f'No chunks above threshold {threshold}. Scores: {[c["score"] for c in chunks]}')
return relevant
retrieved = retrieve_chunks(query_vector, index, top_k=5)
filtered = filter_by_score(retrieved)
if not filtered:
print('Responding: no relevant information found')Passaggio 4: formattare il blocco di contesto
Assembli i chunk recuperati in un blocco di contesto strutturato che l'LLM possa leggere. Indichi la fonte di ogni chunk, così il modello potrà citarla accuratamente. Inserisca un separatore tra i chunk per maggiore chiarezza. Mantenga il contesto complessivo entro il proprio budget di token: conti i token con tiktoken e tronchi o elimini i chunk con punteggio più basso se supera il limite. Il blocco di contesto viene inserito nel prompt tra l'istruzione di sistema e la domanda dell'utente.
def format_context(chunks):
parts = []
for i, chunk in enumerate(chunks, start=1):
source_label = chunk['source']
if chunk.get('page'):
source_label += f", page {chunk['page']}"
parts.append(
f'[Document {i} | Source: {source_label}]\n{chunk["text"]}'
)
return '\n\n---\n\n'.join(parts)
context = format_context(filtered)
print(f'Context block: {len(context)} characters')Passaggio 5: creare il prompt aumentato
Combini il blocco di contesto, l'istruzione di sistema e la domanda dell'utente nel prompt finale. Il messaggio di sistema indica al modello di utilizzare solo il contesto fornito e di citare le fonti. Il messaggio dell'utente contiene il contesto formattato seguito dalla domanda. Questa netta separazione impedisce al modello di confondere il contenuto del contesto con la domanda e rende inequivocabile il confine tra i dati recuperati e l'input dell'utente.
def build_prompt(question, context):
system_message = (
'You are a helpful assistant. Answer the question using ONLY '
'the information in the provided documents. '
'Cite the document number(s) used, like [Doc 1]. '
'If the documents do not contain the answer, say so.'
)
user_message = (
f'Documents:\n\n{context}\n\n'
f'Question: {question}'
)
return system_message, user_messagePassaggio 6: chiamare l'LLM e ottenere la risposta
Invii il prompt assemblato all'LLM utilizzando la Chat Completions API. Utilizzi una temperatura bassa (da 0,0 a 0,3) per le domande e risposte fattuali, così da ottenere risposte coerenti e basate sulle fonti. Temperature più alte producono risposte più creative, ma aumentano il rischio che il modello aggiunga informazioni non presenti nel contesto. Analizzi la risposta e restituisca sia il testo della risposta sia le fonti recuperate, in modo che l'applicazione possa mostrare le citazioni all'utente.
def generate_answer(question, context, sources):
system_msg, user_msg = build_prompt(question, context)
response = client.chat.completions.create(
model='gpt-4o',
temperature=0.1, # low temperature for factual Q&A
messages=[
{'role': 'system', 'content': system_msg},
{'role': 'user', 'content': user_msg}
]
)
answer = response.choices[0].message.content
return {
'answer': answer,
'sources': sources,
'tokens_used': response.usage.total_tokens
}Assemblare tutti i componenti
La pipeline di query completa esegue questi passaggi in sequenza. Ogni passaggio è una funzione pura che può essere testata in modo indipendente, e i dati passano ordinatamente da un passaggio a quello successivo. L'aggiunta di log a ogni passaggio rende la pipeline osservabile: è possibile vedere esattamente quali chunk sono stati recuperati, quale punteggio avevano, come è stato assemblato il contesto e quanti token sono stati utilizzati. Questa visibilità è essenziale per il debugging e per migliorare la qualità del recupero.
def answer_question(user_question, vector_index):
# Step 1: Embed query
q_vector = embed_query(user_question)
# Step 2: Retrieve
chunks = retrieve_chunks(q_vector, vector_index, top_k=5)
# Step 3: Filter low-confidence matches
chunks = filter_by_score(chunks, threshold=0.70)
if not chunks:
return {'answer': 'I do not have information about that topic.', 'sources': []}
# Step 4 & 5: Format and build prompt
context = format_context(chunks)
sources = [c['source'] for c in chunks]
# Step 6: Generate
return generate_answer(user_question, context, sources)Ottimizzazione della latenza
La pipeline di query presenta due passaggi vincolati dall'I/O: la chiamata per l'embedding e la chiamata all'LLM. Li esegua senza attese non necessarie: la chiamata per l'embedding è rapida (<100 ms), mentre quella all'LLM è lenta (500 ms-3 s). Per ridurre la latenza percepita, trasmetta in streaming la risposta dell'LLM, in modo che i token compaiano man mano che vengono generati anziché attendere la risposta completa. Memorizzi nella cache l'embedding delle query identiche ripetute per evitare chiamate API ridondanti.
async def answer_question_streaming(question, index):
q_vector = embed_query(question)
chunks = retrieve_chunks(q_vector, index, top_k=5)
chunks = filter_by_score(chunks)
if not chunks:
yield 'I do not have information about that topic.'
return
context = format_context(chunks)
system_msg, user_msg = build_prompt(question, context)
stream = await client.chat.completions.create(
model='gpt-4o',
stream=True,
messages=[
{'role': 'system', 'content': system_msg},
{'role': 'user', 'content': user_msg}
]
)
async for chunk in stream:
delta = chunk.choices[0].delta.content or ''
yield deltaLogging per l'osservabilità
Le pipeline RAG in produzione richiedono un logging strutturato per poter diagnosticare i casi in cui il recupero non funziona o l'LLM fornisce una risposta errata. Registri per ogni richiesta la query, gli ID e i punteggi dei chunk recuperati, il numero di token del contesto, la risposta e la latenza. Memorizzi questi log in un database o in una piattaforma di osservabilità. Quando gli utenti segnalano risposte errate, può riprodurre la query esatta e verificare quali chunk sono stati recuperati e perché non erano sufficienti.
import time
import logging
import json
def answer_question_with_logging(question, index):
start = time.time()
q_vector = embed_query(question)
chunks = retrieve_chunks(q_vector, index, top_k=5)
chunks = filter_by_score(chunks)
context = format_context(chunks)
result = generate_answer(question, context, [c['source'] for c in chunks])
latency_ms = (time.time() - start) * 1000
log_entry = {
'question': question,
'num_chunks_retrieved': len(chunks),
'chunk_scores': [c['score'] for c in chunks],
'tokens_used': result.get('tokens_used'),
'latency_ms': round(latency_ms)
}
logging.info(json.dumps(log_entry))
return resultMemorizzazione nella cache degli embedding delle query
Se l'applicazione riceve molte query ripetute o quasi identiche, come nel caso dei bot FAQ in cui gli utenti pongono spesso le stesse domande, la memorizzazione nella cache degli embedding delle query è un'ottimizzazione semplice e molto efficace. Calcoli l'hash della stringa della query, verifichi nella cache Redis la presenza dell'embedding corrispondente e chiami l'API per gli embedding solo in caso di cache miss. In produzione, nei bot FAQ e nei chatbot di supporto sono comuni tassi di cache hit degli embedding del 30-60%; ciò elimina una parte consistente dei costi delle API e riduce la latenza di 50-100 ms per ogni query servita dalla cache.
import hashlib
import json
import redis
r = redis.Redis(host='localhost', port=6379)
EMBED_CACHE_TTL = 86400 # 24 hours
def embed_query_cached(question):
cache_key = 'embed:' + hashlib.sha256(question.encode()).hexdigest()
cached = r.get(cache_key)
if cached:
return json.loads(cached) # cache hit
# Cache miss: call the API
vector = embed_query(question)
r.setex(cache_key, EMBED_CACHE_TTL, json.dumps(vector))
return vectorVerifica rapida
Verifichi la propria comprensione dei concetti di AI Engineering trattati in questa lezione.
Riepilogo della lezione
In questa lezione ha imparato a utilizzare: la pipeline di query in sei passaggi (incorporare la query, recuperare i chunk, filtrare in base al punteggio, formattare il contesto, creare il prompt e generare la risposta), il filtro basato sulla soglia del punteggio per gestire le query al di fuori della copertura dell'indice e i miglioramenti per la produzione, tra cui le risposte in streaming, il logging strutturato e l'ottimizzazione della latenza. Nel prossimo capitolo impareremo a valutare se il sistema RAG completo funziona effettivamente in modo corretto.
Impara Python con un tutor IA — gratis
Scrivi ed esegui vero codice nel tuo browser, ricevi aiuto istantaneo da un tutor IA disponibile 24/7, e riprendi da dove hai lasciato sul web o nell'app.
- Corsi
- 30
- Lezioni
- 120
Domande Frequenti
La lezione «Interrogare, recuperare e generare» è gratuita?
Sì — il testo completo di «Interrogare, recuperare e generare» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso AI Engineering Academy, passa a CoddyKit PRO. Il corso AI Engineering Academy include 4 lezioni in totale.
Cosa imparerò in «Interrogare, recuperare e generare»?
Scriverà la pipeline di query che incorpora la domanda dell'utente, recupera i chunk principali, formatta un prompt aumentato, chiama l'LLM e restituisce una risposta con citazioni. Eserciti AI Engineering Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare AI Engineering Academy?
Non è richiesta alcuna esperienza precedente. AI Engineering Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 4 di 4.
Quanto tempo richiede la lezione «Interrogare, recuperare e generare»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione AI Engineering Academy?
Sì. Ogni lezione AI Engineering Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Caricamento dei documenti ed estrazione del testo
- Strategie di chunking: fisso, per frasi e ricorsivo
- Indicizzazione: incorporare e memorizzare i chunk
- Interrogare, recuperare e generare