Net优雅实现AI知识库基于Ollama模型,Qdrant作为向量数据库实现RAG流程AI检索增强
实现架构设计
使用Ollama作为本地LLM推理引擎,Qdrant作为向量数据库,结合Semantic Kernel实现RAG流程。架构分为文档处理、向量存储、检索增强三个核心模块。
具体实现可参考NetCoreKevin的Kevin.RAG模块
基于.NET构建的企业级SaaSAI智能体应用架构,采用前后端分离设计,具备以下核心特性:
前端技术:
- Vue3前端框架
- IDS4单点登录系统
- 一库多租户解决方案
- 多级缓存机制
- CAP事件集成
- SignalR实时通信
- 领域驱动设计
- AI智能体框架RAGAI检索增强
- RabbitMQ消息队列
- 项目地址:github:https://github.com/junkai-li/NetCoreKevin
Gitee: https://gitee.com/netkevin-li/NetCoreKevin
核心NuGet包配置
<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
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 :
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 流程:
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处理原始文档:
/// <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配置示例:
"QdrantClientSetting": {
"Url": "localhost"
},
"OllamaApiSetting": {
"Url": "http://****:11434/v1"
}
性能优化建议
启用批量插入模式提升Qdrant写入性能,配置BatchSize参数:
await _client.UpsertAsync(
_collectionName,
points,
batchSize: 100);
为Ollama调用添加重试策略:
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
实现混合检索策略,结合关键词和向量搜索:
var hybridResults = await _client.SearchAsync(
_collectionName,
queryVector: embedding,
queryText: question,
scoreThreshold: 0.5);
使用实例
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;
}

浙公网安备 33010602011771号