news 2026/5/30 19:02:56

基于Microsoft.Extensions.AI核心库实现RAG应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Microsoft.Extensions.AI核心库实现RAG应用

今天我们就来实战一个RAG问答应用,把之前所学的串起来。如果你觉得对你有帮助,可以V我50,毕竟今天是Crazy星期四。

前提知识点:向量存储、词嵌入、语义搜索、提示词工程、函数调用。

案例需求背景

假设我们在一家名叫“易速鲜花”的电商网站工作,顾名思义,这是一家从事鲜花电商的网站。我们有一些运营手册、员工手册之类的文档(例如下图所示的一些pdf文件),想要将其导入知识库并创建一个AI机器人,负责日常为员工解答一些政策性的问题。

例如,员工想要了解奖励标准、行为准则、报销流程等等,都可以通过和这个AI机器人对话就可以快速了解最新的政策和流程。

在接下来的Demo中,我们会使用以下工具:

(1) LLM 采用 Qwen2.5-7B-Instruct,可以使用SiliconFlow平台提供的API,你也可以改为你喜欢的其他模型如DeepSeek,但是建议不要用大炮打蚊子哈。

注册地址:https://cloud.siliconflow.cn/i/DomqCefW

(2) Qdrant 作为 向量数据库,可以使用Docker在你本地运行一个:

docker run -p 6333:6333 -p 6334:6334 \-v $(pwd)/qdrant_storage:/qdrant/storage \qdrant/qdrant

(3) Ollama 运行 bge-m3 模型 作为 Emedding生成器,可以自行拉取一个在你本地运行:

ollama pull bge-m3

构建你的RAG应用

创建一个控制台应用程序,添加一些必要的文件目录 和 配置文件(json),最终的解决方案如下图所示。

在Documents目录下放了我们要导入的一些pdf文档,例如公司运营手册、员工手册等等。

在Models目录下放了一些公用的model类,其中TextSnippet类作为向量存储的实体类,而TextSearchResult类则作为向量搜索结果的模型类。

(1)TextSnippet

这里我们的TextEmbedding字段就是我们的向量值,它有1024维。

注意:这里的维度是我们自己定义的,你也可以改为你想要的维度数量,但是你的词嵌入模型需要支持你想要的维度数量。

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导入时作为一个临时存储源数据文档内容。

public sealed class RawContent{ public string? Text { get; init; } public int PageNumber { get; init; }}

在Plugins目录下放了一些公用的帮助类,如PdfDataLoader可以实现PDF文件的读取和导入向量数据库,VectorDataSearcher可以实现根据用户的query搜索向量数据库获取TopN个近似文档,而UniqueKeyGenerator则用来生成唯一的ID Key。

(1)PdfDataLoader

作为PDF文件的导入核心逻辑,它实现了PDF文档读取、切分、生成指定维度的向量 并 存入向量数据库。

注意:这里只考虑了文本格式的内容,如果你还想考虑文件中的图片将其转成文本,你需要增加一个LLM来帮你做图片转文本的工作。

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

和上一篇文章介绍的内容类似,主要做语义搜索,获取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

这个主要是一个代理,后续我们主要使用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)

下面我们分解几个核心步骤来实现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:

# ChatClientvar 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();# EmbeddingGeneratorvar embedingGenerator = new OllamaEmbeddingGenerator(new Uri(config["Embeddings:Ollama:EndPoint"]), config["Embeddings:Ollama:ModelId"]);# VectorStorevar 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 genratorvar uniqueKeyGenerator = new UniqueKeyGenerator<Guid>(() => Guid.NewGuid());// Get the collection in qdrantvar ragVectorRecordCollection = vectorStore.GetCollection<Guid, TextSnippet<Guid>>(ragConfig["CollectionName"]);// Get the PDF loadervar pdfLoader = new PdfDataLoader<Guid>(uniqueKeyGenerator, ragVectorRecordCollection, embedingGenerator);// Start to load PDF to VectorStorevar 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对话机器人:

重点关注这里的提示词模板,我们做了几件事情:

(1)给AI设定一个人设:鲜花网站的AI对话机器人,告知其负责的职责。

(2)告诉AI要使用相关工具(向量搜索插件)进行相关背景信息的搜索获取,然后将结果 连同 用户的问题 组成一个新的提示词,最后将这个新的提示词发给大模型进行处理。

(3)告诉AI在输出信息时要把引用的文档信息链接也一同输出。

Console.WriteLine("[LOG] Now starting the chatting window for you...");Console.ForegroundColor = ConsoleColor.Green;var promptTemplate = """ 你是一个专业的AI聊天机器人,为易速鲜花网站的所有员工提供信息咨询服务。 请使用下面的提示使用工具从向量数据库中获取相关信息来回答用户提出的问题: {{#with (SearchPlugin-GetTextSearchResults question)}} {{#each this}} Value: {{Value}} Link: {{Link}} Score: {{Score}} ----------------- {{/each}} {{/with}} 输出要求:请在回复中引用相关信息的地方包括对相关信息的引用。 用户问题: {{question}} """;var chatHistory = new List<ChatMessage>{ new ChatMessage(ChatRole.System, "你是一个专业的AI聊天机器人,为易速鲜花网站的所有员工提供信息咨询服务。")};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); var response = result.ToString(); Console.Write(response); history.Add(new ChatMessage(ChatRole.Assistant, response)); Console.WriteLine();}

调试验证

首先,看看PDF导入中的log显示:

其次,验证下Qdrant中是否新增了导入的PDF文档数据:

最后,和AI机器人对话咨询问题:

问题1及其回复:

问题2及其回复:

更多的问题,就留给你去调戏了。

小结

本文介绍了如何基于Microsoft.Extensions.AI + Microsoft.Extensions.VectorData 一步一步地实现一个RAG(检索增强生成)应用,相信会对你有所帮助。

如果你也是.NET程序员希望参与AI应用的开发,那就快快了解和使用基于Microsoft.Extensioins.AI + Microsoft.Extensions.VectorData 的生态组件库吧。

示例源码GitHub: https://github.com/edisontalk/EdisonTalk.AI.Agents

>> 点击本文底部“阅读原文”即可直达

参考内容

Semantic Kernel .NET Sample Demos : https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/Demos?wt.mc_id=MVP_397012

推荐内容

Microsoft Learn: https://learn.microsoft.com/zh-cn/dotnet/ai/ai-extensions?wt.mc_id=MVP_397012

eShopSupport: https://github.com/dotnet/eShopSupport?wt.mc_id=MVP_397012

devblogs: https://devblogs.microsoft.com/dotnet/e-shop-infused-with-ai-comprehensive-intelligent-dotnet-app-sample?wt.mc_id=MVP_397012

年终总结:Edison的2024年终总结

数字化转型:我在传统企业做数字化转型

C#刷算法题:C#刷剑指Offer算法题系列文章目录

C#刷设计模式:C#刷23种设计模式系列文章目录

.NET面试:.NET开发面试知识体系

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/29 2:16:52

GZCVL T-III 车桥综合性能测试系统

一、产品概述GZCVL T-III 车桥综合性能测试系统是广州文明机电有限公司自主研发的专业测试设备&#xff0c;专为汽车车桥总成 (驱动桥、转向桥) 的性能测试、疲劳寿命评估和故障诊断设计。该系统采用模块化设计&#xff0c;可全面模拟车辆各种行驶工况&#xff0c;精确测试车桥…

作者头像 李华
网站建设 2026/5/30 12:00:19

[株式会社UI2] 基础设施工程师

主要工作内容 系统设计开发&#xff08;需求定义・设计&#xff5e;综合测试・发布&#xff09; 服务器搭建、性能改善 技术调研、环境构建 运维维护&#xff08;监控、定期维护、恢复作业&#xff09; 新开发项目&#xff1a;组成15人团队&#xff08;依据开发阶段团队人员可能…

作者头像 李华
网站建设 2026/5/28 17:46:13

OpenHashTab 终极指南:3分钟快速掌握文件校验神器

你是否曾经下载重要文件后担心文件被篡改&#xff1f;或是需要验证软件安装包的真实性却不知从何下手&#xff1f;文件哈希校验正是解决这些安全顾虑的最佳方案&#xff0c;而OpenHashTab让这一过程变得前所未有的简单。 【免费下载链接】OpenHashTab &#x1f4dd; File hashi…

作者头像 李华
网站建设 2026/5/28 17:46:27

我如何自学数据科学

原文&#xff1a;towardsdatascience.com/how-i-self-study-data-science-7fa0c5ec58b5 你是否曾经因为数据科学的大小而感到不知所措&#xff0c;想知道从哪里开始或如何让你的学习坚持下去&#xff1f; 我以前在学习数据科学主题时漫无目的地尝试&#xff0c;但现在我有一个…

作者头像 李华
网站建设 2026/5/28 23:14:51

FaceFusion如何调整肤色匹配度?色彩一致性优化策略

FaceFusion如何调整肤色匹配度&#xff1f;色彩一致性优化策略在数字人、虚拟主播和AI换脸应用日益普及的今天&#xff0c;一个看似微小却极为关键的问题正不断挑战着视觉真实感的边界——为什么换完脸后总觉得“哪里不对劲”&#xff1f;答案往往藏在细节里&#xff1a;不是五…

作者头像 李华