第18章 接口回归、异常演练与轻量压测

本章目标

前面 17 章,我们已经把 KnowHub 的核心功能、用户端和管理端都讲完了。

到这里,一个常见问题会出现:

text 复制代码
项目功能已经写完了,是不是就可以收工?

不能。

对一个工程项目来说,"写完代码"和"系统可信"不是一回事。

尤其是 RAG 平台,它不是一个简单的增删改查系统。它的链路很长:

text 复制代码
注册登录
-> Gateway 鉴权
-> 用户隔离
-> 知识库管理
-> 文档上传
-> 文件存储
-> 文档解析
-> 文本切片
-> 索引任务
-> Embedding
-> pgvector 向量写入
-> 向量检索
-> Prompt 构造
-> 大模型调用
-> Sentinel 限流
-> AI 降级
-> qa_log 记录
-> 用户端和管理端展示

只要其中一个环节出问题,用户看到的就可能是:

  • 登录失败。
  • 上传失败。
  • 索引一直不成功。
  • 检索不到内容。
  • 问答没有引用。
  • 模型超时。
  • 管理端看不到数据。

所以第 18 章要解决的问题是:

text 复制代码
如何证明当前系统的主链路可用、异常场景可控、基础性能有认知?

本章会讲三件事:

  1. 接口回归。
  2. 异常演练。
  3. 轻量压测。

这三件事听起来像测试岗位的内容,但后端开发必须懂。因为企业项目里,开发不是只写代码,还要能证明自己的代码在当前版本下是可靠的。

本章内容基于 KnowHub 当前代码,以及第 13 周已经整理过的接口回归、异常演练和轻量压测归档。需要提前说明:这里讲的是学习项目和校招展示级别的验证,不是生产级测试体系,也不是高并发压测报告。


18.1 为什么开发完成后还要做验证

很多初学者做项目时,会停在"我点了一下页面,能跑"这个层面。

但企业里不是这样。

企业系统要回答三个更严格的问题:

  1. 正常路径还能不能跑?
  2. 错误路径会不会失控?
  3. 接口耗时大概在什么范围?

这三个问题分别对应:

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=0data 包含 userIdusername 第 4 章
AUTH-02 重复注册应失败 POST /auth/register code!=0,返回可理解的重复用户提示 第 4 章
AUTH-03 正确密码登录成功 POST /auth/login code=0data 包含 tokentokenType=Bearer 第 4 章
AUTH-04 错误密码登录失败 POST /auth/login code!=0,不会返回 Token 第 4 章
AUTH-05 /auth/me 识别当前用户 GET /auth/me code=0data 包含当前用户信息 第 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} kbIduserIdname 与创建结果一致 第 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=INDEXEDchunkCount>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,知识库详情里的 kbIduserId,问答响应里的 answerreferences,都必须能支撑下一步测试继续执行。


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 的测试清单逐条映射成请求,并在工具中保存环境变量,例如 baseUrltokenAtokenBkbId。本章关注的是"测什么"和"怎么判断测过了",不是强制使用某一种测试工具。


18.5 认证接口回归

认证是所有后续接口的入口。

如果认证没过,后面知识库、文档、问答都不应该继续测。

认证回归至少覆盖:

text 复制代码
POST /auth/register
POST /auth/login
GET  /auth/me

注册

注册要验证:

  • 用户名合法。
  • 密码合法。
  • 返回用户 ID。
  • 重复注册能返回业务错误。

登录

登录要验证:

  • 正确用户名密码能返回 Token。
  • 错误密码返回认证失败。
  • 返回的 tokenType 是 Bearer。
  • Token 中携带用户身份和角色。

当前用户

/auth/me 是一个很关键的接口。

它证明两件事:

  1. 前端带上了 Token。
  2. 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 知识库。

典型步骤是:

  1. 注册并登录 userA。
  2. 注册并登录 userB。
  3. userA 创建 kbA。
  4. userB 创建 kbB。
  5. userA 访问 kbA 成功。
  6. 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

常用参数包括:

  • question
  • topK
  • similarityThreshold

回归重点是:

  • 能返回相关切片。
  • 返回相似度。
  • 返回结果受 TopK 和阈值影响。
  • 返回结果属于当前用户当前知识库。

如果检索结果为空,先不要说大模型有问题。

应该先看:

  • 文档是否索引成功。
  • 切片是否存在。
  • 问题是否和文档内容相关。
  • 相似度阈值是否太高。
  • Embedding 维度是否一致。

RAG 问答

接口是:

text 复制代码
POST /kb/{kbId}/chat

回归重点是返回字段:

  • answer
  • references
  • qaLogId
  • retrievalCostTimeMs
  • modelCostTimeMs
  • costTimeMs
  • modelSuccess
  • modelFallback

正常情况下,回答应该带引用来源。

如果 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=42900message 包含"服务繁忙"或类似文案。

如果 5 次全部成功,优先检查:

text 复制代码
`rag.sentinel.chat-flow.qps` 是否设置过高
`@SentinelResource(value = "knowledgeChat")` 是否和 `resource` 一致
`KnowledgeChatSentinelRuleConfig` 是否加载了规则

二、AI 降级验证

AI 降级可以通过两种方式模拟:

  1. 临时把 fallback timeout 改短,比如改为 1s
  2. 临时把聊天模型 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 次请求样本量极小、无并发压力、环境是单机开发环境,只能说明"这几个接口大概多慢",不能证明系统支持高并发或大规模文档。写成生产级高并发压测是不真实的,会经不起追问。成熟的工程表达是真实、克制、可追问,明确写出"这是本机轻量压测,不是生产压测"。

  1. 为什么功能开发完成后,还需要做接口回归?
  2. 接口回归、异常演练、轻量压测分别解决什么问题?
  3. 为什么所有外部业务接口应该优先通过 Gateway 测试?
  4. Gateway 根路径返回 404 为什么不代表系统故障?
  5. /auth/me 在认证回归中为什么很重要?
  6. userA 访问 userB 知识库为什么返回 40400,而不是直接告诉他无权限?
  7. 为什么文档索引没有 SUCCESS 前,不适合直接判断 RAG 问答效果?
  8. modelFallback=true 在测试中说明什么?
  9. Sentinel 返回 42900 为什么是正常的稳定性保护结果?
  10. PowerShell 5.1 发送中文 JSON 为什么可能影响检索效果?
  11. 为什么 /kb/chat 的平均耗时明显高于 /auth/me
  12. 为什么不能把本机 5 次轻量压测写成生产级高并发压测?
相关推荐
卷福同学1 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端
逻辑君1 小时前
ANNA 认知引擎 · Humanoid 机器人训练白皮书
人工智能·深度学习·机器学习·机器人
hans汉斯1 小时前
计算机科学与应用|改进MeanShift算法在智能监控视频中的应用研究
图像处理·人工智能·功能测试·深度学习·算法·音视频
重庆传粉科技1 小时前
AI推荐生态下品牌内容筛选标准重构与GEO优化路径
人工智能
CIO_Alliance2 小时前
2026年最新iPaaS选型核心关键指标整合
人工智能·ai·ai+ipaas·企业cio联盟·企业级ai化转型
nvvas2 小时前
AI 智能体架构全解:从记忆、工具到多智能体协作的硬核实践
人工智能
veminhe2 小时前
元数据索引有关的错
人工智能·python
一次旅行2 小时前
AutoAWQ完整实战:MIT激活感知AWQ量化,模型显存减半、推理提速且精度无损
人工智能·python·算法
立心者02 小时前
Sdcb Chats .. 发布,彻底移除 Azure.AI.OpenAI 专用包
人工智能·flask·azure