欢迎光临
我们一直在努力

.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;
}

赞(0)
未经允许不得转载:171主机测评 » .Net优雅实现AI知识库基于Ollama模型,Qdrant作为向量数据库实现RAG流程AI检索增强
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址