RAG Architecture and Internals: Hur det verkligen fungerar
I Del 1, delade ut RAG, ', dess ursprung, ,, grunderna, M SK3 och varför det spelar roll,.. Ni förstår konceptet som är högt uppsatt i - och :. Ni får fram relevant information. ,, använder den sedan för att generera svar... Nu dyker vi djupt in i den tekniska arkitekturen, MSC9, och exakt hur RAG-system fungerar under kapseln, från sklumpstrategier till LLM-interner, som token och KV-キャッシュar.
Inleiding
Navigation Det här är en del 2 av RAG-serien
- Del 1: källor och grunder - Historien , motivation
- Del 2: Arkitektur och inre (den här artikeln) - En teknisk djupdykning i hur RAG fungerar
- Del 3: RAG i praktiken - Att bygga riktiga system
- Del 4a: ONNX & Garantiimplementering - CPU-vänligt semantisk sökgrund
- Del 4b: Semantisk sökande i aktivitet - Typanledningen , hybridsökningen , och gränssnittet
- Del 5: Hybridsökning & Auto-Indexering - Produktionsintegreringsmönster
- Del 6: GraphRAG - kunskapsgrafer för korpusen
Om du inte har läst delen ' kan jag föreslå att börja där för att förstå
- Vad är RAG och varför det är viktigt
- Historien från Keyword-sökning till semantisk förståelse
- RAG vs fin-tunning och andra angrepp
Detta artikel antar att du förstår dessa grunder och fokuserar på teknisk arkitektur , implementeringsdetails, och LLM inre.
Hur RAG fungerar: Den kompletta bilden
Låt oss bryta ner exakt vad som händer i ett RAG-system från det ögonblick du lägger till ett dokument till när en användare får ett svar.
Fas 1: Indexering (Präparerande av kunskapsbasen
Innan RAG kan hämta något, måste du indexera din kunskapsbas.
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
steg 1: Textuttvingning
Extrahera enkel text från dina källdokument. Det skulle kunna vara
- Markdown-filer (likna mina bloggar
- PDFs ( för dokumentationen)
- HTML (for web scraping)
- Databasisrekord
- E-poster , chatlogs, etc.
Ett exempel från min blogg:
// 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();
}
steg 2:
Det är här som de flesta RAG implementeringar misslyckas med.
Varför chunking är viktigt:
- LLMs har tokengränser (kolltextfenster)
- Mindre chunks = mer exakt uppfinning
- Men bitar måste ha tillräckligt med kontext för att vara meningsfulla
dåliga chunking:
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."
Bra sammanslagning:
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"
Ett exempel från min semantiska sökimplementation:
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;
}
}
Samma hamneringsstrategier:
- Fixerad-storlek: Enkel men bryter semantiska gränser
- mening-baserad: respekterar grammatiken men kan vara för liten
- Absatz-baserade: Naturlig men variabel storlek
- Seksie-baserad: Som bäst för strukturerad innehåll (min preferens
- Sliding-fenster med överlappande: Försäkrar att ingen kontext går förlorad vid gränser
steg 3: Generera inbäddar
Embeddings är magin som gör det semantiska att söka efter.
Key concept: liknande betydelser → liknande vektorer
"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]
De första två vektorerna skulle vara "tilllägsna" i vektorrymmet | ( | hög kosinelikhet |), | medan den tredje är långt borta |
Hur inbäddar genereras: Moderna embedding-modeller är nervnätverk tränade på massiva textdatasamlingar för att lära sig semantiska relationer.
- alla-MiniLM: 384 dimensioner, snabbtM SK3 bra kvalitet ( vad jag använder på den här bloggen
- text-koppelning-3-klein (OpenAI
- BGE-bas: 768 dimensioner , tillstånd M SK3 av - den - art open source
Ett exempel från min ONNX embedding service:
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);
}
Varför normalisering är viktigt: Efter L2 normalisering , blir kosineliknande en enkel punktprodukt
steg 4: lagra i vektordatabasen
Vektordatabaser är optimerade för att lagra och söka efter höga dimensionella vektorer.-Vektorer i dimensioner.
Nyckeloperationer:
- Lägg upp: Lägg till eller uppdatera en vektor med metadata
- söka: Hitta K som är mest likvärdig med en frågavektor
- Filter: Kombinera vektorsökningar med metadatafilter
exempel Qdrant implementering:
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 }
);
}
Beliebta vektordatabaser:
- Qdrant: snabbt , självständigt- anpassningsbarM SK3 utmärkt CMST4 stöd MST5 min val
- pgvector: PostgreSQL-extensionen (trev om du redan använder Postgres
- Pinecone: Manad service ( dyrt men bra
- Veva: egenskapen -rikt, bra för komplexa schema
- ChromaDB: Python
Vi ” ' ” kommer att undersöka hur dessa databaser används i kommande artiklarna“ . ”
Fas 2: Uppfinning ( Verkrävande av relevant information
När en användare ställer en fråga , behöver RAG-systemet hitta den mest relevanta informationen från kunskapsbasen.
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
steg 1: Generera sökintegrering
Användarens fråga konverteras till en vektor med hjälp av samma inbäddade modell som används för indexering. Detta är viktigt - olika modeller producerar okompatibla vektorer .
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;
}
steg 2: Similarity Search
Vektordatabasen beräknar likheterna mellan sökvektorn och alla lagrade vektorer. Samma mätningar:
Kosine likhet (mesta populärt för normaliserade vektorer):
similarity = (A · B) / (||A|| × ||B||)
Omfattningen: M SK1 till 1 | | ( | högre |= | mer lik |
Euklidiska avstånd ( för onormaliserade vektorer ):
distance = sqrt(Σ(Ai - Bi)²)
Omfattningen: M SK1 till ∞ | | ( | lägre ♫ = | mer lik ♫
Dot-Produkt ( när vektorer är före-normaliseradeM SK2
similarity = A · B
Omfattningen: M SK1 till 1 | | ( | högre |= | mer lik |
Ett exempel från min Qdrant- tjänst:
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();
steg 3: Re-ranking (Optionellt men tillämpt)
Det första urvalet är snabbt men ungefär. Reranking använder en mer sofistikerad modell för att återkora de bästa K-resultaten
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
Varför reranking hjälper?
- Vinniga embeddingsmodeller optimerar hastigheten, ,, och förlorar lite precision
- Reranking-modeller är långsammare men mer precisa
- Två steg för - balanserar hastighet och kvalitet
exempel omställningsimplementering:
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();
}
Fas 3: Generering | ( | Utformandet av svaret |)
Nu när vi har den relevanta informationen, ,, så får vi den till LLM tillsammans med användarens fråga.
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
steg 1: Vinnig byggande
Det är här som RAG blir en art. Du behöver strukturera prompten så att LLM
- använder den tillhandahållna kontexten ( inte dess inre kunskaper )
- Citer källor när möjligt
- Omfamnar när kontexten inte innehåller ett svar
- Håller en konsekvent ton
Ett exempel från min advokats GPT-system:
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();
}
steg 2: LLM-inferens
Den konstruerade prompten går till LLM för att generera
- Cloud API: OpenAI , Anthropic ClaudeM SK2 Google PaLM
- Lokal modell: med hjälp av llamaM SK1 cpp , ONNX Runtime, eller TorchSharp
exempel med hjälp av lokal LLM:
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();
}
Nyckelparameter förklarade:
- Temperatur: Kontrollerar slumpmässighet ♫ (0 ♫ ♫= ♫ väljer alltid det mest troliga ♫
- Högsta P: kärnprobering - räkna ut bara tokens som utgör högsta P sannolikhetsmassa
- Max tokens: Limitresponslängd
- Stoppa sekvenser: När att sluta generera
steg 3: Utgångspunkt-Processing
Efter att LLM genererar ett svar, behöver vi ofta
- Extrahera citat och konvertera dem till länkar
- Formatera kodblock
- Lägg till metadata ( källor , självförtroende poäng
- Protokollera interaktionen för debugging
Ett exempel: post-processing:
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;
}
Att förstå LLM: s inre element, :, tokens, ,, KV-キャッシュ, M SK2 och kontextuella Windows
Innan vi går vidare till praktiska tillämpningar, så är det nödvändigt att förstå hur LLMs fungerar internt. Den här kunskapen hjälper dig optimera RAG-system och undvika vanliga fall.
Vad är tokens?
Tokens är de fundamentala enheterna som LLMs processar.
exempel tokenisering:
Input: "Understanding Docker containers"
Tokens: ["Under", "standing", " Docker", " containers"]
Olika modeller använder olika tokeniseringsstrategier:
- GPT-modeller: Använd byterM SK1Pairkodering (BPE) med ~50K ordspråk
- Claude: En liknande BPE-metod
- Llama-modeller: SentencePiece tokenisering
Varför är tokenisering viktigt för 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;
}
}
Kontextfenstergränser:
- GPT-3.5: M SK1K-token
- GPT-4: M SK1K-128K tokens ( beroende på varianten
- Claude 3.5 Sonnet
- Llama 3: 8K-tokener (om man kan utöka dem
I RAG-system, måste du få plats
Total tokens = System prompt + Retrieved context + User query + Response buffer
Om din RAG tar fram 10 dokument av ♫ 500 ♫ tokens var och en ♫
Praktisk RAG-tokenhantering:
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;
}
}
Det hemliga vapenet KV Cache
När en LLM genererar text, så processar den inte allt från början för varje token. Key-Value (KVM SK2 cache att komma ihåg vad den redan har beräknat
Hur förvandlar fungerar (Simplifierad)
Transformer använder en "synnerhet "-mekanism där varje token ♫ " ♫ tittar på ♫" ♫ ♫ МSK4 ♫ ser på ♪ ) ♫ alla tidigare tokens för att förstå sammanhanget ♫
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
Utan KV cache:
- steg 1: process 1 token → O(1)
- steg 2: process 2 token från grunden ♫ → ♫ O ♫
- steg 3: process 3 token från grunden ♫ → ♫ O ♫
- Totalt: O(1 + ♫ ♫ 2 ♫
Med KV cache:
- steg 1: process 1 token
- steg 2: process 1 ny token
- steg 3: process 1 ny token
- sammantaget : O(NM SK2
Detta skapar en generation dramatiskt snabbare - skillnaden mellan
KV-kassträdstrukturen
KV-キャッシュen bildar ett "träd" på grund av hur uppmärksamheten fungerar i transformärern.
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
Varje lager lagrar
- Nyckel (K): Representationer som används för att räkna uppmärksamhetsnotar
- Valuer (VM SK1: Representationer som blandas ihop baserat på uppmärksamhet
För en modell med:
- 32 lager
- 4096 dolda dimensioner
- 32 uppmärksamhetspunkter
- 8K kontextfenster
KV-キャッシュen för en sekvens är:
2 (K and V) × 32 layers × 4096 dimensions × 8192 tokens × 2 bytes (FP16)
≈ 4.3 GB of VRAM!
Det är därför långa kontextfenster är minne
KV Cache i RAG System
RAG-system kan utnyttja KV-キャッシュoptimering på smarta sätt:
Vinnig kassering (understödd av några APIs som 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);
}
}
Varför är detta kraftfullt:
- Den första frågan: Beräknar KV-cache för systemuppdraget + kontext | ( | långsamt
- Efterföljande frågor med samma kontext: Återanvände cached KV M SK1x snabbare!)
- Bara användarfrågadelen behöver ny beräkning
Praktisk exempel:
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!)
Alla tre frågeställningar använder samma sökta sammanhang, så KV cachet för det sammanhanget är återanvänd
Symbolgränser och RAG-strategi
Att förstå tokens och KV-キャッシュ ger information om dina RAG-Architekturdecisioner:
1. Storlek
Mindre bitar = mer exakt återväckning , men mer överhead
// 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
}
}
2. Kontextfenster tillämpning
Don' inte maxa ut kontextfenolet - lämna plats för generationen
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
}
3. Multi-Turna RAG konversationer
I chatbots, ökar konversationens historia med varje gång man vänder på
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!
Lösningen: Sliding window with 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;
}
}
4. Symbolkostnads optimering
API-based LLMs charge per token . RAG kan explodera kostnaderna om man inte är försiktig
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!
}
Redoneringsstrategier:
- Retritera färre , bättre- klassificerade dokument
- Använd snabb kassering (Anthropic Claude: 90% billigare för kasserade tokens
- Använd billigare modeller för att återställa
- Komprimera kontexten med hjälp av sammanfattning
Att visualisera den kompletta RAG-flödet med tokens
Här kan man se hur token, ,, KV-cache och RAG passar ihop.
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
Nyckelinsikter
- Input-tokener (2,812) bearbetas en gång för att bygga upp den första KV-キャッシュen
- Generering händer en token vid ett tillfälle, genom att återanvända KV cachet
- Varje ny token lägger till KV-skiffern för framtida tokens att delta i
- Hela VRAM som behövs = Modells vikter + KV kass för alla token
- Längsre sammanhang = större KV-キャッシュ = mer VRAM
praktiska konsekvenser för RAG
Att förstå token och KV cache leder till bättre RAG design:
1. Framtid -beräkningar och cache, vanliga kontexter
// 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. Optimera chunkgränserna :
// 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. Kontrolla användandet av token i produktionen
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);
}
}
}
slutsats: Arkitekturkunskap
Vi'" har täckt den kompletta tekniska strukturen av RAG-system :.
Fas 1: Indexering
- Text extraktion från olika källor
- Strategier för att slippa det. (sektionen -baserade, meningenM SK3baseradesMska4 med överlappandeM Ska5
- Embedding generering (ONNX, API-tjänster
- Vektorförvaraning (Qdrant
Uppfinning av fas 2:
- Rädsla inbäddar ( samma modell som indexering !)
- Ähnlichhetssökning ( kosinus , euklidisktM SK2 punktprodukt)
- Optionellt omskiljande för bättre precision
- Metadatafilterning för raffinerade resultat
Fas 3: Generering
- Vinniga konstruktioner (föreställning + instruktioner
- LLM: s slutsats (temperatur, toppen -pM SK3 max tokensMSC4
- Utveckling efter -processing | ( | Citation extraktion | МSK2 | Formatering
Internal LLM
- Saker: De fundamentala enheterna ( inte tecken M SK2
- KV Cache: Varför är generationen snabb M SK1 linjärt , inte kvadratiskt)
- Kontextfenster: Att hantera tokensgränser i RAG
- Kustoptimering : Caching, komprimering, smarta spårning
Nyckliga tekniska insikter
- Samma inbäddade modeller för indexering och extrahering (criticalM SK1
- Att skapa något spelar mer roll än du tror - bevarar semantisk koherens
- Reranking förbättrar precisionen på bekostnad av latens
- Att hantera token är nödvändigt - uppskattning innan sökningen
- KV-caching gör RAG möjlig - återanvända promptberäkningar
- Kontextfenster fylls snabbt - 10 docs
Continue to Part 3: RAG in Practice
Ni förstår nu hur RAG fungerar på den tekniska nivån. Men teorin får dig bara så långt . Hur bygger du egentligen dessa system?
I Del 3: RAG i praktiken, vi går från arkitektur till implementation
Verklig-world-applikationer:
- Related posts recommendation on this blog
- Semantisk bloggsökning
- Att bygga en "avrättare GPT" skrivassistent
Samma utmaningar och lösningar:
- Maskerande strategier som bevarar kontext
- Att förbättra inbäddarkvaliteten för ditt domain
- Att hantera kontextfenster dynamiskt
- Att förhindra hallucinationer trots att de har ett sammanhang
- Att hålla indexet upptill datum
avancerade tekniker:
- Hypotetiska Dokument Embeddings (HyDE)
- Self-besöker med LLM-analyserade filter
- Multi- sök RAG för omfattande resultat
- Kontextuell kompression för att minska tokenanvändningen
- Multi-hop RAG för komplexa frågor
- långvarig konversationsminne
Att börja:
- Tydlig implementationsplan
- praktiska koder exempel
- Optimeringsstrategier
- När inte använda RAG
Fortsätt till del 3: RAG i praktiken
resurser
Grunddokument:
- Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks - Den ursprungliga RAG pappret
- Dense Passage Retrieval for Open-Domain Question Answering - DPR grund
- Attityd är allt du behöver - Transformator
verktyg och ramverk:
- Qdrant - Vektordatabas
- Runtime på ONNX - Lokala inbäddar
- LLamaSharp - Lokal LLM-förklaring
- Sanningtransformatorer - Embedding-modeller
Weiterlesen:
- Anthropic: Kontextuell Absorbering - avancerade tekniker
Serienavigation:
- Del 1: källor och grunder - Histori och motivation
- Del 2: Arkitektur och inre (den här artikeln) - Teknikligt djupdykning
- Del 3: RAG i praktiken - Att bygga riktiga system