.Net优雅实现AI知识库基于Ollama模型,Qdrant作为向量数据库实现RAG流程AI检索增强

实现架构设计

使用Ollama作为本地LLM推理引擎,Qdrant作为向量数据库,结合Semantic Kernel实现RAG流程。架构分为文档处理、向量存储、检索增强三个核心模块。

具体实现可参考NetCoreKevin的Kevin.RAG模块

基于.NET构建的企业级SaaSAI智能体应用架构,采用前后端分离设计,具备以下核心特性:

前端技术:

核心NuGet包配置

xml 复制代码
<ItemGroup>
  <!-- 文档处理 -->
  <PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" />
  <PackageReference Include="Microsoft.KernelMemory" Version="0.98.250508.3" />
  
  <!-- 向量数据库 -->
  <PackageReference Include="Qdrant.Client" Version="1.16.1" />
  
  <!-- 语义内核 -->
  <PackageReference Include="Microsoft.SemanticKernel" Version="1.57.0" />
  <PackageReference Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.57.0"/>
  
  <!-- 辅助工具 -->
  <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
  <PackageReference Include="SharpToken" Version="2.0.0" />
</ItemGroup>

Ollama集成配置

创建自定义Ollama连接器创建OllamaApiService

csharp 复制代码
    public class OllamaApiService : IOllamaApiService
    {
        private readonly string Url;
        private readonly string DefaultModel;
        private readonly OllamaApiClient ollamaApiClient;

        public OllamaApiService(IOptionsMonitor<OllamaApiSetting> config)
        {
            try
            {
                Url = config.CurrentValue.Url;
                DefaultModel = config.CurrentValue.DefaultModel;
                if (!string.IsNullOrEmpty(Url) && !string.IsNullOrEmpty(DefaultModel))
                {
                    ollamaApiClient = new OllamaApiClient(new Uri(Url), DefaultModel);
                }
            }
            catch (Exception)
            {

                Console.WriteLine("Kevin.RAG请检查OllamaApi配置是否正确");
            }

        }
        public async Task<Embedding<float>> GetEmbedding(string text)
        {
            if (ollamaApiClient == default)
            {
                throw new ArgumentException($"请检查OllamaApi配置是否正确");
            }
            return await Microsoft.Extensions.AI.EmbeddingGeneratorExtensions.GenerateAsync<string, Embedding<float>>(ollamaApiClient, text, options: null, cancellationToken: default).ConfigureAwait(false);
        }
    }

Qdrant向量存储实现

创建向量存储服务封装QdrantClientService :

csharp 复制代码
      public class QdrantClientService : IQdrantClientService
    {
        private readonly string Url;
        private readonly string ApiKey;
        private readonly string CertificateThumbprint;
        private readonly QdrantClient QdrantClient;
        private IOllamaApiService OllamaApiService { get; set; }
        public QdrantClientService(IOptionsMonitor<QdrantClientSetting> config, IOllamaApiService ollamaApiService)
        {
            try
            {
                Url = config.CurrentValue.Url;
                ApiKey = config.CurrentValue.ApiKey;
                CertificateThumbprint = config.CurrentValue.CertificateThumbprint;
                OllamaApiService = ollamaApiService;
                if (!string.IsNullOrEmpty(Url))
                {
                    if (!string.IsNullOrEmpty(ApiKey))
                    {
                        var channel = QdrantChannel.ForAddress(Url, new ClientConfiguration
                        {
                            ApiKey = ApiKey,
                            CertificateThumbprint = CertificateThumbprint
                        });
                        var grpcClient = new QdrantGrpcClient(channel);
                        QdrantClient = new QdrantClient(grpcClient);
                    }
                    else
                    {
                        QdrantClient = new QdrantClient(Url);
                    }
                }
            }
            catch (Exception)
            { 
                Console.WriteLine("Kevin.RAG请检查Qdrant配置是否正确");
            }
           
        }
        public async Task<bool> AddData(string collectionName, List<DocumentChunkDto> data)
        {
            if (QdrantClient == default)
            {
                throw new ArgumentException($"请检查OllamaApi配置是否正确");
            }
            if (QdrantClient != null)
            {
                foreach (var item in data)
                {
                    item.ContentVector = await OllamaApiService.GetEmbedding(item.Content);
                }
                var points = data.Select(i => new PointStruct
                {
                    Id = (ulong)i.Id,
                    Vectors = i.ContentVector.Vector.ToArray(),
                    /*# 与 collection 的 vector size 对应*/
                    Payload =
                     {
                       ["Content"]   = i.Content,
                       ["SourceFile"] = i.SourceFile,
                       ["Title"] = i.Title,
                       ["Category"] = i.Category,
                       ["ChunkIndex"] = i.ChunkIndex,
                       ["CreatedAt"]=i.CreatedAt.ToString()
                     }
                }).ToList();  
                if (!(await IsValidateCollectionExists(collectionName)))
                {
                    await QdrantClient.CreateCollectionAsync(collectionName, new VectorParams { Size = 1024, Distance = Distance.Cosine });
                } 
                var result = await QdrantClient.UpsertAsync(collectionName, points);
                return true;
            }
            return false;
        }

        // 验证集合是否存在
        public async Task<bool> IsValidateCollectionExists(string collectionName)
        {
            if (QdrantClient == default)
            {
                throw new ArgumentException($"请检查OllamaApi配置是否正确");
            }
            try
            {
                var collectionNameInfo = await QdrantClient.GetCollectionInfoAsync(collectionName); 
            }
            catch (Exception)
            {

                return false;
            }
            return true;
         
        }

        public async Task<List<DocumentChunkDto>> Search(string collectionName,
            String query, ulong limit = 10, double? Score = null)
        {
            if (QdrantClient == default)
            {
                throw new ArgumentException($"请检查OllamaApi配置是否正确");
            }
            var data = await QdrantClient.SearchAsync(collectionName, (await OllamaApiService.GetEmbedding(query)).Vector, limit: limit);
            var relust = data.Select(i =>
            {
                var payload = i.Payload;
                return new DocumentChunkDto
                {
                    Id = long.Parse(i.Id.Num.ToString()),
                    Content = payload["Content"].StringValue,
                    SourceFile = payload["SourceFile"].StringValue,
                    Title = payload["Title"].StringValue,
                    Category = payload["Category"].StringValue,
                    ChunkIndex = Convert.ToInt32(payload["ChunkIndex"].IntegerValue),
                    CreatedAt = DateTimeOffset.Parse(payload["CreatedAt"].StringValue ?? string.Empty),
                    Score = i.Score
                };
            }).ToList();
            if (Score != default)
            {
                relust = relust.Where(i => i.Score >= Score).ToList();
            }
            return relust;
        }

        public void Dispose()
        {
            QdrantClient.Dispose();
        }
    }

RAG管道构建

整合组件实现完整RAGService 流程:

csharp 复制代码
 public class RAGService : IRAGService
 {
     private IQdrantClientService QdrantClientService { get; set; }
     public RAGService(IQdrantClientService qdrantClientService)
     {
         QdrantClientService = qdrantClientService;
     }
     public async Task<(bool, string)> GetSystemPrompt(string collectionName, string question, int topK = 3, double? Score = null)
     {
         Console.WriteLine($"\n问题:{question}");
         Console.WriteLine("正在检索相关文档...");
         var documents = await QdrantClientService.Search(collectionName, question, (ulong)topK);
         if (documents.Count == 0)
         {
             return (false, "抱歉,我没有找到相关的文档来回答您的问题。");
         }
         Console.WriteLine($"找到 {documents.Count} 个相关文档");
         // 3. 构建上下文
         var context = string.Join("\n\n---\n\n", documents.Select((doc, index) =>
             $"文档 {index + 1}(来源:{doc.SourceFile}):\n{doc.Content}"));
         // 4. 构建提示词
         var systemPrompt = @" 
                             重要规则:
                             1. 只使用文档中的信息来回答
                             2. 如果文档中没有相关信息,请明确告知用户
                             3. 不要编造或推测文档中没有的信息
                             4. 回答要清晰、准确、有条理
                             5. 可以引用文档来源";

         var userPrompt = $@"文档内容:
                             {context} 
                             用户问题:
                             {question} 
                             请基于以上文档内容回答问题。"; 
         return (true, systemPrompt + "\n" + userPrompt);
     }
 }

文档预处理流水线

使用DocumentProcessor处理原始文档:

csharp 复制代码
    /// <summary>
    /// 文档处理服务,负责清理和分块
    /// </summary>
    public class DocumentProcessor
    {
        private readonly int _chunkSize;
        private readonly int _chunkOverlap;

        public DocumentProcessor(int chunkSize = 500, int chunkOverlap = 50)
        {
            _chunkSize = chunkSize;
            _chunkOverlap = chunkOverlap;
        }

        /// <summary>
        /// 清理文档内容
        /// </summary>
        public string CleanDocument(string content)
        {
            if (string.IsNullOrWhiteSpace(content))
                return string.Empty;

            // 1. 统一换行符
            content = content.Replace("\r\n", "\n").Replace("\r", "\n");

            // 2. 移除多余的空白字符(但保留单个空格和换行)
            content = Regex.Replace(content, @"[ \t]+", " ");
            content = Regex.Replace(content, @"\n{3,}", "\n\n");

            // 3. 移除首尾空白
            content = content.Trim();

            return content;
        }

        /// <summary>
        /// 按段落分块
        /// </summary>
        public List<string> ChunkByParagraph(string content)
        {
            var chunks = new List<string>();

            // 按双换行符分割段落
            var paragraphs = content.Split(new[] { "\n\n" }, StringSplitOptions.RemoveEmptyEntries);

            var currentChunk = new List<string>();
            var currentLength = 0;

            foreach (var paragraph in paragraphs)
            {
                var paragraphLength = paragraph.Length;

                // 如果当前块加上新段落超过限制,保存当前块
                if (currentLength + paragraphLength > _chunkSize && currentChunk.Count > 0)
                {
                    chunks.Add(string.Join("\n\n", currentChunk));

                    // 保留最后一个段落作为重叠
                    if (_chunkOverlap > 0 && currentChunk.Count > 0)
                    {
                        currentChunk = new List<string> { currentChunk[^1] };
                        currentLength = currentChunk[0].Length;
                    }
                    else
                    {
                        currentChunk.Clear();
                        currentLength = 0;
                    }
                }

                currentChunk.Add(paragraph);
                currentLength += paragraphLength;
            }

            // 添加最后一个块
            if (currentChunk.Count > 0)
            {
                chunks.Add(string.Join("\n\n", currentChunk));
            }

            return chunks;
        }

        /// <summary>
        /// 按固定大小分块
        /// </summary>
        public List<string> ChunkBySize(string content)
        {
            var chunks = new List<string>();
            var start = 0;

            while (start < content.Length)
            {
                var length = Math.Min(_chunkSize, content.Length - start);

                // 尝试在句子边界处切分
                if (start + length < content.Length)
                {
                    var lastPeriod = content.LastIndexOfAny(new[] { '。', '!', '?', '.', '!', '?' },
                                                            start + length, length);
                    if (lastPeriod > start)
                    {
                        length = lastPeriod - start + 1;
                    }
                }

                chunks.Add(content.Substring(start, length).Trim());
                start += length - _chunkOverlap;
            }

            return chunks;
        }
    }

部署配置示例

appsettings.json配置示例:

json 复制代码
  "QdrantClientSetting": {
    "Url": "localhost"
  },
  "OllamaApiSetting": {
    "Url": "http://****:11434/v1"
  }

性能优化建议

启用批量插入模式提升Qdrant写入性能,配置BatchSize参数:

csharp 复制代码
await _client.UpsertAsync(
    _collectionName,
    points,
    batchSize: 100);

为Ollama调用添加重试策略:

csharp 复制代码
var retryPolicy = Policy
    .Handle<HttpRequestException>()
    .WaitAndRetryAsync(3, retryAttempt => 
        TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

实现混合检索策略,结合关键词和向量搜索:

csharp 复制代码
var hybridResults = await _client.SearchAsync(
    _collectionName,
    queryVector: embedding,
    queryText: question,
    scoreThreshold: 0.5);

使用实例

csharp 复制代码
TestRag()
        {
            Console.WriteLine("=== RAG 系统演示 ===\n");
            // 4. 准备示例文档
            var sampleDocuments = new Dictionary<string, string>
            {
                ["产品手册.txt"] = @"
我们的智能音箱支持多种功能:

语音控制:
- 唤醒词是'你好小智'
- 可以控制音乐播放、查询天气、设置闹钟等
- 支持中文和英文语音识别

音乐播放:
- 支持 QQ 音乐、网易云音乐、酷狗音乐
- 可以通过语音控制播放、暂停、切歌
- 支持歌单、专辑、歌手搜索

智能家居:
- 可以控制智能灯泡、插座、空调、窗帘
- 支持场景模式(如回家模式、睡眠模式)
- 可以设置定时任务",

                ["FAQ.txt"] = @"
常见问题:

Q: 如何连接 WiFi?
A: 首次使用时,打开手机 App,选择'添加设备',按照提示连接 WiFi。

Q: 如何重置设备?
A: 长按设备顶部的重置按钮 10 秒,直到指示灯闪烁。

Q: 支持哪些音乐服务?
A: 目前支持 QQ 音乐、网易云音乐和酷狗音乐。

Q: 如何更新固件?
A: 设备会自动检查更新,也可以在 App 中手动检查更新。",

                ["技术规格.txt"] = @"
技术规格:

硬件:
- 处理器:四核 ARM Cortex-A53
- 内存:1GB RAM
- 存储:8GB Flash
- 扬声器:5W 全频扬声器
- 麦克风:4 麦克风阵列

连接:
- WiFi:2.4GHz/5GHz 双频
- 蓝牙:5.0
- 接口:USB-C 电源接口

尺寸和重量:
- 尺寸:100mm × 100mm × 50mm
- 重量:300g
- 电源:12V/1.5A"
            };

            // 5. 处理和上传文档
            Console.WriteLine("\n正在处理文档...");
            var allChunks = new List<DocumentChunkDto>();
            // 2. 初始化服务
            var documentProcessor = new DocumentProcessor(chunkSize: 500, chunkOverlap: 50);
            foreach (var (fileName, content) in sampleDocuments)
            {
                // 清理文档
                var cleanedContent = documentProcessor.CleanDocument(content);

                // 分块
                var chunks = documentProcessor.ChunkByParagraph(cleanedContent);

                Console.WriteLine($"文档 '{fileName}' 分成了 {chunks.Count} 个块");


                // 创建文档块对象
                for (int i = 0; i < chunks.Count; i++)
                {
                    allChunks.Add(new DocumentChunkDto
                    {
                        Content = chunks[i],
                        SourceFile = fileName,
                        Id = _snowflakeIdService.GetNextId(),
                        CreatedAt = DateTime.Now,
                        Title = fileName.Replace(".txt", ""),
                        Category = "产品文档",
                        ChunkIndex = i
                    });
                }
            }
            Console.WriteLine($"\n正在上传 {allChunks.Count} 个文档块到向量数据库...");
            // 上传到向量数据库
            await _qdrantClientService.AddData("RAG_Documents", allChunks);
            // 6. 测试 RAG 查询
            Console.WriteLine("\n=== 开始测试 RAG 查询 ===\n");
            var testQuestions = new[]
            {
                "这个音箱支持哪些音乐服务?",
                "如何连接 WiFi?",
                "音箱的重量是多少?",
                "可以控制哪些智能家居设备?"
            };
            foreach (var question in testQuestions)
            {
                var answer = await _qdrantClientService.Search("RAG_Documents", question);
                Console.WriteLine($"答案:{answer.ToJson()}");
                Console.WriteLine(new string('-', 80));
                Console.WriteLine( await _rAGServicevice.GetSystemPrompt("RAG_Documents",question));
            }
            Console.WriteLine("\n感谢使用 RAG 系统!");
            
            return true;
        }
相关推荐
NAGNIP1 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab3 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab3 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
哥不是小萝莉4 小时前
OpenClaw 架构设计全解析
ai
AngelPP6 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年6 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼7 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS7 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
warm3snow7 小时前
Claude Code 黑客马拉松:5 个获奖项目,没有一个是"纯码农"做的
ai·大模型·llm·agent·skill·mcp
天翼云开发者社区8 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤