- 从文件模块的多部分上传处理器中移除未使用的实体引用 - 从即时通讯模块的群组成员处理器中移除未使用的实体引用 - 从RAG模块的AI模型配置中移除未使用的实体引用 - 从监控模块的告警规则配置中移除未使用的实体引用 - 从平台模块的角色权限处理器中移除未使用的实体引用 - 从脚本模块的执行日志实体引用中进行清理 - 从多个基础设施配置类中移除未使用的依赖项 - 优化测试文件中的导入声明,移除不必要的引用
165 lines
5.9 KiB
C#
165 lines
5.9 KiB
C#
using BuildFlow.Application.RagQA.Commands;
|
|
using BuildFlow.Application.RagQA.Handlers;
|
|
using BuildFlow.Domain.Entities.Document;
|
|
using BuildFlow.Domain.Entities.KnowledgeBase;
|
|
using BuildFlow.Domain.Enums;
|
|
using BuildFlow.Domain.Interfaces;
|
|
using BuildFlow.Infrastructure.Persistence;
|
|
using BuildFlow.Rag.Tests.Stub;
|
|
using FluentAssertions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Xunit;
|
|
|
|
namespace BuildFlow.Rag.Tests;
|
|
|
|
/// <summary>
|
|
/// [SOP-RAG-01] 知识库 RAG 问答全链路端到端测试。
|
|
/// 用 Stub IRagRetrievalService 返回预设 sources,绕过 pgvector 真实召回。
|
|
/// 验证:RagQueryCommandHandler 取 KB → RetrieveAsync → IAIChatAgent.RunAsync → 返回 answer+sources。
|
|
/// 飞书文档: https://ecn1rid3e27z.feishu.cn/wiki/F9S2wBS77i35bkkwJyVccDlfnyg
|
|
/// </summary>
|
|
public class RagQAEndToEndTests
|
|
{
|
|
private static async Task<RagDbContext> CreateDbAsync(string name)
|
|
{
|
|
var options = new DbContextOptionsBuilder<RagDbContext>()
|
|
.UseInMemoryDatabase($"rag-qa-{name}-{Guid.NewGuid():N}")
|
|
.Options;
|
|
await using var db = new RagDbContext(options);
|
|
return new RagDbContext(options);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Query_WithSources_ReturnsAnswerAndSources()
|
|
{
|
|
// Given: 已创建知识库
|
|
await using var db = await CreateDbAsync("with-sources");
|
|
var kb = new KnowledgeBaseEntity
|
|
{
|
|
Name = "测试知识库",
|
|
RetrievalMode = RetrievalModeEnum.Hybrid,
|
|
RetrievalTopK = 5,
|
|
ContextTopK = 3,
|
|
SimilarityThreshold = 0.3,
|
|
VectorWeight = 0.7,
|
|
CreatedBy = "test",
|
|
UpdatedBy = "test",
|
|
OperatorIP = "127.0.0.1"
|
|
};
|
|
db.KnowledgeBases.Add(kb);
|
|
|
|
var documentId = Guid.NewGuid();
|
|
var chunkId = Guid.NewGuid();
|
|
db.Documents.Add(new DocumentEntity
|
|
{
|
|
Id = documentId,
|
|
KnowledgeBaseId = kb.Id,
|
|
Title = "技术架构",
|
|
FileName = "arch.md",
|
|
FilePath = "test/arch.md",
|
|
FileSize = 100,
|
|
ContentType = "text/markdown",
|
|
Status = DocumentStatusEnum.Completed,
|
|
CreatedBy = "test",
|
|
UpdatedBy = "test",
|
|
OperatorIP = "127.0.0.1"
|
|
});
|
|
await db.SaveChangesAsync();
|
|
|
|
// Stub RetrievalService 返回预设 sources
|
|
var retrieval = new StubRagRetrievalService
|
|
{
|
|
Hits =
|
|
[
|
|
new RetrievalHit(
|
|
documentId,
|
|
"技术架构",
|
|
chunkId,
|
|
"BuildFlow 使用 PostgreSQL + pgvector",
|
|
0.87,
|
|
"BuildFlow 使用 PostgreSQL + pgvector")
|
|
]
|
|
};
|
|
var chatAgent = new StubAIChatAgent { ReplyText = "BuildFlow 使用 pgvector 向量数据库" };
|
|
|
|
var handler = new RagQueryCommandHandler(db, retrieval, chatAgent);
|
|
|
|
// When: 调用 RAG 查询
|
|
var result = await handler.Handle(
|
|
new RAGQueryCommand(kb.Id, "BuildFlow 用了什么向量数据库?"),
|
|
CancellationToken.None);
|
|
|
|
// Then: 返回 answer + sources
|
|
result.Answer.Should().NotBeNullOrEmpty("应有 LLM 响应");
|
|
result.Answer.Should().Be("BuildFlow 使用 pgvector 向量数据库");
|
|
result.Sources.Should().NotBeEmpty("sources 非空");
|
|
result.Sources[0].DocumentId.Should().Be(documentId, "DocumentId 匹配上传文档");
|
|
result.Sources[0].Similarity.Should().BeGreaterThan(0, "相似度>0");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Query_NoSources_ReturnsNotFoundMessage()
|
|
{
|
|
// Given: 知识库存在但 RetrievalService 返回空
|
|
await using var db = await CreateDbAsync("no-sources");
|
|
var kb = new KnowledgeBaseEntity
|
|
{
|
|
Name = "空知识库",
|
|
CreatedBy = "test",
|
|
UpdatedBy = "test",
|
|
OperatorIP = "127.0.0.1"
|
|
};
|
|
db.KnowledgeBases.Add(kb);
|
|
await db.SaveChangesAsync();
|
|
|
|
var retrieval = new StubRagRetrievalService { Hits = [] };
|
|
var chatAgent = new StubAIChatAgent();
|
|
|
|
var handler = new RagQueryCommandHandler(db, retrieval, chatAgent);
|
|
|
|
// When: 查询无结果
|
|
var result = await handler.Handle(
|
|
new RAGQueryCommand(kb.Id, "不存在的内容"),
|
|
CancellationToken.None);
|
|
|
|
// Then: 返回未找到提示,sources 空
|
|
result.Sources.Should().BeEmpty("无召回结果");
|
|
result.Answer.Should().NotBeNull("应返回提示文本");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(RetrievalModeEnum.Vector)]
|
|
[InlineData(RetrievalModeEnum.Keyword)]
|
|
[InlineData(RetrievalModeEnum.Hybrid)]
|
|
public async Task Query_WorksAcrossRetrievalModes(RetrievalModeEnum mode)
|
|
{
|
|
// Given: 不同 RetrievalModeEnum 的知识库(Stub 不依赖真实召回算法)
|
|
await using var db = await CreateDbAsync($"mode-{mode}");
|
|
var kb = new KnowledgeBaseEntity
|
|
{
|
|
Name = $"模式测试-{mode}",
|
|
RetrievalMode = mode,
|
|
CreatedBy = "test",
|
|
UpdatedBy = "test",
|
|
OperatorIP = "127.0.0.1"
|
|
};
|
|
db.KnowledgeBases.Add(kb);
|
|
await db.SaveChangesAsync();
|
|
|
|
var documentId = Guid.NewGuid();
|
|
var retrieval = new StubRagRetrievalService
|
|
{
|
|
Hits = [new RetrievalHit(documentId, "doc", Guid.NewGuid(), "内容", 0.9, "内容")]
|
|
};
|
|
var chatAgent = new StubAIChatAgent { ReplyText = "回答" };
|
|
var handler = new RagQueryCommandHandler(db, retrieval, chatAgent);
|
|
|
|
// When
|
|
var result = await handler.Handle(new RAGQueryCommand(kb.Id, "问题"), CancellationToken.None);
|
|
|
|
// Then: 三种模式都能返回结果(Stub 绕过真实算法差异)
|
|
result.Sources.Should().HaveCount(1);
|
|
result.Answer.Should().Be("回答");
|
|
}
|
|
}
|