# RAG 建筑和内部:如何真正发挥作用

内 [第一部分 第一部分](/blog/rag-primer)我们覆盖了RAG的起源、基本原理和原因。你理解高层次的概念:检索相关信息,然后用它来生成回应。现在我们深入到技术结构中 — — 精确地说,RAG系统是如何在引擎盖下运行的,从挤压策略到LLM内部,比如象征性和KV缓冲。

<datetime class="hidden">2025-11-22T09:30</datetime>

<!-- category -- AI, RAG, Machine Learning, Semantic Search, LLM, AI-Article -->
# 一. 导言 导言 导言 导言 导言 导言 一,导言 导言 导言 导言 导言 导言

**系列导航:** 这是RAG系列第2部分:

- [第1部分:起源和基本要点](/blog/rag-primer) - 历史、动机和核心概念
- **第2部分:建筑和内部** (本条) - 从技术深层下潜了解RAG如何运作
- [第3部分:在实务中协助通知书](/blog/rag-practical-applications) - 建立实际系统、挑战和先进技术
- [第4a部分:ONNX和Qdrant执行](/blog/semantic-search-with-onnx-and-qdrant) - CPU友好语义搜索基金会
- [第4b部分:语义搜索行动](/blog/semantic-search-in-action) - 头型、混合搜索和UI
- [第5部分:混合搜索和自动插入](/blog/rag-hybrid-search-and-indexing) - 生产一体化模式
- [第6部分:图表](/blog/graphrag-knowledge-graphs-for-rag) - 用于了解人身知识的知识图

如果你还没读第一部分,我建议从那里开始理解:

- ARAG是什么以及它为何重要
- 从关键字搜索到语义理解的历史
- RAG相对于微调和其他方法

本篇文章假定你理解这些基本要点,并着重阐述 **技术结构、实施细节和LLLM内部**.

[TOC]

# RAG如何工作:完整图片

让我们从添加文档到用户得到答案的那一刻, 将RAG系统中发生的事情细分为准确的。

## 第1阶段:指数化(筹备知识库)

在RAG 获取任何文件之前, 您需要将您的知识库索引。 这是一个一次性的过程( 虽然您可以在以后添加新文件 ) 。

```mermaid
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
```

### 步骤1:案文摘录

从源文档中提取纯文本。 这可以是 :

- Markdown 文件( 如我的博客文章)
- PDFs(用于文件)
- HTML(用于网络剪切)
- 数据库记录
- 电子邮件、聊天日志等

**我博客上的例子:**

```csharp
// 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();
}
```

### 步骤2:踢打

这是大多数 RAG 执行失败的地方。 您不能在段落边界上分割 - 您需要语义一致的块块 。

**块块为何重要 :**

- LLMs有象征性限制(文本窗口)
- 小块 = 更精确的检索
- 但块块必须包含足够的上下文, 才能有意义

**坏区块 :**

```
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"
```

**我的语义搜索实施实例 :**

```csharp
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;
    }
}
```

**共同的散列战略:**

- **固定规模**:简单但打破语义边界
- **基于判刑的**:尊重语法,但可能太小
- **以段落为基础的**: 自然但变化大小
- **基于科**:结构化内容的最佳(我偏好)
- **存在重叠的滑动窗口**:确保边界不会失去上下文

### 步骤3:产生嵌入式

嵌入是使语义搜索成为可能的魔力。嵌入是代表文字含义的矢量(数组) 。

**关键概念:** 类似的意思  类似矢量

```
"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]
```

前两个矢量将是矢量空间的“接近”(高焦距相似性),而第三个矢量距离很远。

**如何生成嵌入 :**
现代嵌入模型是接受大量文本数据集培训的神经网络,以学习语义关系。

- **全部米尼LM-L6-v2**: 384 维度, 快速, 优质(我在这个博客上所使用的)
- **文字组装-3-小型** 1536维,质量很高
- **BGE 基准**:768维,最先进的开放源

**我的ONNX嵌入服务实例:**

```csharp
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正常化之后,焦距相似性成为简单的点产品,使搜索速度快得多。

### 第4步:储存在矢量数据库

为储存和搜索高维矢量,优化了矢量数据库,与使用SQL查询的传统数据库不同,矢量数据库使用相似性搜索。

**关键业务:**

- **上层**:以元数据添加或更新矢量
- **搜索搜索**:在查询矢量中查找 K 最相似的矢量
- **过滤过滤器**:将矢量搜索与元数据过滤器相结合

**实例 Qdrant 执行 :**

```csharp
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 }
    );
}
```

**大众矢量数据库:**

- **解冻**快速、可自我接受、优秀的 C# 支持( 我的选择)
- **pgvictor 变量**: PostgreSQL 扩展名( 如果您已经在使用 Postgres )
- **松松**:管理式服务(昂贵但良好)
- **断断**具有丰富的地貌, 适合复杂的方案
- **色谱( ChromaDDB)**: 以Python为焦点,轻量级

我们将探讨在即将发表的文章中建立这些数据库。

## 第2阶段:检索(查明相关信息)

当用户提问时,区域咨询组系统需要从知识库中找到最相关的信息。

```mermaid
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
```

### 步骤1:生成查询嵌入

用户的问题被转换为使用 **相同的嵌入模型** 用于编制索引。这至关重要 -- -- 不同的模型产生互不兼容的矢量。

```csharp
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;
}
```

### 第2步:相似性搜索

矢量数据库计算查询矢量和所有存储的矢量之间的相似性。

**余心相似性** (正常化矢量最受欢迎):

```
similarity = (A · B) / (||A|| × ||B||)
```

范围: -1 至 1( 高( 高) = 更相似)

**欧几里地距离** (对于未正常化的矢量):

```
distance = sqrt(Σ(Ai - Bi)²)
```

范围: 0 到 ( 更低 = 更相似 )

**点产品** (当矢量提前正常化时):

```
similarity = A · B
```

范围: -1 至 1( 高( 高) = 更相似)

**我的 Qdrant 服务中的示例 :**

```csharp
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();
```

### 步骤3:重新排序(备选但建议)

初始检索是快速的, 但大约是近似 。 重新排序使用更复杂的模型来重新排序最高 K 结果 。

```mermaid
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
```

**为何重新排名会帮助:**

- 快速嵌入模型优化速度,牺牲某些精确度
- 排名模型慢,但更准确
- 双阶段办法平衡速度和质量

**重排执行的示例:**

```csharp
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();
}
```

## 第3阶段:产生(确定答案)

现在我们有了相关的信息, 我们把它和用户的问题一起 提供给LLM。

```mermaid
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
```

### 步骤1:迅速建造

这是RAG成为艺术的地方。 您需要构造提示, 所以LLM :

- 使用的提供背景(不是内部知识)
- 可能时点选来源
- 当上下文不包含答案时承认
- 保持一致的语调/风格

**我的律师GPT系统的示例提示模板:**

```csharp
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();
}
```

### 第2步:LLM 推断

建造的灵丹妙药代代传给LLM。这可以是:

- **AIPI 云云**OpenAI、人类克洛德、Google PaLM
- **当地模式**使用 llama.cpp, ONNX 运行时间, 或火炬沙尔普

**使用当地LLM的示例:**

```csharp
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();
}
```

**解释的关键参数 :**

- **温度**: 控制随机性( 0 = 总是最可能选择, 1 = 随机抽样)
- **顶P**:核心取样 - 仅考虑构成顶级P概率质量的符号
- **最大调当量**: 限制答复长度
- **停止序列**:何时停止生产

### 第3步:处理后

在LLM产生反应后,我们常常需要:

- 提取引文并将其转换为链接
- 格式化代码块
- 添加元数据( 来源、 信用分数)
- 记录调试的交互

**后处理示例:**

```csharp
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;
}
```

# 理解LLM 内部LLM: Tokens, KV缓存和上下文窗口

在我们转向实际应用之前,必须了解LLMS的内部工作方式。这种知识有助于你优化RAG系统,避免常见的陷阱。

## 什么是托肯斯?

Tokens 是LLMM 处理的基本单位。 文本不是直接反馈给模型 - 它首先被细分成符号 。

**示例符号 :**

```
Input:  "Understanding Docker containers"
Tokens: ["Under", "standing", " Docker", " containers"]
```

不同的模型使用不同的代谢战略:

- **GPT模型**:使用 ~ 50K 词汇表的字元对称编码(BPE)
- **克劳德**BPE: 类似的BPE方法
- **Llama 模型**: 句式符号化

**为什么象征性化对RAG很重要:**

```csharp
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;
    }
}
```

**上下文窗口限制 :**

- GPT- 3. 5: 16K 象征性品
- GPT-4: 8K-1128K象征性品(视变式而定)
- Claude 3. 5 Sonnet: 200K 纪念品
- Llama 3: 8K 象征性品(尽管可以展延)

在RAG系统中,您必须适合:

```
Total tokens = System prompt + Retrieved context + User query + Response buffer
```

如果您的RAG 检索到10份文件, 每份文件500个符号, 那就是5000个符号, 仅用于上下文- 在查询和回复之前!

**实际的RAG象征性管理:**

```csharp
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;
    }
}
```

## KV Cache:LLM的秘密武器

当一个 LLLM 生成文本时, 它不会从头到脚地对每个标记的所有东西进行再处理。 它使用一个 **按键值( KV) 缓存** 来记住它已经计算了什么。

### 变形人如何工作(简化)

变换器使用一种“ 注意” 机制, 使每个象征性的“ 注意” (查看) 能够理解上下文 。

```mermaid
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 缓存 :**

- 第1步:进程1象征性的 
- 第2步:从零开始的处理 2 标记  O(2)
- 第3步:从零开始处理3个象征性品 
- 共计:O(1+2+2+3+3+...+N)=O(N2)

**使用 KV 缓存 :**

- 第1步:进程1象征性,缓存 K、V  O(1)
- 步骤2:进程1新牌,重新使用缓存的K、V  O(1)
- 步骤3:进程1新标志,重新使用缓存的K、V  O(1)
- 共计:O(N)

代代代代代代代代代代代代代代代代代代代代代代代代 **快速快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快快** - 10个象征/秒和100个象征/秒之间的差额。

### KV 缓存树结构Name

KV 缓存组成一个“ 树 ” , 因为注意在变压器中是如何起作用的。 模型中的每个层都有自己的 K, V 矩阵 。

```mermaid
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
```

**每一层商店:**

- **密钥( K)**:用于计算注意分数的表示式
- **价值(五)**:基于关注的混合代表

模型的模型包括:

- 32层
- 4096 隐藏尺寸
- 32个主管(32个主管)
- 8K 8K 上下文窗口

一个序列的 KV 缓存是 :

```
2 (K and V) × 32 layers × 4096 dimensions × 8192 tokens × 2 bytes (FP16)
≈ 4.3 GB of VRAM!
```

这就是为什么长上下文窗口需要大量记忆。

### RAG 系统中的 KV 缓存

RAG系统可以巧妙地利用KV缓存优化:

**即时缓存** (得到人类克洛德等一些APIs的支持):

```csharp
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);
    }
}
```

**为何如此强大:**

- 第一个查询:计算系统快速 + 上下文的 KV 缓存( 慢)
- 以相同背景进行后续查询: 重新使用缓存 KV (10x更快! )
- 只有用户查询部分需要新计算

**实例:**

```
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 缓存被重新使用 。

## 火力限制和RAG战略

获取标记和 KV 缓存通知告知您的 RAG 架构决定 :

### 1. 弹跳大小

较小块 = 更精确的检索,但更高的管理费 :

```csharp
// 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. 背景窗口利用

不要在上下文窗口中排到最大 - 离开房间的世代 :

```csharp
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. 多发RAG对话

在聊天室里, 对话历史随时间而增加:

```
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!
```

**解决方案: 使用再检索窗口滑动窗口**

```csharp
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. 名价成本优化

基于API的LLMs按象征性收费。若不小心,RAG可能会爆炸成本:

```csharp
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!
}
```

**降低成本战略:**

1. 获取数量较少、排位更优的文件
2. 使用即时缓存( 亚热带克洛德: 缓存标牌更便宜 90%)
3. 使用更便宜的重新排名模式,最后一代昂贵
4. 使用汇总压缩环境

## 将全RAG与 Toks 一同流动的视觉化

以下是标志、KV缓存和RAG的搭配方式:

```mermaid
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
```

**关键洞察力 :**

1. **输入符号** (2,812,2,812)经过一次处理,以建立最初的KV缓存
2. **代 代 代** 一次发生一个标记, 重复使用 KV 缓存
3. **每个新标志** 添加到 KV 缓存的 KV 缓存, 用于未来待处理的标记
4. **档案和记录管理共计** 需要 = 模型重量 + KV 缓存,用于所有代号 :
5. **长期** = 更大的 KV 缓存 = 更多的 VRAM

## 对ARAG的实际影响

理解标记和 KV 缓存导致更好的RAG 设计:

**1. 预先计算和缓存常见情况:**

```csharp
// 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. 优化块边界:**

```csharp
// 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. 监测生产中的象征性使用:**

```csharp
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阶段:指数化**

- 从各种来源提取的文本
- 整件战略(按款、按句、有重叠)
- 嵌入生成(ONNX、API服务)
- 矢量储存 (Qdrant, pgvictor, Pinecone)

**第2阶段: 第二阶段:检索**

- 查询嵌入( 与索引模式相同 !)
- 近似性搜索(cosine, eccidean, dot 产品)
- 提高精确度的可选重新排序
- 改进结果的元数据过滤

**第3阶段:产生**

- 即时构建(EXText+指示+查询)
- LLM 推理( 温度、 顶部、 最大代号)
- 后处理(抽引、格式化)

**LLM 内部内部**

- 基本单位( 不是字符! )
- KV 缓存: 为什么一代是快( 线性, 不是二次) ?
- 上下文窗口: 在 RAG 中管理象征性限制
- 成本优化:缓存、压缩、智能检索

**关键技术见解:**

1. **相同的嵌入模型** 用于索引和检索(关键! )
2. **踢球比你想象的更重要** - 保持语义一致性
3. **排级提高精确度** 以延时费用计算
4. **名声管理至关重要** - 查询前的估计
5. **KV 缓存使RAG变得可行** - 再利用快速计算
6. **上下文窗口快速填充** - 10 docs × 500 象征性 = 5K 象征性

# 继续第3部分:在实际操作中RAG

你现在明白了 **RAG 如何工作** 技术层面。 但理论只能让你到目前为止。 您如何实际建立这些系统? 您将面对什么挑战? 您可以使用什么先进技术 ?

内 **[第3部分:在实务中协助通知书](/blog/rag-practical-applications)**我们从结构转向执行:

**现实世界应用:**

- 这个博客上的相关文章建议
- 语义博客搜索
- 建造一个“律师GPT”书写助理

**共同挑战和解决办法:**

- 保持环境的启动战略
- 改进您域域的嵌入质量
- 动态管理环境窗口
- 尽管有背景,但防止幻觉
- 不断更新索引

**先进技术:**

- 假设文档嵌入( HyDE)
- 使用 LLM 分隔过滤器自制
- 综合结果的多查询RAG
- 用于减少象征性使用的上下文压缩
- 用于复杂查询的多霍多RAG
- 长期对话记忆

**正在开始 :**

- 每周每周执行计划
- 实用守则实例
- 优化优化战略
- 不使用 RAG 时

**[继续第3部分:在实务中协助 ](/blog/rag-practical-applications)**

## 资源资源资源 资源资源资源 资源资源 资源资源

**基础文件:**

- [知识密集 NLP 任务检索支持生成](https://arxiv.org/abs/2005.11401) - 最初的RAG文件
- [用于 Open- Domay 问答的宽度通道检索器](https://arxiv.org/abs/2004.04906) - DPR基金会
- [你们需要的注意力](https://arxiv.org/abs/1706.03762) - 变压器

**工具和框架:**

- [解冻](https://qdrant.tech/) - 矢量数据库
- [ONNX 运行时间](https://onnxruntime.ai/) - 当地嵌入
- [拉马沙尔普](https://github.com/SciSharp/LLamaSharp) - 当地LLM 推断
- [句子变换器](https://www.sbert.net/) - 嵌入模型

**进一步阅读:**

- [热带: 上下文检索](https://www.anthropic.com/index/contextual-retrieval) - 先进技术

**系列导航 :**

- [第1部分:起源和基本要点](/blog/rag-primer) - 历史和动机
- **第2部分:建筑和内部** (本条) -- -- 技术深层潜水
- [第3部分:在实务中协助通知书](/blog/rag-practical-applications) - 建立实际系统

**[接续到第3部分 ](/blog/rag-practical-applications)**