本章目标
前面 17 章,我们已经把 KnowHub 的核心功能、用户端和管理端都讲完了。
到这里,一个常见问题会出现:
text
项目功能已经写完了,是不是就可以收工?
不能。
对一个工程项目来说,"写完代码"和"系统可信"不是一回事。
尤其是 RAG 平台,它不是一个简单的增删改查系统。它的链路很长:
text
注册登录
-> Gateway 鉴权
-> 用户隔离
-> 知识库管理
-> 文档上传
-> 文件存储
-> 文档解析
-> 文本切片
-> 索引任务
-> Embedding
-> pgvector 向量写入
-> 向量检索
-> Prompt 构造
-> 大模型调用
-> Sentinel 限流
-> AI 降级
-> qa_log 记录
-> 用户端和管理端展示
只要其中一个环节出问题,用户看到的就可能是:
- 登录失败。
- 上传失败。
- 索引一直不成功。
- 检索不到内容。
- 问答没有引用。
- 模型超时。
- 管理端看不到数据。
所以第 18 章要解决的问题是:
text
如何证明当前系统的主链路可用、异常场景可控、基础性能有认知?
本章会讲三件事:
- 接口回归。
- 异常演练。
- 轻量压测。
这三件事听起来像测试岗位的内容,但后端开发必须懂。因为企业项目里,开发不是只写代码,还要能证明自己的代码在当前版本下是可靠的。
本章内容基于 KnowHub 当前代码,以及第 13 周已经整理过的接口回归、异常演练和轻量压测归档。需要提前说明:这里讲的是学习项目和校招展示级别的验证,不是生产级测试体系,也不是高并发压测报告。
18.1 为什么开发完成后还要做验证
很多初学者做项目时,会停在"我点了一下页面,能跑"这个层面。
但企业里不是这样。
企业系统要回答三个更严格的问题:
- 正常路径还能不能跑?
- 错误路径会不会失控?
- 接口耗时大概在什么范围?
这三个问题分别对应:
text
接口回归
异常演练
轻量压测
比如你改了 Gateway 鉴权逻辑,注册和登录可能没问题,但知识库接口可能突然收不到 X-User-Id。
你改了文档索引逻辑,上传接口可能返回成功,但任务可能一直卡在 RUNNING。
你改了问答降级逻辑,正常问答可能能返回,但模型超时时可能直接抛 500。
这就是为什么需要系统化验证。
验证不是为了形式,而是为了把"我感觉能跑"变成"我有证据证明能跑"。
18.2 先分清三个概念
本章标题里有三个词:接口回归、异常演练、轻量压测。
它们不是一回事。
接口回归
接口回归的目标是证明:
text
以前能正常工作的核心接口,在当前版本下仍然能正常工作。
它关注的是正常流程。
比如:
- 注册能成功。
- 登录能拿到 Token。
/auth/me能识别当前用户。- 创建知识库能成功。
- 上传文档能生成索引任务。
- 索引成功后能查到切片。
- 向量检索能返回结果。
- RAG 问答能返回答案和引用。
异常演练
异常演练的目标是证明:
text
错误请求不会把系统打崩,也不会返回一堆看不懂的 500 堆栈。
它关注的是错误流程。
比如:
- 不带 Token 会怎样?
- 伪造 Token 会怎样?
- 普通用户访问管理员接口会怎样?
- userA 访问 userB 的知识库会怎样?
- 上传 exe 文件会怎样?
- 空问题检索会怎样?
- AI 模型超时会怎样?
轻量压测
轻量压测的目标是建立基础性能认知。
它不是生产压测。
它只是回答:
text
在本机小规模请求下,这几个关键接口大概多慢?
比如第 13 周归档中记录过:
text
/auth/me:5 次成功,平均 21.8 ms
/kb/list:5 次成功,平均 21.4 ms
/kb/13/search:5 次成功,平均 602.2 ms
/kb/13/chat:5 次成功,平均 1754 ms
这个结果不能说明系统支持高并发,但能说明一个基本事实:
text
认证和列表接口很快,向量检索明显更慢,RAG 问答最慢。
这就是轻量压测的价值。
18.2.4 回归测试概要清单
下面这张表可以当作本章接口回归的"菜单",也可以当作项目验收时的"打勾清单"。
| 测试编号 | 测试场景 | 对应接口 | 通过标准 | 关联章节 |
|---|---|---|---|---|
| AUTH-01 | 注册新用户成功 | POST /auth/register |
code=0,data 包含 userId 和 username |
第 4 章 |
| AUTH-02 | 重复注册应失败 | POST /auth/register |
code!=0,返回可理解的重复用户提示 |
第 4 章 |
| AUTH-03 | 正确密码登录成功 | POST /auth/login |
code=0,data 包含 token,tokenType=Bearer |
第 4 章 |
| AUTH-04 | 错误密码登录失败 | POST /auth/login |
code!=0,不会返回 Token |
第 4 章 |
| AUTH-05 | /auth/me 识别当前用户 |
GET /auth/me |
code=0,data 包含当前用户信息 |
第 4 章 |
| ISO-01 | userA 创建知识库 kbA 成功 | POST /kb |
code=0,返回 kbA 的 ID |
第 5、6 章 |
| ISO-02 | userA 访问 kbA 成功 | GET /kb/{kbA_id} |
返回知识库详情,userId 为 userA |
第 5、6 章 |
| ISO-03 | userA 访问 userB 的 kbB 失败 | GET /kb/{kbB_id} |
返回 40400 或资源不存在 |
第 5 章 |
| ISO-04 | 伪造 userId 参数不能改变返回数据 | GET /kb/list?userId={userB_id} |
返回的仍是 userA 自己的知识库 | 第 5 章 |
| KB-01 | 创建知识库成功 | POST /kb |
code=0,返回知识库 ID |
第 6 章 |
| KB-02 | 查询列表只含自己知识库 | GET /kb/list |
列表中不出现其他用户知识库 | 第 6 章 |
| KB-03 | 查询详情正确 | GET /kb/{kbId} |
kbId、userId、name 与创建结果一致 |
第 6 章 |
| KB-04 | 删除后不可访问 | DELETE /kb/{kbId}、GET /kb/{kbId} |
删除后详情接口返回资源不存在 | 第 6 章 |
| DOC-01 | 上传支持的 TXT/MD 文件成功 | POST /kb/{kbId}/documents |
code=0,返回 documentId |
第 7 章 |
| DOC-02 | 上传不支持文件类型返回错误 | POST /kb/{kbId}/documents |
code!=0,提示文件类型不支持 |
第 7 章 |
| DOC-03 | 文档上传后索引任务存在 | 查询 index_task 表或任务接口 |
有对应 documentId 的任务记录 |
第 9 章 |
| DOC-04 | 索引成功后文档状态正确 | 文档详情或数据库查询 | document_info.indexStatus=INDEXED,chunkCount>0 |
第 9、10、11 章 |
| DOC-05 | 查询切片返回正确列表 | GET /kb/{kbId}/documents/{documentId}/chunks |
返回 chunk 列表,内容和文档相关 | 第 8、11 章 |
| RAG-01 | 向量检索返回相关结果 | GET /kb/{kbId}/search |
results 数量大于 0,内容和问题相关 |
第 11 章 |
| RAG-02 | RAG 问答返回答案和引用 | POST /kb/{kbId}/chat |
answer 非空,references 数量大于 0 |
第 12 章 |
| RAG-03 | 无相关文档时返回无法确定 | POST /kb/{kbId}/chat |
返回"无法确定",modelCalled=false |
第 12 章 |
| ADMIN-01 | ADMIN 可查看全局用户 | GET /admin/users |
返回所有用户列表 | 第 17 章 |
| ADMIN-02 | ADMIN 可查看全局知识库、文档、任务、日志 | /admin/knowledge-bases 等 |
返回全局数据,不按当前用户隔离 | 第 17 章 |
| ADMIN-03 | 普通用户访问后台接口被拒绝 | GET /admin/users |
返回 403 或业务错误码 | 第 17 章 |
| ADMIN-04 | FAILED/TIMEOUT 任务可重试 | POST /admin/index-tasks/{taskId}/retry |
返回成功,并重新投递任务 | 第 9、17 章 |
| ADMIN-05 | SUCCESS 任务不能重试 | POST /admin/index-tasks/{taskId}/retry |
code!=0,提示状态不允许重试 |
第 9、17 章 |
| STAB-01 | Redis owner 缓存存在且 TTL 合理 | redis-cli GET/TTL rag:kb:owner:{kbId} |
key 存在,TTL 小于配置值且大于 0 | 第 13 章 |
| STAB-02 | Sentinel 高频请求触发限流 | POST /kb/{kbId}/chat |
高频请求下返回 42900 |
第 14 章 |
| STAB-03 | AI 模型超时或不可用时降级 | POST /kb/{kbId}/chat |
modelFallback=true,接口不抛 500 |
第 14、15 章 |
这张表的使用方式很简单:每次大改代码后,从 AUTH-01 开始逐项验证。全部通过,才能说当前版本完成了核心回归。
18.3 测试前置环境
做回归测试之前,先确认环境。
不要一上来就调用接口。
如果 MySQL 没启动,你测知识库接口没有意义。
如果 pgvector 没启动,你测向量检索没有意义。
如果模型配置不可用,你测 RAG 问答就要预期可能进入降级。
KnowHub 的基础依赖包括:
text
MySQL:3306
PostgreSQL/pgvector:5432
Redis:6379
RabbitMQ:5672,管理端 15672
MinIO:API 9002,Console 9001
Nacos:8848
后端服务端口包括:
text
Gateway:9000
auth-service:9101
knowledge-service:9102
task-service:9103
前端包括:
text
rag-user-web:默认 5173,端口被占用时 Vite 自动切换
rag-admin-web:Vite 本地端口,默认 host 为 127.0.0.1
这里要注意:一次回归不一定会把所有基础设施都测到最深。比如某一轮测试主要验证接口链路,RabbitMQ 和 MinIO 可以先作为依赖可用性检查;如果进入完整成书版联调,就要继续验证消息队列消费和对象存储读写。
测试前先确认 JDK 17:
powershell
$env:JAVA_HOME='D:\Program Files\Java\jdk-17.0.3.1'
$env:Path="$env:JAVA_HOME\bin;$env:Path"
java -version
再确认后端构建:
powershell
cd D:\rag\rag-platform
.\mvnw.cmd clean package -DskipTests
这里使用 -DskipTests 是因为当前章节重点是接口级验证,不是 Maven 单元测试。
18.4 健康检查和统一入口
KnowHub 的外部业务接口统一通过 Gateway 调用。
也就是说,测试时不要一会儿调 9101,一会儿调 9102,一会儿调 9103。
统一入口是:
text
http://localhost:9000
PowerShell 里可以先设置:
powershell
$base = "http://localhost:9000"
健康检查可以调用:
powershell
Invoke-RestMethod -Method Get -Uri "$base/auth-internal/health" |
ConvertTo-Json -Depth 10
Invoke-RestMethod -Method Get -Uri "$base/knowledge-internal/health" |
ConvertTo-Json -Depth 10
Invoke-RestMethod -Method Get -Uri "$base/task-internal/health" |
ConvertTo-Json -Depth 10
期望结果是三个服务都能返回正常健康信息。
这里有一个常见误区:
text
浏览器访问 http://localhost:9000/ 返回 404,是不是 Gateway 坏了?
不是。
Gateway 根路径没有页面,所以 404 是正常现象。你应该访问具体接口,而不是根路径。
18.4.4 回归测试判定标准说明
本章所有测试用例都遵循同一套判定标准。
HTTP 层面,正常请求应该返回 2xx;参数错误、鉴权失败、越权访问、限流等可预期异常应该返回 4xx 或统一业务错误结构;不应该出现未处理的 500 Internal Server Error。如果出现 500,要优先看全局异常处理、日志堆栈和服务内部异常。
业务层面,正常请求的响应体 code=0;异常请求 code!=0,并且 message 要包含可理解的业务描述。响应结构应该保持统一:
json
{
"code": 0,
"message": "success",
"data": {}
}
不应该返回裸字符串、HTML 错误页或 Java 异常堆栈。
字段层面,正常返回的关键字段不能为 null。例如登录响应里的 token,知识库详情里的 kbId 和 userId,问答响应里的 answer 和 references,都必须能支撑下一步测试继续执行。
18.4.5 非 Windows 环境的测试工具说明
本章脚本使用 Windows PowerShell 5.1 编写,适合大多数 Windows 学习环境。
如果你使用 macOS 或 Linux,不要求必须安装 PowerShell。可以用 curl 等价替换每个 Invoke-RestMethod 命令,测试逻辑完全相同:设置请求方法、URL、请求头、JSON Body,然后检查 HTTP 状态码和响应体 code 字段。
也可以使用 Postman、Apifox 这类 GUI 工具手动执行回归测试。做法是把 18.2.4 的测试清单逐条映射成请求,并在工具中保存环境变量,例如 baseUrl、tokenA、tokenB、kbId。本章关注的是"测什么"和"怎么判断测过了",不是强制使用某一种测试工具。
18.5 认证接口回归
认证是所有后续接口的入口。
如果认证没过,后面知识库、文档、问答都不应该继续测。
认证回归至少覆盖:
text
POST /auth/register
POST /auth/login
GET /auth/me
注册
注册要验证:
- 用户名合法。
- 密码合法。
- 返回用户 ID。
- 重复注册能返回业务错误。
登录
登录要验证:
- 正确用户名密码能返回 Token。
- 错误密码返回认证失败。
- 返回的
tokenType是 Bearer。 - Token 中携带用户身份和角色。
当前用户
/auth/me 是一个很关键的接口。
它证明两件事:
- 前端带上了 Token。
- Gateway 能解析 Token 并把用户信息透传给后端。
调用时需要请求头:
powershell
$headers = @{ Authorization = "Bearer $token" }
然后调用:
powershell
Invoke-RestMethod -Method Get -Uri "$base/auth/me" -Headers $headers |
ConvertTo-Json -Depth 10
如果这里失败,先不要测知识库。
因为后面的资源隔离全部依赖当前用户身份。
18.5.4 认证回归测试脚本
下面给出一个完整的 Windows PowerShell 5.1 脚本,用于验证注册、重复注册、登录、/auth/me 和错误密码五个认证场景。
powershell
$base = "http://localhost:9000"
$passCount = 0
$failCount = 0
function Pass($name) {
Write-Host "[PASS] $name" -ForegroundColor Green
$script:passCount++
}
function Fail($name, $message) {
Write-Host "[FAIL] $name - $message" -ForegroundColor Red
$script:failCount++
}
function To-JsonBody($obj) {
return ($obj | ConvertTo-Json -Depth 10)
}
$timestamp = Get-Date -Format "yyyyMMddHHmmss"
$username = "test_regression_user_$timestamp"
$password = "Pass123456"
# 步骤一:注册新用户,判定标准:code=0,data 中包含 userId 和 username。
try {
$registerBody = To-JsonBody @{ username = $username; password = $password; nickname = "回归测试用户" }
$registerResp = Invoke-RestMethod -Method Post -Uri "$base/auth/register" -ContentType "application/json" -Body $registerBody
if ($registerResp.code -eq 0 -and $registerResp.data.userId -and $registerResp.data.username -eq $username) {
Pass "注册新用户成功"
} else {
Fail "注册新用户成功" ($registerResp | ConvertTo-Json -Depth 10)
}
} catch {
Fail "注册新用户成功" $_.Exception.Message
}
# 步骤二:重复注册,判定标准:code != 0。
try {
$duplicateResp = Invoke-RestMethod -Method Post -Uri "$base/auth/register" -ContentType "application/json" -Body $registerBody
if ($duplicateResp.code -ne 0) {
Pass "重复注册被拒绝"
} else {
Fail "重复注册被拒绝" "重复注册返回了 code=0"
}
} catch {
Pass "重复注册被拒绝"
}
# 步骤三:正确密码登录,判定标准:code=0,data 中包含 token,tokenType=Bearer。
$token = $null
try {
$loginBody = To-JsonBody @{ username = $username; password = $password }
$loginResp = Invoke-RestMethod -Method Post -Uri "$base/auth/login" -ContentType "application/json" -Body $loginBody
if ($loginResp.code -eq 0 -and $loginResp.data.token -and $loginResp.data.tokenType -eq "Bearer") {
$token = $loginResp.data.token
Pass "正确密码登录成功"
} else {
Fail "正确密码登录成功" ($loginResp | ConvertTo-Json -Depth 10)
}
} catch {
Fail "正确密码登录成功" $_.Exception.Message
}
# 步骤四:携带 Token 调用 /auth/me,判定标准:返回当前用户信息。
try {
$headers = @{ Authorization = "Bearer $token" }
$meResp = Invoke-RestMethod -Method Get -Uri "$base/auth/me" -Headers $headers
if ($meResp.code -eq 0 -and $meResp.data.username -eq $username) {
Pass "/auth/me 识别当前用户"
} else {
Fail "/auth/me 识别当前用户" ($meResp | ConvertTo-Json -Depth 10)
}
} catch {
Fail "/auth/me 识别当前用户" $_.Exception.Message
}
# 步骤五:错误密码登录,判定标准:code != 0,不能返回 token。
try {
$badLoginBody = To-JsonBody @{ username = $username; password = "wrong-password" }
$badLoginResp = Invoke-RestMethod -Method Post -Uri "$base/auth/login" -ContentType "application/json" -Body $badLoginBody
if ($badLoginResp.code -ne 0 -and -not $badLoginResp.data.token) {
Pass "错误密码登录失败"
} else {
Fail "错误密码登录失败" "错误密码登录返回了成功结果"
}
} catch {
Pass "错误密码登录失败"
}
Write-Host ""
Write-Host "认证回归:$passCount/5 通过,失败 $failCount 项"
如果第一步注册失败,后续登录和 /auth/me 都会受到影响。实际排查时要从第一个失败点开始看,不要只看最后汇总。
18.6 用户隔离回归
用户隔离是 KnowHub 项目的重点能力之一。
测试时至少准备两个用户:
text
userA
userB
测试目标是:
text
userA 只能访问 userA 知识库,不能访问 userB 知识库。
典型步骤是:
- 注册并登录 userA。
- 注册并登录 userB。
- userA 创建 kbA。
- userB 创建 kbB。
- userA 访问 kbA 成功。
- userA 访问 kbB 失败。
跨用户访问失败时,项目采用 40400 这类资源不存在或业务失败结果。
为什么不是直接返回"你无权访问 userB 的知识库"?
因为那样会泄露资源存在性。
更安全的做法是:
text
对当前用户来说,这个资源就像不存在。
伪造 userId 测试
用户隔离还要验证一个细节:
text
前端传入的 userId 不能改变当前用户身份。
比如 userA 调用:
text
/kb/list?userId=999999
系统仍然应该返回 userA 自己的知识库,而不是 userId=999999 的知识库。
这证明 knowledge-service 使用的是 Gateway 透传的当前用户,而不是信任前端参数。
18.6.5 用户隔离测试脚本
下面脚本用于验证两个用户之间的知识库隔离。
powershell
$base = "http://localhost:9000"
$passCount = 0
$failCount = 0
function Pass($name) { Write-Host "[PASS] $name" -ForegroundColor Green; $script:passCount++ }
function Fail($name, $message) { Write-Host "[FAIL] $name - $message" -ForegroundColor Red; $script:failCount++ }
function Json($obj) { return ($obj | ConvertTo-Json -Depth 10) }
function RegisterAndLogin($username, $password) {
$registerBody = Json @{ username = $username; password = $password; nickname = $username }
try {
Invoke-RestMethod -Method Post -Uri "$base/auth/register" -ContentType "application/json" -Body $registerBody | Out-Null
} catch {
# 如果用户已存在,可以继续登录;本脚本关注隔离逻辑。
}
$loginBody = Json @{ username = $username; password = $password }
$loginResp = Invoke-RestMethod -Method Post -Uri "$base/auth/login" -ContentType "application/json" -Body $loginBody
return @{
Token = $loginResp.data.token
UserId = $loginResp.data.userId
}
}
$timestamp = Get-Date -Format "yyyyMMddHHmmss"
$userA = RegisterAndLogin "userA_$timestamp" "Pass123456"
$userB = RegisterAndLogin "userB_$timestamp" "Pass123456"
$headersA = @{ Authorization = "Bearer $($userA.Token)" }
$headersB = @{ Authorization = "Bearer $($userB.Token)" }
try {
$kbABody = Json @{ name = "userA知识库"; description = "隔离测试 A" }
$kbAResp = Invoke-RestMethod -Method Post -Uri "$base/kb" -Headers $headersA -ContentType "application/json" -Body $kbABody
$kbAId = $kbAResp.data.kbId
$kbBBody = Json @{ name = "userB知识库"; description = "隔离测试 B" }
$kbBResp = Invoke-RestMethod -Method Post -Uri "$base/kb" -Headers $headersB -ContentType "application/json" -Body $kbBBody
$kbBId = $kbBResp.data.kbId
if ($kbAId -and $kbBId) { Pass "userA 和 userB 分别创建知识库" } else { Fail "创建知识库" "未返回 kbId" }
} catch {
Fail "创建知识库" $_.Exception.Message
}
try {
$ownResp = Invoke-RestMethod -Method Get -Uri "$base/kb/$kbAId" -Headers $headersA
if ($ownResp.code -eq 0 -and $ownResp.data.kbId -eq $kbAId) { Pass "userA 访问 kbA 成功" } else { Fail "userA 访问 kbA" "返回数据不匹配" }
} catch {
Fail "userA 访问 kbA" $_.Exception.Message
}
try {
$crossResp = Invoke-RestMethod -Method Get -Uri "$base/kb/$kbBId" -Headers $headersA
if ($crossResp.code -eq 40400 -or $crossResp.message -like "*不存在*") { Pass "userA 访问 kbB 被拒绝" } else { Fail "userA 访问 kbB" "跨用户访问未被拒绝" }
} catch {
Pass "userA 访问 kbB 被拒绝"
}
try {
$listResp = Invoke-RestMethod -Method Get -Uri "$base/kb/list?userId=$($userB.UserId)" -Headers $headersA
$hasUserBData = $false
foreach ($kb in $listResp.data) {
if ($kb.userId -eq $userB.UserId) { $hasUserBData = $true }
}
if (-not $hasUserBData) { Pass "伪造 userId 参数不能越权" } else { Fail "伪造 userId 参数不能越权" "返回了 userB 的知识库" }
} catch {
Fail "伪造 userId 参数不能越权" $_.Exception.Message
}
Write-Host ""
Write-Host "用户隔离回归:$passCount/4 通过,失败 $failCount 项"
这个脚本的核心不是创建知识库,而是验证"当前用户身份只来自 Token 和 Gateway 透传",不会因为前端多传一个 userId 参数而改变。
18.7 知识库、文档和索引任务回归
认证和用户隔离通过后,再测知识库和文档。
知识库回归
知识库接口包括:
text
POST /kb
GET /kb/list
GET /kb/{kbId}
DELETE /kb/{kbId}
回归重点是:
- 创建知识库成功。
- 查询列表能看到自己的知识库。
- 查询详情能拿到正确数据。
- 删除后不能再访问。
- 删除后 owner 缓存同步处理。
文档上传回归
文档上传接口是:
text
POST /kb/{kbId}/documents
上传后要确认:
- 返回文档 ID。
- 文档属于当前知识库。
- 生成文档元数据。
- 生成索引任务。
PowerShell 5.1 上传 multipart 文件时,推荐用 curl.exe:
powershell
$uploadJson = curl.exe -s -X POST `
-H "Authorization: Bearer $token" `
-F "file=@$filePath" `
"$base/kb/$kbId/documents"
索引任务回归
上传文档后,查询最新任务:
text
GET /kb/{kbId}/documents/{documentId}/index-task
然后轮询任务状态。
期望最终进入:
text
SUCCESS
如果进入 FAILED,要看 errorMessage。
如果长时间 RUNNING,要看 task-service 和 knowledge-service 日志。
文档切片回归
任务成功后,查询文档切片:
text
GET /kb/{kbId}/documents/{documentId}/chunks
期望:
text
能返回 chunk 列表
chunk 内容来自刚上传的文档
chunkCount > 0
这一步很重要,因为它证明文档不是只上传了,而是真的完成了解析和切片。
18.8 向量检索和 RAG 问答回归
文档索引成功后,才适合测检索和问答。
如果文档还没索引完成,直接测问答,很容易误判系统有问题。
向量检索
接口是:
text
GET /kb/{kbId}/search
常用参数包括:
questiontopKsimilarityThreshold
回归重点是:
- 能返回相关切片。
- 返回相似度。
- 返回结果受 TopK 和阈值影响。
- 返回结果属于当前用户当前知识库。
如果检索结果为空,先不要说大模型有问题。
应该先看:
- 文档是否索引成功。
- 切片是否存在。
- 问题是否和文档内容相关。
- 相似度阈值是否太高。
- Embedding 维度是否一致。
RAG 问答
接口是:
text
POST /kb/{kbId}/chat
回归重点是返回字段:
answerreferencesqaLogIdretrievalCostTimeMsmodelCostTimeMscostTimeMsmodelSuccessmodelFallback
正常情况下,回答应该带引用来源。
如果 modelFallback=true,说明模型调用失败或超时后进入降级逻辑。
这不是系统崩溃,而是系统稳定性设计的一部分。
18.9 管理端接口回归
第 17 章已经讲过后台管理端。
第 18 章要把它纳入回归清单。
管理端接口统一是 /admin/**,必须使用 ADMIN Token。
用户管理接口
text
GET /admin/users
PUT /admin/users/{userId}/role
PUT /admin/users/{userId}/status
回归重点:
- ADMIN 可以查询用户。
- USER 访问应被拒绝。
- 角色只能是 USER 或 ADMIN。
- 状态只能是 0 或 1。
知识库、文档和问答日志接口
text
GET /admin/knowledge-bases
GET /admin/documents
GET /admin/qa-logs
回归重点:
- ADMIN 可以看全局数据。
- 支持 userId、kbId、status、keyword、modelFallback 等过滤条件。
- 普通用户不能通过这些接口查看全局数据。
索引任务管理接口
text
GET /admin/index-tasks
POST /admin/index-tasks/{taskId}/retry
回归重点:
- ADMIN 可以查询全局任务。
- FAILED 或 TIMEOUT 任务可以重试。
- 普通用户不能访问后台任务列表。
这个部分能证明第 17 章管理端不是静态页面,而是真的和后端管理接口、Gateway ADMIN 鉴权连起来了。
18.10 Redis、Sentinel 和 AI 降级验证
接口回归不只测业务接口,还要覆盖系统增强能力。
Redis 缓存验证
第 13 周清单中验证了两类缓存:
text
rag:kb:owner:{kbId}
rag:document:index-task:{userId}:{kbId}:{documentId}
owner 缓存能加速知识库归属判断。
文档任务状态缓存能加速任务状态查询。
但要记住:
text
缓存只是加速层,不是权限真相来源。
跨用户访问时,即使缓存存在,也不能绕过用户隔离。
Sentinel 限流验证
问答接口最容易被刷。
所以第 13 周测试里验证了 Sentinel 限流:
text
并发发起 5 个 /kb/13/chat 请求
成功响应:2 个,code=0
限流响应:3 个,code=42900
这说明限流规则生效了。
AI 降级验证
AI 模型调用失败或超时后,系统不会直接抛 500,而是返回降级答案,并记录日志。
测试时重点看:
text
modelFallback=true
qa_log 中有模型失败状态和错误原因
这一步能证明第 14 章讲的限流和降级不是停留在理论上,而是可以通过接口验证。
18.10.4 Sentinel 限流和 AI 降级实操验证
这一小节给出两个可以手动执行的故障模拟步骤。
一、Sentinel 限流验证
先确认第 14 章中的 Sentinel 配置已经开启,默认可以使用:
yaml
rag:
sentinel:
chat-flow:
enabled: true
resource: knowledgeChat
qps: 1
然后准备一个已经索引完成的 kbId 和登录 Token,连续发起 5 次问答请求:
powershell
$base = "http://localhost:9000"
$kbId = 13
$token = "替换为登录后拿到的 token"
$headers = @{ Authorization = "Bearer $token" }
$body = @{ question = "请根据知识库内容做一个简要总结"; topK = 5; similarityThreshold = 0.3 } | ConvertTo-Json
1..5 | ForEach-Object {
Start-Job -ScriptBlock {
param($base, $kbId, $headers, $body)
Invoke-RestMethod -Method Post -Uri "$base/kb/$kbId/chat" -Headers $headers -ContentType "application/json" -Body $body
} -ArgumentList $base, $kbId, $headers, $body
} | Wait-Job | Receive-Job
预期结果是至少 2 到 3 次返回 code=42900,message 包含"服务繁忙"或类似文案。
如果 5 次全部成功,优先检查:
text
`rag.sentinel.chat-flow.qps` 是否设置过高
`@SentinelResource(value = "knowledgeChat")` 是否和 `resource` 一致
`KnowledgeChatSentinelRuleConfig` 是否加载了规则
二、AI 降级验证
AI 降级可以通过两种方式模拟:
- 临时把
fallbacktimeout改短,比如改为1s。 - 临时把聊天模型 API Key 改成无效值。
然后发起一次正常问答。预期结果:
text
modelFallback=true
answer 为降级提示文案
接口不返回 500
qa_log 中记录模型失败或超时原因
故障模拟完成后,必须恢复配置。尤其是 API Key 和 timeout,不要把临时故障配置提交到代码仓库。
18.11 异常演练清单
异常演练要覆盖常见错误。
第 13 周第二阶段已经验证过这些场景:
text
错误密码登录
重复注册
不带 Token 访问受保护接口
伪造 Token 访问受保护接口
访问不存在知识库
跨用户访问知识库
查询不存在文档切片
重试不存在文档索引任务
上传非法文件类型
创建空名称知识库
空检索问题
非法 TopK
这些场景看起来琐碎,但非常重要。
因为企业项目不是只面对正确输入。
真实用户会填错密码,会上传不支持的文件,会在前端卡顿时重复提交,也可能有人故意构造非法请求。
异常演练要证明:
text
系统能拒绝错误请求,并返回统一、可理解的业务结构。
第 13 周归档中的关键错误码包括:
text
40000:参数校验失败
40001:认证失败
40003:非法文件类型
40100:未认证
40101:Token 无效
40400:资源不存在或越权访问
42900:限流
最关键的结论是:
text
所有测试场景都返回统一业务结构,没有出现未处理 500 堆栈。
这句话对项目展示很有价值。
它说明系统不是只会走正常路径,也能优雅处理错误路径。
18.12 PowerShell 和中文 JSON 问题
这是一个非常实际的问题。
Windows PowerShell 5.1 直接发送中文 JSON 时,可能导致后端收到:
text
????
比如直接这样写:
powershell
Invoke-RestMethod -Body (@{} | ConvertTo-Json)
在某些情况下,中文内容可能出现编码问题。
这会导致什么后果?
如果问题内容变成问号,向量检索就无法召回正确切片。
所以第 13 周归档中记录了更稳定的做法:
powershell
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($jsonPath, $json, $utf8NoBom)
curl.exe --data-binary "@$jsonPath"
这不是小问题。
因为很多初学者会误以为"RAG 检索效果差",但真实原因可能只是请求体中文编码错了。
所以在 Windows 环境做接口回归时,中文 JSON 一定要注意编码。
18.13 轻量压测怎么设计
轻量压测不要一开始就追求复杂工具。
对于当前阶段,先用 PowerShell 脚本循环请求几个关键接口就够了。
第 13 周压测覆盖了四类接口:
text
/auth/me
/kb/list
/kb/{kbId}/search
/kb/{kbId}/chat
为什么选这四个?
因为它们代表四种不同成本:
| 接口 | 特点 |
|---|---|
/auth/me |
认证和 Token 解析,轻量接口 |
/kb/list |
普通数据库查询,轻量业务接口 |
/kb/search |
向量化问题 + pgvector 检索,较重接口 |
/kb/chat |
检索 + 模型调用,最重接口 |
压测脚本要记录:
- 成功次数。
- 失败次数。
- 每次耗时。
- 平均耗时。
- 返回码。
不要只看"请求成功了"。
你要看不同接口的耗时差异。
18.13.4 轻量压测脚本
下面脚本用于生成 18.14 节中的轻量压测数据。
运行前提:
- 已经有可用账号,并拿到 Token。
- 已经创建知识库,并完成文档上传和索引。
$kbId指向一个有切片、有可检索内容的知识库,否则/kb/search和/kb/chat可能返回空结果。
powershell
$base = "http://localhost:9000"
$kbId = 13
$token = "替换为登录后拿到的 token"
$headers = @{ Authorization = "Bearer $token" }
$tests = @(
@{ Name = "/auth/me"; Method = "Get"; Url = "$base/auth/me"; HasBody = $false; Body = $null },
@{ Name = "/kb/list"; Method = "Get"; Url = "$base/kb/list"; HasBody = $false; Body = $null },
@{ Name = "/kb/$kbId/search"; Method = "Post"; Url = "$base/kb/$kbId/search"; HasBody = $true; Body = @{ question = "测试文档主要讲了什么"; topK = 5; similarityThreshold = 0.3 } },
@{ Name = "/kb/$kbId/chat"; Method = "Post"; Url = "$base/kb/$kbId/chat"; HasBody = $true; Body = @{ question = "请根据知识库内容总结核心观点"; topK = 5; similarityThreshold = 0.3 } }
)
function Invoke-TimedRequest($test) {
$startTime = Get-Date
$ok = $false
$statusCode = 200
$errorMessage = $null
try {
if (-not $test.HasBody) {
$resp = Invoke-RestMethod -Method $test.Method -Uri $test.Url -Headers $headers
} else {
$json = $test.Body | ConvertTo-Json -Depth 10
$resp = Invoke-RestMethod -Method $test.Method -Uri $test.Url -Headers $headers -ContentType "application/json" -Body $json
}
# 统一业务结构下,code=0 才算本次请求业务成功。
$ok = ($resp.code -eq 0)
} catch {
$statusCode = 500
if ($_.Exception.Response -ne $null) {
$statusCode = [int]$_.Exception.Response.StatusCode
}
$errorMessage = $_.Exception.Message
}
$elapsed = (Get-Date) - $startTime
return New-Object PSObject -Property @{
Name = $test.Name
CostMs = [math]::Round($elapsed.TotalMilliseconds, 1)
StatusCode = $statusCode
Success = $ok
Error = $errorMessage
}
}
$results = @()
foreach ($test in $tests) {
for ($i = 1; $i -le 5; $i++) {
Write-Host "[$($test.Name)] 第 $i 次请求..."
$results += Invoke-TimedRequest $test
Start-Sleep -Milliseconds 300
}
}
$summary = $results |
Group-Object Name |
ForEach-Object {
$group = $_.Group
New-Object PSObject -Property @{
Interface = $_.Name
SuccessCount = ($group | Where-Object { $_.Success }).Count
TotalCount = $group.Count
AvgCostMs = [math]::Round((($group | Measure-Object CostMs -Average).Average), 1)
}
}
$summary | Format-Table -AutoSize
18.14 节的数据就是用这个脚本在本机 16 核 32GB 笔记本上跑出来的。环境不同、模型服务不同、文档内容不同,结果都会有差异。这里的目标不是证明高并发能力,而是让读者知道四类接口的相对耗时差异。
18.14 第 13 周真实压测结果
第 13 周轻量压测结果如下:
text
/auth/me:5 次成功,平均 21.8 ms
/kb/list:5 次成功,平均 21.4 ms
/kb/13/search:5 次成功,平均 602.2 ms
/kb/13/chat:5 次成功,平均 1754 ms
这个结果说明什么?
第一,认证和列表接口很快。
因为它们主要做的是 Token 解析和数据库查询。
第二,向量检索明显更慢。
因为它要处理问题向量化和 pgvector 检索。
第三,RAG 问答最慢。
因为它除了检索,还要调用大模型。
所以性能分析不能只看一个平均值。
你要知道不同接口慢在哪里:
text
/auth/me 慢 -> 看 Gateway/JWT/auth-service
/kb/list 慢 -> 看数据库和 owner 查询
/kb/search 慢 -> 看 Embedding 和 pgvector
/kb/chat 慢 -> 看检索、Prompt 和模型调用
第 13 周归档还记录了限流结果:
text
并发发起 5 个 /kb/13/chat 请求
成功响应:2 个,code=0
限流响应:3 个,code=42900
这说明系统不是无限接收问答请求,而是会按 Sentinel 规则保护模型接口。
18.15 如何整理测试证据
测试做完后,一定要归档。
否则过几天你自己都不记得测了什么。
第 13 周归档的结构很值得参考:
text
接口回归测试清单
接口回归测试执行记录
接口回归测试归档
异常演练执行记录
异常演练归档
轻量压测脚本
轻量压测结果
压测脚本和结果归档
测试证据整理和整体归档
一个好的测试归档至少要包括:
- 测试目标。
- 前置条件。
- 测试清单。
- 执行结果。
- 关键数据。
- 异常结论。
- 性能数据。
- 边界说明。
其中"边界说明"特别重要。
第 13 周归档里明确写了:
text
这是本机轻量压测,不是生产压测。
不能写成支持高并发或大规模文档。
这就是成熟的工程表达。
真实、克制、可追问。
18.16 本章的边界和后续扩展
本章讲的是学习项目阶段的验证方法。
它已经能证明:
- 主链路能跑通。
- 用户隔离有效。
- 文档索引可验证。
- 检索和问答可验证。
- Redis 缓存可观察。
- Sentinel 限流可触发。
- AI 降级可返回。
- 管理端接口可纳入回归。
- 关键接口有基础耗时数据。
但它不是完整测试体系。
后续如果继续升级,可以补:
- JUnit 单元测试。
- Spring Boot 集成测试。
- Testcontainers 启动 MySQL、Redis、PostgreSQL。
- Postman 或 Apifox 自动化集合。
- GitHub Actions 或 Jenkins CI。
- JMeter、k6 或 Gatling 压测。
- Prometheus + Grafana 指标观测。
- SkyWalking 或 OpenTelemetry 链路追踪。
- 管理端操作审计测试。
这些可以作为后续演进方向,不需要在当前阶段全部完成。
18.17 本章和前面章节的关系
第 18 章把前面开发内容转成验证内容。
可以这样对应:
text
第 4 章:JWT 和 Gateway -> 认证回归、无 Token、伪造 Token
第 5 章:用户资源隔离 -> userA/userB 跨用户访问演练
第 6 章:知识库和文档元数据 -> 知识库、文档接口回归
第 8 章:文档解析与切片 -> chunk 查询回归
第 9 章:RabbitMQ 与索引任务 -> 索引任务状态和重试验证
第 10 章:Embedding -> 向量化耗时和失败排查
第 11 章:pgvector -> 向量检索回归和耗时观察
第 12 章:RAG 问答 -> answer、references、qaLogId 验证
第 13 章:Redis -> owner 缓存和任务状态缓存验证
第 14 章:Sentinel 和降级 -> 42900 和 modelFallback 验证
第 15 章:日志排障 -> 异常演练结果归档
第 16 章:用户端前端 -> 浏览器端操作回归
第 17 章:管理端 -> /admin/** 接口回归
第 18 章:测试验证 -> 证明系统当前版本可信
这说明测试不是额外负担,而是对前面章节的验收。
本章小结
这一章我们讲了接口回归、异常演练与轻量压测。
接口回归证明主链路仍然可用,异常演练证明错误场景能被系统以统一业务结构处理,轻量压测帮助我们建立基础性能认知。KnowHub 的第 13 周归档已经验证了认证、用户隔离、文档上传、索引任务、向量检索、RAG 问答、Redis 缓存、Sentinel 限流和 AI 降级,并记录了 /auth/me、/kb/list、/kb/search、/kb/chat 的本机轻量压测结果。
本章最重要的原则是:测试结论要真实。可以说"完成了核心接口回归、异常演练和本机轻量压测",不能说"完成生产级高并发压测"。这种表达既能体现工程能力,也能经得起追问。
下一章,我们会进入 Docker Compose、虚拟机部署与项目交付,把项目从本机开发和验证推进到更完整的部署交付阶段。
思考题
以下是本章思考题的参考答案,供你自查理解。
1. 为什么功能开发完成后,还需要做接口回归?
因为"写完代码"和"系统可信"不是一回事。开发过程中会不断修改代码,比如改 Gateway 鉴权、文档索引、问答降级逻辑,这些改动可能让原本正常的接口悄悄失效。接口回归就是证明"以前能正常工作的核心接口,在当前版本下仍然能正常工作",把"我感觉能跑"变成"我有证据证明能跑"。
2. 接口回归、异常演练、轻量压测分别解决什么问题?
- 接口回归:解决"正常路径还能不能跑"的问题,验证主链路可用。
- 异常演练:解决"错误路径会不会失控"的问题,验证错误请求能被统一业务结构优雅处理,不抛 500 堆栈。
- 轻量压测:解决"接口耗时大概在什么范围"的问题,建立基础性能认知。
3. 为什么所有外部业务接口应该优先通过 Gateway 测试?
因为 KnowHub 的外部业务接口统一通过 Gateway 调用,Gateway 承担了鉴权、Token 解析、用户信息透传、限流等统一职责。如果绕过 Gateway 直接调 9101/9102/9103,就测不到这些关键链路,也无法验证真实用户访问时的完整行为。统一入口测试才能证明整条链路是通的。
4. Gateway 根路径返回 404 为什么不代表系统故障?
因为 Gateway 根路径没有页面,它只是一个路由入口,不是 Web 站点首页。浏览器访问 http://localhost:9000/ 返回 404 是正常现象,说明 Gateway 在正常工作并拒绝了不存在的路由。应该访问具体接口(如 /auth/me、/kb/list)来验证,而不是访问根路径。
5. /auth/me 在认证回归中为什么很重要?
因为它同时证明两件事:一是前端确实带上了 Token,二是 Gateway 能正确解析 Token 并把用户信息透传给后端。如果 /auth/me 失败,说明认证链路有问题,后面所有依赖当前用户身份的资源隔离测试都没有意义,所以要先测它。
6. userA 访问 userB 知识库为什么返回 40400,而不是直接告诉他无权限?
因为直接返回"你无权访问"会泄露资源存在性------攻击者可以借此探测某个知识库是否真实存在。返回 40400(资源不存在或越权访问)让当前用户感觉"这个资源就像不存在",既保护了资源隐私,也避免了信息泄露,是更安全的做法。
7. 为什么文档索引没有 SUCCESS 前,不适合直接判断 RAG 问答效果?
因为 RAG 问答依赖文档切片和向量检索结果。如果文档还没索引完成,切片不存在或向量未写入,检索就召回不到内容,问答自然无法给出正确答案。此时判断问答效果会误判系统有问题,应该先确认文档索引进入 SUCCESS、切片存在,再测检索和问答。
8. modelFallback=true 在测试中说明什么?
说明模型调用失败或超时后,系统进入了降级逻辑,返回了降级提示文案,而不是直接抛 500。这是系统稳定性设计的一部分,证明第 14 章讲的 AI 降级不是停留在理论上,而是可以通过接口验证的。
9. Sentinel 返回 42900 为什么是正常的稳定性保护结果?
因为问答接口最容易被高频请求刷爆,Sentinel 限流就是为了保护模型接口不被过量请求打垮。返回 42900 说明限流规则生效了,系统在按规则拒绝超出阈值的请求,而不是无限接收导致模型服务崩溃。这是稳定性保护机制在正常工作。
10. PowerShell 5.1 发送中文 JSON 为什么可能影响检索效果?
因为 Windows PowerShell 5.1 直接发送中文 JSON 时,可能因为编码问题把中文变成 ????。如果问题内容变成问号,向量检索就无法正确向量化,召回不到正确切片,导致检索效果变差。很多初学者误以为"RAG 检索效果差",真实原因可能只是请求体中文编码错了。
11. 为什么 /kb/chat 的平均耗时明显高于 /auth/me?
因为两者成本完全不同。/auth/me 只做 Token 解析和用户信息查询,是轻量接口;而 /kb/chat 除了向量检索,还要构造 Prompt 并调用大模型,是最重的接口。所以 /kb/chat 平均耗时(1754 ms)远高于 /auth/me(21.8 ms),这是符合预期的。
12. 为什么不能把本机 5 次轻量压测写成生产级高并发压测?
因为本机 5 次请求样本量极小、无并发压力、环境是单机开发环境,只能说明"这几个接口大概多慢",不能证明系统支持高并发或大规模文档。写成生产级高并发压测是不真实的,会经不起追问。成熟的工程表达是真实、克制、可追问,明确写出"这是本机轻量压测,不是生产压测"。
- 为什么功能开发完成后,还需要做接口回归?
- 接口回归、异常演练、轻量压测分别解决什么问题?
- 为什么所有外部业务接口应该优先通过 Gateway 测试?
- Gateway 根路径返回 404 为什么不代表系统故障?
/auth/me在认证回归中为什么很重要?- userA 访问 userB 知识库为什么返回 40400,而不是直接告诉他无权限?
- 为什么文档索引没有 SUCCESS 前,不适合直接判断 RAG 问答效果?
modelFallback=true在测试中说明什么?- Sentinel 返回 42900 为什么是正常的稳定性保护结果?
- PowerShell 5.1 发送中文 JSON 为什么可能影响检索效果?
- 为什么
/kb/chat的平均耗时明显高于/auth/me? - 为什么不能把本机 5 次轻量压测写成生产级高并发压测?