- 在JWT中添加permver权限版本号claim,用于实时权限变更检测 - 实现PermissionVersionMiddleware中间件,在认证后授权前校验权限版本 - 修改AssignPermissionsCommandHandler,角色权限变更时自增相关用户权限版本号 - 修改AssignRolesCommandHandler,用户角色变更时自增权限版本号强制重登录 - 集成Redis存储权限版本镜像,支持分布式环境下的版本同步 - 添加PermissionVersion字段到User实体,用于存储权限版本号 - 实现分布式登录失败计数与账号锁定功能,增强安全防护 - 配置文件中新增AccessTokenMinutes和RefreshTokenDays可配置项 - 更新CORS策略配置,添加127.0.0.1本地访问支持 - 完善异常处理,统一使用ErrorCode枚举进行错误码管理 - 添加SOP测试单元设计文档,规范端到端测试流程
18 KiB
SOP 测试单元工厂设计 · TDD + SDD 三驱动融合,推送飞书
- 状态:已批准(Approved)
- 日期:2026-07-07
- 作者:brainstorming session
- 影响范围:全 6 后端模块(POC 阶段聚焦
rag);产出物推送至飞书知识库7659208807054117857的"产品"目录
一、目标与背景
把 BuildFlow"现在的功能"拆解为可执行、可追踪、可复制的最小 SOP 测试单元,通过三位一体的工作流落地到飞书(文档 + 任务联动),实现端到端场景的标准化验证。
三驱动定义(与用户对齐)
| 驱动 | 全称 | 在本项目中的具体含义 |
|---|---|---|
| TDD-文档 | Technical Design Document | 技术设计文档:架构、接口契约、数据模型、时序图、边界条件 |
| TDD-测试 | Test-Driven Development | 测试驱动开发:先写 xUnit 测试(红)→ 实现(绿)→ 重构 |
| SDD | Skill-Driven Development | 技能驱动开发:复用项目已有的 .agents/skills/rag-coding-api 编码规范(MediatR CQRS、测试规范、模块边界) |
现状(已通过代码探索确认)
| 维度 | 现状 | 与本设计的关系 |
|---|---|---|
| 测试体系 | 8 个 xUnit 测试项目,1472 源文件 vs 92 测试文件,FluentAssertions + EF InMemory | 成熟,但全部是单元/Handler 级,无端到端 SOP 测试 |
| RAG 测试 | BuildFlow.Rag.Tests 仅 7 个文件,覆盖 sanitizer/parser/catalog/validator |
关键缺口:无 ProcessDocumentCommandHandler、SendMessageCommandHandler、RagQueryCommandHandler、RAGStreamEndpoint 的集成测试 |
| 文档 | 仅 scripting/x6-workflow/CI-CD 三份设计文档,散落各子工程 | 无统一 PRD/SDD/SOP 体系 |
| 技能规范 | .agents/skills/rag-coding-api(27KB)已强制 MediatR CQRS、禁 service bridge、测试规范 |
SDD 的载体已就绪,本设计直接复用 |
| 飞书 | 已有知识库 7659208807054117857,含"服务器配置""项目开发指南"两个根节点 |
本设计在其下新建"产品"目录承载 SOP 产出 |
二、核心决策(已与用户确认)
| # | 决策点 | 选择 | 理由 |
|---|---|---|---|
| 1 | 功能范围 | 全部 6 后端模块 | rag/file/im/platform/workflow/scripting 全覆盖 |
| 2 | 推进策略 | POC 先行:RAG 跑通再复制 | 风险低、模式可复制、工作量可控 |
| 3 | 测试单元粒度 | 端到端场景 | 用户视角完整流程(非原子用例) |
| 4 | 飞书产出形式 | 飞书文档 + 飞书任务联动 | 文档沉淀设计,任务追踪执行 |
| 5 | RAG 场景 | A=知识库 RAG 问答全链路 + B=多轮对话流式问答 | 互补覆盖 RAG 两条主干(检索 vs 对话) |
| 6 | pgvector 隔离 | Stub 先行 + Testcontainers 集成测试补充 | POC 用 Stub 快速跑通,后续补真实召回验证 |
| 7 | 实现顺序 | 文档先行 → 测试后补 | 文档引导实现,飞书文档作为单一事实来源 |
| 8 | 模板化 | 8 段式文档骨架 + 5 任务结构 | Phase 2 可填模板复制到其余 5 模块 |
三、SOP 测试单元工厂架构
3.1 生产流水线
[SDD 规范] [TDD 设计] [TDD 实现]
rag-coding-api skill → 技术设计文档 → xUnit 测试先行
(MediatR CQRS / (架构/接口/数据模型/ (红色测试 →
测试规范) 时序/边界) 绿色实现 → 重构)
│ │ │
└──────────────────────┴───────────────────────┘
│
▼
┌─────────────────────────┐
│ 最小 SOP 测试单元 │
│ (端到端场景) │
└────────────┬────────────┘
│
┌─────────────┴─────────────┐
▼ ▼
┌─────────────────┐ ┌────────────────────┐
│ 飞书文档 │ │ 飞书任务 │
│ (SOP + 设计) │◄──────►│ (检查项可勾选) │
│ 评审/沉淀 │ deep │ 执行/追踪 │
│ │ link │ │
└─────────────────┘ └────────────────────┘
3.2 飞书知识库目录结构
📚 Wiki 空间 7659208807054117857
├── 📄 服务器配置 (已存在)
├── 📄 项目开发指南 (已存在)
└── 📁 产品/ ← 已创建 node_token=StjYwzzsSisql1ki2oqc5KFEnZd
├── 📄 产品概览 - BuildFlow 模块地图
└── 📁 SOP 测试单元/
├── 📁 RAG 模块/
│ ├── 📄 [SOP-RAG-01] 知识库 RAG 问答全链路
│ └── 📄 [SOP-RAG-02] 多轮对话流式问答
├── 📁 File 模块/ (Phase 2)
├── 📁 IM 模块/ (Phase 2)
├── 📁 Platform 模块/ (Phase 2)
├── 📁 Workflow 模块/ (Phase 2)
└── 📁 Scripting 模块/ (Phase 2)
四、SOP 测试单元标准模板
每个 SOP 测试单元(无论 RAG 还是后续模块)都遵循同一个 8 段式文档骨架 + 5 任务结构。这是 Phase 2 能快速复制的关键。
4.1 飞书文档骨架(8 段式)
# [SOP-XX] 场景名称
## 模块: rag | 场景类型: 端到端 | 状态: 草稿/评审/通过
## 1. 用户故事与价值
作为...我希望...以便...
## 2. 前置条件 (SDD 规范约束)
- 遵守 rag-coding-api skill: MediatR CQRS / 测试规范
- 依赖的外部服务及隔离方式 (Stub/Testcontainer)
## 3. 技术设计 (TDD 文档部分)
3.1 架构图 (mermaid 时序图)
3.2 涉及的 Command/Query/Handler 清单
3.3 数据模型变更 (实体/字段)
3.4 关键接口契约 (输入/输出)
## 4. SOP 执行步骤 (端到端)
Step 1...N: 每步含 API 调用 / Handler / 预期结果
## 5. 验收标准 (Definition of Done)
☐ 可勾选的检查项列表
## 6. 测试设计 (TDD 测试部分)
6.1 测试文件位置与命名
6.2 Stub/Fixture 设计
6.3 测试用例清单 (Given/When/Then)
## 7. 执行结果记录
- 测试运行命令与输出
- 覆盖率
- 已知限制
## 8. 变更日志
4.2 飞书任务结构(每个 SOP 单元 = 1 父任务 + 5 子任务)
父任务: [SOP-XX] 场景名称 - 端到端验证
├── ☐ T1: 编写 TDD 技术设计文档 (description 含飞书文档 deep link)
├── ☐ T2: 编写 xUnit 测试用例 (红色阶段)
├── ☐ T3: 实现/对齐 Handler 逻辑 (绿色阶段)
├── ☐ T4: 跑通端到端测试,记录结果 (链接到文档§7)
└── ☐ T5: 评审 + 标记场景状态为「通过」
任务描述嵌入飞书文档 deep link,实现文档 ↔ 任务双向联动。
五、RAG 模块 POC 场景详细设计
5.1 场景 A:[SOP-RAG-01] 知识库 RAG 问答全链路
用户故事:作为知识工作者,我上传一份文档到知识库,提问后能获得基于该文档的准确回答和引用来源。
覆盖链路:建 KB → 上传文档 → 触发处理流水线(提取/分块/向量化/入库)→ RAG 检索问答。
SOP 步骤(端到端):
| Step | API 调用 | MediatR | 预期结果 |
|---|---|---|---|
| 1 | POST /knowledge-bases |
CreateKnowledgeBaseCommand | KB 创建,配 RetrievalMode=Hybrid/TopK=5/阈值=0.7 |
| 2 | POST /knowledge-bases/{id}/documents (multipart) |
UploadDocumentCommand | Document 落库 Status=Pending |
| 3 | POST /documents/{id}/process |
ProcessDocumentCommand | 4 阶段流水线:提取→分块→向量化→入库,Status=Processed |
| 4 | POST /rag/query |
RagQueryCommandHandler | 返回 {answer, sources[]},sources 非空 |
涉及 Handler:CreateKnowledgeBaseCommandHandler、UploadDocumentCommandHandler、ProcessDocumentCommandHandler、RagQueryCommandHandler、IRagRetrievalService、IAIChatAgent。
验收标准(DoD):
- ☐ 文档 Status 从 Pending → Processing → Processed
- ☐ DocumentChunk 数量 = 预期分块数,Content 非空
- ☐ /rag/query 返回 sources[] 非空且 DocumentId 匹配
- ☐ answer 包含 LLM 响应文本
- ☐ 全程无未捕获异常
测试隔离策略:
| 外部依赖 | 处理方式 |
|---|---|
| pgvector | POC:Stub IRagRetrievalService 返回预设 sources;补充:Testcontainers + 真实 PG+pgvector 集成测试 |
| LM Studio | Stub IAIChatAgent(RunAsync 返回固定文本)+ Stub IEmbeddingService(返回固定 768 维向量) |
| 对象存储 | Stub IFileStorageClient(复用 StubFileStorageClient 模式) |
| 文档提取 | Stub IDocumentExtractorFactory + IDocumentExtractor(返回固定文本) |
测试文件:tests/BuildFlow.Rag.Tests/RagQAEndToEndTests.cs
5.1.1 分块策略详解(ChunkingMode 6 种模式)
分块在 ProcessDocumentCommand 阶段 2,由 ITextChunker.Chunk(extracted, mode, size, overlap, separator) 完成(实现 Infrastructure/AI/TextChunker.cs,设计参考 Dify):
| 模式 | 值 | 切分逻辑 |
|---|---|---|
| General | 0(默认) | 段落感知:先按 \n\n 切段,超长段按句子标点二切,overlap 取最后 1-2 句 |
| Heading | 1 | 按 Markdown H1-H6 切,维护 headingStack,生成 HeadingPath 如 "第三章 > 3.1 概述" |
| ParentChild | 2 | 父块=整段(上下文,最大 chunkSize),子块=段落再切(chunkSize/3,精确匹配),父子都向量化 |
| Separator | 3 | 按自定义分隔符(默认 \n\n---\n\n)切分,超长用 General 二切 |
| ByPage | 4 | PDF 每页/PPT 每张一块,页过长用 General 二切,子块共享 PageNumber |
| Token | 5 | 按 token 估算:charBudget = chunkSize × 1.5(注释称"中文=2/英文=1"但实际用固定 1.5 系数,存在实现差异) |
ParentChild 回填机制:先 SaveChanges 拿父块 Guid → 用 parentIdMap[Index→Guid] 反查 → 写入子块 ParentChunkId(两轮 SaveChanges)。
参数优先级:模式 doc.ChunkingModeOverride ?? kb.ChunkingMode(文档级覆盖优先);ChunkSize/ChunkOverlap/Separator 全部来自 kb,默认 500/50/null。
5.1.2 向量化策略详解
- 接口:
IEmbeddingService.EmbedAsync(单条)+EmbedBatchAsync(批量),实现Infrastructure/AI/EmbeddingService.cs,OpenAI 兼容协议 - 批处理:
EmbeddingBatchSize = 64,≤64 一次调用,否则按 64 步长切片按原顺序拼接(顺序必须与输入一致) - 向量维度 = 768(非 384/1536):默认模型
embeddinggemma-300m输出 768 维,DB 列vector(768),换模型必须同步改迁移 - 写入方式:EF 忽略 Embedding 列(
DocumentChunkConfiguration.Ignore),逐块 raw SQLUPDATE document_chunks SET embedding='[...]'::vector WHERE "Id"=...(N 条 SQL,无批量优化) - TokenCount 粗估:
Content.Length / 4,非真实 tokenizer
5.1.3 召回与重排算法详解
IRagRetrievalService.RetrieveAsync(kb, question) → List<RetrievalHit>,4 步流水线:
- 粗召回
RecallAsync:SQLLIMIT RetrievalTopK(默认 20) - 阈值过滤:
VectorScore >= SimilarityThreshold(默认 0.3),纯 Keyword 跳过 - 内存重排:按 mode 计算 finalScore,
Take(ContextTopK)(默认 5) - 父块回填
EnrichWithContextAsync(当前空壳,见差异清单)
Vector 召回 SQL(pgvector 余弦距离):
SELECT ..., 1 - (dc.embedding <=> '{q}'::vector) AS similarity
FROM document_chunks dc JOIN documents d ON ...
WHERE d."KnowledgeBaseId" = '{kbId}' AND dc.embedding IS NOT NULL
AND dc."ParentChunkId" IS NULL -- 只召回父块/独立块
ORDER BY dc.embedding <=> '{q}'::vector LIMIT 20
Keyword 召回:ExtractKeywords 分词(英文 [a-zA-Z]+≥2,中文 ≤4 整体/>4 做 2-4 字 N-gram),每个关键词 CASE WHEN Content ILIKE @kwN THEN 1 ELSE 0 相加得命中数。
Hybrid 重排加权:finalScore = VectorScore × VectorWeight + keywordScore × (1-VectorWeight),默认 VectorWeight=0.7(70% 向量 + 30% 关键词)。
5.1.4 设计意图 vs 当前实现差异清单(技术债务)
代码探索发现 4 处差异,POC 用 Stub 会绕过,但 Testcontainers 集成测试必须覆盖:
| # | 差异点 | 设计意图 | 当前实现 |
|---|---|---|---|
| 1 | RerankStrategy 未读取 | 枚举 None/Keyword/LLM 可切换重排 | 重排分支依据是 RetrievalMode,该字段从未被读取,LLM 重排未实现 |
| 2 | Hybrid 非双路融合 | 向量召回 + 关键词召回融合(RRF/加权) | Hybrid 只执行向量召回 SQL,关键词仅在内存重排加权参与,未做关键词路独立召回 |
| 3 | 父块回填空壳 | 召回子块→回填父块获完整上下文 | EnrichWithContextAsync 无 DB 查询,ContextContent=Content;SQL ParentChunkId IS NULL 召回的是父块而非子块 |
| 4 | 表名不一致(潜在 bug) | EF 表 b_document_chunks |
raw SQL 写 document_chunks(无 b_ 前缀),可能导致向量写入失败 |
5.2 场景 B:[SOP-RAG-02] 多轮对话流式问答
用户故事:作为用户,我与 AI 进行多轮对话,AI 能记住上下文并以流式方式逐字返回。
覆盖链路:建会话 → 流式提问 → 多轮历史拼装 → SSE 协议 → 缓存一致性。
SOP 步骤(端到端):
| Step | API 调用 | 处理路径 | 预期结果 |
|---|---|---|---|
| 1 | POST /chat/conversations |
CreateConversationCommand | 会话创建(不绑 KB) |
| 2 | POST /chat/conversations/{id}/stream |
StreamMessageEndpoint(直连) | SSE token 流,event:done 返回 messageId |
| 3 | POST /chat/conversations/{id}/stream(第二轮) |
同上 | 历史拼装含第一轮 user+assistant |
| 4 | 校验 | IChatMessageCache ↔ DB | cache 与 DB ChatMessage 一致 |
涉及 Handler/Endpoint:CreateConversationCommandHandler、StreamMessageEndpoint、IAIChatAgent.RunStreamingAsync、IChatMessageCache、AiChatOutputSanitizer。
验收标准(DoD):
- ☐ 两轮 SSE 均以 event:done 结束并返回 messageId
- ☐ 第二轮请求时,历史拼装包含第一轮的 user+assistant 消息
- ☐ AiChatOutputSanitizer 生效(去开场白/think 标签)
- ☐ Redis cache 与 DB 消息记录一致
- ☐ 会话可删除(DELETE 返回成功)
测试隔离策略:
| 外部依赖 | 处理方式 |
|---|---|
| LM Studio | Stub IAIChatAgent.RunStreamingAsync(yield 固定 token 序列) |
| Redis | Stub IChatMessageCache(内存字典模拟)或用真实 Redis |
测试文件:tests/BuildFlow.Rag.Tests/ChatStreamEndToEndTests.cs
六、Stub/Fixture 设计(共享测试基础设施)
复用并扩展现有 PreviewDocumentChunksQueryHandlerTests 的 Stub 模式,新增以下共享测试基础设施(放 tests/BuildFlow.Rag.Tests/Stub/):
| Stub/Fixture | 实现接口 | 用途 |
|---|---|---|
StubFileStorageClient |
IFileStorageClient |
提供文件流(内存 Stream),不调真实对象存储 |
StubDocumentExtractor |
IDocumentExtractor |
返回固定提取文本 |
StubDocumentExtractorFactory |
IDocumentExtractorFactory |
按扩展名分发 Stub |
StubEmbeddingService |
IEmbeddingService |
返回固定维度向量(如 384 维) |
StubAIChatAgent |
IAIChatAgent |
RunAsync 返回固定文本;RunStreamingAsync yield 固定 token |
StubRagRetrievalService |
IRagRetrievalService |
返回预设 sources 列表(避开 pgvector) |
StubChatMessageCache |
IChatMessageCache |
内存字典模拟 Redis cache |
测试 Host 搭建模式:参考 BuildFlow.IM.Integration.Tests 的 ImServerFixture/ImServerCollection 模式,搭建 RagEndToEndFixture(注册 Stub 到 DI 容器 + EF InMemory + WebApplicationFactory)。
七、Phase 划分与里程碑
Phase 1:RAG POC(本次实现)
| 里程碑 | 交付物 | 验收 |
|---|---|---|
| M1: 飞书落地 | 产品目录 + 产品概览 + RAG 2 份 SOP 文档 + 对应飞书任务 | 文档可访问,任务可勾选 |
| M2: 测试实现 | 2 个 E2E 测试文件 + 7 个 Stub 类 + RagEndToEndFixture | dotnet test 全绿 |
| M3: POC 验收 | 测试输出回填飞书文档§7,场景状态标记「通过」 | 模板固化,可复制 |
Phase 2:复制到其余 5 模块(后续)
按 file → im → platform → workflow → scripting 顺序,每模块选 1-2 个端到端场景,套用 Phase 1 固化的模板。
八、风险与缓解
| 风险 | 缓解 |
|---|---|
| pgvector 无法 InMemory 测试 | POC 用 Stub RetrievalService;补充 Testcontainers 集成测试(需 Docker) |
| SSE 流式测试断言复杂 | 用 HttpContent.ReadAsStreamAsync 逐行解析 SSE,参考现有 OpenAiCompatibleChatStreamParserTests |
| StreamMessageEndpoint 直连不经 MediatR | 测试用 WebApplicationFactory 走真实 HTTP,而非直接调 Endpoint |
| Phase 2 模块差异大 | 8 段式模板的可变部分集中在§3 技术设计,其余段落复用 |
九、未来扩展(YAGNI - 本次不做)
- Web 前端端到端 SOP(Playwright,现有体系已支持)
- Portal/Client 端 SOP
- 飞书任务自动同步测试结果(CI/CD webhook)
- SOP 执行自动化(CLI 一键跑全部场景)
附录 A:关键文件索引
| 路径 | 作用 |
|---|---|
build-flow.api/src/modules/rag/Application/ |
Command/Query/Handler 全清单 |
build-flow.api/src/modules/rag/Domain/Entities/ |
KnowledgeBase/Document/DocumentChunk/Conversation/ChatMessage |
build-flow.api/src/modules/rag/Infrastructure/AI/RagRetrievalService.cs |
向量检索核心 |
build-flow.api/src/BuildFlow.Host/Endpoints/Rag/ |
HTTP 路由 |
build-flow.api/tests/BuildFlow.Rag.Tests/ |
现有测试 + 本次新增 E2E |
.agents/skills/rag-coding-api/SKILL.md |
SDD 规范来源 |
飞书 Wiki 7659208807054117857 / 产品节点 StjYwzzsSisql1ki2oqc5KFEnZd |
SOP 产出落点 |