news 2026/1/30 13:32:21

基于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/1/30 7:11:03

Langchain-Chatchat能否支持API网关统一接入?

Langchain-Chatchat能否支持API网关统一接入&#xff1f; 在企业智能化浪潮中&#xff0c;如何安全、可控地将大模型能力嵌入内部系统&#xff0c;成为IT架构师面临的核心挑战。一个典型的场景是&#xff1a;HR部门希望员工通过OA系统一键查询“年假怎么申请”&#xff0c;而答…

作者头像 李华
网站建设 2026/1/29 13:15:47

FaceFusion能否用于科学可视化?大脑活动映射面部

FaceFusion能否用于科学可视化&#xff1f;大脑活动映射面部在神经科学实验室里&#xff0c;研究人员盯着屏幕上跳动的脑电波形和静态的fMRI热图&#xff0c;试图解读情绪、注意力或疼痛背后的大脑密码。这些数据维度高、动态性强&#xff0c;却往往以抽象符号呈现——一条曲线…

作者头像 李华
网站建设 2026/1/25 11:43:00

Langchain-Chatchat能否实现文档变更自动检测同步?

Langchain-Chatchat能否实现文档变更自动检测同步&#xff1f; 在企业知识库系统日益智能化的今天&#xff0c;一个现实问题反复浮现&#xff1a;当用户修改了原始文档后&#xff0c;AI问答系统是否能“立刻知道”&#xff1f;比如&#xff0c;法务部门刚更新了一份合同模板&am…

作者头像 李华
网站建设 2026/1/28 9:42:41

AI 智能体企业级自动化评估实用指南

大家好&#xff0c;我是玄姐。1. AI 智能体评估实用指南了解如何借助结构化评估框架对企业级 AI 智能体进行评估&#xff0c;涵盖模型测试、产品测试、场景化分析、性能指标及持续监控等方面。1.1 目录AI 智能体评估实用指南为何智能体评估比你想象的更具挑战性评估与测试&…

作者头像 李华
网站建设 2026/1/30 0:44:38

产后恢复难题多?蓝丝带专业支持,助万千妈妈重拾美丽自信

产后恢复是每一位新生妈妈面临的重要健康阶段&#xff0c;选择科学、专业且可靠的服务&#xff0c;不仅关系到身体的康复效果&#xff0c;也影响着长期的生活质量与心理状态。在众多提供产后护理的机构中&#xff0c;如何辨别其专业性与可靠性&#xff0c;成为许多家庭关注的焦…

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

Langchain-Chatchat能否实现文档分类自动打标?

Langchain-Chatchat能否实现文档分类自动打标&#xff1f; 在企业知识管理的日常中&#xff0c;一个常见的场景是&#xff1a;IT部门收到上百份新上传的合同、报告和操作手册&#xff0c;却无人能快速说清“哪些是财务类&#xff1f;哪些属于技术规范&#xff1f;”人工归类费时…

作者头像 李华