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

前言:本项目聚焦企业级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

第八章 引入RAG知识库与文档管理核心能力及事件总线

1.RAG 系统架构概览

2.知识库领域模型设计

3.嵌入模型选择:云端/本地

4.嵌入模型选型评估与量化版本

5.实现 RAG文件存储服务

6.消息队列集成

7.构建 RAG应用服务层

引入RAG知识库与文档管理核心能力及事件总线

新增RAG领域模型(嵌入模型、知识库、文档、切片)及其EF配置,完善数据库结构。集成RagService与EventBus,支持RabbitMQ事件驱动。实现知识库创建、文档上传API,支持本地文件存储与幂等校验。扩展通用服务接口,优化依赖注入与项目结构,修复部分注册与兼容性问题,为后续RAG检索增强功能奠定基础。

时效性缺失、私有领域空白RAG 检索增强生成技术在让LLM回答问题之前,先去外部知识库中检素相关的信息,然后将检素到的信息作为参考资料喂给LLM,让它基于资料生成答案

1.索引阶段(后台异步运行的数据处理流程 文本转换为向量==>>构建语义索引)

2.检索与生成阶段(能够在线实时响应用户请求的流程)

ETL(提取、转换、加载)流:

1.加载(格式解析、编码标准化、元数据提取)

2.分割(LLM的上下文窗口有限)递归字符分割、重叠窗口

3.嵌入(人类语言翻译成机器语言,嵌入模型==>>文本转换为高维向量==>>高维的语义空间,余弦相似度 -1~1)

4.存储(文本块内容、向量数据库、元数据==>>持久化存储、向量数据库)

1111

知识库管理状态

嵌入模型

1粉丝 南浔:为何对接各个厂家的模型请求参数基本一样,ur,appkey,他们是商量好的吗?

OpenAI事实标准

111

如何选嵌入模型

闭源厂商云端模型 API:优势:接入成本低、弹性扩展-劣势:数据隐私风险、长期成本不可控、网络延迟

开源模型本地私有化:优势:绝对的数据安全、零增量成本、高性能与低延迟劣势:硬件门槛高、维护复杂度

FP16(16位浮点数)INT8(Q8 0)==>>映射为8位整数,体积减半INT4(Q4) ==>映射为4为整数

cur http://127.0.0.1:1234/v1/embeddings\\-H"Content-Type:application/json"\\d'"model":"text-embedding-qwen3-embedding-4b","input":"Some text to embed"

111

RAG数据接入:

1.用户通过API创建知识库

2.用户上传文件(异步)

3.系统计算文件Hash(幂等性)

4.现在数据库生成文档记录

5.发送消息到RabbitMQ

聚合根

KnowledgeBase

cs 复制代码
using Zilor.AICopilot.SharedKernel.Domain;

namespace Zilor.AICopilot.Core.Rag.Aggregates.KnowledgeBase;

public class KnowledgeBase : IAggregateRoot
{
    private readonly List<Document> _documents = [];

    protected KnowledgeBase()
    {
    }
    
    public KnowledgeBase(string name, string description, Guid embeddingModelId)
    {
        Id = Guid.NewGuid();
        Name = name;
        Description = description;
        EmbeddingModelId = embeddingModelId;
    }
    
    public Guid Id { get; set; }
    public string Name { get; private set; } = string.Empty;
    public string Description { get; private set; } = string.Empty;
    
    /// <summary>
    /// 嵌入模型ID。一个知识库内的所有文档必须使用相同的嵌入模型。
    /// </summary>
    public Guid EmbeddingModelId { get; private set; }
    
    // 导航属性:对外只暴露只读集合
    public IReadOnlyCollection<Document> Documents => _documents.AsReadOnly();

    /// <summary>
    /// 添加新文档到知识库
    /// </summary>
    public Document AddDocument(string name, string filePath, string extension, string fileHash)
    {
        var document = new Document(Id, name, filePath, extension, fileHash);
        _documents.Add(document);
        return document;
    }

    /// <summary>
    /// 移除文档
    /// </summary>
    public void RemoveDocument(int documentId)
    {
        var doc = _documents.FirstOrDefault(d => d.Id == documentId);
        if (doc != null)
        {
            _documents.Remove(doc);
        }
    }

    public void UpdateInfo(string name, string description)
    {
        Name = name;
        Description = description;
    }
}

Document

cs 复制代码
using Zilor.AICopilot.SharedKernel.Domain;

namespace Zilor.AICopilot.Core.Rag.Aggregates.KnowledgeBase;

public class Document : IEntity<int>
{
    private readonly List<DocumentChunk> _chunks = [];

    protected Document()
    {
    }

    internal Document(Guid knowledgeBaseId, string name, string filePath, string extension, string fileHash)
    {
        KnowledgeBaseId = knowledgeBaseId;
        Name = name;
        FilePath = filePath;
        Extension = extension;
        FileHash = fileHash;
        Status = DocumentStatus.Pending;
        CreatedAt = DateTime.UtcNow;
    }

    public int Id { get; set; }
    
    public Guid KnowledgeBaseId { get; private set; }
    
    /// <summary>
    /// 原始文件名
    /// </summary>
    public string Name { get; private set; } = string.Empty;
    
    /// <summary>
    /// 文件存储路径
    /// </summary>
    public string FilePath { get; private set; } = string.Empty;
    
    /// <summary>
    /// 文件扩展名
    /// </summary>
    public string Extension { get; private set; } = string.Empty;
    
    /// <summary>
    /// 文件哈希值
    /// </summary>
    public string FileHash { get; private set; } = string.Empty;
    
    /// <summary>
    /// 文档处理状态
    /// </summary>
    public DocumentStatus Status { get; private set; }
    
    /// <summary>
    /// 切片数量
    /// </summary>
    public int ChunkCount { get; private set; }
    
    /// <summary>
    /// 错误信息
    /// </summary>
    public string? ErrorMessage { get; private set; }
    
    public DateTime CreatedAt { get; private set; }
    public DateTime? ProcessedAt { get; private set; }

    // 导航属性
    public KnowledgeBase KnowledgeBase { get; private set; } = null!;
    public IReadOnlyCollection<DocumentChunk> Chunks => _chunks.AsReadOnly();

    #region 领域行为方法

    /// <summary>
    /// 开始解析文档
    /// </summary>
    public void StartParsing()
    {
        if (Status != DocumentStatus.Pending && Status != DocumentStatus.Failed)
            throw new InvalidOperationException($"当前状态 {Status} 不允许开始解析");
            
        Status = DocumentStatus.Parsing;
        ErrorMessage = null;
    }

    /// <summary>
    /// 完成解析,准备切片
    /// </summary>
    public void CompleteParsing()
    {
        if (Status != DocumentStatus.Parsing) return;
        Status = DocumentStatus.Splitting;
    }

    /// <summary>
    /// 添加文档切片
    /// </summary>
    public void AddChunk(int index, string content)
    {
        // 允许在 Splitting 或 Embedding 阶段添加/重新生成切片
        if (Status != DocumentStatus.Splitting && Status != DocumentStatus.Embedding)
             throw new InvalidOperationException($"当前状态 {Status} 不允许添加切片");

        var chunk = new DocumentChunk(Id, index, content);
        _chunks.Add(chunk);
        ChunkCount = _chunks.Count;
    }
    
    /// <summary>
    /// 清空所有切片(例如重新处理时)
    /// </summary>
    public void ClearChunks()
    {
        _chunks.Clear();
        ChunkCount = 0;
    }

    /// <summary>
    /// 开始向量化
    /// </summary>
    public void StartEmbedding()
    {
        Status = DocumentStatus.Embedding;
    }

    /// <summary>
    /// 标记切片已向量化完成(更新向量ID)
    /// </summary>
    public void MarkChunkAsEmbedded(int chunkId, string vectorId)
    {
        var chunk = _chunks.FirstOrDefault(c => c.Id == chunkId);
        chunk?.SetVectorId(vectorId);
    }

    /// <summary>
    /// 文档处理全部完成
    /// </summary>
    public void MarkAsIndexed()
    {
        Status = DocumentStatus.Indexed;
        ProcessedAt = DateTime.UtcNow;
    }

    /// <summary>
    /// 标记处理失败
    /// </summary>
    public void MarkAsFailed(string errorMessage)
    {
        Status = DocumentStatus.Failed;
        ErrorMessage = errorMessage;
    }

    #endregion
}

public enum DocumentStatus
{
    Pending = 0,      // 等待处理
    Parsing = 1,      // 正在读取/解析内容
    Splitting = 2,    // 正在进行文本切片
    Embedding = 3,    // 正在调用模型生成向量
    Indexed = 4,      // 索引完成,可用于检索
    Failed = 5        // 处理失败
}

DocumentChunk 向量切片

cs 复制代码
using Zilor.AICopilot.SharedKernel.Domain;

namespace Zilor.AICopilot.Core.Rag.Aggregates.KnowledgeBase;

public class DocumentChunk : IEntity<int>
{
    protected DocumentChunk()
    {
    }

    internal DocumentChunk(int documentId, int index, string content)
    {
        DocumentId = documentId;
        Index = index;
        Content = content;
        CreatedAt = DateTime.UtcNow;
    }

    public int Id { get; set; }
    
    public int DocumentId { get; private set; }
    
    /// <summary>
    /// 切片序号
    /// </summary>
    public int Index { get; private set; }
    
    /// <summary>
    /// 文本内容
    /// </summary>
    public string Content { get; private set; } = string.Empty;
    
    /// <summary>
    /// 向量数据库中的ID
    /// </summary>
    public string? VectorId { get; private set; }
    
    public DateTime CreatedAt { get; private set; }

    // 导航属性
    public Document Document { get; private set; } = null!;

    /// <summary>
    /// 设置向量ID (当向量化完成后调用)
    /// </summary>
    public void SetVectorId(string vectorId)
    {
        VectorId = vectorId;
    }
}

建模

cs 复制代码
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Zilor.AICopilot.Core.Rag.Aggregates.KnowledgeBase;

namespace Zilor.AICopilot.EntityFrameworkCore.Configuration.Rag;

public class DocumentChunkConfiguration : IEntityTypeConfiguration<DocumentChunk>
{
    public void Configure(EntityTypeBuilder<DocumentChunk> builder)
    {
        builder.ToTable("document_chunks");

        builder.HasKey(c => c.Id);
        builder.Property(c => c.Id).HasColumnName("id")
            .ValueGeneratedOnAdd();

        builder.Property(c => c.DocumentId)
            .IsRequired()
            .HasColumnName("document_id");

        builder.Property(c => c.Index)
            .IsRequired()
            .HasColumnName("index");

        // 内容字段,根据数据库类型可能需要配置为 TEXT
        builder.Property(c => c.Content)
            .IsRequired()
            .HasColumnType("text")
            .HasColumnName("content");

        builder.Property(c => c.VectorId)
            .HasMaxLength(100)
            .HasColumnName("vector_id"); // 允许为空,因为刚切分完可能还没向量化

        builder.Property(c => c.CreatedAt)
            .IsRequired()
            .HasColumnName("created_at");
            
        // 索引配置:通常会根据文档ID查询切片,并按顺序排序
        builder.HasIndex(c => new { c.DocumentId, c.Index })
            .IsUnique(); // 保证同一文档内切片序号不重复
    }
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Zilor.AICopilot.Core.Rag.Aggregates.KnowledgeBase;

namespace Zilor.AICopilot.EntityFrameworkCore.Configuration.Rag;

public class DocumentConfiguration : IEntityTypeConfiguration<Document>
{
    public void Configure(EntityTypeBuilder<Document> builder)
    {
        builder.ToTable("documents");

        builder.HasKey(d => d.Id);
        builder.Property(d => d.Id).HasColumnName("id")
            .ValueGeneratedOnAdd();

        builder.Property(d => d.KnowledgeBaseId)
            .IsRequired()
            .HasColumnName("knowledge_base_id");

        builder.Property(d => d.Name)
            .IsRequired()
            .HasMaxLength(256)
            .HasColumnName("name");

        builder.Property(d => d.FilePath)
            .IsRequired()
            .HasMaxLength(500)
            .HasColumnName("file_path");

        builder.Property(d => d.Extension)
            .IsRequired()
            .HasMaxLength(50)
            .HasColumnName("extension");

        builder.Property(d => d.FileHash)
            .IsRequired()
            .HasMaxLength(64) // 使用 SHA256,通常为 64 字符
            .HasColumnName("file_hash");

        // 状态枚举:建议存为字符串,方便数据库直观查看
        builder.Property(d => d.Status)
            .IsRequired()
            .HasMaxLength(50)
            .HasConversion<string>() 
            .HasColumnName("status");

        builder.Property(d => d.ChunkCount)
            .IsRequired()
            .HasColumnName("chunk_count");

        builder.Property(d => d.ErrorMessage)
            .HasColumnName("error_message"); // 允许为空

        builder.Property(d => d.CreatedAt)
            .IsRequired()
            .HasColumnName("created_at");

        builder.Property(d => d.ProcessedAt)
            .HasColumnName("processed_at"); // 允许为空
        
        // 配置导航属性 Chunks
        builder.HasMany(d => d.Chunks)
            .WithOne(c => c.Document)
            .HasForeignKey(c => c.DocumentId)
            .IsRequired()
            .OnDelete(DeleteBehavior.Cascade); // 删除文档时级联删除切片
    }
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Zilor.AICopilot.Core.Rag.Aggregates.EmbeddingModel;

namespace Zilor.AICopilot.EntityFrameworkCore.Configuration.Rag;

public class EmbeddingModelConfiguration : IEntityTypeConfiguration<EmbeddingModel>
{
    public void Configure(EntityTypeBuilder<EmbeddingModel> builder)
    {
        builder.ToTable("embedding_models");

        builder.HasKey(e => e.Id);
        builder.Property(e => e.Id).HasColumnName("id");

        builder.Property(e => e.Name)
            .IsRequired()
            .HasMaxLength(100)
            .HasColumnName("name");
        
        // 建议添加唯一索引,防止同名模型
        builder.HasIndex(e => e.Name).IsUnique();

        builder.Property(e => e.Provider)
            .IsRequired()
            .HasMaxLength(50)
            .HasColumnName("provider");
        
        builder.Property(e => e.BaseUrl)
            .IsRequired()
            .HasMaxLength(500)
            .HasColumnName("base_url");
        
        builder.Property(e => e.ApiKey)
            .HasMaxLength(256)
            .HasColumnName("api_key");

        builder.Property(e => e.ModelName)
            .IsRequired()
            .HasMaxLength(100)
            .HasColumnName("model_name");

        builder.Property(e => e.Dimensions)
            .IsRequired()
            .HasColumnName("dimensions");

        builder.Property(e => e.MaxTokens)
            .IsRequired()
            .HasColumnName("max_tokens");

        builder.Property(e => e.IsEnabled)
            .IsRequired()
            .HasColumnName("is_enabled");
    }
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Zilor.AICopilot.Core.Rag.Aggregates.KnowledgeBase;

namespace Zilor.AICopilot.EntityFrameworkCore.Configuration.Rag;

public class KnowledgeBaseConfiguration : IEntityTypeConfiguration<KnowledgeBase>
{
    public void Configure(EntityTypeBuilder<KnowledgeBase> builder)
    {
        builder.ToTable("knowledge_bases");

        builder.HasKey(kb => kb.Id);
        builder.Property(kb => kb.Id).HasColumnName("id");

        builder.Property(kb => kb.Name)
            .IsRequired()
            .HasMaxLength(200)
            .HasColumnName("name");

        builder.Property(kb => kb.Description)
            .HasMaxLength(1000)
            .HasColumnName("description");

        builder.Property(kb => kb.EmbeddingModelId)
            .IsRequired()
            .HasColumnName("embedding_model_id");
        
        // 配置导航属性 Documents
        builder.HasMany(kb => kb.Documents)
            .WithOne(d => d.KnowledgeBase)
            .HasForeignKey(d => d.KnowledgeBaseId)
            .IsRequired()
            .OnDelete(DeleteBehavior.Cascade); // 删除知识库时级联删除文档
    }
}

实现

RAG数据接入:

1.用户通过API创建知识库

2.用户上传文件(异步)

cs 复制代码
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Zilor.AICopilot.HttpApi.Infrastructure;
using Zilor.AICopilot.RagService.Commands.Documents;
using Zilor.AICopilot.Services.Common;

namespace Zilor.AICopilot.HttpApi.Controllers;

[Route("/api/rag")]
[Authorize]
public class RagController : ApiControllerBase
{
    /// <summary>
    /// 创建知识库
    /// </summary>
    [HttpPost("knowledge-base")]
    public async Task<IActionResult> CreateKnowledgeBase(CreateKnowledgeBaseCommand command)
    {
        var result = await Sender.Send(command);
        return ReturnResult(result);
    }

    /// <summary>
    /// 上传文档
    /// </summary>
    [HttpPost("document")]
    [DisableRequestSizeLimit] // 允许上传大文件
    public async Task<IActionResult> UploadDocument(
        [FromForm] Guid knowledgeBaseId, 
        IFormFile file)
    {
        if (file.Length == 0)
        {
            return BadRequest(new { error = "请选择文件" });
        }

        // 将 IFormFile 转换为流
        await using var stream = file.OpenReadStream();
        
        var command = new UploadDocumentCommand(
            knowledgeBaseId, 
            new FileUploadStream(file.FileName, stream));

        var result = await Sender.Send(command);
        return ReturnResult(result);
    }
}

3.系统计算文件Hash(幂等性)

cs 复制代码
using System.Security.Cryptography;
using MassTransit;
using Zilor.AICopilot.Core.Rag.Aggregates.KnowledgeBase;
using Zilor.AICopilot.Services.Common.Attributes;
using Zilor.AICopilot.Services.Common.Contracts;
using Zilor.AICopilot.Services.Common.Events;
using Zilor.AICopilot.SharedKernel.Messaging;
using Zilor.AICopilot.SharedKernel.Repository;
using Zilor.AICopilot.SharedKernel.Result;

namespace Zilor.AICopilot.RagService.Commands.Documents;


public record UploadDocumentDto(int Id, string Status);

public record FileUploadStream(string FileName, Stream Stream);

[AuthorizeRequirement("Rag.UploadDocument")]
public record UploadDocumentCommand(
    Guid KnowledgeBaseId, 
    FileUploadStream File) : ICommand<Result<UploadDocumentDto>>;

public class UploadDocumentCommandHandler(
    IRepository<KnowledgeBase> kbRepo,
    IFileStorageService fileStorage,
    IPublishEndpoint publishEndpoint) 
    : ICommandHandler<UploadDocumentCommand, Result<UploadDocumentDto>>
{
    public async Task<Result<UploadDocumentDto>> Handle(
        UploadDocumentCommand request, 
        CancellationToken cancellationToken)
    {
        // 1. 获取知识库聚合根(并急切加载 Documents 集合)
        // 使用我们刚扩展的 GetAsync 方法,通过 includes 参数加载子实体
        var kb = await kbRepo.GetAsync(
            kb => kb.Id == request.KnowledgeBaseId, 
            includes: [k => k.Documents], 
            cancellationToken);

        if (kb == null) return Result.NotFound("知识库不存在");
        
        // 2. 计算文件 Hash (SHA256)
        string fileHash;
        using (var sha256 = SHA256.Create())
        {
            // 确保流从头开始
            if (request.File.Stream.CanSeek) request.File.Stream.Position = 0;
            
            var hashBytes = await sha256.ComputeHashAsync(request.File.Stream, cancellationToken);
            fileHash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
            
            // 计算完 Hash 后,必须重置流位置,否则后续保存文件时会读到空内容
            if (request.File.Stream.CanSeek) request.File.Stream.Position = 0;
        }
        
        // 3. 检查文件是否已存在 (基于 Hash 实现幂等性)
        // 因为 Documents 已经加载到内存中,我们可以直接使用 LINQ 查询
        var existingDoc = kb.Documents.FirstOrDefault(d => d.FileHash == fileHash);
        if (existingDoc != null)
        {
            // 如果文件已存在,直接返回成功,并返回现有的文档 ID
            // 这实现了接口的幂等性:多次上传同一文件不会产生副作用
            return Result.Success(new UploadDocumentDto(existingDoc.Id, existingDoc.Status.ToString()));
        }
        
        // 4. 保存物理文件 (只有当文件不存在时才执行 IO 操作)
        var extension = Path.GetExtension(request.File.FileName).ToLower();
        var savedPath = await fileStorage.SaveAsync(request.File.Stream, request.File.FileName, cancellationToken);

        // 5. 领域模型行为:添加文档
        // 这一步是纯内存操作,修改了聚合根的状态
        var document = kb.AddDocument(request.File.FileName, savedPath, extension, fileHash);

        // 6. 持久化到数据库
        await kbRepo.SaveChangesAsync(cancellationToken);

        // 7. 发送集成事件 (通知后台 Worker 开始索引)
        await publishEndpoint.Publish(new DocumentUploadedEvent
        {
            DocumentId = document.Id,
            KnowledgeBaseId = kb.Id,
            FilePath = savedPath,
            FileName = request.File.FileName
        }, cancellationToken);

        return Result.Success(new UploadDocumentDto(document.Id, document.Status.ToString()));
    }
}

4.现在数据库生成文档记录

5.发送消息到RabbitMQ

配置

对象

基础设施

cs 复制代码
using System.Reflection;
using MassTransit;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

namespace Zilor.AICopilot.EventBus;

public static class DependencyInjection
{
    public static void AddEventBus(this IHostApplicationBuilder builder, params Assembly[] assemblies) 
    {
        builder.Services.AddMassTransit(x =>
        {
            if (assemblies.Length > 0)
            {
                x.AddConsumers(assemblies);
            }
            
            x.SetKebabCaseEndpointNameFormatter();

            // 默认配置 RabbitMQ
            x.UsingRabbitMq((context, cfg) =>
            {
                // 从 Aspire 注入的连接字符串中读取配置
                // 连接字符串名必须与 AppHost 中 .AddRabbitMQ("eventbus") 的名称一致
                var connectionString = builder.Configuration.GetConnectionString("eventbus");
                cfg.Host(connectionString);
                
                cfg.ConfigureEndpoints(context);
            });
        });
    }
}

服务层

rag模型

cs 复制代码
using System;
using System.Threading;
using System.Threading.Tasks;
using Zilor.AICopilot.Core.Rag.Aggregates.EmbeddingModel;
using Zilor.AICopilot.Core.Rag.Aggregates.KnowledgeBase;
using Zilor.AICopilot.Services.Common.Attributes;
using Zilor.AICopilot.SharedKernel.Messaging;
using Zilor.AICopilot.SharedKernel.Repository;
using Zilor.AICopilot.SharedKernel.Result;

namespace Zilor.AICopilot.Services.Common;

public record CreatedKnowledgeBaseDto(Guid Id, string Name);

[AuthorizeRequirement("Rag.CreateKnowledgeBase")]
public record CreateKnowledgeBaseCommand(
    string Name, 
    string Description, 
    Guid EmbeddingModelId) : ICommand<Result<CreatedKnowledgeBaseDto>>;

public class CreateKnowledgeBaseCommandHandler(
    IRepository<KnowledgeBase> kbRepo,
    IReadRepository<EmbeddingModel> modelRepo)
    : ICommandHandler<CreateKnowledgeBaseCommand, Result<CreatedKnowledgeBaseDto>>
{
    public async Task<Result<CreatedKnowledgeBaseDto>> Handle(
        CreateKnowledgeBaseCommand request, 
        CancellationToken cancellationToken)
    {
        // 1. 校验嵌入模型是否存在
        // 知识库必须绑定一个具体的 Embedding 模型,因为这决定了向量的维度
        var embeddingModel = await modelRepo.GetByIdAsync(request.EmbeddingModelId, cancellationToken);
        if (embeddingModel == null)
        {
            return Result.NotFound("指定的嵌入模型不存在");
        }

        // 2. 创建实体
        var kb = new KnowledgeBase(request.Name, request.Description, request.EmbeddingModelId);

        // 3. 持久化
        kbRepo.Add(kb);
        await kbRepo.SaveChangesAsync(cancellationToken);

        return Result.Success(new CreatedKnowledgeBaseDto(kb.Id, kb.Name));
    }
}

异步与并发处理

RAG后台服务(实现文档索引)

文件存储:变化点

cs 复制代码
using Zilor.AICopilot.Services.Common.Contracts;

namespace Zilor.AICopilot.Infrastructure.Storage;

public class LocalFileStorageService : IFileStorageService
{
    private const string RootPath = "D:\\";
    private const string UploadRoot = "uploads";

    public async Task<string> SaveAsync(Stream stream, string fileName, CancellationToken cancellationToken = default)
    {
        // 1. 构建存储路径:uploads/2025/12/01/guid_filename.pdf
        var datePath = DateTime.Now.ToString("yyyy/MM/dd");
        var uniqueFileName = $"{Guid.NewGuid()}_{fileName}";
        var relativePath = Path.Combine(UploadRoot, datePath);
        
        var fullDirectory = Path.Combine(RootPath, relativePath);

        if (!Directory.Exists(fullDirectory))
        {
            Directory.CreateDirectory(fullDirectory);
        }

        var fullPath = Path.Combine(fullDirectory, uniqueFileName);

        // 2. 写入文件
        await using var fileStream = new FileStream(fullPath, FileMode.Create);
        if (stream.CanSeek) stream.Position = 0;
        await stream.CopyToAsync(fileStream, cancellationToken);

        // 3. 返回相对路径(统一使用正斜杠,方便跨平台和URL访问)
        return Path.Combine(relativePath, uniqueFileName).Replace("\\", "/");
    }

    public Task<Stream?> GetAsync(string path, CancellationToken cancellationToken = default)
    {
        var fullPath = Path.Combine(RootPath, path);

        if (!File.Exists(fullPath)) return Task.FromResult<Stream?>(null);

        var stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read);
        return Task.FromResult<Stream?>(stream);
    }

    public Task DeleteAsync(string path, CancellationToken cancellationToken = default)
    {
        var fullPath = Path.Combine(RootPath, path);

        if (File.Exists(fullPath))
        {
            File.Delete(fullPath);
        }

        return Task.CompletedTask;
    }
}

星:接下来redlis会用上吗?

阿伦:刚才很多服务的是什么工具

星:aspire,阿伦前边的课没有听

6666

RabbitMQ 密码

1

2

3

4默认账号密码

5 上面密码pass

6

本地AI工具 坑呀

https://lmstudio.ai/download

如何安装https://cloud.tencent.com/developer/article/2475814

---入坑开始

不翻墙在 LM Studio 下载模型,核心是修改镜像源手动下载后导入,以下是两种高效可行方法,优先推荐镜像源修改,操作简单且适配软件内下载流程。


方法一:修改镜像源(推荐,软件内直接下载)

  1. 关闭 LM Studio:确保软件完全退出,避免文件占用。
  2. 定位核心文件
    • Windows:安装目录(如D:\Program Files\LM Studio)→ resources\app\.webpack\main\index.jsresources\app\.webpack\renderer\main_window.js
    • macOS:右键LM Studio.app→显示包内容→Contents\Resources\app\.webpack\main\index.jsContents\Resources\app\.webpack\renderer\main_window.js
  3. 替换镜像 :用 VS Code 等编辑器打开上述两个文件,批量将所有 huggingface.co 替换为国内镜像 hf-mirror.com,保存文件。
  4. 重启并下载:重新打开 LM Studio,在模型搜索页选择 GGUF 格式模型(如 Q4_K_M 量化版),点击下载即可。
  5. 快捷工具(可选) :执行命令npx lmstudio-mirror-switcher,一键完成镜像切换与备份,适合不想手动改文件的用户。

方法二:手动下载导入(稳定可靠,适合大文件)

  1. 获取模型文件

    • 镜像网站下载:访问hf-mirror.com或魔搭社区(modelscope.cn),搜索所需 GGUF 格式模型(如lmstudio-community/DeepSeek-R1-Distill-Qwen-14B-GGUF),下载对应量化版本(如 Q4_K_M)。
    • 命令行下载(推荐):安装 huggingface-cli,配置环境变量后下载,示例命令如下:

    bash

    运行

    复制代码
    # Windows(cmd)
    set HF_ENDPOINT=https://hf-mirror.com
    huggingface-cli download 模型仓库名 模型文件名 --local-dir 本地保存路径
    # macOS/Linux(终端)
    export HF_ENDPOINT=https://hf-mirror.com
    huggingface-cli download 模型仓库名 模型文件名 --local-dir 本地保存路径
  2. 放置模型到指定目录

    • 默认目录:Windows 一般为C:\Users\用户名\.lmstudio\models;macOS 为/Users/用户名/.lmstudio/models;Linux 为~/.lmstudio/models
    • 自定义目录:在 LM Studio 中点击设置→模型目录→Change,选择自定义路径,将下载的模型文件放入该目录下的对应模型文件夹。
  3. 刷新加载:重启 LM Studio,模型会自动加载到库中,可直接使用。


注意事项

  • 模型格式:LM Studio 优先支持 GGUF 格式,下载时注意选择对应版本,避免格式不兼容。
  • 目录结构:手动导入时,需按模型仓库名/模型文件名.gguf的结构存放,确保软件正常识别。
  • 存储路径:建议将模型放在非系统盘,预留足够空间(7B 模型约 4GB,14B 模型约 8GB)。

选择

git clone --depth 1 https://huggingface.co/Qwen/Qwen3-Embedding-0.6B-GGUF /e/Users/Administrator/.lmstudio/models/Qwen3-Embedding-0.6B-GGUF

https://hf-mirror.com/Qwen/Qwen3-Embedding-0.6B-GGUF/tree/main

方案 1:下载支持 AVX 指令集的老旧 GGUF 模型(无需换硬件) 部分早期 GGUF 模型没有强制要求 AVX2,可针对性下载: 优先选择 7B 及以下参数量、早期量化版本(如 Q4_0、Q5_0) 的模型,例如: TheBloke/Llama-2-7B-Chat-GGUF(找标注 avx 兼容的版本) TheBloke/Mistral-7B-v0.1-GGUF(老旧量化版) 下载渠道:在 hf-mirror.com 搜索模型时,筛选 2023 年底前发布的 GGUF 文件,查看模型说明中的「CPU 要求」,确认支持 AVX。 有支持的数据量小的Qwen3吗

有适配纯 AVX 的小参数量 Qwen3 GGUF 模型,优先选Qwen3-0.6B (最小、最易跑),其次是 Qwen3-1.7B,核心要下早期低量化版 + 适配纯 AVX 的转换版,搭配手动导入 + 指令集强制兼容,可在你的 Xeon E5-2450(仅 AVX)上运行。


一、可直接用的小参数量 Qwen3 模型(适配 AVX)

模型名称 参数量 推荐量化版 适配理由 下载链接(hf-mirror)
Qwen3-0.6B-GGUF 0.6B Q4_0 / Q5_0(早期版) 最小参数量,早期 GGUF 转换未强制 AVX2 https://hf-mirror.com/Qwen/Qwen3-0.6B-GGUF
Qwen3-1.7B-GGUF 1.7B Q4_K_M(2024 年初转换版) 轻量,部分社区转换版保留 AVX 兼容 https://hf-mirror.com/Qwen/Qwen3-1.7B-GGUF

二、AVX 兼容关键操作(必做,否则仍加载失败)

  1. 选对文件 + 目录合规
    • 下载上述模型的 Q4_0 / Q5_0 低量化版(Q8_0 等新版可能隐含 AVX2 依赖)。
    • 存放结构:E:\LMStudio_Models\Qwen3-0.6B-GGUF\Qwen3-0.6B-Q4_0.gguf(目录必须在 LM Studio 安装目录外)。
  2. 强制指令集兼容(修改配置)
    • 关闭 LM Studio,打开 %APPDATA%\LM Studio\config.json,添加 "cpuInstructionSetOverride": "avx",保存后重启。
  3. 手动导入 + 刷新识别
    • 放入合规目录后,在 LM Studio 的「Local Models」页点击右上角「Refresh」,即可显示模型,点击「Load」加载。

三、下载与导入步骤(避免镜像 / 缓存问题)

  1. 从 hf-mirror 链接下载目标量化文件(用 IDM / 迅雷,避免文件损坏)。
  2. 按「模型目录 \ 仓库名 \ 模型.gguf」存放,给目录 Administrator 完全控制权限。
  3. 修改 config.json 启用 AVX 兼容,重启 LM Studio,刷新本地模型列表,加载验证。

四、常见问题与修复

问题 修复方案
加载报 illegal instruction 确认用 Q4_0/Q5_0 低量化,再次核对 config.json 的 avx 强制配置
模型不显示 检查目录结构,重新刷新,确保无.tmp/.part 临时后缀
推理极慢 0.6B 模型在 AVX 下约 1--5 tokens/sec,属正常,可换 Q4_0 进一步提速

五、替代方案(模型仍无法加载时)

若上述步骤无效,可下载 Qwen2-0.5B/1.8B 的早期 GGUF 版(更老旧,AVX 兼容更稳),能力接近 Qwen3 小模型,且社区验证纯 AVX 可运行。

需要我帮你下载并打包Qwen3-0.6B-Q4_0.gguf(AVX 兼容版)+ 目录结构模板 + config.json 修改脚本,你解压后直接放入目录就能用吗?

---入坑结束

https://hf-mirror.com/models?num_parameters=min:0,max:6B&sort=trending&search=Qwen3-0.6B-GGUF+Q4_0

源码地址

代码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

在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

C# net deepseek RAG AI开发 全流程 介绍
https://blog.csdn.net/cao919/article/details/147915384

WPF halcon 机器视觉
https://blog.csdn.net/cao919/article/details/134790240

相关推荐
2501_933329552 小时前
Infoseek数字公关AI中台技术解析:如何构建企业级舆情监测与智能处置系统
开发语言·人工智能
Eiceblue2 小时前
.NET框架下Windows、Linux、Mac环境C#打印PDF全指南
linux·windows·.net
AI即插即用2 小时前
即插即用系列 | AAAI 2026 WaveFormer: 当视觉建模遇上波动方程,频率-时间解耦的新SOTA
图像处理·人工智能·深度学习·神经网络·计算机视觉·视觉检测
轻览月2 小时前
【DL】复杂卷积神经网络Ⅰ
人工智能·神经网络·cnn
diediedei2 小时前
机器学习模型部署:将模型转化为Web API
jvm·数据库·python
m0_561359672 小时前
使用Python自动收发邮件
jvm·数据库·python
逄逄不是胖胖2 小时前
《动手学深度学习》-55-2RNN的简单实现
人工智能·深度学习
冰菓Neko2 小时前
科目四刷题总结
人工智能
天空属于哈夫克32 小时前
企业微信外部群运营升级:API 主动推送消息开发实战
java·数据库·mysql