This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Saturday, 22 November 2025
Dentro Parte 1, abbiamo coperto le origini di RAG, fondamentali, e perché importa. Si capisce il concetto di alto livello: recuperare informazioni pertinenti, quindi usarlo per generare risposte. Ora ci tuffiamo in profondità nell'architettura tecnica esattamente come i sistemi RAG funzionano sotto il cofano, dalle strategie di chunking a interni LLM come gettoni e cache KV.
Navigazione in serie: Questa è la parte 2 della serie RAG:
Se non hai letto la Parte 1, ti consiglio di iniziare da lì per capire:
Questo articolo presuppone di capire queste basi e si concentra su architettura tecnica, dettagli di implementazione e interni LLM.
Dividiamo esattamente ciò che accade in un sistema RAG, dal momento in cui si aggiunge un documento a quando un utente ottiene una risposta.
Prima RAG può recuperare qualsiasi cosa, è necessario indicizzare la vostra base di conoscenze. Questo è un processo una tantum (anche se è possibile aggiungere nuovi documenti in seguito).
flowchart TB
A[Source Documents] -->|1. Extract Text| B[Text Extraction]
B -->|2. Split into Chunks| C[Chunking Service]
C -->|3. Generate Embeddings| D[Embedding Model]
D -->|4. Store Vectors| E[Vector Database]
B -.Metadata.-> E
subgraph "Example: Blog Post"
F["Understanding Docker: A containerization platform..."]
end
subgraph "Chunks"
G["Chunk 1: Title + Intro"]
H["Chunk 2: Benefits Section"]
end
subgraph "Embeddings"
I["0.234, 0.891, 0.567, ..."]
J["0.445, 0.123, 0.789, ..."]
end
F --> G
F --> H
G --> I
H --> J
style D stroke:#f9f,stroke-width:2px
style E stroke:#bbf,stroke-width:2px
Estrarre testo semplice dai documenti di origine. Questo potrebbe essere:
Esempio dal mio blog:
// From MarkdownRenderingService
public string ExtractPlainText(string markdown)
{
// Remove code blocks
var withoutCode = Regex.Replace(markdown, @"```[\s\S]*?```", "");
// Convert markdown to plain text
var document = Markdown.Parse(withoutCode);
var plainText = document.ToPlainText();
return plainText.Trim();
}
Qui è dove la maggior parte delle implementazioni RAG falliscono. Non puoi dividere solo i limiti dei paragrafi - hai bisogno di pezzi semanticamente coerenti.
Perché il chunking conta:
Brutta chiacchierata:
Chunk 1: "Docker is a containerization platform. It allows you"
Chunk 2: "to package applications with their dependencies. This"
Chunk 3: "ensures consistency across environments."
Buona chiacchierata:
Chunk 1: "Docker is a containerization platform. It allows you to package applications with their dependencies. This ensures consistency across environments."
Chunk 2: "Benefits of Docker:
- Isolation: Each container runs in its own environment
- Portability: Containers run anywhere Docker is installed
- Efficiency: Lightweight compared to virtual machines"
Esempio dalla mia implementazione di ricerca semantica:
public class TextChunker
{
private const int TargetChunkSize = 500; // ~500 words
private const int ChunkOverlap = 50; // 50 words overlap
public List<Chunk> ChunkDocument(string text, string sourceId)
{
var chunks = new List<Chunk>();
// Split on section boundaries first (## headers in markdown)
var sections = SplitOnHeaders(text);
foreach (var section in sections)
{
// If section is small enough, keep it whole
if (section.WordCount < TargetChunkSize)
{
chunks.Add(new Chunk
{
Text = section.Text,
SourceId = sourceId,
SectionHeader = section.Header
});
}
else
{
// Split large sections on sentence boundaries
var subChunks = SplitOnSentences(section.Text, TargetChunkSize, ChunkOverlap);
chunks.AddRange(subChunks.Select(c => new Chunk
{
Text = c,
SourceId = sourceId,
SectionHeader = section.Header
}));
}
}
return chunks;
}
}
Strategie comuni:
Le inserzioni sono la magia che rende possibile la ricerca semantica. L'inserzione è un vettore (array di numeri) che rappresenta il significato del testo.
Concetto chiave: Significati simili → vettori simili
"Docker container" → [0.234, -0.891, 0.567, ..., 0.123]
"containerization platform" → [0.221, -0.903, 0.534, ..., 0.119]
"apple fruit" → [0.891, 0.234, -0.567, ..., -0.789]
I primi due vettori sarebbero "vicini" nello spazio vettoriale (alta somiglianza del coseno), mentre il terzo è lontano.
Come vengono generati gli inserti: Moderni modelli di integrazione sono reti neurali addestrati su enormi set di dati di testo per imparare relazioni semantiche. Modelli popolari:
Esempio dal mio servizio di integrazione ONNX:
public async Task<float[]> GenerateEmbeddingAsync(string text)
{
// Tokenize the input text
var tokens = Tokenize(text);
// Create input tensors for ONNX model
var inputIds = CreateInputTensor(tokens);
var attentionMask = CreateAttentionMaskTensor(tokens.Length);
var tokenTypeIds = CreateTokenTypeIdsTensor(tokens.Length);
// Run ONNX inference
var inputs = new List<NamedOnnxValue>
{
NamedOnnxValue.CreateFromTensor("input_ids", inputIds),
NamedOnnxValue.CreateFromTensor("attention_mask", attentionMask),
NamedOnnxValue.CreateFromTensor("token_type_ids", tokenTypeIds)
};
using var results = _session.Run(inputs);
// Extract the output (sentence embedding)
var output = results.First().AsTensor<float>();
var embedding = output.ToArray();
// L2 normalize the vector for cosine similarity
return NormalizeVector(embedding);
}
Perché la normalizzazione conta: Dopo la normalizzazione L2, la somiglianza del coseno diventa un semplice prodotto puntino, rendendo la ricerca molto più veloce.
I database vettoriali sono ottimizzati per la memorizzazione e la ricerca di vettori ad alta dimensione. A differenza dei database tradizionali che utilizzano query SQL, i database vettoriali utilizzano la ricerca di similarità.
Operazioni chiave:
Esempio di implementazione di Qdrant:
public async Task IndexDocumentAsync(
string id,
float[] embedding,
Dictionary<string, object> metadata)
{
var point = new PointStruct
{
Id = new PointId { Uuid = id },
Vectors = embedding,
Payload =
{
["title"] = metadata["title"],
["source"] = metadata["source"],
["chunk_index"] = metadata["chunk_index"],
["created_at"] = DateTime.UtcNow.ToString("O")
}
};
await _client.UpsertAsync(
collectionName: "blog_posts",
points: new[] { point }
);
}
Database vettoriali popolari:
Esploreremo la creazione di questi database nei prossimi articoli.
Quando un utente pone una domanda, il sistema RAG deve trovare le informazioni più rilevanti dalla base di conoscenze.
flowchart LR
A["User Query:<br/>'How do I use Docker Compose?'"] --> B[Generate Query Embedding]
B --> C["Query Vector:<br/>[0.445, -0.123, ...]"]
C --> D[Vector Search]
D --> E[Vector Database]
E --> F[Top K Similar Chunks]
F --> G["Results:<br/>1. Docker Compose Basics 0.92<br/>2. Multi-Container Setup 0.87<br/>3. Service Configuration 0.83"]
style B stroke:#f9f,stroke-width:3px
style D stroke:#bbf,stroke-width:3px
La domanda dell'utente viene convertita in un vettore usando il stesso modello di inserimento usato per l'indicizzazione. Questo è critico - diversi modelli producono vettori incompatibili.
public async Task<List<SearchResult>> SearchAsync(string query, int limit = 10)
{
// Same embedding model used for indexing
var queryEmbedding = await _embeddingService.GenerateEmbeddingAsync(query);
// Search in vector store
var results = await _vectorStoreService.SearchAsync(
queryEmbedding,
limit
);
return results;
}
Il database vettoriale calcola la somiglianza tra il vettore query e tutti i vettori memorizzati.
Similarità del coseno (più popolare per i vettori normalizzati):
similarity = (A · B) / (||A|| × ||B||)
Intervallo: da -1 a 1 (più alto = più simile)
Distanza euclidea (per vettori non normalizzati):
distance = sqrt(Σ(Ai - Bi)²)
Gamma: da 0 a ✓ (più basso = più simile)
Prodotto punto (quando i vettori sono prenormalizzati):
similarity = A · B
Intervallo: da -1 a 1 (più alto = più simile)
Esempio dal mio servizio Qdrant:
var searchResults = await _client.SearchAsync(
collectionName: "blog_posts",
vector: queryEmbedding,
limit: (ulong)limit,
scoreThreshold: 0.7f, // Only return results with >70% similarity
payloadSelector: true // Include all metadata
);
return searchResults.Select(hit => new SearchResult
{
Text = hit.Payload["text"].StringValue,
Title = hit.Payload["title"].StringValue,
Score = hit.Score,
Source = hit.Payload["source"].StringValue
}).ToList();
Il recupero iniziale è veloce ma approssimativo. Rilanciare utilizza un modello più sofisticato per riscoprire i migliori risultati K.
flowchart LR
A[Vector Search:<br/>Top 50 Results] --> B[Reranking Model]
B --> C[Reranked:<br/>Top 10 Results]
style B stroke:#f9f,stroke-width:3px
Perché riranking aiuta:
Esempio di ridimensionamento dell'attuazione:
public async Task<List<SearchResult>> SearchWithRerankAsync(
string query,
int initialLimit = 50,
int finalLimit = 10)
{
// Stage 1: Fast vector search
var candidates = await SearchAsync(query, initialLimit);
// Stage 2: Precise reranking
var rerankedResults = await _rerankingService.RerankAsync(
query,
candidates
);
return rerankedResults.Take(finalLimit).ToList();
}
Ora che abbiamo informazioni rilevanti, la diamo all'LLM insieme alla domanda dell'utente.
flowchart TB
A[User Query] --> B[Retrieved Context 1]
A --> C[Retrieved Context 2]
A --> D[Retrieved Context 3]
B --> E[Construct Prompt]
C --> E
D --> E
A --> E
E --> F["System: You are a helpful assistant...\n\nContext:\n1. Docker Compose allows...\n2. Services are defined...\n3. Volumes persist data...\n\nQuestion: How do I use Docker Compose?\n\nAnswer:"]
F --> G[LLM]
G --> H[Generated Answer with Citations]
style E stroke:#f9f,stroke-width:2px
style G stroke:#bbf,stroke-width:2px
Qui è dove RAG diventa un'arte. È necessario strutturare il prompt in modo LLM:
Esempio template prompt dal mio sistema GPT Avvocato:
public string BuildRAGPrompt(string query, List<SearchResult> context)
{
var sb = new StringBuilder();
sb.AppendLine("You are a technical writing assistant. Your task is to answer the user's question using ONLY the provided context from past blog posts.");
sb.AppendLine();
sb.AppendLine("CONTEXT:");
sb.AppendLine("========");
for (int i = 0; i < context.Count; i++)
{
sb.AppendLine($"[{i + 1}] {context[i].Title}");
sb.AppendLine($"Source: {context[i].Source}");
sb.AppendLine($"Content: {context[i].Text}");
sb.AppendLine($"Relevance: {context[i].Score:P0}");
sb.AppendLine();
}
sb.AppendLine("========");
sb.AppendLine();
sb.AppendLine("INSTRUCTIONS:");
sb.AppendLine("- Answer the question using the provided context");
sb.AppendLine("- Cite sources using [1], [2], etc.");
sb.AppendLine("- If the context doesn't contain enough information, say so");
sb.AppendLine("- Maintain the technical, practical tone of the blog");
sb.AppendLine();
sb.AppendLine($"QUESTION: {query}");
sb.AppendLine();
sb.AppendLine("ANSWER:");
return sb.ToString();
}
Il prompt costruito va alla LLM per la generazione. Questo può essere:
Esempio usando LLM locale:
public async Task<string> GenerateResponseAsync(string prompt)
{
var result = await _llamaSharp.InferAsync(prompt, new InferenceParams
{
Temperature = 0.7f, // Creativity (0 = deterministic, 1 = creative)
TopP = 0.9f, // Nucleus sampling
MaxTokens = 500, // Response length limit
StopSequences = new[] { "\n\n", "User:", "Question:" }
});
return result.Text.Trim();
}
Parametri chiave spiegati:
Dopo che l'LLM genera una risposta, abbiamo spesso bisogno di:
Esempio di post-elaborazione:
public RAGResponse PostProcess(string llmOutput, List<SearchResult> sources)
{
var response = new RAGResponse
{
Answer = llmOutput,
Sources = new List<Source>()
};
// Extract citations like [1], [2]
var citations = Regex.Matches(llmOutput, @"\[(\d+)\]");
foreach (Match match in citations)
{
int index = int.Parse(match.Groups[1].Value) - 1;
if (index >= 0 && index < sources.Count)
{
var source = sources[index];
response.Sources.Add(new Source
{
Title = source.Title,
Url = GenerateUrl(source.Source),
RelevanceScore = source.Score
});
}
}
// Convert markdown citations to hyperlinks
response.FormattedAnswer = Regex.Replace(
llmOutput,
@"\[(\d+)\]",
m => {
int index = int.Parse(m.Groups[1].Value) - 1;
if (index >= 0 && index < sources.Count)
{
var url = GenerateUrl(sources[index].Source);
return $"[[{m.Groups[1].Value}]]({url})";
}
return m.Value;
}
);
return response;
}
Prima di passare alle applicazioni pratiche, è essenziale capire come funzionano internamente gli LLM. Questa conoscenza consente di ottimizzare i sistemi RAG ed evitare insidie comuni.
I token sono le unità fondamentali che LLMs elabora. Il testo non viene alimentato direttamente ai modelli - è diviso per primo in gettoni.
Esempio di tokenizzazione:
Input: "Understanding Docker containers"
Tokens: ["Under", "standing", " Docker", " containers"]
Diversi modelli utilizzano diverse strategie di tokenizzazione:
Perché la tokenization è importante per RAG:
public class TokenCounter
{
// Rough approximation: 1 token ≈ 0.75 words (English)
public int EstimateTokens(string text)
{
var wordCount = text.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
return (int)(wordCount / 0.75);
}
public int EstimateTokensAccurate(string text, ITokenizer tokenizer)
{
// Use actual tokenizer for precision
return tokenizer.Encode(text).Count;
}
}
Limiti della finestra contestuale:
Nei sistemi RAG, è necessario adattarsi:
Total tokens = System prompt + Retrieved context + User query + Response buffer
Se il tuo RAG recupera 10 documenti di 500 token ciascuno, sono 5.000 token solo per il contesto - prima della query e della risposta!
Gestione pratica del token RAG:
public class ContextWindowManager
{
private readonly int _maxContextTokens;
private readonly int _systemPromptTokens;
private readonly int _responseBufferTokens;
public ContextWindowManager(
int totalContextWindow = 4096,
int systemPromptTokens = 300,
int responseBufferTokens = 500)
{
_maxContextTokens = totalContextWindow;
_systemPromptTokens = systemPromptTokens;
_responseBufferTokens = responseBufferTokens;
}
public List<SearchResult> FitContextInWindow(
List<SearchResult> retrievedDocs,
string query)
{
var queryTokens = EstimateTokens(query);
// Available tokens for retrieved context
var availableForContext = _maxContextTokens
- _systemPromptTokens
- queryTokens
- _responseBufferTokens;
var selectedDocs = new List<SearchResult>();
var currentTokens = 0;
foreach (var doc in retrievedDocs.OrderByDescending(d => d.Score))
{
var docTokens = EstimateTokens(doc.Text);
if (currentTokens + docTokens <= availableForContext)
{
selectedDocs.Add(doc);
currentTokens += docTokens;
}
else
{
break; // Context window full
}
}
return selectedDocs;
}
private int EstimateTokens(string text)
{
// Rule of thumb: 1 token ≈ 4 characters
return text.Length / 4;
}
}
Quando un LLM genera testo, non rielabora tutto da zero per ogni token. Cache del valore chiave (KV) per ricordare ciò che ha già calcolato.
I trasformatori usano un meccanismo di "attenzione" dove ogni token "attenda" (guarda) tutti i token precedenti per capire il contesto.
flowchart TB
subgraph "Generation Step 1: 'Docker'"
A1[Input: 'Docker'] --> B1[Compute K,V for 'Docker']
B1 --> C1[Store in KV Cache]
C1 --> D1[Generate: 'is']
end
subgraph "Generation Step 2: 'is'"
A2[Input: 'is'] --> B2[Compute K,V for 'is']
B2 --> C2[Store in KV Cache]
C2 --> E2[Retrieve KV for 'Docker']
E2 --> F2[Attend: 'is' to 'Docker']
F2 --> D2[Generate: 'a']
end
subgraph "Generation Step 3: 'a'"
A3[Input: 'a'] --> B3[Compute K,V for 'a']
B3 --> C3[Store in KV Cache]
C3 --> E3[Retrieve KV for 'Docker', 'is']
E3 --> F3[Attend: 'a' to all previous]
F3 --> D3[Generate: 'container']
end
D1 --> A2
D2 --> A3
style C1 stroke:#f9f,stroke-width:3px
style C2 stroke:#f9f,stroke-width:3px
style C3 stroke:#f9f,stroke-width:3px
Senza cache KV:
Con la cache di KV:
Questo rende la generazione drammaticamente più veloce - la differenza tra 10 gettoni/secondo e 100 gettoni/secondo.
La cache KV forma un "albero" a causa di come funziona l'attenzione nei trasformatori. Ogni livello del modello ha le proprie matrici K,V.
graph TB
A[Input Tokens:<br/>'What is Docker?'] --> B[Layer 1 Attention]
B --> C[Layer 1 KV Cache]
B --> D[Layer 2 Attention]
D --> E[Layer 2 KV Cache]
D --> F[Layer 3 Attention]
F --> G[Layer 3 KV Cache]
F --> H[... up to Layer N]
H --> I[Output: 'Docker is']
C -.Key-Value pairs<br/>for all input tokens.-> C
E -.Key-Value pairs<br/>for all input tokens.-> E
G -.Key-Value pairs<br/>for all input tokens.-> G
style C stroke:#bbf,stroke-width:2px
style E stroke:#bbf,stroke-width:2px
style G stroke:#bbf,stroke-width:2px
Ogni layer memorizza:
Per un modello con:
La cache di KV per una sequenza è:
2 (K and V) × 32 layers × 4096 dimensions × 8192 tokens × 2 bytes (FP16)
≈ 4.3 GB of VRAM!
Questo è il motivo per cui le lunghe finestre di contesto sono ad alta intensità di memoria.
I sistemi RAG possono sfruttare l'ottimizzazione della cache KV in modi intelligenti:
Prompt cacheing (sostenuto da alcune API come Anthropic Claude):
public class CachedRAGService
{
// System prompt and retrieved context can be cached!
public async Task<string> GenerateWithCachedContextAsync(
string systemPrompt, // Cached
List<SearchResult> context, // Cached
string userQuery) // Not cached, changes each time
{
var contextText = FormatContext(context);
// The KV cache for systemPrompt + contextText is reused across queries
var prompt = $@"
{systemPrompt}
CONTEXT:
{contextText}
QUERY: {userQuery}
ANSWER:";
return await _llm.GenerateAsync(prompt, useCaching: true);
}
}
Perché questo è potente:
Esempio pratico:
Query 1: "How do I use Docker?" → 2 seconds (no cache)
Query 2: "What are Docker benefits?" → 0.2 seconds (cache hit!)
Query 3: "Docker vs VMs?" → 0.2 seconds (cache hit!)
Tutte e tre le query usano lo stesso contesto recuperato, quindi la cache di KV per quel contesto viene riutilizzata.
Capire i gettoni e la cache di KV informa le decisioni dell'architettura RAG:
Pezzi più piccoli = recupero più preciso, ma più in alto:
// Option A: Small chunks (200 tokens each)
// Retrieve 20 chunks = 4,000 tokens
// Pro: Very precise, only relevant info
// Con: More KV cache entries, slower attention
// Option B: Larger chunks (500 tokens each)
// Retrieve 8 chunks = 4,000 tokens
// Pro: Better context coherence, fewer KV entries
// Con: More noise, less precise
public class AdaptiveChunker
{
public int DetermineChunkSize(int contextWindowSize)
{
if (contextWindowSize <= 4096)
return 200; // Small chunks for limited windows
if (contextWindowSize <= 16384)
return 500; // Medium chunks
return 1000; // Large chunks for big windows
}
}
Non superare la finestra contestuale - lasciare spazio per la generazione:
public class SafeContextManager
{
public int GetSafeContextLimit(int totalContextWindow)
{
// Use only 75% for input, reserve 25% for output
return (int)(totalContextWindow * 0.75);
}
// Example: 4K model
// Total: 4096 tokens
// Safe input: 3072 tokens
// Reserved for output: 1024 tokens
}
Nei chatbot, la storia della conversazione cresce ad ogni turno:
Turn 1:
System + Context + Query1 = 3000 tokens
Response1 = 300 tokens
Total: 3300 tokens
Turn 2:
System + Context + Query1 + Response1 + Query2 = 3650 tokens
Response2 = 300 tokens
Total: 3950 tokens
Turn 3:
System + Context + Query1 + Response1 + Query2 + Response2 + Query3 = 4250 tokens
ERROR: Context window exceeded!
Soluzione: Finestra scorrevole con re-retrieval
public class ConversationalRAG
{
private readonly int _maxHistoryTokens = 1000;
public async Task<string> ChatAsync(
List<ConversationTurn> history,
string newQuery)
{
// Re-retrieve context based on current query
var context = await RetrieveContextAsync(newQuery);
// Keep only recent conversation history
var relevantHistory = TrimHistory(history, _maxHistoryTokens);
var prompt = BuildPrompt(context, relevantHistory, newQuery);
return await _llm.GenerateAsync(prompt);
}
private List<ConversationTurn> TrimHistory(
List<ConversationTurn> history,
int maxTokens)
{
var trimmed = new List<ConversationTurn>();
var currentTokens = 0;
// Keep most recent turns
foreach (var turn in history.Reverse())
{
var turnTokens = EstimateTokens(turn.Query) + EstimateTokens(turn.Response);
if (currentTokens + turnTokens <= maxTokens)
{
trimmed.Insert(0, turn);
currentTokens += turnTokens;
}
else
{
break;
}
}
return trimmed;
}
}
API-based LLM carica per token. RAG può esplodere i costi se non attenzione:
public class CostAwareRAG
{
// OpenAI GPT-4 pricing (example):
// Input: $0.03 per 1K tokens
// Output: $0.06 per 1K tokens
public decimal EstimateQueryCost(
int systemPromptTokens,
int retrievedContextTokens,
int queryTokens,
int expectedResponseTokens)
{
var inputTokens = systemPromptTokens + retrievedContextTokens + queryTokens;
var outputTokens = expectedResponseTokens;
var inputCost = (inputTokens / 1000m) * 0.03m;
var outputCost = (outputTokens / 1000m) * 0.06m;
return inputCost + outputCost;
}
// Example:
// System: 300 tokens
// Context: 3000 tokens (10 retrieved docs)
// Query: 50 tokens
// Response: 500 tokens
//
// Cost = ((300 + 3000 + 50) / 1000 * 0.03) + (500 / 1000 * 0.06)
// = (3350 / 1000 * 0.03) + (500 / 1000 * 0.06)
// = $0.1005 + $0.03
// = $0.1305 per query
//
// At 1000 queries/day = $130/day = $3,900/month!
}
Strategie di riduzione dei costi:
Ecco come i gettoni, la cache di KV e i RAG si adattano insieme:
flowchart TB
A[User Query:<br/>'How does Docker work?'<br/>≈ 12 tokens] --> B[Generate Query Embedding]
B --> C[Vector Search]
C --> D[Retrieved Docs:<br/>5 docs × 500 tokens<br/>= 2,500 tokens]
D --> E[Construct Prompt]
A --> E
E --> F["Complete Prompt:<br/>System: 300 tokens<br/>Context: 2,500 tokens<br/>Query: 12 tokens<br/>Total: 2,812 tokens"]
F --> G[Tokenize Prompt]
G --> H["Token IDs:<br/>[245, 1034, 8829, ...]<br/>2,812 token IDs"]
H --> I[LLM Layer 1]
I --> J[Compute K,V]
J --> K[KV Cache Layer 1:<br/>2,812 K,V pairs]
I --> L[LLM Layer 2]
L --> M[Compute K,V]
M --> N[KV Cache Layer 2:<br/>2,812 K,V pairs]
L --> O[... Layers 3-32]
O --> P[Generate Token 1: 'Docker']
P --> Q[Add to KV Cache]
Q --> R[Generate Token 2: 'is']
R --> S[Add to KV Cache]
S --> T[... until completion]
T --> U["Response: 'Docker is a containerization platform...'<br/>≈ 400 tokens"]
style K stroke:#f9f,stroke-width:2px
style N stroke:#f9f,stroke-width:2px
style Q stroke:#bbf,stroke-width:2px
style S stroke:#bbf,stroke-width:2px
Intuizioni chiave:
Capire i gettoni e la cache di KV porta a una migliore progettazione RAG:
1. Pre-computa e cache contesti comuni:
// Cache KV for frequently used system prompts + static context
var cachedSystemContext = await _llm.PrecomputeKVCache(systemPrompt + staticContext);
// Reuse for each query (much faster)
foreach (var query in userQueries)
{
var response = await _llm.GenerateAsync(query, reuseKVCache: cachedSystemContext);
}
2. Ottimizzare i confini del pezzo:
// Bad: Arbitrary 500-character chunks
var chunks = text.Chunk(500);
// Good: Chunk on sentence boundaries, measure in tokens
public List<string> ChunkByTokens(string text, int maxTokensPerChunk)
{
var sentences = SplitIntoSentences(text);
var chunks = new List<string>();
var currentChunk = new StringBuilder();
var currentTokens = 0;
foreach (var sentence in sentences)
{
var sentenceTokens = EstimateTokens(sentence);
if (currentTokens + sentenceTokens > maxTokensPerChunk && currentTokens > 0)
{
chunks.Add(currentChunk.ToString());
currentChunk.Clear();
currentTokens = 0;
}
currentChunk.Append(sentence).Append(" ");
currentTokens += sentenceTokens;
}
if (currentTokens > 0)
chunks.Add(currentChunk.ToString());
return chunks;
}
3. Monitorare l'uso token nella produzione:
public class RAGTelemetry
{
public void LogRAGQuery(
string query,
List<SearchResult> retrievedDocs,
string response)
{
var queryTokens = EstimateTokens(query);
var contextTokens = retrievedDocs.Sum(d => EstimateTokens(d.Text));
var responseTokens = EstimateTokens(response);
var totalTokens = queryTokens + contextTokens + responseTokens;
_logger.LogInformation(
"RAG Query: {Query} | Context: {ContextTokens} tokens from {DocCount} docs | " +
"Response: {ResponseTokens} tokens | Total: {TotalTokens} tokens",
query, contextTokens, retrievedDocs.Count, responseTokens, totalTokens
);
// Alert if approaching context limit
if (totalTokens > _maxTokens * 0.9)
{
_logger.LogWarning("Approaching token limit: {TotalTokens}/{MaxTokens}",
totalTokens, _maxTokens);
}
}
}
Abbiamo coperto l'architettura tecnica completa dei sistemi RAG:
Fase 1: Indicizzazione
Fase 2: recupero
Fase 3: Generazione
Interni LLM
Principali intuizioni tecniche:
Ora capisci. come agisce RAG a livello tecnico. Ma la teoria ti porta solo fino ad ora. Come si costruiscono effettivamente questi sistemi? Quali sfide affronterete? Quali tecniche avanzate si possono utilizzare?
Dentro Parte 3: RAG nella pratica, passiamo dall'architettura all'implementazione:
Applicazioni del mondo reale:
Sfide e soluzioni comuni:
Tecniche avanzate:
Iniziare:
Continuare alla parte 3: RAG nella pratica →
Documenti di fondazione:
Strumenti e quadri:
Ulteriore lettura:
Navigazione in serie:
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.