Back to "RAG in 实践:建立现实世界应用"

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

AI AI-Article LLM Machine Learning RAG Semantic Search

RAG in 实践:建立现实世界应用

Saturday, 22 November 2025

第一部分 第一部分第二部分 第二部分 在此系列中, 我们覆盖了RAG的起源、 基本原理和技术结构。 您了解RAG是什么, 为何重要, 以及它是如何在引擎盖下运行的。 现在是时候将知识付诸实践了。 文章展示了您如何建立真正的RAG系统, 使用工作 C# 代码, 解决共同的挑战, 并使用最新研究的先进技术 。

一. 导言 导言 导言 导言 导言 导言 一,导言 导言 导言 导言 导言 导言

系列导航: 这是RAG系列第3部分:

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

  • RAG是什么及其为何重要(第一部分)
  • 从关键字搜索到语义理解(第一部分)的历史
  • 完整的RAG输油管:编制索引、检索、生成(第二部分)
  • LLM 内部( LLM) : 神记、 KV 缓存、 上下文窗口( 第 2 部分)
  • RAG相对于微调和其他方法(第一部分)

本条假定你理解这些基本要点,并着重阐述 实施、优化和现实世界模式.

此博客上真实世界RAG应用程序

我在这个博客上建立了几个RAG动力功能。让我给你们展示具体的例子。

1. 有关员额建议

每一篇文章都使用语义相似性显示“相关文章”。

如何运作:

  1. 每篇博客文章发表后就被嵌入
  2. 查看柱子时, 我们从 Qdrant 提取嵌入的柱子
  3. 寻找5个最相似的邮后嵌入
  4. 显示为“ 相关职位”

为什么它比标签更好:

  • 标记需要手工分类
  • 语义搜索发现,即使没有匹配标签, 语义搜索也发现在概念上与概念上相关
  • “纸箱合成”和“集装箱管弦”是相关的语义

代码片断 :

public async Task<List<SearchResult>> GetRelatedPostsAsync(
    string currentPostSlug,
    string language,
    int limit = 5)
{
    // Get the current post's embedding
    var currentPost = await _vectorStore.GetByIdAsync(currentPostSlug);

    if (currentPost == null)
        return new List<SearchResult>();

    // Find similar posts
    var similarPosts = await _vectorStore.SearchAsync(
        currentPost.Embedding,
        limit: limit + 1,  // +1 because result includes the current post
        filter: new Filter
        {
            Must =
            {
                new Condition
                {
                    Field = "language",
                    Match = new Match { Keyword = language }
                }
            },
            MustNot =
            {
                new Condition
                {
                    Field = "slug",
                    Match = new Match { Keyword = currentPostSlug }
                }
            }
        }
    );

    return similarPosts.Take(limit).ToList();
}

2. 语义博客搜索

博客上的搜索框使用RAG式语义搜索(虽然没有下一代部分,

用户经验:

  • 搜索“ 建立数据库” 查找关于 PostgreSQL、 实体框架、 迁移的文章
  • 搜索“部署” 查找有关 Docker 、 主机、 CI/ CD 的站点
  • 不需要精确关键字匹配

执行: 我会在即将到来的关于矢量数据库的文章里 写上这个

3. “律师GPT”书写助理

我正在建立一个完整的RAG系统 来帮助我写新的博客文章

使用实例 : 当我开始写“将认证添加到 ASP. NET Core” 时,

  1. 嵌入我目前的草稿
  2. 搜索过去有关认证、ASP.NET、安全等相关内容的岗位
  3. 我曾经用过的相关代码片段
  4. 自动生成与相关员额的内部链接
  5. 与我的写作风格保持一致

全部RAG输油管:

public async Task<WritingAssistanceResponse> GetSuggestionsAsync(
    string currentDraft,
    string topic)
{
    // 1. Embed the current draft
    var draftEmbedding = await _embeddingService.GenerateEmbeddingAsync(
        currentDraft
    );

    // 2. Retrieve related past content
    var relatedPosts = await _vectorStore.SearchAsync(
        draftEmbedding,
        limit: 5
    );

    // 3. Build context for LLM
    var prompt = BuildWritingAssistancePrompt(
        currentDraft,
        topic,
        relatedPosts
    );

    // 4. Generate suggestions using local LLM
    var suggestions = await _llmService.GenerateAsync(prompt);

    // 5. Extract and format citations
    var response = ExtractCitations(suggestions, relatedPosts);

    return response;
}

这是操作中的RAG - 检索(语义搜索)+增强(附加上下文)+生成(LLM建议)。

RAG 共同挑战和解决方案

建筑生产RAG系统并不是微不足道的。我遇到的挑战是如何解决的。

挑战1:冲击战略

问题: 您如何分割文档 ? 太小 = 失去上下文 。 太大 = 不相干的信息 。

解决方案 : 基于文档结构的混合区块 。

public class SmartChunker
{
    public List<Chunk> ChunkDocument(string markdown, string sourceId)
    {
        var chunks = new List<Chunk>();

        // Parse markdown into sections
        var document = Markdown.Parse(markdown);
        var sections = ExtractSections(document);

        foreach (var section in sections)
        {
            var wordCount = CountWords(section.Content);

            if (wordCount < MinChunkSize)
            {
                // Merge small sections
                MergeWithPrevious(chunks, section);
            }
            else if (wordCount > MaxChunkSize)
            {
                // Split large sections
                var subChunks = SplitSection(section);
                chunks.AddRange(subChunks);
            }
            else
            {
                // Just right
                chunks.Add(CreateChunk(section, sourceId));
            }
        }

        return chunks;
    }
}

最佳做法:

  • 有关文件结构(标题,段落)
  • 在块之间添加重叠( 50- 100个单词)
  • 保护代码区块完整保存
  • 在上下文的每个块块中包含区域标题

挑战2:包含质量

问题: 通用嵌入模型可能无法捕捉特定域的语义。

解决办法:

备选方案1:精细嵌入 (已改进)

# Using sentence-transformers in Python
from sentence_transformers import SentenceTransformer, InputExample, losses

model = SentenceTransformer('all-MiniLM-L6-v2')

# Create training examples from your domain
train_examples = [
    InputExample(texts=['Docker Compose', 'container orchestration'], label=0.9),
    InputExample(texts=['Entity Framework', 'ORM database'], label=0.9),
    InputExample(texts=['Docker', 'apple fruit'], label=0.1)
]

# Fine-tune
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)
train_loss = losses.CosineSimilarityLoss(model)
model.fit(train_objectives=[(train_dataloader, train_loss)], epochs=1)

备选方案2:混合嵌入 (复合多模型)

public async Task<float[]> GenerateHybridEmbeddingAsync(string text)
{
    var semantic = await _semanticModel.GenerateEmbeddingAsync(text);
    var keyword = await _keywordModel.GenerateEmbeddingAsync(text);

    // Concatenate or weighted average
    return CombineEmbeddings(semantic, keyword);
}

备选办法3:增加元数据过滤

var results = await _vectorStore.SearchAsync(
    queryEmbedding,
    limit: 10,
    filter: new Filter
    {
        Must =
        {
            new Condition { Field = "category", Match = new Match { Keyword = "ASP.NET" } },
            new Condition { Field = "date", Range = new Range { Gte = "2024-01-01" } }
        }
    }
);

挑战3:背景窗口管理

问题: LLM 具有象征性限制。 您如何在窗口中匹配查询 + 上下文 + 提示 ?

解决方案 : 动态环境选择和概述。

public string BuildContextAwarePrompt(
    string query,
    List<SearchResult> retrievedDocs,
    int maxTokens = 4096)
{
    var promptTemplate = GetPromptTemplate();
    var queryTokens = CountTokens(query);
    var templateTokens = CountTokens(promptTemplate);

    // Reserve tokens for: prompt + query + response
    var availableForContext = maxTokens - queryTokens - templateTokens - 500; // 500 for response

    // Add context until we hit limit
    var selectedContext = new List<SearchResult>();
    var currentTokens = 0;

    foreach (var doc in retrievedDocs.OrderByDescending(d => d.Score))
    {
        var docTokens = CountTokens(doc.Text);

        if (currentTokens + docTokens <= availableForContext)
        {
            selectedContext.Add(doc);
            currentTokens += docTokens;
        }
        else
        {
            // Try summarizing the doc if it's important
            if (doc.Score > 0.85)
            {
                var summary = await SummarizeAsync(doc.Text, maxTokens: 200);
                var summaryTokens = CountTokens(summary);

                if (currentTokens + summaryTokens <= availableForContext)
                {
                    selectedContext.Add(new SearchResult
                    {
                        Text = summary,
                        Title = doc.Title,
                        Score = doc.Score
                    });
                    currentTokens += summaryTokens;
                }
            }
        }
    }

    return FormatPrompt(query, selectedContext);
}

挑战4:不顾RAG而幻觉

问题: 即便在某种情况下,LLMs有时也忽略了它,产生幻觉。

解决办法:

1. 即时工程:

var systemPrompt = @"
You are a technical assistant.

CRITICAL RULES:
1. ONLY use information from the provided CONTEXT sections
2. If the context doesn't contain the answer, say 'I don't have enough information in the provided context to answer that'
3. DO NOT use your training data to supplement answers
4. Always cite the source using [1], [2] notation
5. If you're unsure, say so

CONTEXT:
{context}

QUESTION: {query}

ANSWER (following all rules above):
";

2. 发电后验证:

public async Task<bool> ValidateResponseAgainstContext(
    string response,
    List<SearchResult> context)
{
    // Check if response contains claims not in context
    var responseSentences = SplitIntoSentences(response);

    foreach (var sentence in responseSentences)
    {
        var isSupported = await IsClaimSupportedByContext(sentence, context);

        if (!isSupported)
        {
            _logger.LogWarning("Hallucination detected: {Sentence}", sentence);
            return false;
        }
    }

    return true;
}

3. 迭代改进:

public async Task<string> GenerateWithValidationAsync(
    string query,
    List<SearchResult> context,
    int maxAttempts = 3)
{
    for (int attempt = 0; attempt < maxAttempts; attempt++)
    {
        var response = await _llm.GenerateAsync(
            BuildPrompt(query, context)
        );

        var isValid = await ValidateResponseAgainstContext(response, context);

        if (isValid)
            return response;

        // Refine prompt for next attempt
        query = $"{query}\n\nPrevious attempt hallucinated. Stick strictly to the context.";
    }

    return "I couldn't generate a reliable answer. Please rephrase your question.";
}

挑战5:保持指数不断更新

问题: 当您添加新文档时,矢量数据库需要保持当前状态。

解决方案 : 自动索引输油管。

public class BlogIndexingBackgroundService : BackgroundService
{
    private readonly IVectorStoreService _vectorStore;
    private readonly IMarkdownService _markdownService;
    private readonly ILogger<BlogIndexingBackgroundService> _logger;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await IndexNewPostsAsync(stoppingToken);

                // Check for updates every hour
                await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in indexing service");
            }
        }
    }

    private async Task IndexNewPostsAsync(CancellationToken ct)
    {
        var allPosts = await _markdownService.GetAllPostsAsync();

        foreach (var post in allPosts)
        {
            var existingDoc = await _vectorStore.GetByIdAsync(post.Slug);

            // Check if content changed
            var currentHash = ComputeHash(post.Content);

            if (existingDoc == null || existingDoc.ContentHash != currentHash)
            {
                _logger.LogInformation("Indexing updated post: {Title}", post.Title);

                var chunks = _chunker.ChunkDocument(post.Content, post.Slug);

                foreach (var chunk in chunks)
                {
                    var embedding = await _embeddingService.GenerateEmbeddingAsync(chunk.Text);

                    await _vectorStore.UpsertAsync(
                        id: $"{post.Slug}_{chunk.Index}",
                        embedding: embedding,
                        metadata: new Dictionary<string, object>
                        {
                            ["slug"] = post.Slug,
                            ["title"] = post.Title,
                            ["chunk_index"] = chunk.Index,
                            ["content_hash"] = currentHash
                        },
                        ct: ct
                    );
                }
            }
        }
    }
}

RAG 先进技术

让我们探索一下最近的研究中最先进的RAG技术

1. 假设文件嵌入(HyDE)

问题: 用户查询往往很短,而且结构不完善。文档块详细,写得很好。这种不匹配会伤害检索。

解决方案 : 生成一个假想的理想文件 来回答询问,嵌入,然后搜索。

public async Task<List<SearchResult>> HyDESearchAsync(string query)
{
    // Generate hypothetical answer (even if hallucinated)
    var hypotheticalAnswer = await _llm.GenerateAsync($@"
        Write a detailed, technical paragraph that would perfectly answer this question:

        Question: {query}

        Paragraph:"
    );

    // Embed the hypothetical answer
    var embedding = await _embeddingService.GenerateEmbeddingAsync(
        hypotheticalAnswer
    );

    // Search using this embedding
    return await _vectorStore.SearchAsync(embedding);
}

为什么它起作用: 假设答案使用与实际文件相似的语言和结构,从而改进检索。

2. 自我调查

问题: 用户查询往往将语义搜索与元数据过滤器混在一起。

示例: “ 关于 Docker 的最近文章” = 语义 (“ Docker ” ) + 过滤器 (日期 > 2024-01-01-01)

解决方案 : 使用 LLM 将查询解析为语义+元数据过滤器 。

public async Task<SearchQuery> ParseSelfQueryAsync(string naturalLanguageQuery)
{
    var parsingPrompt = $@"
        Parse this search query into:
        1. Semantic search query (what the user is looking for)
        2. Metadata filters (category, date range, etc.)

        User Query: {naturalLanguageQuery}

        Output JSON:
        {{
            ""semantic_query"": ""the core concept"",
            ""filters"": {{
                ""category"": ""...",
                ""date_after"": ""..."",
                ""date_before"": ""...""
            }}
        }}
    ";

    var jsonResponse = await _llm.GenerateAsync(parsingPrompt);
    var parsed = JsonSerializer.Deserialize<SearchQuery>(jsonResponse);

    return parsed;
}

// Use parsed query
var parsedQuery = await ParseSelfQueryAsync("Recent ASP.NET posts about authentication");
// semantic_query: "authentication"
// filters: { category: "ASP.NET", date_after: "2024-01-01" }

var results = await _vectorStore.SearchAsync(
    embedding: await _embeddingService.GenerateEmbeddingAsync(parsedQuery.SemanticQuery),
    filter: BuildFilter(parsedQuery.Filters)
);

3. 多查询RAG

问题: 单一查询可能因措辞而错过相关文件。

解决方案 : 生成查询的多种变量, 全部搜索, 合并结果 。

public async Task<List<SearchResult>> MultiQuerySearchAsync(string query)
{
    // Generate query variations
    var variations = await _llm.GenerateAsync($@"
        Generate 3 different ways to phrase this search query:

        Original: {query}

        Variations (one per line):
    ");

    var queries = variations.Split('\n', StringSplitOptions.RemoveEmptyEntries)
        .Prepend(query) // Include original
        .ToList();

    // Search with all variations
    var allResults = new List<SearchResult>();

    foreach (var q in queries)
    {
        var embedding = await _embeddingService.GenerateEmbeddingAsync(q);
        var results = await _vectorStore.SearchAsync(embedding, limit: 10);
        allResults.AddRange(results);
    }

    // Deduplicate and merge scores
    var merged = allResults
        .GroupBy(r => r.Id)
        .Select(g => new SearchResult
        {
            Id = g.Key,
            Text = g.First().Text,
            Title = g.First().Title,
            Score = g.Max(r => r.Score) // Take best score
        })
        .OrderByDescending(r => r.Score)
        .ToList();

    return merged;
}

4. 背景压缩

问题: 回收的块含有不相关的信息。 发送所有的废品标记 。

解决方案 : 使用较小的 LLM 压缩只检索到相关部分的上下文。

public async Task<string> CompressContextAsync(
    string query,
    List<SearchResult> retrievedDocs)
{
    var compressed = new List<string>();

    foreach (var doc in retrievedDocs)
    {
        var compressionPrompt = $@"
            Extract only the sentences from this document that are relevant to answering the question.

            Question: {query}

            Document:
            {doc.Text}

            Relevant excerpts (maintain original wording):
        ";

        var relevantExcerpt = await _smallLLM.GenerateAsync(compressionPrompt);

        if (!string.IsNullOrWhiteSpace(relevantExcerpt))
        {
            compressed.Add($"From '{doc.Title}':\n{relevantExcerpt}");
        }
    }

    return string.Join("\n\n", compressed);
}

5. 循环回收(Multi-Hop RAG)

问题: 复杂的问题需要来自多种来源的信息,而这些信息需要连接起来。

例如:“博客使用什么数据库? 如何进行语义搜索?”

  • Hop 1: 查找使用什么数据库( PostgreSQL)
  • 霍普2: 找出语义搜索是如何与该数据库合作的

解决方案 : 循环检索和合成。

public async Task<string> MultiHopRAGAsync(string complexQuery, int maxHops = 3)
{
    var currentQuery = complexQuery;
    var allContext = new List<SearchResult>();

    for (int hop = 0; hop < maxHops; hop++)
    {
        // Retrieve for current query
        var results = await SearchAsync(currentQuery, limit: 5);
        allContext.AddRange(results);

        // Check if we have enough information
        var synthesisPrompt = $@"
            Original question: {complexQuery}

            Context so far:
            {FormatContext(allContext)}

            Can you answer the original question with this context?
            If yes, provide the answer.
            If no, what additional information do you need? (be specific)
        ";

        var synthesis = await _llm.GenerateAsync(synthesisPrompt);

        if (synthesis.Contains("yes", StringComparison.OrdinalIgnoreCase))
        {
            // We have enough information
            return ExtractAnswer(synthesis);
        }

        // Extract what we need for next hop
        currentQuery = ExtractNextQuery(synthesis);
    }

    // Final synthesis with all gathered context
    return await GenerateFinalAnswerAsync(complexQuery, allContext);
}

6. 具有RAG+概述的长期背景记忆

问题: 如何建立能记住数月或数年前对话的人工智能系统?

解决方案 : 将RAG与渐进式汇总结合起来,以创建耐久、可搜索的内存。

这就是在 DiSE(稀释合成合成进化) - 一个我正在建造的先进系统 使用基于RAG的背景内存 来永久保持共享的谈话历史

实例设想:

User (Today): "Remember George's specs?"
AI: "Yes, you discussed George's prescription requirements in our conversation
     from 5 years ago (2019-03-15). He needed progressive lenses with..."

如何运作:

flowchart TB
    A[User Message] --> B[Store in RAG Memory]
    B --> C[Extract Key Entities & Topics]
    C --> D[Link to Past Conversations]

    E[Periodic Summarization] --> F[Summarize Old Conversations]
    F --> G[Store Summary with High-Level Tags]
    G --> H[Keep Original for Retrieval]

    I[Future Query: 'George's specs'] --> J[Semantic Search in RAG]
    J --> K[Find: 2019 conversation]
    K --> L[Retrieve Original Context]
    L --> M[LLM generates response with 5-year-old context!]

    style B stroke:#f9f,stroke-width:3px
    style J stroke:#bbf,stroke-width:3px

实施办法:

public class LongTermConversationalMemory
{
    private readonly IVectorStoreService _vectorStore;
    private readonly IEmbeddingService _embeddingService;

    public async Task StoreConversationAsync(
        string conversationId,
        string userId,
        List<ConversationTurn> turns,
        DateTime timestamp)
    {
        // Extract key entities and topics
        var entities = await ExtractEntitiesAsync(turns);
        var topics = await ExtractTopicsAsync(turns);

        // Create searchable representation
        var conversationText = string.Join("\n", turns.Select(t =>
            $"{t.Speaker}: {t.Message}"));

        // Generate embedding
        var embedding = await _embeddingService.GenerateEmbeddingAsync(
            conversationText);

        // Store in RAG with rich metadata
        await _vectorStore.IndexDocumentAsync(
            id: $"conv_{conversationId}",
            embedding: embedding,
            metadata: new Dictionary<string, object>
            {
                ["user_id"] = userId,
                ["timestamp"] = timestamp.ToString("O"),
                ["entities"] = entities,  // ["George", "specs", "prescription"]
                ["topics"] = topics,      // ["healthcare", "eyewear"]
                ["full_text"] = conversationText,
                ["turn_count"] = turns.Count
            }
        );
    }

    public async Task<List<PastContext>> RetrieveRelevantPastAsync(
        string currentQuery,
        string userId,
        int limit = 5)
    {
        // Embed the current query
        var queryEmbedding = await _embeddingService.GenerateEmbeddingAsync(
            currentQuery);

        // Search past conversations
        var results = await _vectorStore.SearchAsync(
            queryEmbedding,
            limit: limit,
            filter: new Filter
            {
                Must =
                {
                    new Condition { Field = "user_id", Match = new Match { Keyword = userId } }
                }
            }
        );

        return results.Select(r => new PastContext
        {
            ConversationId = r.Id,
            Timestamp = DateTime.Parse(r.Metadata["timestamp"].ToString()),
            Entities = (List<string>)r.Metadata["entities"],
            FullText = r.Metadata["full_text"].ToString(),
            Relevance = r.Score
        }).ToList();
    }

    // Periodic summarization to keep memory manageable
    public async Task SummarizeOldConversationsAsync(DateTime olderThan)
    {
        var oldConversations = await _vectorStore.FindByDateRangeAsync(
            endDate: olderThan);

        foreach (var conv in oldConversations)
        {
            // Generate summary using LLM
            var summary = await _llm.GenerateAsync($@"
                Summarize this conversation, preserving key facts and entities:

                {conv.FullText}

                Summary:");

            // Update document with summary while keeping original
            await _vectorStore.UpdateAsync(
                id: conv.Id,
                additionalMetadata: new Dictionary<string, object>
                {
                    ["summary"] = summary,
                    ["summarized_at"] = DateTime.UtcNow.ToString("O")
                }
            );
        }
    }
}

为何如此强大:

  1. 无限内存范围 -多年前的谈话 和昨天的一样畅通
  2. 语义搜索 - "乔治的外形" 发现谈话 即使储存在"乔治的处方眼罩要求"
  3. 实体连接 - 所有提到"乔治"的谈话都相连
  4. 隐私保护 - 能够实施每个用户的记忆隔离
  5. 成本效益高 - 总结防止储存爆炸,同时保持可搜索性

DISE 中真实世界的例子 :

DISE 使用此方法来记住 :

  • 工具引用 从几个月前开始(什么奏效,什么失败)
  • 代码模式 在以往项目中取得成功的项目
  • 用户用户偏好 在先前对话中确立
  • 域知识 随时间累积累积

这创造了一个AI系统, 真正从每次互动中“吸取教训”, 建立机构记忆, 而不是每次会议都重新开始。

考虑以下挑战的挑战:

  1. 隐私隐私:旧对话必须适当隔离每个用户
  2. 相关性衰减:并非所有老资料都保持相关
  3. 储存费用:比额表需要总结战略
  4. 一致性:使摘要与原件保持一致
  5. 检索精度:平衡兼顾最近和历史上重要背景

这个技术把RAG从"搜索我的文件" 转变为"记住我们讨论过的一切" 长期人工智能助理的游戏变换器

不使用 RAG 时

RAG不是总能解决问题的 是时候避免了

1. 一般性知识问题

  • "法国首都是什么?"
  • RAG 增加延迟和成本,无益

2. 创意写作

  • 诗歌、小说、集思广益
  • RAG 以现有案文为基础,限制创造力

3. 实时数据需求

  • 股票价格、实场体育分数
  • 最好由API的整合,而不是由RAG来服务

4. 数学推理

  • "计算百分之十五的小费83.47美元"
  • LLMs 能够做算术;RAG 帮不上忙

5. 非常小的知识库

  • 如果您的知识基础适合上下文窗口, 请将其全部包含在内
  • 5份文件的RAG管理费不值得

6. 当控制LLM培训时

  • 如果你在数据上从零到零地训练一个模型 微微调整可能更好
  • 使用你无法控制的预训练模型时 RAG是最好的

以RAG为起点:实用指南

想建立自己的RAG系统吗?

第1周:启动简单

目标: 获得基本的检索 工作与无LLM。

// 1. Choose an embedding service (start with API for simplicity)
var openAI = new OpenAIClient(apiKey);

// 2. Embed a few test documents
var docs = new[]
{
    "Docker is a containerization platform",
    "Kubernetes orchestrates containers",
    "Entity Framework is an ORM for .NET"
};

var embeddings = new List<float[]>();
foreach (var doc in docs)
{
    var response = await openAI.GetEmbeddingsAsync(
        new EmbeddingsOptions("text-embedding-3-small", new[] { doc })
    );
    embeddings.Add(response.Value.Data[0].Embedding.ToArray());
}

// 3. Implement basic search (in-memory for now)
var query = "container orchestration";
var queryEmbedding = await GetEmbeddingAsync(query);

var results = embeddings
    .Select((emb, idx) => new
    {
        Text = docs[idx],
        Score = CosineSimilarity(queryEmbedding, emb)
    })
    .OrderByDescending(r => r.Score)
    .ToList();

// 4. Verify search works
foreach (var result in results)
{
    Console.WriteLine($"{result.Score:F3}: {result.Text}");
}
// Expected: Kubernetes scores highest

第2周:添加矢量数据库

目标: 缩放为真实的文档收藏 。

今后执行的步骤:

  1. 在 docker 中运行 Qdrant
  2. 您的文件索引索引
  3. 执行搜索端点

我将在即将发表的关于矢量数据库的文章中详细论述这一点。

第3周:增加LLM生成

目标: 完成RAG输油管

// 1. Retrieve context
var context = await SearchAsync(query, limit: 3);

// 2. Build prompt
var prompt = $@"
    Answer the question using this context:

    {FormatContext(context)}

    Question: {query}

    Answer:";

// 3. Generate (start with API)
var response = await openAI.GetChatCompletionsAsync(new ChatCompletionsOptions
{
    Messages =
    {
        new ChatMessage(ChatRole.System, "You are a helpful assistant."),
        new ChatMessage(ChatRole.User, prompt)
    },
    Temperature = 0.7f,
    MaxTokens = 500
});

return response.Value.Choices[0].Message.Content;

第4周:优化和波兰语

  • 添加缓存( 保存 API 成本)
  • 改进填布战略
  • 添加元数据过滤
  • 执行混合搜索
  • 添加引证提取

升入本地( 可选)

一旦基本知识发挥作用,就迁移到当地推论(我将在即将发表的文章中述及这一点):

  • 本地LLM 与 GPU加速度的本地LLM 推推
  • 使用 ONNX 运行时间

结论 结论 结论 结论 结论

RAG(检索-提款一代)是使LLMs更加准确、更新和可信赖的有力技术,其方法是以实际文件作为答复的依据。

  1. 检索 利用语义搜索从知识库获得的相关信息
  2. 增加 使用检索到的上下文, LLM 的快速
  3. 生成生成 以有引文的实际来源为依据的答复

协助通知书的主要优点是:

  • 总是更新(只需更新知识库)
  • 来源(减少幻觉)
  • 可解释(可列举来源)
  • 成本效益高(没有昂贵的再培训)
  • 隐私保护( 完全本地运行)
  • 特定域(使用您自己的文件)

何时使用 RAG :

  • 知识密集型任务
  • 经常更新信息
  • 私人/专有数据
  • 需要引证和可审计性
  • 成本和简单问题

避免 RAG 时 :

  • 一般知识问题
  • 创意写作
  • 实时数据(使用 APIPS代替)
  • 非常小的知识基础

利用HYDE、多查询检索和背景压缩等先进技术,该领域正在迅速发展,但核心概念仍然简单:让LLMS在正确的时间获得正确的信息。

开始简单、测量结果和循环。 RAG是当今建立可靠的AI系统最实际的方法之一。

结论:从理论到生产

你们已经完成了三个部分的RAG系列:

第1部分:起源和基本要点

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

第2部分:建筑和内部

  • 完整的RAG输油管线:编制索引、检索、生成
  • LLM 内部( LLM) : 神记、 KV 缓存、 上下文窗口
  • C#代码技术实施细节(C#代码)

第3部分:实践中的RCG(本条)

  • 现实世界实施(相关员额、语义搜索、书写助理)
  • 共同挑战和解决办法(灌注、嵌入质量、幻觉)
  • 先进技术(HyDE、自查、多查询、背景压缩、长期记忆)
  • 不使用 RAG 时
  • 获取启动指南

你现在有完整的图象: 从了解RAG的起源到建立具有先进优化的生产系统。

下一个是什么?

现在你们从理论到实践都了解RAG, 即将发表的文章将展示你们如何在 C# 建立完整、可生产、可生产的RAG系统:

即将到来:

  • CPU 友好语义搜索 - 与ONNX内嵌入器一起进行语义搜索,在任何没有 GPU 要求的 VPS 上运行
  • 自住式矢量数据库 - 与多克(Docker)对Qdrant的完整设置指南,包括索引编制、搜索和优化战略
  • 建造一名RAG书写助理 - 建立以现有内容作为知识库的AI-动力写作助理的完整系列

这些文章将引领你从理论到实践, 完整的工作守则,部署策略, 和现实世界优化,

继续关注实际操作的实施指南, 将RAG知识转化为工作系统!

资源资源资源 资源资源资源 资源资源 资源资源

基础文件:

工具和框架:

进一步阅读:

本RAG系列:

快乐的建筑!

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.