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
Το Μέρος 1Κατανοείτε την έννοια υψηλού επιπέδου: να ανακτήσει τις σχετικές πληροφορίες, στη συνέχεια να το χρησιμοποιήσετε για να δημιουργήσετε απαντήσεις. Τώρα βουτάμε βαθιά στην τεχνική αρχιτεκτονική;Ακριβώς πώς λειτουργούν τα συστήματα RAG κάτω από το καπό, από στρατηγικές κοπής σε LLM εσωτερικά, όπως μάρκες και KV caches.
Πλοήγηση σειράς: Αυτό είναι το μέρος 2 της σειράς RAG:
Αν δεν έχετε διαβάσει το Μέρος 1, προτείνω να ξεκινήσετε από εκεί για να καταλάβετε:
Αυτό το άρθρο υποθέτει ότι καταλαβαίνετε αυτά τα βασικά και επικεντρώνεται σε τεχνική αρχιτεκτονική, λεπτομέρειες εφαρμογής και εσωτερικά LLM.
Ας αναλύσουμε ακριβώς τι συμβαίνει σε ένα σύστημα RAG, από τη στιγμή που θα προσθέσετε ένα έγγραφο στο πότε ένας χρήστης παίρνει μια απάντηση.
Πριν RAG μπορεί να ανακτήσει οτιδήποτε, θα πρέπει να αξιολογήσετε τη βάση γνώσεων σας. Αυτή είναι μια διαδικασία μιας φοράς (αν και μπορείτε να προσθέσετε νέα έγγραφα αργότερα).
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
Απόσπασμα απλού κειμένου από τα αρχικά σας έγγραφα.
Παράδειγμα από το 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();
}
Δεν μπορείτε απλά να χωρίσετε στα όρια των παραγράφων - χρειάζεστε σημασιολογικά συνεκτικά κομμάτια.
Γιατί το πνιγμό έχει σημασία:
Κακό κουδούνισμα:
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."
Καλό κουδούνισμα:
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"
Παράδειγμα από τη σημασιολογική μου εφαρμογή αναζήτησης:
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;
}
}
Κοινές στρατηγικές πνιγμού:
Μια ενσωμάτωση είναι ένα διάνυσμα (συστοιχία αριθμών) που αντιπροσωπεύει την έννοια του κειμένου.
Βασική έννοια: Παρόμοιες έννοιες → παρόμοιοι φορείς
"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]
Οι δύο πρώτοι φορείς θα είναι "κοντά" στο διανυσματικό χώρο (υψηλή ομοιότητα συνημίτονου), ενώ ο τρίτος είναι μακριά.
Πώς δημιουργούνται οι παρεμβολές: Μοντέρνα μοντέλα ενσωμάτωσης είναι νευρικά δίκτυα εκπαιδευμένα σε μαζικά σύνολα κειμένου για να μάθουν σημασιολογικές σχέσεις.
Παράδειγμα από την υπηρεσία προσθήκης 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);
}
Γιατί η ομαλοποίηση έχει σημασία: Μετά την ομαλοποίηση L2, η ομοιότητα συνημίτονου γίνεται ένα απλό προϊόν κουκκίδων, κάνοντας την αναζήτηση πολύ γρηγορότερα.
Σε αντίθεση με τις παραδοσιακές βάσεις δεδομένων που χρησιμοποιούν ερωτήματα SQL, οι διανυσματικές βάσεις δεδομένων χρησιμοποιούν αναζήτηση ομοιότητας.
Βασικές πράξεις:
Παράδειγμα εφαρμογής 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 }
);
}
Δημοφιλείς βάσεις δεδομένων διάνυσμα:
Θα εξερευνήσουμε τη δημιουργία αυτών των βάσεων δεδομένων σε επερχόμενα άρθρα.
Όταν ένας χρήστης κάνει μια ερώτηση, το σύστημα RAG πρέπει να βρει τις πιο σχετικές πληροφορίες από τη βάση γνώσεων.
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
Η ερώτηση του χρήστη μετατρέπεται σε ένα διάνυσμα με τη χρήση του το ίδιο μοντέλο ενσωμάτωσης Αυτό είναι κρίσιμο - διαφορετικά μοντέλα παράγουν ασύμβατους φορείς.
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;
}
Η διανυσματική βάση δεδομένων υπολογίζει την ομοιότητα μεταξύ του φορέα έρευνας και όλων των αποθηκευμένων διανυσμάτων.
Ομοιότητα συνημίτονου (Το πιο δημοφιλές για ομαλοποιημένα διανυσματικά):
similarity = (A · B) / (||A|| × ||B||)
Εύρος: -1 έως 1 (υψηλότερα = πιο παρόμοια)
Απόσταση Ευκλείδειας (για μη φυσιολογικούς φορείς):
distance = sqrt(Σ(Ai - Bi)²)
Εύρος: 0 έως ↔ (κατώτερο = πιο παρόμοιο)
Προϊόν κουκκίδας (όταν οι φορείς είναι προ-κανονισμένοι):
similarity = A · B
Εύρος: -1 έως 1 (υψηλότερα = πιο παρόμοια)
Παράδειγμα από την υπηρεσία 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();
Η αρχική ανάκτηση είναι γρήγορη αλλά κατά προσέγγιση. Η επαναφορά χρησιμοποιεί ένα πιο εξελιγμένο μοντέλο για να ξανασκοράρει τα κορυφαία αποτελέσματα 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
Γιατί η επανασύνδεση βοηθάει:
Παράδειγμα επανεγγραφής της εφαρμογής:
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();
}
Τώρα που έχουμε σχετικές πληροφορίες, τις δίνουμε στην LLM μαζί με την ερώτηση του χρήστη.
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
Αυτό είναι όπου RAG γίνεται μια τέχνη. Θα πρέπει να δομήσει την ώθηση έτσι το LLM:
Παράδειγμα γρήγορου προτύπου από το σύστημα GPT του δικηγόρου μου:
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();
}
Η κατασκευασμένη προτροπή πηγαίνει στο LLM για γενιά. Αυτό μπορεί να είναι:
Παράδειγμα χρησιμοποιώντας τοπική 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();
}
Οι βασικές παράμετροι εξηγούνται:
Μετά το LLM παράγει μια απάντηση, συχνά πρέπει να:
Παράδειγμα μετά την επεξεργασία:
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;
}
Πριν προχωρήσουμε σε πρακτικές εφαρμογές, είναι απαραίτητο να κατανοήσουμε πώς λειτουργούν οι LLMs εσωτερικά. Αυτή η γνώση σας βοηθά να βελτιστοποιήσετε τα συστήματα RAG και να αποφύγετε τις κοινές παγίδες.
Tokens είναι οι βασικές μονάδες που LLMs διαδικασία. Το κείμενο δεν τροφοδοτείται απευθείας σε μοντέλα - είναι για πρώτη φορά χωρισμένο σε μάρκες.
Παράδειγμα σηματοδότησης:
Input: "Understanding Docker containers"
Tokens: ["Under", "standing", " Docker", " containers"]
Διαφορετικά μοντέλα χρησιμοποιούν διαφορετικές στρατηγικές σήμανσης:
Γιατί η σήμανση έχει σημασία για τις ΚΓΠΕ:
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;
}
}
Όρια παραθύρου πλαισίου:
Στα συστήματα RAG, πρέπει να ταιριάζετε:
Total tokens = System prompt + Retrieved context + User query + Response buffer
Εάν RAG σας ανακτήσει 10 έγγραφα των 500 μάρκων το καθένα, αυτό είναι 5.000 μάρκες μόνο για το πλαίσιο - πριν από την ερώτηση και την απάντηση!
Πρακτικές ΚΓΠΕ συμβολική διαχείριση:
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;
}
}
Όταν ένα LLM παράγει κείμενο, δεν επεξεργάζεται τα πάντα από το μηδέν για κάθε μάρκα. Key-Value (KV) cache να θυμάμαι τι έχει ήδη υπολογίσει.
Μετασχηματιστές χρησιμοποιούν έναν μηχανισμό "προσοχής" όπου κάθε σύμβολο "προσκολλάται" (βλέπει) όλες τις προηγούμενες μάρκες για να κατανοήσουν τα συμφραζόμενα.
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
Χωρίς KV cache:
Με KV cache:
Αυτό κάνει τη γενιά δραματικά γρηγορότερα - τη διαφορά μεταξύ 10 μάρκων/δευτερόλεπτο και 100 μάρκων/δευτερόλεπτο.
Το KV cache σχηματίζει ένα "δέντρο" λόγω του πώς η προσοχή λειτουργεί στους μετασχηματιστές. Κάθε στρώμα στο μοντέλο έχει το δικό του K,V matrices.
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
Κάθε στρώμα αποθηκεύει:
Για μοντέλο με:
Η κρύπτη KV για μία ακολουθία είναι:
2 (K and V) × 32 layers × 4096 dimensions × 8192 tokens × 2 bytes (FP16)
≈ 4.3 GB of VRAM!
Αυτός είναι ο λόγος για τον οποίο μακριά παράθυρα πλαίσιο είναι έντονη μνήμη.
Τα συστήματα RAG μπορούν να αξιοποιήσουν τη βελτιστοποίηση της μνήμης KV με έξυπνους τρόπους:
Απρόσκοπτη αποθήκευση (υποστηριγμένο από κάποιους APIs όπως ο Ανθρωπιστής 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);
}
}
Γιατί αυτό είναι ισχυρό:
Πρακτικό παράδειγμα:
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!)
Και τα τρία ερωτήματα χρησιμοποιούν το ίδιο ανακτημένο πλαίσιο, οπότε η μνήμη KV για αυτό το πλαίσιο ξαναχρησιμοποιείται.
Κατανόηση μάρκων και KV cache ενημερώνει τις αποφάσεις αρχιτεκτονικής RAG σας:
Μικρότερα κομμάτια = πιο ακριβής ανάκτηση, αλλά πιο πάνω:
// 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
}
}
Μην μεγιστοποιήσετε το παράθυρο πλαίσιο - αφήστε χώρο για την γενιά:
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
}
Σε chatbots, η ιστορία συζήτησης μεγαλώνει με κάθε στροφή:
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!
Λύση: Συρόμενο παράθυρο με 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 LLMs χρέωση ανά μάρκα. RAG μπορεί να εκραγεί το κόστος, εάν δεν είναι προσεκτικοί:
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!
}
Στρατηγικές μείωσης του κόστους:
Εδώ είναι πώς οι μάρκες, KV cache, και RAG ταιριάζουν μεταξύ τους:
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
Βασικές γνώσεις:
Κατανόηση σημάτων και KV cache οδηγεί σε καλύτερο σχεδιασμό RAG:
1. Προσυγκρούστε και cache κοινά συμφραζόμενα:
// 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. Βελτιστοποιήστε τα όρια κομματιών:
// 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. Παρακολουθήστε τη χρήση σημάδι στην παραγωγή:
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);
}
}
}
Έχουμε καλύψει την πλήρη τεχνική αρχιτεκτονική των συστημάτων RAG:
Φάση 1: Ευρεσιτεχνία
Φάση 2: Ανάκτηση
Φάση 3: Γενιά
LLM Internals
Βασικές τεχνικές γνώσεις:
Τώρα καταλαβαίνεις. πώς λειτουργεί το RAG Πώς μπορείτε πραγματικά να οικοδομήσετε αυτά τα συστήματα; Ποιες προκλήσεις θα αντιμετωπίσετε; Ποιες προηγμένες τεχνικές μπορείτε να χρησιμοποιήσετε;
Το Μέρος 3: Οι ΚΓΠΕ στην πράξη, μετακινούμαστε από την αρχιτεκτονική στην εφαρμογή:
Αληθινές εφαρμογές:
Κοινές προκλήσεις και λύσεις:
Προηγμένες τεχνικές:
Ξεκινάμε:
Συνέχεια στο Μέρος 3: Οι ΚΓΠΕ στην Πρακτική →
Ιδρυτικές εφημερίδες:
Εργαλεία και πλαίσια:
Περαιτέρω ανάγνωση:
Πλοήγηση σειράς:
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.