基于Microsoft.Extensions.AI核心庫實(shí)現(xiàn)RAG應(yīng)用
當(dāng)前位置:點(diǎn)晴教程→知識管理交流
→『 技術(shù)文檔交流 』
大家好,我是Edison。 之前我們了解 Microsoft.Extensions.AI 和 Microsoft.Extensions.VectorData 兩個重要的AI應(yīng)用核心庫。基于對他們的了解,今天我們就可以來實(shí)戰(zhàn)一個RAG問答應(yīng)用,把之前所學(xué)的串起來。 前提知識點(diǎn):向量存儲、詞嵌入、向量搜索、提示詞工程、函數(shù)調(diào)用。 案例需求背景假設(shè)我們在一家名叫“易速鮮花”的電商網(wǎng)站工作,顧名思義,這是一家從事鮮花電商的網(wǎng)站。我們有一些運(yùn)營手冊、員工手冊之類的文檔(例如下圖所示的一些pdf文件),想要將其導(dǎo)入知識庫并創(chuàng)建一個AI機(jī)器人,負(fù)責(zé)日常為員工解答一些政策性的問題。 例如,員工想要了解獎勵標(biāo)準(zhǔn)、行為準(zhǔn)備、報銷流程等等,都可以通過和這個AI機(jī)器人對話就可以快速了解最新的政策和流程。 在接下來的Demo中,我們會使用以下工具: (1) LLM 采用 Qwen2.5-7B-Instruct,可以使用SiliconFlow平臺提供的API,你也可以改為你喜歡的其他模型如DeepSeek,但是建議不要用大炮打蚊子哈。 注冊地址:點(diǎn)此注冊 (2) Qdrant 作為 向量數(shù)據(jù)庫,可以使用Docker在你本地運(yùn)行一個: docker run -p 6333:6333 -p 6334:6334 \ -v $(pwd)/qdrant_storage:/qdrant/storage \ qdrant/qdrant (3) Ollama 運(yùn)行 bge-m3 模型 作為 Emedding生成器,可以自行拉取一個在你本地運(yùn)行: ollama pull bge-m3 構(gòu)建你的RAG應(yīng)用創(chuàng)建一個控制臺應(yīng)用程序,添加一些必要的文件目錄 和 配置文件(json),最終的解決方案如下圖所示。 在Documents目錄下放了我們要導(dǎo)入的一些pdf文檔,例如公司運(yùn)營手冊、員工手冊等等。 在Models目錄下放了一些公用的model類,其中TextSnippet類作為向量存儲的實(shí)體類,而TextSearchResult類則作為向量搜索結(jié)果的模型類。 (1)TextSnippet 這里我們的TextEmbedding字段就是我們的向量值,它有1024維。 注意:這里的維度是我們自己定義的,你也可以改為你想要的維度數(shù)量,但是你的詞嵌入模型需要支持你想要的維度數(shù)量。 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 這個類主要用來返回給LLM做推理用的,我這里只需要三個字段:Value, Link 和 Score 即可。 public class TextSearchResult { public string Value { get; set; } public string? Link { get; set; } public double? Score { get; set; } } (3)RawContent 這個類主要用來在PDF導(dǎo)入時作為一個臨時存儲源數(shù)據(jù)文檔內(nèi)容。 public sealed class RawContent { public string? Text { get; init; } public int PageNumber { get; init; } } 在Plugins目錄下放了一些公用的幫助類,如PdfDataLoader可以實(shí)現(xiàn)PDF文件的讀取和導(dǎo)入向量數(shù)據(jù)庫,VectorDataSearcher可以實(shí)現(xiàn)根據(jù)用戶的query搜索向量數(shù)據(jù)庫獲取TopN個近似文檔,而UniqueKeyGenerator則用來生成唯一的ID Key。 (1)PdfDataLoader 作為PDF文件的導(dǎo)入核心邏輯,它實(shí)現(xiàn)了PDF文檔讀取、切分、生成指定維度的向量 并 存入向量數(shù)據(jù)庫。 注意:這里只考慮了文本格式的內(nèi)容,如果你還想考慮文件中的圖片將其轉(zhuǎn)成文本,你需要增加一個LLM來幫你做圖片轉(zhuǎn)文本的工作。 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 和上一篇文章介紹的內(nèi)容類似,主要做語義搜索,獲取TopN個近似內(nèi)容。 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 這個主要是一個代理,后續(xù)我們主要使用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(); } 串聯(lián)實(shí)現(xiàn)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) 下面我們分解幾個核心步驟來實(shí)現(xiàn)RAG問答。 Step1. 配置文件appsettings.json: { "LLM": { "EndPoint": "https://api.siliconflow.cn", "ApiKey": "sk-**********************", // Replace with your ApiKey "ModelId": "Qwen/Qwen2.5-7B-Instruct" }, "Embeddings": { "Ollama": { "EndPoint": "http://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. 導(dǎo)入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. 構(gòu)建AI對話機(jī)器人: 重點(diǎn)關(guān)注這里的提示詞模板,我們做了幾件事情: (1)給AI設(shè)定一個人設(shè):鮮花網(wǎng)站的AI對話機(jī)器人,告知其負(fù)責(zé)的職責(zé)。 (2)告訴AI要使用相關(guān)工具(向量搜索插件)進(jìn)行相關(guān)背景信息的搜索獲取,然后將結(jié)果 連同 用戶的問題 組成一個新的提示詞,最后將這個新的提示詞發(fā)給大模型進(jìn)行處理。 (3)告訴AI在輸出信息時要把引用的文檔信息鏈接也一同輸出。 Console.WriteLine("[LOG] Now starting the chatting window for you..."); Console.ForegroundColor = ConsoleColor.Green; var promptTemplate = """ 你是一個專業(yè)的AI聊天機(jī)器人,為易速鮮花網(wǎng)站的所有員工提供信息咨詢服務(wù)。 請使用下面的提示使用工具從向量數(shù)據(jù)庫中獲取相關(guān)信息來回答用戶提出的問題: {{#with (SearchPlugin-GetTextSearchResults question)}} {{#each this}} Value: {{Value}} Link: {{Link}} Score: {{Score}} ----------------- {{/each}} {{/with}} 輸出要求:請?jiān)诨貜?fù)中引用相關(guān)信息的地方包括對相關(guān)信息的引用。 用戶問題: {{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($"助手> 今天有什么可以幫到你的?"); 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); 調(diào)試驗(yàn)證首先,看看PDF導(dǎo)入中的log顯示: 其次,驗(yàn)證下Qdrant中是否新增了導(dǎo)入的PDF文檔數(shù)據(jù): 最后,和AI機(jī)器人對話咨詢問題: 問題1及其回復(fù): 問題2及其回復(fù): 更多的問題,就留給你去調(diào)戲了。 小結(jié)本文介紹了如何基于Microsoft.Extensions.AI + Microsoft.Extensions.VectorData 一步一步地實(shí)現(xiàn)一個RAG(檢索增強(qiáng)生成)應(yīng)用,相信會對你有所幫助。 如果你也是.NET程序員希望參與AI應(yīng)用的開發(fā),那就快快了解和使用基于Microsoft.Extensioins.AI + Microsoft.Extensions.VectorData 的生態(tài)組件庫吧。 示例源碼GitHub:點(diǎn)此查看 轉(zhuǎn)自https://www.cnblogs.com/edisonchou/p/-/introduction-to-vector-rag-demo 該文章在 2025/3/10 9:41:31 編輯過 |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |