基于(yu)Microsoft.Extensions.AI核心(xin)庫實(shi)現RAG應用
大(da)家好,我是Edison。
之前我(wo)們(men)了解 和 兩個重要的(de)AI應(ying)用核心庫。基于對他(ta)們(men)的(de)了解,今(jin)天(tian)我(wo)們(men)就可(ke)以來實(shi)戰一個RAG問(wen)答(da)應(ying)用,把(ba)之前所學的(de)串起來。
前(qian)提(ti)知識(shi)點:向量存(cun)儲(chu)、詞(ci)嵌入、向量搜索、提(ti)示詞(ci)工程、函數(shu)調用。
案例需求背景
假(jia)設我(wo)們(men)在一(yi)(yi)家名叫“易(yi)速鮮(xian)花(hua)”的(de)(de)電(dian)商網站工作,顧名思義,這(zhe)是一(yi)(yi)家從事鮮(xian)花(hua)電(dian)商的(de)(de)網站。我(wo)們(men)有一(yi)(yi)些運(yun)營手冊(ce)、員工手冊(ce)之類的(de)(de)文檔(例如下(xia)圖(tu)所示的(de)(de)一(yi)(yi)些pdf文件),想要將其導入知識庫并創建一(yi)(yi)個AI機器人,負責日常為(wei)員工解答一(yi)(yi)些政策(ce)性的(de)(de)問題。
例如,員(yuan)工想要了解(jie)獎勵標準(zhun)(zhun)、行為準(zhun)(zhun)備、報銷流程(cheng)等等,都可以通過(guo)和這個AI機器人對話就可以快速了解(jie)最新的(de)政策和流程(cheng)。
在接下(xia)來的(de)Demo中,我(wo)們會使用以下(xia)工(gong)具:
(1) LLM 采用 Qwen2.5-7B-Instruct,可以使(shi)用SiliconFlow平臺提供的API,你也可以改(gai)為你喜歡的其他(ta)模型如DeepSeek,但是建議不要用大炮打蚊(wen)子(zi)哈。
注冊地址:
(2) Qdrant 作為(wei) 向量數據庫,可以使用Docker在(zai)你本地(di)運行一個:
docker run -p 6333:6333 -p 6334:6334 \ -v $(pwd)/qdrant_storage:/qdrant/storage \ qdrant/qdrant
(3) Ollama 運行 bge-m3 模型 作為(wei) Emedding生成(cheng)器,可以自行拉取一個(ge)在你本地運行:
ollama pull bge-m3
構建你的RAG應用
創建一(yi)個控制臺(tai)應(ying)用程序(xu),添加一(yi)些(xie)必要的文件(jian)目錄(lu) 和 配(pei)置(zhi)文件(jian)(json),最終的解決方案(an)如(ru)下圖(tu)所(suo)示。

在(zai)Documents目錄下放了我們要(yao)導(dao)入的一些pdf文檔,例如(ru)公司運營手(shou)冊(ce)、員工(gong)手(shou)冊(ce)等(deng)等(deng)。
在Models目(mu)錄下(xia)放了一(yi)些(xie)公用的model類(lei),其中TextSnippet類(lei)作為(wei)向量(liang)存(cun)儲的實(shi)體類(lei),而TextSearchResult類(lei)則作為(wei)向量(liang)搜(sou)索(suo)結果的模型類(lei)。
(1)TextSnippet
這里我(wo)(wo)們的TextEmbedding字段就是我(wo)(wo)們的向量值,它有1024維。
注意:這里的(de)維度(du)是我們自(zi)己定義(yi)的(de),你也可以改為你想要的(de)維度(du)數量(liang),但是你的(de)詞嵌入模型(xing)需要支持(chi)你想要的(de)維度(du)數量(liang)。
public sealed class TextSnippet<TKey> { [VectorStoreRecordKey] public required TKey Key { get; set; } [VectorStoreRecordData] public string? Text { get; set; } [VectorStoreRecordData] public string? ReferenceDescription { get; set; } [VectorStoreRecordData] public string? ReferenceLink { get; set; } [VectorStoreRecordVector(Dimensions: 1024)] public ReadOnlyMemory<float> TextEmbedding { get; set; } }
(2)TextSearchResult
這(zhe)個類主要用(yong)來返(fan)回給LLM做推理用(yong)的,我這(zhe)里只需要三個字段:Value, Link 和 Score 即可。
public class TextSearchResult { public string Value { get; set; } public string? Link { get; set; } public double? Score { get; set; } }
(3)RawContent
這個類主要(yao)用(yong)來在(zai)PDF導入時(shi)作為一個臨時(shi)存儲源數據文檔(dang)內容。
public sealed class RawContent { public string? Text { get; init; } public int PageNumber { get; init; } }
在Plugins目錄下(xia)放了一些公用(yong)(yong)的(de)幫助類,如PdfDataLoader可(ke)(ke)以實(shi)現PDF文件的(de)讀取(qu)和導入向量數據庫,VectorDataSearcher可(ke)(ke)以實(shi)現根(gen)據用(yong)(yong)戶的(de)query搜索向量數據庫獲取(qu)TopN個近似文檔,而UniqueKeyGenerator則用(yong)(yong)來(lai)生(sheng)成(cheng)唯一的(de)ID Key。
(1)PdfDataLoader
作為PDF文(wen)件的導入(ru)核心邏輯,它實現了PDF文(wen)檔讀(du)取、切分、生成指(zhi)定維(wei)度(du)的向(xiang)量(liang) 并(bing) 存(cun)入(ru)向(xiang)量(liang)數據庫。
注意:這里(li)只考(kao)(kao)慮了文(wen)本(ben)格式的內容,如果你(ni)還想考(kao)(kao)慮文(wen)件中的圖(tu)片(pian)將其轉成文(wen)本(ben),你(ni)需要增(zeng)加一個LLM來幫你(ni)做圖(tu)片(pian)轉文(wen)本(ben)的工作。
public sealed class PdfDataLoader<TKey> where TKey : notnull { private readonly IVectorStoreRecordCollection<TKey, TextSnippet<TKey>> _vectorStoreRecordCollection; private readonly UniqueKeyGenerator<TKey> _uniqueKeyGenerator; private readonly IEmbeddingGenerator<string, Embedding<float>> _embeddingGenerator; public PdfDataLoader( UniqueKeyGenerator<TKey> uniqueKeyGenerator, IVectorStoreRecordCollection<TKey, TextSnippet<TKey>> vectorStoreRecordCollection, IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator) { _vectorStoreRecordCollection = vectorStoreRecordCollection; _uniqueKeyGenerator = uniqueKeyGenerator; _embeddingGenerator = embeddingGenerator; } public async Task LoadPdf(string pdfPath, int batchSize, int betweenBatchDelayInMs) { // Create the collection if it doesn't exist. await _vectorStoreRecordCollection.CreateCollectionIfNotExistsAsync(); // Load the text and images from the PDF file and split them into batches. var sections = LoadAllTexts(pdfPath); var batches = sections.Chunk(batchSize); // Process each batch of content items. foreach (var batch in batches) { // Get text contents var textContentTasks = batch.Select(async content => { if (content.Text != null) return content; return new RawContent { Text = string.Empty, PageNumber = content.PageNumber }; }); var textContent = (await Task.WhenAll(textContentTasks)) .Where(c => !string.IsNullOrEmpty(c.Text)) .ToList(); // Map each paragraph to a TextSnippet and generate an embedding for it. var recordTasks = textContent.Select(async content => new TextSnippet<TKey> { Key = _uniqueKeyGenerator.GenerateKey(), Text = content.Text, ReferenceDescription = $"{new FileInfo(pdfPath).Name}#page={content.PageNumber}", ReferenceLink = $"{new Uri(new FileInfo(pdfPath).FullName).AbsoluteUri}#page={content.PageNumber}", TextEmbedding = await _embeddingGenerator.GenerateEmbeddingVectorAsync(content.Text!) }); // Upsert the records into the vector store. var records = await Task.WhenAll(recordTasks); var upsertedKeys = _vectorStoreRecordCollection.UpsertBatchAsync(records); await foreach (var key in upsertedKeys) { Console.WriteLine($"Upserted record '{key}' into VectorDB"); } await Task.Delay(betweenBatchDelayInMs); } } private static IEnumerable<RawContent> LoadAllTexts(string pdfPath) { using (PdfDocument document = PdfDocument.Open(pdfPath)) { foreach (Page page in document.GetPages()) { var blocks = DefaultPageSegmenter.Instance.GetBlocks(page.GetWords()); foreach (var block in blocks) yield return new RawContent { Text = block.Text, PageNumber = page.Number }; } } } }
(2)VectorDataSearcher
和介紹的內容類似,主要做語義搜(sou)索,獲取(qu)TopN個近似內容。
public class VectorDataSearcher<TKey> where TKey : notnull { private readonly IVectorStoreRecordCollection<TKey, TextSnippet<TKey>> _vectorStoreRecordCollection; private readonly IEmbeddingGenerator<string, Embedding<float>> _embeddingGenerator; public VectorDataSearcher(IVectorStoreRecordCollection<TKey, TextSnippet<TKey>> vectorStoreRecordCollection, IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator) { _vectorStoreRecordCollection = vectorStoreRecordCollection; _embeddingGenerator = embeddingGenerator; } [Description("Get top N text search results from vector store by user's query (N is 1 by default)")] [return: Description("Collection of text search result")] public async Task<IEnumerable<TextSearchResult>> GetTextSearchResults(string query, int topN = 1) { var queryEmbedding = await _embeddingGenerator.GenerateEmbeddingVectorAsync(query); // Query from vector data store var searchOptions = new VectorSearchOptions() { Top = topN, VectorPropertyName = nameof(TextSnippet<TKey>.TextEmbedding) }; var searchResults = await _vectorStoreRecordCollection.VectorizedSearchAsync(queryEmbedding, searchOptions); var responseResults = new List<TextSearchResult>(); await foreach (var result in searchResults.Results) { responseResults.Add(new TextSearchResult() { Value = result.Record.Text ?? string.Empty, Link = result.Record.ReferenceLink ?? string.Empty, Score = result.Score }); } return responseResults; } }
(3)UniqueKeyGenerator
這個主要(yao)是一個代理(li),后續我們主要(yao)使用(yong)Guid作為Key。
public sealed class UniqueKeyGenerator<TKey>(Func<TKey> generator) where TKey : notnull { /// <summary> /// Generate a unique key. /// </summary> /// <returns>The unique key that was generated.</returns> public TKey GenerateKey() => generator(); }
串聯實現RAG問答
安裝NuGet包:
Microsoft.Extensions.AI (preview) Microsoft.Extensions.Ollama (preivew) Microsoft.Extensions.AI.OpenAI (preivew) Microsoft.Extensions.VectorData.Abstractions (preivew) Microsoft.SemanticKernel.Connectors.Qdrant (preivew) PdfPig (0.1.9) Microsoft.Extensions.Configuration (8.0.0) Microsoft.Extensions.Configuration.Json (8.0.0)
下面我們分解(jie)幾個核心步驟來實現(xian)RAG問答。
Step1. 配置文件appsettings.json:
{ "LLM": { "EndPoint": "//api.siliconflow.cn", "ApiKey": "sk-**********************", // Replace with your ApiKey "ModelId": "Qwen/Qwen2.5-7B-Instruct" }, "Embeddings": { "Ollama": { "EndPoint": "//localhost:11434", "ModelId": "bge-m3" } }, "VectorStores": { "Qdrant": { "Host": "edt-dev-server", "Port": 6334, "ApiKey": "EdisonTalk@2025" } }, "RAG": { "CollectionName": "oneflower", "DataLoadingBatchSize": 10, "DataLoadingBetweenBatchDelayInMilliseconds": 1000, "PdfFileFolder": "Documents" } }
Step2. 加載配置:
var config = new ConfigurationBuilder() .AddJsonFile($"appsettings.json") .Build();
Step3. 初始化ChatClient、Embedding生成器 以及 VectorStore:
# ChatClient var apiKeyCredential = new ApiKeyCredential(config["LLM:ApiKey"]); var aiClientOptions = new OpenAIClientOptions(); aiClientOptions.Endpoint = new Uri(config["LLM:EndPoint"]); var aiClient = new OpenAIClient(apiKeyCredential, aiClientOptions) .AsChatClient(config["LLM:ModelId"]); var chatClient = new ChatClientBuilder(aiClient) .UseFunctionInvocation() .Build(); # EmbeddingGenerator var embedingGenerator = new OllamaEmbeddingGenerator(new Uri(config["Embeddings:Ollama:EndPoint"]), config["Embeddings:Ollama:ModelId"]); # VectorStore var vectorStore = new QdrantVectorStore(new QdrantClient(host: config["VectorStores:Qdrant:Host"], port: int.Parse(config["VectorStores:Qdrant:Port"]), apiKey: config["VectorStores:Qdrant:ApiKey"]));
Step4. 導入PDF文檔到VectorStore:
var ragConfig = config.GetSection("RAG"); // Get the unique key genrator var uniqueKeyGenerator = new UniqueKeyGenerator<Guid>(() => Guid.NewGuid()); // Get the collection in qdrant var ragVectorRecordCollection = vectorStore.GetCollection<Guid, TextSnippet<Guid>>(ragConfig["CollectionName"]); // Get the PDF loader var pdfLoader = new PdfDataLoader<Guid>(uniqueKeyGenerator, ragVectorRecordCollection, embedingGenerator); // Start to load PDF to VectorStore var pdfFilePath = ragConfig["PdfFileFolder"]; var pdfFiles = Directory.GetFiles(pdfFilePath); try { foreach (var pdfFile in pdfFiles) { Console.WriteLine($"[LOG] Start Loading PDF into vector store: {pdfFile}"); await pdfLoader.LoadPdf( pdfFile, int.Parse(ragConfig["DataLoadingBatchSize"]), int.Parse(ragConfig["DataLoadingBetweenBatchDelayInMilliseconds"])); Console.WriteLine($"[LOG] Finished Loading PDF into vector store: {pdfFile}"); } Console.WriteLine($"[LOG] All PDFs loaded into vector store succeed!"); } catch (Exception ex) { Console.WriteLine($"[ERROR] Failed to load PDFs: {ex.Message}"); return; }
Step5. 構建AI對話機器人:
重點關注這里(li)的提(ti)示(shi)詞(ci)模板(ban),我們做了(le)幾件事情(qing):
(1)給AI設定一個(ge)人設:鮮花網站的AI對話機器人,告知其負(fu)責的職(zhi)責。
(2)告訴AI要使用(yong)相(xiang)(xiang)關工具(向量搜索插件)進行相(xiang)(xiang)關背景信息(xi)的搜索獲取,然后將結(jie)果 連同 用(yong)戶的問題 組(zu)成(cheng)一(yi)個新的提(ti)示(shi)詞,最后將這個新的提(ti)示(shi)詞發給大模型進行處理(li)。
(3)告訴AI在輸出信息時(shi)要(yao)把引用的文檔(dang)信息鏈接(jie)也一(yi)同輸出。
Console.WriteLine("[LOG] Now starting the chatting window for you..."); Console.ForegroundColor = ConsoleColor.Green; var promptTemplate = """ 你是一(yi)個專(zhuan)業的(de)AI聊(liao)天機器(qi)人,為易速(su)鮮花(hua)網站的(de)所(suo)有員工提(ti)(ti)供(gong)信(xin)息(xi)咨詢服務。 請使用(yong)下面的(de)提(ti)(ti)示使用(yong)工具(ju)從向量數據庫(ku)中獲取(qu)相關信(xin)息(xi)來回答用(yong)戶提(ti)(ti)出的(de)問(wen)題: {{#with (SearchPlugin-GetTextSearchResults question)}} {{#each this}} Value: {{Value}} Link: {{Link}} Score: {{Score}} ----------------- {{/each}} {{/with}} 輸出要求:請在回復中引用相(xiang)關(guan)信息的地方包(bao)括對(dui)相(xiang)關(guan)信息的引用。 用戶問題: {{question}} """; var history = new List<ChatMessage>(); var vectorSearchTool = new VectorDataSearcher<Guid>(ragVectorRecordCollection, embedingGenerator); var chatOptions = new ChatOptions() { Tools = [ AIFunctionFactory.Create(vectorSearchTool.GetTextSearchResults) ] }; // Prompt the user for a question. Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine($"助手> 今天有什么可(ke)以幫到你的?"); while (true) { // Read the user question. Console.ForegroundColor = ConsoleColor.White; Console.Write("用戶> "); var question = Console.ReadLine(); // Exit the application if the user didn't type anything. if (!string.IsNullOrWhiteSpace(question) && question.ToUpper() == "EXIT") break; var ragPrompt = promptTemplate.Replace("{question}", question); history.Add(new ChatMessage(ChatRole.User, ragPrompt)); Console.ForegroundColor = ConsoleColor.Green; Console.Write("助手> "); var result = await chatClient.GetResponseAsync(history, chatOptions);
var response = result.ToString(); Console.Write(response); history.Add(new ChatMessage(ChatRole.Assistant, response)); Console.WriteLine(); }
調試驗證(zheng)
首先(xian),看(kan)看(kan)PDF導入中的(de)log顯示:

其次,驗證下(xia)Qdrant中是否新增了導入(ru)的PDF文檔(dang)數(shu)據(ju):

最后,和AI機器人對(dui)話咨詢問題(ti):
問題1及其回(hui)復:

問題2及(ji)其回復:

更多(duo)的問題,就留給你去調戲(xi)了。
小(xiao)結
本文(wen)介(jie)紹(shao)了如(ru)何基于Microsoft.Extensions.AI + Microsoft.Extensions.VectorData 一(yi)步(bu)一(yi)步(bu)地實現(xian)一(yi)個RAG(檢(jian)索增(zeng)強生成)應(ying)用,相(xiang)信會對你有所幫(bang)助。
如果你也是.NET程序員希(xi)望參與AI應用的開發,那就(jiu)快快了解和(he)使用基于Microsoft.Extensioins.AI + Microsoft.Extensions.VectorData 的生(sheng)態組件庫吧。
示例源碼
GitHub:
參考內容
Semantic Kernel 《》
推薦(jian)內容

作者:
出處:
本(ben)文(wen)(wen)(wen)版權(quan)歸(gui)作者(zhe)(zhe)和博客園共有(you),歡迎(ying)轉載,但未經作者(zhe)(zhe)同意必須保留此段(duan)聲明,且在文(wen)(wen)(wen)章(zhang)頁面明顯位置給出(chu)原文(wen)(wen)(wen)鏈接(jie)。
var

本文介紹了如何基于Microsoft.Extensions.AI + Microsoft.Extensions.VectorData 一步一步地實現一個RAG(檢索增強生成)應用,相信會對你有所幫助。如果你也是.NET程序員希望參與AI應用的開發,那就快快了解和使用基于Microsoft.Extensioins.AI + Microsoft.Extensions.VectorData 的生態組件庫吧。