.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

相关推荐
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免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤