Semantic Kernel + Qdrant 实现RAG检索增强-简单案例

🎯 Rag检索增强-核心流程

┌─────────────┐     ┌──────────────┐     ┌─────────────┐     ┌──────────────┐
│  用户提问   │ ──→ │  向量检索    │ ──→ │ 构建 Prompt │ ──→ │  大模型生成  │
│ "怎么删除"  │     │ 查 Top_N 相关知识│     │ + 参考资料   │     │   最终答案   │
└─────────────┘     └──────────────┘     └─────────────┘     └──────────────┘

RAG

RAG 检索增强生成(Retrieval Augmented Generation),已经成为当下最火热的LLM应用方案和打开方式了。

理解起来不难,就是通过自有垂域数据库检索相关信息,然后合并成为提示模板,给大模型润色生成回答。

每当将大模型应用于实际业务场景时发现,通用的基础大模型基本无法满足实际业务需求,主要有以下几方面原因:

  • 知识的局限性:大模型自身的知识完全源于训练数据,而现有的主流大模型(deepseek、gpt、通义千问…)的训练集基本都是构建于网络公开的数据,对于一些实时性的、非公开的或私域的数据是没有。
  • 幻觉问题:所有的深度学习模型的底层原理都是基于数学概率,模型输出实质上是一系列数值运算,大模型也不例外,所以它经常会一本正经地胡说八道,尤其是在大模型自身不具备某一方面的知识或不擅长的任务场景。
  • 数据安全性:对于企业来说,数据安全至关重要,没有企业愿意承担数据泄露的风险,尤其是大公司,没有人将私域数据上传第三方平台进行训练会推理。这也导致完全依赖通用大模型自身能力的应用方案不得不在数据安全和效果方面进行取舍。

而RAG就是解决上述问题的有效方案。

一句话总结:

RAG(中文为检索增强生成) = 检索技术 + LLM 提示。

例如,向 LLM 提问一个问题(qustion),RAG 从各种数据源检索相关的信息,并将检索到的信息和问题(answer)注入到 提示词模板中,LLM 最后给出答案。

许多产品基于 RAG 构建,从基于 web 搜索引擎和 LLM 的问答服务到使用私有数据的应用程序。

其实,早在2019年,就有Faiss 实现了基于嵌入的向量搜索技术,现在 RAG 推动了海量的,高性能向量搜索领域的发展。后来比如 chroma、http://weaviate.io 和 pinecone 这些基于开源搜索索引引擎(主要是 faiss 和 nmslib)向量数据库初创公司,增加了输入文本的额外存储和其他工具。

一个简单案例,实现最小RAG检索增强

using AiTest; 
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Qdrant.Client;
using Qdrant.Client.Grpc;
using Sdcb.DashScope;
using System.Net.Http;
using System.Text;
using System.Text.Json; 
  
class Program
{
    private static readonly HttpClient _httpClient = new HttpClient();
    private static readonly string DashScopeApiKey = ConstParm.apiKey;//千问APIKEY
    private static readonly string QdrantHost = ConstParm.QdrantHostIp;//Qdrant 主机地址
    private static readonly string CollectionName = "rag-knowledge"; //集合名称

    static async Task Main(string[] args)
    {
        Console.WriteLine("🚀 RAG 检索增强生成演示开始!\n");
        //QdrantApiKey Qdrant私有的ApiKey
        var qdrantClient = new QdrantClient(QdrantHost,  6334,apiKey: ConstParm.QdrantApiKey);

        try
        {
            // ========== 1️⃣ 初始化知识库 ==========
            await InitializeKnowledgeBase(qdrantClient);

            // ========== 2️⃣ 用户提问 ==========
            var question = "Qdrant 怎么删除数据?";
            Console.WriteLine($"\n❓ 用户问题:{question}\n");

            // ========== 3️⃣ 检索相关知识 ==========
            var contexts = await RetrieveContext(qdrantClient, question, topK: 3);

            // ========== 4️⃣ 结合检索的结果-contexts,构建 Prompt ==========
            var prompt = BuildRagPrompt(question, contexts);

            // ========== 5️⃣ _httpClient简易方式:调用大模型生成答案 ==========
            var answer = await GenerateAnswer(prompt);
            Console.WriteLine($"\n🤖 _httpClient简易方式_AI 回答:{answer}\n");

            // ========== 5️⃣ 企业级应用 Semantic Kernel:调用大模型生成答案 ==========
            var answerByKernel = await GenerateAnswerBySemantic(prompt);
            Console.WriteLine($"\n🤖 Semantic Kernel企业级_AI 回答:{answerByKernel}\n");


            Console.ReadKey();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"❌ 错误:{ex.Message}");
        }
        finally
        {
            qdrantClient.Dispose();
        }
    }

    // ========== 1. 初始化知识库(插入数据)==========
    static async Task InitializeKnowledgeBase(QdrantClient client)
    {
        // 创建集合
        if (await client.CollectionExistsAsync(CollectionName))
            await client.DeleteCollectionAsync(CollectionName);

        await client.CreateCollectionAsync(CollectionName, new VectorParams
        {
            Size = 1536,  // text-embedding-v2 维度
            Distance = Distance.Cosine
        });

        // 建立索引
        await client.CreatePayloadIndexAsync(CollectionName, "category", PayloadSchemaType.Keyword);
        await client.CreatePayloadIndexAsync(CollectionName, "content", PayloadSchemaType.Text);

        // 📚 插入知识库数据(实际场景可从文档/数据库加载)
        var knowledge = new[]
        {
            new { Category = "删除操作", Content = "Qdrant 删除数据用 DeleteAsync 方法,可以按 ID 删除:await client.DeleteAsync(collectionName, ids: new[] { new PointId { Uuid = id } })" },
            new { Category = "删除操作", Content = "Qdrant 支持按条件批量删除:使用 Filter 过滤,例如删除所有 category=体育 的数据" },
            new { Category = "更新操作", Content = "Qdrant 更新数据用 UpsertAsync,如果 ID 已存在则覆盖,不存在则插入" },
            new { Category = "更新操作", Content = "Qdrant 只更新部分字段用 SetPayloadAsync:await client.SetPayloadAsync(collectionName, payload, ids: ...)" },
            new { Category = "查询操作", Content = "Qdrant 向量搜索用 SearchAsync,传入 query 向量和 limit 参数" },
            new { Category = "查询操作", Content = "Qdrant 精确查询用 RetrieveAsync:await client.RetrieveAsync(collectionName, ids: ...)" }
        };

        var points = new List<PointStruct>();
        foreach (var item in knowledge)
        {
            var vector = await GenerateEmbeddingAsync(item.Content);
            points.Add(new PointStruct
            {
                Id = new PointId { Uuid = Guid.NewGuid().ToString() },
                Vectors = vector,
                Payload =
                {
                    ["category"] = item.Category,
                    ["content"] = item.Content
                }
            });
        }

        await client.UpsertAsync(CollectionName, points);
        Console.WriteLine($"✅ 知识库初始化完成,共 {knowledge.Length} 条数据");
    }

    // ========== 2. 检索上下文 ==========
    static async Task<List<string>> RetrieveContext(QdrantClient client, string query, int topK = 3)
    {
        // ========== Step 1: 问题向量化 ==========
        // 用户问题:"Qdrant 怎么删除数据?" → float[1536] 向量
        var queryVector = await GenerateEmbeddingAsync(query);

        // ========== Step 2: 向量相似度搜索 ==========
        var results = await client.SearchAsync(
            collectionName: CollectionName,
            vector: queryVector,
            limit: (ulong)topK,  // 返回 Top 3 最相似的
            payloadSelector: new WithPayloadSelector { Enable = true }// ✅ 返回 payload(含 content)
        );
        // ========== Step 3: 格式化结果 ==========
        var contexts = results.Select(r =>
        {
            var content = r.Payload["content"].StringValue;
            var score = r.Score;
            return $"[相关度:{score:F2}] {content}";
        }).ToList();
        // ========== Step 4: 打印调试信息 ==========
        Console.WriteLine("📖 检索到的上下文:");
        foreach (var ctx in contexts)
            Console.WriteLine($"   {ctx}");

        return contexts;
    }

    // ========== 3. 构建 RAG Prompt ==========
    static string BuildRagPrompt(string question, List<string> contexts)
    {
        var contextText = string.Join("\n", contexts.Select((c, i) => $"{i + 1}. {c}"));

        return $@"你是一个智能助手。请根据以下参考资料回答问题。

【参考资料】
{contextText}

【问题】
{question}

【要求】
- 只根据参考资料回答
- 如果资料中没有相关信息,说不知道
- 需要详细回答";
    }

    // ========== 4.1 _httpClient 方式 调用大模型生成答案 ==========
    static async Task<string> GenerateAnswer(string prompt)
    {
        var endpoint = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";
        var requestBody = new
        {
            model = ConstParm.modelId,
            messages = new[]
            {
                new { role = "user", content = prompt }
            }
        };

        var json = JsonSerializer.Serialize(requestBody);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        _httpClient.DefaultRequestHeaders.Clear();
        _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {DashScopeApiKey}");

        var response = await _httpClient.PostAsync(endpoint, content);
        var responseJson = await response.Content.ReadAsStringAsync();

        using var doc = JsonDocument.Parse(responseJson);
        var answer = doc.RootElement.GetProperty("choices")[0]
            .GetProperty("message").GetProperty("content").GetString();

        return answer ?? "生成失败";
    }




    // ========== 4.2 semantic kernel 方式  调用大模型生成答案 ==========
    static async Task<string> GenerateAnswerBySemantic(string prompt)
    {
        var builder = Kernel.CreateBuilder();
         
    builder.AddOpenAIChatCompletion(
        modelId: ConstParm.modelId,
        apiKey: ConstParm.apiKey,
        endpoint: new Uri(ConstParm.endpoint));

        var kernel = builder.Build();

        var chatService = kernel.GetRequiredService<IChatCompletionService>();
        var executionSettings = new OpenAIPromptExecutionSettings
        {
            Temperature = 0.7f,      // 创造性 (0~1)
            MaxTokens = 1000,        // 最大输出 token
            TopP = 0.9f              // 核采样
        };
       
        // ✅ 用 ChatHistory 支持多轮对话
        var chatHistory = new ChatHistory();
        chatHistory.AddUserMessage(prompt);

        var reply = await chatService.GetChatMessageContentAsync(
            chatHistory,
            executionSettings: executionSettings
        );
 
        //Console.WriteLine(reply.Content);
        return reply.Content;
    }



    // ========== 5. 生成嵌入向量 ==========
    static async Task<float[]> GenerateEmbeddingAsync(string text)
    {
        var endpoint = "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings";
        var requestBody = new { model = "text-embedding-v2", input = text };
        var json = JsonSerializer.Serialize(requestBody);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        _httpClient.DefaultRequestHeaders.Clear();
        _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {DashScopeApiKey}");

        var response = await _httpClient.PostAsync(endpoint, content);
        var responseJson = await response.Content.ReadAsStringAsync();

        using var doc = JsonDocument.Parse(responseJson);
        var embedding = doc.RootElement.GetProperty("data")[0].GetProperty("embedding");

        var vector = new float[embedding.GetArrayLength()];
        int idx = 0;
        foreach (var item in embedding.EnumerateArray())
            vector[idx++] = item.GetSingle();

        return vector;
    }
}

 
View Code

运行效果

image

 

代码备注

逐行分析:

代码你的备注准确性
GenerateEmbeddingAsync(query) "将用户提问的问题转化为向量" ✅ 正确
client.SearchAsync(..., vector: queryVector, ...) "根据提问问题的向量值进行相似度筛选" ✅ 正确

Semantic Kernel 方式是我主动要求添加的,如下:

📊 两种方案对比

维度直接 HTTP 调用Semantic Kernel
代码量 少(50 行) 多(100+ 行)
学习成本 低(会 HttpClient 就行) 高(SK 概念多)
可维护性 一般 ✅ 优秀
可测试性 难 Mock ✅ 易 Mock
插件扩展 ❌ 手动 ✅ 原生支持
记忆/历史 ❌ 手动管理 ✅ 内置
函数调用 ❌ 不支持 ✅ 原生支持
多模型切换 ❌ 改代码 ✅ 配置即可
适用场景 快速 Demo、简单场景 生产环境、复杂场景

🎯 用 Semantic Kernel 改写(生产级)

    // ========== 4.2 semantic kernel 方式  调用大模型生成答案 ==========
    static async Task<string> GenerateAnswerBySemantic(string prompt)
    {
        var builder = Kernel.CreateBuilder();
         
    builder.AddOpenAIChatCompletion(
        modelId: ConstParm.modelId,
        apiKey: ConstParm.apiKey,
        endpoint: new Uri(ConstParm.endpoint));

        var kernel = builder.Build();

        var chatService = kernel.GetRequiredService<IChatCompletionService>();
        var executionSettings = new OpenAIPromptExecutionSettings
        {
            Temperature = 0.7f,      // 创造性 (0~1)
            MaxTokens = 1000,        // 最大输出 token
            TopP = 0.9f              // 核采样
        };
       
        // ✅ 用 ChatHistory 支持多轮对话
        var chatHistory = new ChatHistory();
        chatHistory.AddUserMessage(prompt);

        var reply = await chatService.GetChatMessageContentAsync(
            chatHistory,
            executionSettings: executionSettings
        );
 
        //Console.WriteLine(reply.Content);
        return reply.Content;
    }
View Code

 

posted @ 2026-08-04 18:10  天才卧龙  阅读(7)  评论(0)    收藏  举报