程序员的AI体验(十):Semantic Kernel RAG
2025-06-14 17:29:33一、RAG核心机制与组件
1. SK RAG架构与抽象层
- 抽象层转变 Semantic Kernel为RAG应用提供了现代化、企业级的抽象层,从单一组件向精细、解耦、类型安全的抽象转变。这种转变使得系统更加健壮、可维护,分离了数据存储、嵌入生成和信息检索的关注点,提升了模块化和可测试性。
- 数据模型映射 通过定义C#类和添加特性,可以将数据模型映射到向量数据库的模式。例如,使用VectorStoreKey、VectorStoreData等特性,能够简化与数据库的交互,避免编写复杂的数据库迁移代码,提高代码的可维护性和可扩展性。
- 技术选型灵活性 这种架构设计增强了技术选型的灵活性,允许开发者根据项目需求选择合适的数据库和嵌入模型,同时保持系统的稳定性和可扩展性。
2. RAG完整生命周期
- 生命周期阶段 RAG应用的核心生命周期包括数据加载与预处理、文本分块、嵌入、索引、检索和增强生成六个阶段。每个阶段都有明确的作用和输入输出,例如数据加载阶段负责读取数据,嵌入阶段将文本转换为向量。
- 标准性与通用性 与LangChain课程中提到的生命周期相比,SemanticKernel的RAG生命周期更加标准化和通用化,适用于多种数据类型和应用场景,为开发者提供了一套清晰的开发流程。
二、文本预处理与分块
文本分块的重要性 文本分块是RAG流程中的关键步骤,能够将长文本分割成适合嵌入和检索的小块,从而提高检索的准确性和效率。
自定义Token计算器
默认的Token计算器在处理中文分词时存在局限性,通过自定义Token计算器可以解决这一问题,确保分块的准确性。
TextChunker静态类 Semantic Kernel提供了TextChunker静态类,包含按行分割和段落组合等核心方法。通过设置最大Token数量和重叠Token参数,可以灵活地控制分块的粒度。
封装不稳定依赖 为了隔离变更风险,可以创建稳定的ITextSplitter接口和SemanticKernelTextSplitter包装类,封装不稳定的依赖,提高代码的稳定性和可维护性。
public interface ITextSplitter
{
List<string> Split(string text, int maxTokensPerChunk, int overlapTokens);
}
// 创建一个包装类来实现该接口
public class SemanticKernelTextSplitter : ITextSplitter
{
public List<string> Split(string text, int maxTokensPerChunk, int overlapTokens)
{
// 内部调用不稳定的 TextChunker
var lines = TextChunker.SplitPlainTextLines(text, maxTokensPerChunk / 3); // 粗略估计行大小
return TextChunker.SplitPlainTextParagraphs(lines, maxTokensPerChunk, overlapTokens);
}
}
- 中文的处理
// 创建一个包装类来实现该接口
public class SemanticKernelTextSplitter : ITextSplitter
{
public List<string> Split(string text, int maxTokensPerChunk, int overlapTokens)
{
// 内部调用不稳定的 TextChunker
var lines = TextChunker.SplitPlainTextLines(text, maxTokensPerChunk / 3); // 粗略估计行大小
var tokenizer = TiktokenTokenizer.CreateForModel("gpt-4");
return TextChunker.SplitPlainTextParagraphs(lines, maxTokensPerChunk, overlapTokens,
tokenCounter: input => tokenizer.CountTokens(input));
}
}
三、向量存储与文本搜索
1. 向量存储与模型映射
向量存储设计
Semantic Kernel的向量存储设计通过定义C#类和添加特性,声明性地描述数据模型如何映射到向量数据库的模式。这种设计简化了与数据库的交互,避免了复杂的模式定义代码。
核心特性 VectorStoreKey、VectorStoreData和VectorStoreVector等核心特性用于配置数据模型。例如,DocumentChunk类通过这些特性定义了数据模型的结构,使其能够无缝映射到向量数据库中。
模型优先的设计理念 这种模型优先的设计理念不仅提高了代码的可维护性和可扩展性,还使得开发者能够专注于业务逻辑,而不是底层的数据库操作。
2. 文本搜索服务
TextSearch接口 Semantic Kernel提供了ITextSearch接口,将复杂的向量检索过程封装成一个简单的基于自然语言的搜索操作。通过在数据模型上添加TextSearchResultValue等特性,可以将检索到的自定义数据模型转换为标准格式。
解耦与灵活性
这种分层抽象的设计实现了上层应用代码与底层数据库技术的解耦。通过更换向量数据库连接器可以无缝迁移到底层不同的数据库,体现了企业级软件设计的灵活性和可维护性。
四、构建标准RAG应用流程
添加nuget包,禁用警告,创建用户机密文件,准备一个data文件
<ItemGroup> <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.8" /> <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.8" /> <PackageReference Include="Microsoft.SemanticKernel" Version="1.63.0" /> <PackageReference Include="Microsoft.ML.Tokenizers" Version="1.0.2" /> <PackageReference Include="Microsoft.ML.Tokenizers.Data.Cl100kBase" Version="1.0.2" /> <PackageReference Include="Microsoft.Extensions.AI" Version="9.8.0" /> <PackageReference Include="OpenAI" Version="2.3.0" /> <PackageReference Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.62.0-preview" /> <PackageReference Include="Microsoft.SemanticKernel.Agents.Core" Version="1.62.0" /> </ItemGroup> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net9.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <NoWarn>$(NoWarn);SKEXP0001;SKEXP0110;SKEXP0050;SKEXP0130</NoWarn> <UserSecretsId>5067eb98-c685-44c1-b897-d95f6ca2bb36</UserSecretsId> </PropertyGroup> <ItemGroup> <None Update="data.txt"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup>
data.txt
Semantic Kernel 是一个开源 SDK,可让您轻松构建能够调用现有代码的代理。作为一个高度可扩展的 SDK,您可以将 Semantic Kernel 与 OpenAI、Azure OpenAI、Hugging Face 等模型结合使用!插件是 Semantic Kernel 的构建块。插件是一个包含向 AI 模型公开的函数的类。插件可以包含本机函数(C#、Python 或 Java 代码)和语义函数(提示)。这允许您将自己的代码添加到 Semantic Kernel 中,并让 AI 模型调用它。
创建一个记录类型
public class DocumentChunk { // 标记这是记录的唯一标识符 [VectorStoreKey] public string Id { get; set; } // 标记这是需要存储的元数据 [VectorStoreData] [TextSearchResultName] public string DocumentName { get; set; } [VectorStoreData] [TextSearchResultLink] public int Index { get; set; } // 存储块的原始文本内容 [VectorStoreData] [TextSearchResultValue] public string Text { get; set; } // 标记这是存储嵌入向量的字段 [VectorStoreVector(Dimensions:2048)] public string Embedding => Text; }
数据加载与分块
Console.WriteLine("--- 步骤 1: 数据加载与分块 ---"); // 从文件读取文本内容 var text = await File.ReadAllTextAsync("data.txt"); var tokenizer = TiktokenTokenizer.CreateForModel("gpt-4"); // 使用 TextChunker 将文本分割成段落 // 这里设置每个块最大 100 个令牌,重叠 10 个令牌 var paragraphs = TextChunker.SplitPlainTextParagraphs( TextChunker.SplitPlainTextLines(text, 20), // 先按行粗略切分 100, 10, tokenCounter: input => tokenizer.CountTokens(input) ); Console.WriteLine($"成功将文本分割成 {paragraphs.Count} 个块。"); Console.WriteLine();
配置嵌入服务
Console.WriteLine("--- 步骤 2: 配置嵌入服务 ---"); // 获取配置 var configuration = new ConfigurationBuilder() .AddUserSecrets<Program>() .Build(); var apiKey = configuration["Qianwen:ApiKey"]!; var endpoint = "https://dashscope.aliyuncs.com/compatible-mode/v1"; var clientOptions = new OpenAIClientOptions { Endpoint = new Uri(endpoint) }; // 构建文本嵌入服务 var embeddingGenerator = new OpenAIClient(new ApiKeyCredential(apiKey), clientOptions) .GetEmbeddingClient("text-embedding-v4") .AsIEmbeddingGenerator().AsTextEmbeddingGenerationService(); Console.WriteLine("嵌入服务配置完成。"); Console.WriteLine();
配置向量数据库
Console.WriteLine("--- 步骤 3: 配置向量数据库 ---"); // 创建一个内存向量存储实例 var vectorStore = new InMemoryVectorStore(); // 从向量存储中获取一个强类型的集合 // 集合名称为 "sk-docs",键类型为 string,记录类型为 DocumentChunk var collection = vectorStore.GetCollection<string, DocumentChunk>("sk-docs"); // 确保集合存在,不存在则创建 await collection.EnsureCollectionExistsAsync(); Console.WriteLine("内存向量数据库和集合配置完成。"); Console.WriteLine();
文本嵌入与持久化
Console.WriteLine("--- 步骤 4: 文本嵌入与持久化 ---"); // 遍历所有文本块,生成嵌入并存储 for (var i = 0; i < paragraphs.Count; i++) { // 为当前文本块生成嵌入向量 var embedding = await embeddingGenerator.GenerateEmbeddingAsync(paragraphs[i]); // 创建一个 DocumentChunk 实例 var chunk = new DocumentChunk { Id = Guid.NewGuid().ToString(), DocumentName = "data.txt", Index = i, Text = paragraphs[i], // Embedding = embedding }; // 将该实例存入向量集合中 await collection.UpsertAsync(chunk); } Console.WriteLine($"已成功嵌入并存储了 {paragraphs.Count} 个文本块。"); Console.WriteLine();
向量检索
Console.WriteLine("--- 步骤 5: 向量检索 ---"); // 用户问题 var userQuery = "Semantic Kernel 中的插件是什么?"; // 为用户问题生成嵌入向量 var queryEmbedding = await embeddingGenerator.GenerateEmbeddingAsync(userQuery); // 在向量集合中执行相似性搜索,获取最相关的 2 个结果 var searchResults = collection.SearchAsync(queryEmbedding, 2); // 收集检索到的文本内容 var context = ""; await foreach (var searchResult in searchResults) { context += searchResult.Record.Text + "\n---\n"; } Console.WriteLine("检索到的上下文:"); Console.WriteLine(context);
检索增强生成
Console.WriteLine("--- 步骤 6: 检索增强生成 ---"); // 构建内核对象,注册AI补全服务 var kernel = Kernel.CreateBuilder() .AddOpenAIChatCompletion( modelId: "qwen-plus", apiKey: apiKey, endpoint: new Uri(endpoint) ).Build(); // 定义一个提示模板,包含上下文和问题两个变量 var promptTemplate = """ 请根据提供的上下文来回答用户的问题。 上下文: {{$context}} 用户问题: {{$query}} """; // 使用内核执行提示 var result = await kernel.InvokePromptAsync( promptTemplate, new KernelArguments { { "context", context }, { "query", userQuery } } ); Console.WriteLine("LLM 生成的答案:"); Console.WriteLine(result);
五、SK RAG与LangChain RAG对比
1. 设计理念对比
- Semantic Kernel设计理念 Semantic Kernel的核心设计理念是编排,强调强类型、面向对象、依赖注入和关注点分离,适合构建长期维护、稳定性和可扩展性要求高的生产级AI应用。
- LangChain设计理念 LangChain的设计理念更侧重于灵活性和快速实验,采用函数式、声明式的编程范式,适合快速原型验证和频繁调整的场景。
- 差异影响 两种设计理念的差异对开发者体验和应用场景产生了显著影响。SK更适合企业级应用,而LangChain更适合快速开发和实验。
2. 开发体验与生态系统
- SK开发体验 Semantic Kernel在C#环境下提供了类型安全的优势,支持OpenTelemetry,为企业级应用提供了可观测性。
- LangChain开发体验 LangChain基于动态类型Python,拥有庞大的AI社区和丰富的第三方工具、模型和数据源集成。通过对比分析,帮助观众理解两个框架在开发效率、错误捕获、性能监控和生态系统支持方面的不同特点。
3. 场景选择建议
- 选择SK的场景 建议在深度集成.NET企业应用、追求稳定性和可维护性、构建标准化的AI服务等场景中选择Semantic Kernel.
- 选择LangChain的场景 建议在快速原型和研究探索、利用丰富的生态系统、Python技术栈主导等场景中选择LangChain。通过具体场景分析,帮助观众根据项目需求和技术栈选择合适的框架。
特性/组件 | SK | LangChain | 对比分析 |
---|---|---|---|
数据加载 | 无内置加载器生态,需手动编码(如File.ReadAllTextAsync) | 拥有庞大的DocumentLoader生态,支持数百种数据源(PDF,Wrod,Web等) | LangChain在数据接入的便捷性和广度上具有明显优势,开箱即用。SK需要开发者自行实现数据加载逻辑 |
文本分块 | 提供TextChunker静态类,支持按行、段落和重叠分块 | 提供丰富的TextSplitter抽象和实现,支持按代码、md等多种方式分块 | LangChain的分块策略更多样化,适应性更强。SK的TextChunker功能基础但够用,且API尚不太稳定 |
向量存储 | 采用抽象的强类型,与C#数据模型紧密绑定 | 提供统一的向量存储包装器接口,支持大量向量数据库的快速集成 | SK的方法提供了编译时类型安全和更好的IDE支持。LangChain的方法则提供了更广泛的数据库支持和更快的集成速度 |
RAG流程构建 | 手动流程:SearchAsync检索-->手动构建上下文-->InvokePromptAsync生成 | 高度封装:提供高级抽象,一行代码即可构建完整RAG链 | LangChain在RAG模式的封装程度上远高于SK,极大地简化了开发。SK则提供了更底层的控制,让开发者能精细地控制流程的每一步 |
六、利用Agent框架构建智能RAG
1. Agentic RAG核心思想
- 传统RAG局限性:传统RAG缺乏动态决策能力,无法处理模糊查询和多步推理任务。
- Agent概念:Agentic RAG引入智能体(Agent)的概念,通过推理与规划、工具选择与使用、迭代执行来处理更复杂的任务。
- 动态循环优势:这种动态的、基于推理的循环提升了RAG系统的灵活性和智能性。通过具体例子,可以展示Agentic RAG在实际应用中的优势。
2. TextSearchProvider实现原理
- 实现lAlContextProvider接囗:TextSearchProvider实现了IAlContextProvider接口,在Agent调用语言模型之前被触发,分析用户输入并进行相关性搜索。
- 按需RAG功能:按需RAG功能使Agent能够自主决定是否需要从知识库中拉取信息,提高了系统的效率和灵活性。
3. 标准RAG与智能RAG的工作模式
- 标准RAG:推模式--->总是先执行检索,推送给语言模型
- 智能RAG:拉模式--->Agent根据内部的推理,自主决定是否需要从数据库中拉取信息
4. 代码示例
public static async Task Run()
{
Console.WriteLine("--- 步骤 1: 数据加载与分块 ---");
// 从文件读取文本内容
var text = await File.ReadAllTextAsync("data.txt");
var tokenizer = TiktokenTokenizer.CreateForModel("gpt-4");
// 使用 TextChunker 将文本分割成段落
// 这里设置每个块最大 100 个令牌,重叠 10 个令牌
var paragraphs = TextChunker.SplitPlainTextParagraphs(
TextChunker.SplitPlainTextLines(text, 20), // 先按行粗略切分
100,
10,
tokenCounter: input => tokenizer.CountTokens(input)
);
Console.WriteLine($"成功将文本分割成 {paragraphs.Count} 个块。");
Console.WriteLine();
Console.WriteLine("--- 步骤 2: 配置嵌入服务 ---");
// 获取配置
var configuration = new ConfigurationBuilder()
.AddUserSecrets<Program>()
.Build();
var apiKey = configuration["Qianwen:ApiKey"]!;
var endpoint = "https://dashscope.aliyuncs.com/compatible-mode/v1";
var clientOptions = new OpenAIClientOptions
{
Endpoint = new Uri(endpoint)
};
// 构建文本嵌入服务
var embeddingGenerator = new OpenAIClient(new ApiKeyCredential(apiKey), clientOptions)
.GetEmbeddingClient("text-embedding-v4")
.AsIEmbeddingGenerator();
Console.WriteLine("嵌入服务配置完成。");
Console.WriteLine();
Console.WriteLine("--- 步骤 3: 配置向量数据库 ---");
// 创建一个内存向量存储实例
var vectorStore = new InMemoryVectorStore(new InMemoryVectorStoreOptions
{
EmbeddingGenerator = embeddingGenerator
});
// 从向量存储中获取一个强类型的集合
// 集合名称为 "sk-docs",键类型为 string,记录类型为 DocumentChunk
var collection = vectorStore.GetCollection<string, DocumentChunk>("sk-docs");
// 确保集合存在,不存在则创建
await collection.EnsureCollectionExistsAsync();
Console.WriteLine("内存向量数据库和集合配置完成。");
Console.WriteLine();
Console.WriteLine("--- 步骤 4: 文本嵌入与持久化 ---");
// 遍历所有文本块,生成嵌入并存储
for (var i = 0; i < paragraphs.Count; i++)
{
// 创建一个 DocumentChunk 实例
var chunk = new DocumentChunk
{
Id = Guid.NewGuid().ToString(),
DocumentName = "data.txt",
Index = i,
Text = paragraphs[i]
};
// 将该实例存入向量集合中
await collection.UpsertAsync(chunk);
}
Console.WriteLine($"已成功嵌入并存储了 {paragraphs.Count} 个文本块。");
Console.WriteLine();
Console.WriteLine("--- 步骤 5: 构建内核,创建 Agent ---");
var kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(
modelId: "qwen-plus",
apiKey: apiKey,
endpoint: new Uri(endpoint)
).Build();
// 创建一个 ChatCompletionAgent
var agent = new ChatCompletionAgent
{
Name = "小唆",
Instructions = "你是一个乐于助人的助手,可以回答有关 Semantic Kernel 的问题。请使用提供的上下文来回答问题。",
Kernel = kernel,
// 这个设置指示是否启用按需 RAG 功能
UseImmutableKernel = true
};
// 创建向量搜索实例,泛型表示向量模型,需要提供向量集合与通用嵌入生成服务
var textSearch = new VectorStoreTextSearch<DocumentChunk>(collection, embeddingGenerator);
// 按需RAG核心组件,需要提供向量搜索实例
var textSearchProvider = new TextSearchProvider(textSearch);
// 创建一个 Agent 线程用于管理对话历史
var agentThread = new ChatHistoryAgentThread();
// 将 TextSearchProvider 添加到线程的上下文提供者列表中
agentThread.AIContextProviders.Add(textSearchProvider);
Console.WriteLine("智能 RAG 助手已启动。输入 'exit' 退出。");
while (true)
{
Console.Write("用户 > ");
var userInput = Console.ReadLine();
if (string.IsNullOrEmpty(userInput)) continue;
if (userInput?.ToLower() == "exit") break;
// 调用 Agent,传入用户输入和配置了 RAG 的线程
Console.Write("助手 > ");
await foreach (var response in agent.InvokeStreamingAsync(userInput, agentThread))
{
Console.Write(response.Message.Content);
}
Console.WriteLine();
}
}