大家好,我是Edison。

之前我们了解 Microsoft.Extensions.AI 和 Microsoft.Extensions.VectorData 两个重要的AI应用核心库。基于对他们的了解,今天我们就可以来实战一个RAG问答应用,把之前所学的串起来。

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

案例需求背景

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

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

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

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

注册地址:点此注册

(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:

# 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对话机器人:

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

(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 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);
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:点此查看

参考内容

Semantic Kernel 《.NET Sample Demos

推荐内容

Microsoft Learn

eShopSupport

devblogs

作者:周旭龙

出处:https://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

var

基于Microsoft.Extensions.AI核心库实现RAG应用的更多相关文章

  1. Asp.Net Core 2.0 项目实战(9) 日志记录,基于Nlog或Microsoft.Extensions.Logging的实现及调用实例

    本文目录 1. Net下日志记录 2. NLog的使用     2.1 添加nuget引用NLog.Web.AspNetCore     2.2 配置文件设置     2.3 依赖配置及调用     ...

  2. 一系列令人敬畏的.NET核心库,工具,框架和软件

    内容 一般 框架,库和工具 API 应用框架 应用模板 身份验证和授权 Blockchain 博特 构建自动化 捆绑和缩小 高速缓存 CMS 代码分析和指标 压缩 编译器,管道工和语言 加密 数据库 ...

  3. 一个在 Java VM 上使用可观测的序列来组成异步的、基于事件的程序的库 RxJava,相当好

    https://github.com/ReactiveX/RxJava https://github.com/ReactiveX/RxAndroid RX (Reactive Extensions,响 ...

  4. 将 WPF、UWP 以及其他各种类型的旧 csproj 迁移成基于 Microsoft.NET.Sdk 的新 csproj

    原文 将 WPF.UWP 以及其他各种类型的旧 csproj 迁移成基于 Microsoft.NET.Sdk 的新 csproj 写过 .NET Standard 类库或者 .NET Core 程序的 ...

  5. Microsoft.Extensions.DependencyInjection 之三:展开测试

    目录 前文回顾 IServiceCallSite CallSiteFactory ServiceProviderEngine CompiledServiceProviderEngine Dynamic ...

  6. Microsoft.Extensions.DependencyInjection 之三:反射可以一战(附源代码)

    目录 前文回顾 IServiceCallSite CallSiteFactory ServiceProviderEngine CompiledServiceProviderEngine Dynamic ...

  7. TensorFlow?PyTorch?Paddle?AI工具库生态之争:ONNX将一统天下

    作者:韩信子@ShowMeAI 深度学习实战系列:https://www.showmeai.tech/tutorials/42 本文地址:https://www.showmeai.tech/artic ...

  8. Microsoft.Extensions.Options支持什么样的配置类?

    在.Net core中,微软放弃了笨重基于XML的.Config配置文件(好吧,像我这种咸鱼早都忘了如何自己写一个Section了). 现在主推新的高度可扩展的配置文件(参见此处) 对于新的配置系统, ...

  9. Swift 正式开源, 包括 Swift 核心库和包管理器

    Swift 正式开源!Swift 团队很高兴宣布 Swift 开始开源新篇章.自从苹果发布 Swfit 编程语言,就成为了历史上发展最快的编程语言之一.Swift 通过设计使得软件编写更加快速更加安全 ...

  10. 基于Microsoft Azure、ASP.NET Core和Docker的博客系统

    欢迎阅读daxnet的新博客:一个基于Microsoft Azure.ASP.NET Core和Docker的博客系统   2008年11月,我在博客园开通了个人帐号,并在博客园发表了自己的第一篇博客 ...

随机推荐

  1. 使用 MOLECULE 迅速包装百度 UEditor

    UEditor: UEditor - 首页http://ueditor.baidu.com/website/ 我们在对话框上放了几个 UEditor,发现第一次弹出对话框时UEditor还没有初始化 ...

  2. 一个.NET开源、易于使用的屏幕录制工具

    前言 一款高效.易用的屏幕录制工具能够极大地提升我们的工作效率和用户体验,今天大姚给大家分享一个.NET开源.免费.易于使用的屏幕录制工具:Captura. 工具介绍 Captura是一款基于.NET ...

  3. re模块:核心函数和方法

    1.compile(pattren,flages=0)   使用任何可选的标记来编译正则表达式的模式然后返回一个正则表达式对象 2.match(pattern,string,flags=0)    尝 ...

  4. Qt开发经验小技巧151-155

    当Qt中编译资源文件太大时,效率很低,或者需要修改资源文件中的文件比如图片.样式表等,需要重新编译可执行文件,这样很不友好,当然Qt都给我们考虑好了策略,此时可以将资源文件转化为二进制的rcc文件,这 ...

  5. Qt 中实现系统主题感知

    [写在前面] 在现代桌面应用程序开发中,系统主题感知是一项重要的功能,它使得应用程序能够根据用户的系统主题设置(如深色模式或浅色模式)自动调整其外观. Qt 作为一个跨平台的C++图形用户界面应用程序 ...

  6. Python中的包、模块和源码的组织关系

  7. B站千万级长连接实时消息系统的架构设计与实践

    本文由哔哩哔哩资深开发工程师黄山成分享,原题"千万长连消息系统",本文进行了排版和内容优化等. 1.引言 在当今数字娱乐时代,弹幕已经成为直播平台上不可或缺的互动元素之一. 用户通 ...

  8. Shapefile代码示例

    Shapefile代码示例 1. 读取Shapefile文件 1.1 实现思路 graph TD A[查找必要文件] --> B[获取文件编码] B --> C[打开图层] C --> ...

  9. 在OERV也可以玩MC(上)

    最近发现一个比较有意思的事情,原来HMCL这个项目也移植到RISC-V上了,之前一直没有发现,因此在OERV(openEuler RISC-V的简称)玩MC也是可以的了.首先,HMCL是一款功能丰富的 ...

  10. weixueyuan-Nginx代理服务器5

    https://www.weixueyuan.net/nginx/proxy_server/ Nginx HTTP代理服务器 代理功能根据应用方式的不同可以分为正向代理和反向代理.正向代理是客户端设置 ...