.net AI开发05 第九章 新增 RAG 文档处理后台服务 RagWorker 及核心流程

前言:本项目聚焦企业级AI助理系统升级,融合数据分析与MCP集成,完善前后端可视化能力,实现AI Copilot"数据分析(Text-to-SQL)"全链路功能,支持多数据源自动发现、安全SQL执行及数据可视化呈现,同时扩展Agent工作流与插件系统,优化数据库结构,新增MCP Server动态接入及插件桥接机制,升级Vue 3前端聊天应用,实现流式对话、审批流等功能全覆盖;该系统采用分层架构设计,构建智能体交互、知识中枢(RAG)、数据分析(NL2SQL)及工具调用(MCP)三大核心模块,可高效支撑自然语言查询、跨系统操作与可视化报表生成,技术选型上选用ASP.NET Core、Semantic Kernel AI框架及Qdrant向量数据库,支持私有化部署,以权限控制保障安全,通过容器化云原生方案搭建企业现有系统的智能化交互层,显著提升全栈一体化能力,为企业级场景落地与多智能体扩展奠定基础。 **--长期找工作,找长期工作

前面章节

.AI开发 1后端框架: ASP.NET Core2.AI框架: Semantic Kernerl (SK)、Agent Framework3.知识库:向量数据库(Qdrant)+关系型数据库(Post

https://blog.csdn.net/cao919/article/details/155895060

.net AI开发02 1后端框架: ASP.NET Core2.AI框架: Semantic Kernerl (SK)、Agent Framework3.知识库:向量数据库(Qdrant)+关系型数据库(Post

https://blog.csdn.net/cao919/article/details/155895060?fromshare=blogdetail&sharetype=blogdetail&sharerId=155895060&sharerefer=PC&sharesource=cao919&sharefrom=from_link

.net AI开发03 新增意图识别与工具选择工作流(IntentWorkflow),支持多智能体协作; 插件体系升级,支持多项目插件自动注册与工具发现; 对话历史与消息存储解耦,采用 Med

https://blog.csdn.net/cao919/article/details/156065076

.net AI开发04 第八章 引入RAG知识库与文档管理核心能力及事件总线

https://blog.csdn.net/cao919/article/details/156990895

第九章 新增 RAG 文档处理后台服务 RagWorker 及核心流程

.NETAI项目实战-09

1.构建RAG后台服务

2.实现文档上传消费程序

3.实现多格式文档解析

4.实现智能文本分割

4.动态嵌入模型实现文本嵌入

5.使用Qdrant 实现向量存储

本文介绍了一个企业级AI助理系统的升级项目,重点实现了RAG文档处理后台服务。该系统采用分层架构设计,包含文档上传、解析、分割、嵌入和向量存储等完整流程。技术栈包括ASP.NET Core、Semantic Kernel、Qdrant向量数据库等组件,支持多格式文档处理。核心功能包括:1) 文档解析器工厂实现PDF等格式解析;2) 文本分割服务处理语义连贯性;3) 嵌入生成器批量处理文本向量化;4) Qdrant向量存储管理。系统通过RabbitMQ实现异步处理,采用容器化部署方案,为企业提供智能化文档处理

星:这种pdf的文档的解析,是不是使用langchain会比较好?嵌入用python来做?

星:提取出来是不是应该是md格式,不应该是文本,要不图片、表格之类的处理不好?

提取文本(文档转文本)文本分割(引入)

1.模型上下文限制(Qwen3-4B 32K)==》2560维度,300~500 Token最佳的短文本块

2.语义的完整性:我最喜欢的编程/语言是C#

txt==》段落结构、句号、换行符==》自然语义边界

3.重叠窗口

Semantic Kernel==》提供文本处理工具库

分词库(GPT分词库)=》长度估算

11111

文本嵌入==》自然语言转换为数值向量的过程

苹果

香蕉

手机的向量==电话的向量

MEAI扩展库

OpenAl

1111

Qdrant+SK=向量存储

Qdrant ==Rust、过滤机制、Aspire集成包

PGsQL+Pgvector

2560维==》坐标

给出一个问题(用户的输入)的坐标Q,在数据点集合V中,找出距离Q最近的K个点

2091段落==》2091坐标点

KNN(K-最近邻)

暴力计算==》坐标Q和每一个点对比距离

(N)===》O(1ogN)

近似搜索算法==》HNSW算法==》地图导航

最顶层关键节点(快速跨越长距离)、最底层所有的数据点

顶层(高速公路)=》中层(主干道)=》底层(小街道)

66666

粉丝爽过就好用pgvector 和用qdrant有什:

6666

编码 code 新增 RAG 文档处理后台服务 RagWorker 及核心流程

本次提交引入 AICopilot.RagWorker Worker Service,实现了文档上传事件消费、解析、分割、嵌入生成与向量存储的完整 RAG 流程。集成 Qdrant、Semantic Kernel、OpenAI 等组件,支持多格式文档解析与批量嵌入,提升了系统的智能文档处理能力。同步完善了服务注册、依赖配置、超时策略及本地开发配置。

构建 aspire rag-worker

cs 复制代码
builder.AddProject<Zilor_AICopilot_RagWorker>("rag-worker")
    .WithReference(postgresdb) // 注入数据库连接
    .WithReference(rabbitmq)   // 注入 RabbitMQ 连接
    .WithReference(qdrant)
    .WaitFor(postgresdb)       // 等待数据库启动
    .WaitFor(rabbitmq)        // 等待 MQ 启动
    .WaitFor(qdrant);
cs 复制代码
using MassTransit;
using Microsoft.EntityFrameworkCore;
using Zilor.AICopilot.Core.Rag.Aggregates.KnowledgeBase;
using Zilor.AICopilot.EntityFrameworkCore;
using Zilor.AICopilot.RagWorker.Services;
using Zilor.AICopilot.Services.Common.Events;

namespace Zilor.AICopilot.RagWorker.Consumers;

public class DocumentUploadedConsumer(
    RagService ragService,
    AiCopilotDbContext dbContext,
    ILogger<DocumentUploadedConsumer> logger) 
    : IConsumer<DocumentUploadedEvent>
{
    public async Task Consume(ConsumeContext<DocumentUploadedEvent> context)
    {
        var message = context.Message;
        logger.LogInformation("接收到文档处理请求: {DocumentId}, 文件: {FileName}", message.DocumentId, message.FileName);

        // 1. 获取文档实体 (包含 KnowledgeBase 信息)
        var document = await dbContext.Documents
            .Include(d => d.KnowledgeBase)
            .FirstOrDefaultAsync(d => d.Id == message.DocumentId);

        if (document == null)
        {
            logger.LogWarning("文档 {DocumentId} 未在数据库中找到,跳过处理。", message.DocumentId);
            return;
        }

        // 2. 幂等性与状态检查
        // 如果文档已经处理成功(Indexed)或正在处理中(Parsing/Splitting/Embedding),则忽略
        // 除非是 Failed 状态,才允许重试
        if (document.Status != DocumentStatus.Pending && document.Status != DocumentStatus.Failed)
        {
            logger.LogInformation("文档 {DocumentId} 当前状态为 {Status},无需重复处理。", message.DocumentId, document.Status);
            return;
        }

        try
        {
            // 3. 开始处理 - 状态流转
            document.StartParsing();
            await dbContext.SaveChangesAsync();

            // TODO: 调用核心 ETL 流程 (Parse -> Split -> Embed -> Store)
            await ragService.IndexDocumentAsync(document); 
            
            // 模拟处理成功
            logger.LogInformation("文档 {DocumentId} 索引流程执行完毕。", message.DocumentId);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "文档 {DocumentId} 处理失败。", message.DocumentId);
            
            // 4. 异常处理 - 记录错误状态
            // 重新从数据库获取最新状态(防止并发冲突),标记为失败
            var errorDoc = await dbContext.Documents.FindAsync(message.DocumentId);
            if (errorDoc != null)
            {
                errorDoc.MarkAsFailed(ex.Message);
                await dbContext.SaveChangesAsync();
            }
            
            // 根据业务需求,决定是否抛出异常以触发 RabbitMQ 的重试机制
            // 这里我们选择吞掉异常,因为已经记录了 Failed 状态,避免死信队列堆积
        }
    }
}

注意

处理

服务 索引构建

Step 1: 加载
Step 2: 解析
Step 3: 分割
Step 4: 嵌入
Step 5: 存储

源码Step 1: 加载Step 2: 解析Step 3: 分割Step 4: 嵌入Step 5: 存储

cs 复制代码
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
using Zilor.AICopilot.Core.Rag.Aggregates.KnowledgeBase;
using Zilor.AICopilot.EntityFrameworkCore;
using Zilor.AICopilot.RagWorker.Models;
using Zilor.AICopilot.RagWorker.Services.Embeddings;
using Zilor.AICopilot.RagWorker.Services.Parsers;
using Zilor.AICopilot.Services.Common.Contracts;

namespace Zilor.AICopilot.RagWorker.Services;


public class RagService(
    IFileStorageService fileStorage,
    DocumentParserFactory parserFactory,
    TextSplitterService textSplitter,
    EmbeddingGeneratorFactory embeddingFactory,
    VectorStore vectorStoreClient,
    AiCopilotDbContext dbContext,
    ILogger<RagService> logger)
{
    public async Task IndexDocumentAsync(Document document, CancellationToken cancellationToken = new())
    {
        logger.LogInformation("开始索引流程: {DocumentName}", document.Name);

        // Step 1: 加载
        var stream = await LoadDocumentAsync(document, cancellationToken);

        // Step 2: 解析
        var text = await ParseDocumentAsync(document, stream, cancellationToken);

        // Step 3: 分割
        var paragraphs = await SplitDocumentAsync(document, text, cancellationToken);

        // Step 4: 嵌入
        var (embeddings, dimensions) = await GenerateEmbeddingsAsync(document, paragraphs, cancellationToken);

        // Step 5: 存储
        await SaveVectorAsync(document, paragraphs, embeddings, dimensions, cancellationToken);

        logger.LogInformation("文档索引完成: {DocumentName}", document.Name);

    }

    // ================================================================
    // Step 1: 加载
    // ================================================================
    private async Task<Stream> LoadDocumentAsync(Document document, CancellationToken ct)
    {
        logger.LogInformation("加载文档...");

        // 从存储中获取文件流
        var stream = await fileStorage.GetAsync(document.FilePath, ct);

        return stream ?? throw new FileNotFoundException($"文件未找到: {document.FilePath}");
    }

    // ================================================================
    // Step 2: 解析
    // ================================================================
    private async Task<string> ParseDocumentAsync(Document document, Stream stream, CancellationToken ct)
    {
        logger.LogInformation("解析文档...");

        // 根据扩展名获取解析器
        var parser = parserFactory.GetParser(document.Extension);

        // 提取文本
        var text = await parser.ParseAsync(stream, ct);

        if (string.IsNullOrWhiteSpace(text))
            throw new InvalidOperationException("文档内容为空或无法提取文本。");

        logger.LogInformation("文本提取完成,长度: {Length} 字符", text.Length);

        // 更新状态:解析完成 -> 准备切片
        document.CompleteParsing();
        await dbContext.SaveChangesAsync(ct);

        return text;
    }

    // ================================================================
    // Step 3: 切片
    // ================================================================
    private async Task<List<string>> SplitDocumentAsync(Document document, string text, CancellationToken ct)
    {
        logger.LogInformation("开始文本切片...");

        // 为了支持重新索引,如果文档之前处理过,需要先清理旧的切片
        if (document.Chunks.Count > 0)
            document.ClearChunks();

        var paragraphs = textSplitter.Split(text);

        logger.LogInformation("文本切片完成,共 {Count} 个切片。", paragraphs.Count);

        // 将切片转换为领域实体
        for (var i = 0; i < paragraphs.Count; i++)
            document.AddChunk(i, paragraphs[i]);

        await dbContext.SaveChangesAsync(ct);

        return paragraphs;
    }
    
    // ================================================================
    // Step 4: 嵌入
    // ================================================================
    private async Task<(List<Embedding<float>>, int)> GenerateEmbeddingsAsync(
        Document document,
        List<string> paragraphs,
        CancellationToken ct)
    {
        logger.LogInformation("开始生成嵌入向量...");

        // 获取嵌入模型配置
        var embeddingModelConfig = await dbContext.EmbeddingModels.AsNoTracking()
            .FirstOrDefaultAsync(em => em.Id == document.KnowledgeBase.EmbeddingModelId,
                cancellationToken: ct);
        
        if (embeddingModelConfig == null)
        {
            throw new InvalidOperationException($"未找到 ID 为 {document.KnowledgeBase.EmbeddingModelId} 的嵌入模型配置");
        }
        
        // 创建嵌入生成器
        using var generator = embeddingFactory.CreateGenerator(embeddingModelConfig);

        // 准备分批
        // [配置建议]
        // - 本地模型: 建议 20 ~ 50 (取决于显卡)
        // - 云端模型: 建议 50 ~ 100
        const int batchSize = 50;
        
        // 用于收集所有生成的向量结果
        var allEmbeddings = new List<Embedding<float>>();
        
        // 将段落切分为多个批次
        var batches = paragraphs.Chunk(batchSize).ToArray();

        logger.LogInformation("共 {Paragraphs} 个段落,将分为 {Batches} 批处理", paragraphs.Count, batches.Length);

        // 循环处理每一批
        for (var i = 0; i < batches.Length; i++)
        {
            logger.LogInformation("正在处理第 {Current}/{Total} 批...", i + 1, batches.Length);

            try
            {
                var batch = batches[i];
                // 调用模型生成当前批次的向量
                var result = await generator.GenerateAsync(batch, cancellationToken: ct);
                // 将结果添加到总列表中
                allEmbeddings.AddRange(result);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "第 {Batch} 批次向量化失败", i + 1);
                throw;
            }
        }

        var dimensions = allEmbeddings.First().Vector.Length;
        logger.LogInformation("向量化完成,共生成 {Count} 个向量,维度: {Dim}", allEmbeddings.Count, dimensions);

        return (allEmbeddings, dimensions);
    }
    
    // ================================================================
    // Step 5: 保存向量
    // ================================================================
    private async Task SaveVectorAsync(
        Document document,
        List<string> chunks,
        List<Embedding<float>> embeddings,
        int dimensions,
        CancellationToken ct)
    {
        logger.LogInformation("保存向量数据...");

        // 基础参数校验
        if (chunks.Count != embeddings.Count)
        {
            throw new ArgumentException($"切片数量 ({chunks.Count}) 与向量数量 ({embeddings.Count}) 不一致");
        }

        if (chunks.Count == 0)
        {
            logger.LogWarning("文档 {DocumentId} 没有切片需要存储", document.Id);
        }

        // 2. 确定集合名称
        // 使用 "kb-" 前缀加上知识库 ID (Guid) 作为集合名,确保名称符合 Qdrant 规范且唯一
        var collectionName = $"kb-{document.KnowledgeBaseId:N}";
        logger.LogInformation("文档 {DocumentName} 将存入集合: {CollectionName}", document.Name, collectionName);

        // 3. 动态获取集合实例
        var definition = VectorDocumentDefinition.Get(dimensions);
        var collection = vectorStoreClient.GetDynamicCollection(collectionName, definition);

        // 4. 确保集合存在
        // 第一次向该知识库上传文档时,会自动创建集合
        await collection.EnsureCollectionExistsAsync(ct);

        // 5. 组装存储记录
        try
        {
            for (var i = 0; i < chunks.Count; i++)
            {
                // 生成一个唯一的记录键值
                var recordKey = (ulong)document.Id.GetHashCode() << 32 | (uint)i;

                await collection.UpsertAsync(new Dictionary<string, object?>
                {
                    { "Key", recordKey },
                    { "Text", chunks[i] },
                    { "DocumentId", document.Id.ToString() },
                    { "KnowledgeBaseId", document.KnowledgeBaseId.ToString() },
                    { "ChunkIndex", i },
                    { "Embedding", embeddings[i].Vector }
                }, ct);
            }

            logger.LogInformation("成功向集合 {Collection} 写入 {Count} 条向量记录。", collectionName, chunks.Count);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "写入向量数据库失败。Collection: {Collection}", collectionName);
            throw;
        }

        document.MarkAsIndexed();
        await dbContext.SaveChangesAsync(ct);
    }

}

.pdf 处理

cs 复制代码
using System.Text;
using UglyToad.PdfPig;

namespace Zilor.AICopilot.RagWorker.Services.Parsers;

public class PdfDocumentParser : IDocumentParser
{
    public string[] SupportedExtensions => [".pdf"];

    public Task<string> ParseAsync(Stream stream, CancellationToken cancellationToken = default)
    {
        return Task.Run(() =>
        {
            var sb = new StringBuilder();

            try
            {
                // PdfPig 需要 Seekable 流,如果流不支持 Seek,需要复制到 MemoryStream
                using var pdfDocument = PdfDocument.Open(stream);

                foreach (var page in pdfDocument.GetPages())
                {
                    // 提取每一页的文本,并用换行符分隔
                    // 实际生产中可能需要更复杂的版面分析算法来处理多栏排版
                    var text = page.Text;
                    if (!string.IsNullOrWhiteSpace(text))
                    {
                        sb.AppendLine(text);
                    }
                }
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException("PDF 解析失败,文件可能已损坏或加密。", ex);
            }

            return sb.ToString();
        }, cancellationToken);
    }
}

DI

token 处理估算

cs 复制代码
using SharpToken;

namespace Zilor.AICopilot.RagWorker.Services.TokenCounter;

public class SharpTokenCounter : ITokenCounter
{
    // cl100k_base 是 GPT-3.5/4 使用的编码器,对于多语言支持较好
    private readonly GptEncoding _encoding = GptEncoding.GetEncoding("cl100k_base");

    public int CountTokens(string text)
    {
        if (string.IsNullOrEmpty(text)) return 0;
        
        // 获取 Token 列表的长度
        return _encoding.Encode(text).Count;
    }
}

03token切片和组合

cs 复制代码
using Microsoft.SemanticKernel.Text;
using Zilor.AICopilot.RagWorker.Services.TokenCounter;
#pragma warning disable SKEXP0050

namespace Zilor.AICopilot.RagWorker.Services;

public class TextSplitterService(ITokenCounter tokenCounter)
{
    // 默认配置:适合 Qwen3-4B 等大多数 Embedding 模型
    private const int DefaultMaxTokensPerParagraph = 500;//段落的最大长度
    private const int DefaultMaxTokensPerLine = 120; //行的最大长度
    private const int DefaultOverlapTokens = 50; //重叠的长度

    /// <summary>
    /// 将长文本分割为语义连贯的段落列表
    /// </summary>
    /// <param name="text">原始文本</param>
    /// <returns>切片后的文本列表</returns>
    public List<string> Split(string text)
    {
        if (string.IsNullOrWhiteSpace(text))
        {
            return [];
        }

        // 1. 预处理:移除可能导致干扰的特殊控制字符
        var cleanText = Preprocess(text);

        // 2. 第一层切割:将文本按行(Line)拆分
        // SK 的逻辑是先按换行符等强分隔符切成小块(Lines),再将这些 Lines 组合成 Paragraphs
        // 这样可以确保尽量不在句子中间强行截断
        var lines = TextChunker.SplitPlainTextLines(
            cleanText, 
            maxTokensPerLine: DefaultMaxTokensPerLine, 
            tokenCounter: tokenCounter.CountTokens);

        // 3. 第二层组合:将 Lines 聚合成 Paragraphs
        // 这一步会严格控制 Token 数量上限,并处理重叠逻辑
        var paragraphs = TextChunker.SplitPlainTextParagraphs(
            lines, 
            maxTokensPerParagraph: DefaultMaxTokensPerParagraph, 
            overlapTokens: DefaultOverlapTokens, 
            tokenCounter: tokenCounter.CountTokens);

        return paragraphs;
    }

    private static string Preprocess(string text)
    {
        // 替换掉 Windows 的 \r\n 为 \n,统一换行符
        // 移除 NULL 字符等
        return text.Replace("\r\n", "\n").Trim();
    }
}

04文本嵌入 自然语言转换为数值向量的过程

cs 复制代码
using System.ClientModel;
using System.ClientModel.Primitives;
using Microsoft.Extensions.AI;
using OpenAI;
using Zilor.AICopilot.Core.Rag.Aggregates.EmbeddingModel;

namespace Zilor.AICopilot.RagWorker.Services.Embeddings;

public class EmbeddingGeneratorFactory(IHttpClientFactory httpClientFactory)
{
    public IEmbeddingGenerator<string, Embedding<float>> CreateGenerator(EmbeddingModel model)
    {
        var endpoint = new Uri(model.BaseUrl);
        var credential = new ApiKeyCredential(model.ApiKey ?? "sk-empty");

        var httpClient = httpClientFactory.CreateClient("EmbeddingClient");
        
        var options = new OpenAIClientOptions
        {
            Endpoint = endpoint,
            // 使用 IHttpClientFactory 创建 HttpClient,复用连接池
            Transport = new HttpClientPipelineTransport(httpClient),
            NetworkTimeout = TimeSpan.FromMinutes(20)
        };

        // 创建 OpenAI 客户端
        var client = new OpenAIClient(credential, options);
        return client
            .GetEmbeddingClient(model.ModelName)
            .AsIEmbeddingGenerator(model.Dimensions);
    }
}

注意

cs 复制代码
// 将默认的 10秒 延长到 5分钟,这对大多数 AI 场景都更友好
options.AttemptTimeout.Timeout = TimeSpan.FromMinutes(5);
options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(10);
options.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(10);
cs 复制代码
  // ================================================================
  // Step 4: 嵌入
  // ================================================================
  private async Task<(List<Embedding<float>>, int)> GenerateEmbeddingsAsync(
      Document document,
      List<string> paragraphs,
      CancellationToken ct)
  {
      logger.LogInformation("开始生成嵌入向量...");

      // 获取嵌入模型配置
      var embeddingModelConfig = await dbContext.EmbeddingModels.AsNoTracking()
          .FirstOrDefaultAsync(em => em.Id == document.KnowledgeBase.EmbeddingModelId,
              cancellationToken: ct);
      
      if (embeddingModelConfig == null)
      {
          throw new InvalidOperationException($"未找到 ID 为 {document.KnowledgeBase.EmbeddingModelId} 的嵌入模型配置");
      }
      
      // 创建嵌入生成器
      using var generator = embeddingFactory.CreateGenerator(embeddingModelConfig);

      // 准备分批
      // [配置建议]
      // - 本地模型: 建议 20 ~ 50 (取决于显卡)
      // - 云端模型: 建议 50 ~ 100
      const int batchSize = 50;
      
      // 用于收集所有生成的向量结果
      var allEmbeddings = new List<Embedding<float>>();
      
      // 将段落切分为多个批次
      var batches = paragraphs.Chunk(batchSize).ToArray();

      logger.LogInformation("共 {Paragraphs} 个段落,将分为 {Batches} 批处理", paragraphs.Count, batches.Length);

      // 循环处理每一批
      for (var i = 0; i < batches.Length; i++)
      {
          logger.LogInformation("正在处理第 {Current}/{Total} 批...", i + 1, batches.Length);

          try
          {
              var batch = batches[i];
              // 调用模型生成当前批次的向量
              var result = await generator.GenerateAsync(batch, cancellationToken: ct);
              // 将结果添加到总列表中
              allEmbeddings.AddRange(result);
          }
          catch (Exception ex)
          {
              logger.LogError(ex, "第 {Batch} 批次向量化失败", i + 1);
              throw;
          }
      }

      var dimensions = allEmbeddings.First().Vector.Length;
      logger.LogInformation("向量化完成,共生成 {Count} 个向量,维度: {Dim}", allEmbeddings.Count, dimensions);

      return (allEmbeddings, dimensions);
  }

向量存储 持久化

codefirst Qdrant 向量实体

cs 复制代码
using Microsoft.Extensions.VectorData;

namespace Zilor.AICopilot.RagWorker.Models;

/// <summary>
/// 对应向量数据库中的一条记录
/// </summary>
public class VectorDocumentRecord
{
    /// <summary>
    /// 记录的唯一标识符
    /// </summary>
    /// <remarks>
    /// 使用 ulong 类型,因为 Qdrant 内部 ID 支持 64 位无符号整数或 UUID。
    /// 这里我们不使用 Guid,而是为了与语义对齐,将在存储时生成唯一 ID。
    /// </remarks>
    [VectorStoreKey]
    public ulong Key { get; set; }

    /// <summary>
    /// 原始文本内容
    /// </summary>
    [VectorStoreData(IsFullTextIndexed = true)]
    public string Text { get; set; } = string.Empty;

    /// <summary>
    /// 关联的文档 ID (元数据)
    /// </summary>
    /// <remarks>
    /// IsFilterable = true 允许我们在检索时按 DocumentId 过滤,
    /// 例如:只查询特定文档的内容。
    /// </remarks>
    [VectorStoreData(IsIndexed = true)]
    public string DocumentId { get; set; } = string.Empty;

    /// <summary>
    /// 关联的知识库 ID (元数据)
    /// </summary>
    [VectorStoreData(IsIndexed = true)]
    public string KnowledgeBaseId { get; set; } = string.Empty;

    /// <summary>
    /// 原始切片在文档中的索引顺序
    /// </summary>
    [VectorStoreData]
    public int ChunkIndex { get; set; }

    /// <summary>
    /// 嵌入向量
    /// </summary>
    /// <remarks>
    /// Dimensions 必须与我们使用的模型(Qwen3-4B)一致,否则插入会报错。
    /// DistanceFunction 定义了相似度计算方式,Cosine (余弦相似度) 是文本检索的标准选择。
    /// </remarks>
    [VectorStoreVector(Dimensions: 2560, DistanceFunction = DistanceFunction.CosineSimilarity, IndexKind = IndexKind.Hnsw)]
    public ReadOnlyMemory<float> Embedding { get; set; }
}
cs 复制代码
using Microsoft.Extensions.VectorData;

namespace Zilor.AICopilot.RagWorker.Models;

public static class VectorDocumentDefinition
{
    public static VectorStoreCollectionDefinition Get(int dimensions)
    {
        VectorStoreCollectionDefinition definition = new()
        {
            Properties = new List<VectorStoreProperty>
            {
                new VectorStoreKeyProperty("Key", typeof(ulong)),
                new VectorStoreDataProperty("Text", typeof(string)) { IsFullTextIndexed = true },
                new VectorStoreDataProperty("DocumentId", typeof(string)){ IsIndexed = true },
                new VectorStoreDataProperty("KnowledgeBaseId", typeof(string)){ IsIndexed = true },
                new VectorStoreDataProperty("ChunkIndex", typeof(int)),
                new VectorStoreVectorProperty("Embedding", typeof(ReadOnlyMemory<float>),
                    dimensions: dimensions)
                {
                    DistanceFunction = DistanceFunction.CosineSimilarity,
                    IndexKind = IndexKind.Hnsw
                }
            }
        };
        return definition;
    }
}

注册抽象的向量存储服务

cs 复制代码
// 注册 Qdrant 客户端
// QdrantClient 是官方客户端,Semantic Kernel 会对其进行封装
builder.AddQdrantClient("qdrant");
// 注册 Semantic Kernel 的 Qdrant 向量存储抽象
builder.Services.AddQdrantVectorStore();

Step 5: 保存向量

标记索引完成

document.MarkAsIndexed();

Qdrant 仪表盘使用

源码地址

代码https://gitcode.com/cao9prc/AINET10fstyle

其他专题

AI开发01 1后端框架: ASP.NET Core2.AI框架: Semantic Kernerl (SK)、Agent Framework3.知识库:向量数据库(Qdrant)+关系型数据库(Post

https://blog.csdn.net/cao919/article/details/155895060

.net AI开发02 1后端框架: ASP.NET Core2.AI框架: Semantic Kernerl (SK)、Agent Framework3.知识库:向量数据库(Qdrant)+关系型数据库(Post

https://blog.csdn.net/cao919/article/details/155895060?fromshare=blogdetail&sharetype=blogdetail&sharerId=155895060&sharerefer=PC&sharesource=cao919&sharefrom=from_link

.net AI开发03 新增意图识别与工具选择工作流(IntentWorkflow),支持多智能体协作; 插件体系升级,支持多项目插件自动注册与工具发现; 对话历史与消息存储解耦,采用 Med

https://blog.csdn.net/cao919/article/details/156065076

.net AI开发04 第八章 引入RAG知识库与文档管理核心能力及事件总线

https://blog.csdn.net/cao919/article/details/156990895

在C# .net中RabbitMQ的核心类型和属性,除了交换机,队列关键的类型 / 属性,影响其行为

https://blog.csdn.net/cao919/article/details/157254797

.net AI MCP 入门 适用于模型上下文协议的 C# SDK 简介(MCP)

https://blog.csdn.net/cao919/article/details/147915384

C# .net ai Agent AI视觉应用 写代码 改作业 识别屏幕 简历处理 标注等

https://blog.csdn.net/cao919/article/details/146504537

.net AI API应用 客户发的信息提取对接上下游系统报价

https://blog.csdn.net/cao919/article/details/156656526

C# net deepseek RAG AI开发 全流程 介绍

https://blog.csdn.net/cao919/article/details/147915384

WPF halcon 机器视觉

https://blog.csdn.net/cao919/article/details/134790240

相关推荐
Testopia2 小时前
AI编程实例 - 基于时间序列分析的工厂设备预测性维护
人工智能·ai编程·#人工智能学习·#时间序列分析
Agentcometoo2 小时前
2026 AI 元年:为什么 AI 正在成为基础设施,而非创新工具
人工智能·智能体来了·ai元年·2026ai元年
赵药师2 小时前
YOLO训练水面漂浮垃圾数据集FLOW_IMG数据集
人工智能·深度学习·yolo
会游泳的石头2 小时前
构建企业级知识库智能问答系统:基于 Java 与 Spring Boot 的轻量实现
java·开发语言·spring boot·ai
wenzhangli72 小时前
ooderAgent Nexus 版本核心场景测试报告
人工智能
deephub2 小时前
Claude Code子代理实战:10个即用模板分享
人工智能·大语言模型·agent·claude code
测试_AI_一辰2 小时前
Agent & RAG 测试工程 03:第一次为 RAG 写回归测试:防幻觉、保一致、守底线
人工智能·笔记·功能测试·测试用例·ai编程
SeatuneWrite2 小时前
**AI漫剧软件2025推荐,解锁沉浸式二次元内容创作新体验
大数据·人工智能·python
Pyeako2 小时前
opencv计算机视觉--Harris角点检测&SIFT特征提取&图片抠图
人工智能·python·opencv·计算机视觉·harris角点检测·sift特征提取·图片抠图