.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

相关推荐
NAGNIP1 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab2 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab2 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
AngelPP6 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年6 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼6 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS6 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区8 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈8 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang8 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx