从接口压测到全链路质量保障:AI智能客服系统的软件测试实践
一、AI智能客服系统 的测试挑战
智能客服系统远不止普通的 CRUD 业务系统,它是一个典型的长连接 + 高并发 + AI 推理链路 + 分布式路由复合体。任何一个环节异常,用户侧感知往往就是"卡死"“丢消息”“答非所问”。
对测试工程师而言,这套系统存在四个核心难点:
| 难点 | 典型表现 | 对测试的冲击 |
|---|---|---|
| 多渠道接入 | WebSocket 主通道 + 第三方 IM SDK 回调 | 同一套消息逻辑要走完全不同的接入路径 |
| 多实例部署 | 服务水平扩容,连接分散在不同节点 | 消息可能发到 A 实例,但用户连在 B 实例,需要跨实例路由 |
| AI 链路复杂 | 敏感词 → 意图识别 → 知识检索 → 大模型生成 | 每一步都有外部依赖,失败模式呈指数级增长 |
| 响应式全栈 | WebFlux + Reactor + R2DBC | 传统断言方式失效,ThreadLocal 不可用,调试门槛陡增 |
一句话概括这类系统的测试本质:在异步、非阻塞、分布式的条件下,保证消息不丢、不重、不乱序、不超时。
二、被测系统技术特征概览
2.1 核心技术栈(通用化)
| 层 | 技术选型 | 说明 |
|---|---|---|
| JDK | Java 21 | LTS,虚拟线程就绪 |
| Web 框架 | Spring Boot 3.x + Spring WebFlux | 响应式编程模型 |
| 服务治理 | Spring Cloud + Nacos | 注册中心 / 配置中心 |
| RPC | Dubbo + Triple(基于 gRPC) | 服务间高效通信 |
| 数据库 | PostgreSQL / MySQL | R2DBC 非阻塞访问 |
| 缓存 / 消息 | Redis(Hash / Stream / 分布式锁) | ReactiveRedisTemplate |
| 向量检索 | 云厂商向量数据库 | 用于 RAG 知识检索 |
| 大模型 | 主流 LLM 云服务 | 支持流式输出 |
| 认证鉴权 | 响应式 Sa-Token | Reactor Context 传递登录态 |
| 对象存储 | 云厂商 OSS | 文件上传 / 下载 |
| 调度 | XXL-Job | 分布式定时任务 |
| 监控 | Micrometer + Prometheus + 分布式追踪 | 指标采集与链路追踪 |
| 压测工具 | 自研 Python WebSocket 压测客户端 | 200 并发 / 200 QPS |
2.2 测试关注点映射
| 模块 | 核心职责 | 测试重点 | 风险等级 |
|---|---|---|---|
| 会话模块 | WebSocket 接入、消息分发、跨实例路由 | 连接管理、消息可靠性、心跳超时、多端路由 | 🔴 P0 |
| AI 编排 | 意图识别、RAG、回复生成、转人工决策 | 三级降级链路、节点分支断言、策略切换 | 🔴 P0 |
| LLM 服务 | 大模型对话 | 流式输出完整性、取消语义、超时降级 | 🟡 P1 |
| RAG 模块 | 向量检索 | 置信度阈值、多分区并发、命中 / 未命中 | 🟡 P1 |
| 网关 | 统一入口、限流、鉴权 | 路由规则、限流阈值、Token 透传 | 🟡 P1 |
| 权限中心 | 认证、RBAC、部门数据隔离 | 登录态、权限拦截、上下文透传 | 🟡 P1 |
| 运营后台 | 前端管理页面 | 前后端接口契约、权限渲染 | 🟢 P2 |
三、IM 核心全链路流程(测试视角)
3.1 消息从发送到接收的完整链路
用户浏览器 / APP │ WebSocket 连接(wss://域名/ws/chat) ▼ WebSocket 接入层 │ 接收 TEXT 帧 → 解析为内部消息对象 ▼ 消息分发器(Dispatcher) │ 按 userType + messageType 路由到对应处理器 ▼ 用户消息处理器(UserMessageProcessor) │ ├─ 1. 敏感词检测 │ └─ 命中 → 替换为 *** │ ├─ 2. AI 回复策略选择 │ ├─ 热门话术缓存命中 → 直接返回 │ ├─ 本地缓存命中 → 直接返回 │ └─ 未命中 → 调用大模型 │ ├─ 3. AI 编排工作流执行 │ │ │ ▼ │ StateGraph 状态图驱动 │ ├─ 意图识别节点 │ ├─ 知识库检索节点(RAG) │ ├─ 回复生成节点 │ ├─ 转人工节点 │ └─ 兜底回复节点 │ ├─ 4. 大模型服务调用 │ │ │ ▼ │ 流式返回 Flux<String> │ 逐 token 推送到前端 │ ├─ 5. 消息持久化 │ │ │ ▼ │ R2DBC → PostgreSQL / MySQL │ └─ 6. 消息推送路由 │ ├─ 本地连接 → 直接内存推送 ├─ Redis 连接缓存命中 → 写入消息通道 └─ 跨节点 → Redis Stream 广播 │ ▼ WebSocketSession.send() → 客户端接收3.2 必须覆盖的 12 条分支清单
| 编号 | 分支场景 | 测试类型 | 预期结果 |
|---|---|---|---|
| 1 | 匿名连接发送消息 | 功能 | 不走分布式路由,Handler 直发,返回提示登录 |
| 2 | 已登录连接发送消息 | 功能 | 走完整业务编排链路 |
| 3 | 本机连接消息发送 | 功能 | 直接内存推送,不写 Redis |
| 4 | 跨实例连接消息发送 | 集成 | 写入 Redis Stream,目标实例消费并推送 |
| 5 | 单端在线 | 功能 | 正常收发 |
| 6 | 多端同时在线(PC + 微信) | 功能 | 消息推送到所有在线端,不漏不重 |
| 7 | 热门话术命中 | 功能 | 返回热门话术列表,不调 AI |
| 8 | 本地缓存命中 | 功能 | 返回缓存回复,不调 AI |
| 9 | 大模型策略触发 | 集成 | 调 AI 服务链路,流式返回 |
| 10 | 敏感词命中 | 功能 | 消息脱敏替换,原始内容仍持久化 |
| 11 | 排队转人工 | 功能 | 分布式锁互斥,仅一个请求能进入转人工 |
| 12 | AI 超时降级 | 容灾 | 返回兜底话术,不阻塞用户 |
四、测试方案分层设计(七层模型)
| 层级 | 工具 / 手段 | 覆盖范围 | 示例 |
|---|---|---|---|
| L1 单元测试 | JUnit 5 + Mockito + Reactor StepVerifier | 单个类 / 方法的分支覆盖 | AI 策略三分支逻辑 |
| L2 切片测试 | @WebFluxTest / @DataR2dbcTest | Web 层路由、R2DBC Repository | WebSocket 握手、消息持久化 |
| L3 集成测试 | Testcontainers(PG / Redis / 注册中心)+ Dubbo 本地调用 | 跨模块端到端编排 | 会话 → AI 编排 → LLM 完整链路 |
| L4 接口测试 | Postman / Newman / Shell 脚本 | REST 接口契约 | 登录、会话 CRUD、知识库管理 |
| L5 WS 协议测试 | 自研 Python WebSocket 客户端 | 长连接入参 / 出参 / 时序 | 握手鉴权、消息推送、心跳 |
| L6 性能压测 | 自研 Python 压测工具 | 200 并发 / 200 QPS | RT / P95 / P99 / 可用率 |
| L7 容灾测试 | 手动 / 自动化故障注入 | 跨实例消息可达性 | 实例宕机、Redis 抖动、网络隔离 |
五、重点一:WebSocket 自动化测试设计
5.1 测试设计思路
WebSocket 测试的核心难点在于:它是异步双向流,不是请求-响应模型。需要同时验证:
- 服务端能正确接收客户端消息
- 服务端能主动推送消息到客户端
- 心跳机制正常工作
- 断连后资源正确释放
5.2 响应式流断言(StepVerifier)
测试目标:验证 WebSocket 文本消息被正确处理并分发。
文字描述测试方案:
- 准备阶段:构造一条模拟的 TEXT 帧,内容为 JSON 格式的消息体(如
{"type":"***","content":"你好"}) - 执行阶段:将该消息传入
Flux.just()模拟客户端输入流 - 断言阶段:使用
StepVerifier验证输出流:- 第一条消息应为 AI 回复类型
- 消息内容包含预期的回复文本
- 流在超时时间内正常完成
关键代码片段思路:
// 1. 构造输入 FluxFlux<WebSocketMessage>input=Flux.just(mockTextMessage(jsonPayload));// 2. 调用被测 Handler 获取输出 FluxFlux<WebSocketMessage>output=handler.handle(input);// 3. StepVerifier 断言StepVerifier.create(output).assertNext(msg->{Stringpayload=msg.getPayloadAsText();assertTrue(payload.contains("AI_REPLY"));}).expectComplete().verify(Duration.ofSeconds(5));5.3 心跳与断连测试
测试目标:验证 PING/PONG 心跳机制与超时断连。
测试方案:
- 模拟客户端连接后不发送任何 PING 帧
- 验证服务端在配置的心跳超时时间后主动关闭连接
- 验证 Redis 中的连接记录被正确清理
关键断言逻辑:
- 使用
expectNoEvent(Duration)验证在心跳间隔内无服务端主动消息 - 超过超时时间后,连接状态变更为 CLOSED
- Redis 中
key:{value}对应的 Hash 记录被删除
5.4 多端消息同步测试
测试目标:验证同一用户多端在线时,消息推送到所有终端。
测试方案:
- 模拟同一用户 ID 建立两个 WebSocket 连接(PC 端 + 移动端)
- 向该用户发送一条消息
- 验证两个连接都收到推送
- 验证消息内容一致、不重复
六、重点二:AI 编排工作流测试
6.1 意图三级降级链路
意图识别是 AI 客服的核心决策点,采用三级降级策略:
Level 1: 向量匹配(向量数据库语义检索) ├─ 命中(score ≥ 阈值) → 直接使用向量匹配结果 └─ 未命中 ↓ Level 2: 小模型意图分类 ├─ 置信度 ≥ 阈值 → 使用小模型结果 └─ 置信度 < 阈值 ↓ Level 3: 大模型兜底(LLM 直接判断) └─ 最终结果测试设计思路
采用数据驱动 + Mock 控制的方式,固定expectedIntent,通过 Mock 控制每一级的输入和输出。
Level 1 向量命中场景
测试步骤:
- Mock 向量数据库客户端,使其返回高置信度结果(如 score=0.92,阈值=0.85)
- 构造输入文本:“我的余额是多少”
- 执行意图识别节点
- 断言:
- 返回的意图为
query_balance - 意图来源标记为
VECTOR_MATCH - 不再调用 Level 2 和 Level 3
- 返回的意图为
Level 2 小模型命中场景
测试步骤:
- Mock 向量数据库返回空(未命中)
- Mock 小模型返回中等置信度结果(如 confidence=0.78,阈值=0.7)
- 构造输入文本:“我要转账”
- 执行意图识别节点
- 断言:
- 返回的意图为
transfer_money - 意图来源标记为
SMALL_MODEL - 不再调用 Level 3
- 返回的意图为
Level 3 大模型兜底场景
测试步骤:
- Mock 向量数据库返回空
- Mock 小模型返回低置信度结果(如 confidence=0.35,阈值=0.7)
- Mock 大模型返回 JSON 格式意图判断:
{"intent": "complaint", "reason": "服务态度"} - 构造输入文本:“你们什么破服务”
- 执行意图识别节点
- 断言:
- 返回的意图为
complaint - 意图来源标记为
LLM_FALLBACK
- 返回的意图为
6.2 RAG 命中 / 未命中分支测试
RAG 命中场景
测试步骤:
- Mock 向量数据库返回高分知识片段(score=0.88,阈值=0.75)
- 内容:“7天无理由退货”
- 内容:“退货需保留包装”
- 构造输入文本:“怎么退货”
- 执行 RAG 节点
- 断言:
- 上下文中包含知识库内容
isKnowledgeHit标记为 true- 后续进入知识增强回复节点
RAG 未命中场景
测试步骤:
- Mock 向量数据库返回低分结果(score=0.21)
- 构造输入文本:“我想去火星旅游”
- 执行 RAG 节点
- 断言:
isKnowledgeHit标记为 falseshouldCallLLM标记为 true- 后续进入大模型直答节点
6.3 转人工决策测试
测试目标:投诉类语句触发转人工流程。
测试步骤:
- 准备启用转人工的 Bot 配置,设置投诉关键词列表(如"投诉"“差评”“垃圾”)
- 构造输入文本:“我要投诉你们的服务态度”
- 执行转人工节点
- 断言:
- 节点状态变更为
TRANSFER_TO_HUMAN - 事件列表中包含
TransferToHumanEvent - 转人工服务层的
initiateTransfer方法被调用一次
- 节点状态变更为
七、重点三:跨实例消息路由测试
7.1 跨实例路由架构
用户 A 连接 → 实例 A(WebSocket 接入) │ │ 消息目标用户在实例 B ▼ 消息分发器判断路由 │ ┌─────────┴─────────┐ │ 目标在本地 │ 目标在远端 │ → 直接推送 │ → 写 Redis Stream └───────────────────┘ │ ▼ 实例 B 消费 Stream → 推送用户 B7.2 Redis Hash 连接存储测试
Redis Key 结构设计:
Key: im:user:channels:{userId} Type: Hash Field: {channelType}:{deviceId} Value: JSON {instanceId, sessionId, channelType, deviceId} TTL: 24h(自动过期,防止僵尸连接)测试用例设计:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 注册连接(userId, instanceId, sessionId, channelType, deviceId) | 返回 true |
| 2 | 查找连接(userId) | 返回完整的 ChannelInfo 对象 |
| 3 | 验证 TTL | 接近 24h(允许误差) |
| 4 | 注销连接(userId, field) | 返回 true |
| 5 | 再次查找 | 返回空 |
八、性能与稳定性测试
8.1 压测方案设计
采用自研 Python WebSocket 压测工具,支持以下模式:
| 模式 | 并发 | QPS | 持续时间 | 目的 |
|---|---|---|---|---|
| 标准压测 | 200 | 200 | 10 分钟 | 验证常规负载能力 |
| 长稳压测 | 50 | 50 | 24 小时 | 验证内存泄漏 / 连接稳定性 |
| 降级专项 | 100 | 100 | 5 分钟 | 模拟 AI 超时,验证降级话术 |
| 峰值冲击 | 500 | 500 | 1 分钟 | 验证限流与熔断策略 |
压测核心指标:
- 连接建立成功率:目标 ≥ 99.9%
- 消息往返延迟(RT):P95 < 500ms
- P99 延迟:< 2s
- 错误率:< 0.1%
- 流式首字延迟(TTFT):< 1.5s
8.2 监控指标与告警阈值
| 指标 | 采集路径 | 告警阈值 | 说明 |
|---|---|---|---|
| 接口 QPS | /actuator/prometheus | P95 > 500ms 告警 | IM 接口响应时间 |
| AI 接口 RT | Prometheus 指标 | P95 > 3s | 大模型调用延迟 |
| WebSocket 在线数 | 自定义指标 | 抖动 > 20% | 连接稳定性 |
| Redis Stream 积压 | XPENDING | > 1000 条 | 消费能力不足 |
| JVM GC 暂停 | jvm_gc_pause_seconds | 单次 > 200ms | 内存压力 |
| 错误率 | 自定义错误计数 | > 0.1% | 整体健康度 |
8.3 分布式追踪排查
TraceId 串联排查流程:
- 从 Prometheus / Grafana 发现异常请求,获取 TraceId
- 在各服务日志中按 TraceId 过滤,按时间排序
- 还原完整调用链:
网关层 | 14:23:01.100 | 收到用户请求 | userId=10086 会话层 | 14:23:01.105 | WebSocket 消息解析完成 AI 编排层 | 14:23:01.110 | 意图识别开始 | input="查余额" 向量检索 | 14:23:01.115 | 向量搜索完成 | topK=5 AI 编排层 | 14:23:01.130 | 意图=query_balance | source=VECTOR LLM 服务 | 14:23:01.135 | 大模型流式开始 | model=qwen-plus 会话层 | 14:23:01.280 | 推送 AI 回复 | chunks=15九、典型故障排查指南
9.1 场景一:消息丢失
排查路径:
1. 确认消息是否到达 WebSocket 层 → 查看 WebSocket Handler 日志,确认收到 TEXT 帧 2. 确认消息是否进入分发器 → 查看 Dispatcher 路由日志 3. 确认路由结果 → 查看路由日志中的标记:LOCAL / REDIS / STREAM 4. 如果走了 STREAM → 检查消费者组是否在线 → XINFO GROUPS 消费者key 5. 如果消费者在线 → 检查 Pending 是否堆积 → XPENDING 消费者key 消费组key 6. 如果 Pending 堆积 → 消费者处理阻塞 → 检查消费者线程池 / 下游服务延迟9.2 场景二:转人工死锁
排查路径:
1. 查看 Redis 中的锁状态 → GET lock:transfer:human:{会话Id} → TTL lock:transfer:human:{会话Id} 2. 如果 TTL = -1(永不过期) → 看门狗续期逻辑异常,锁永远不会释放 → 修复方向:unlock 放入 doFinally() 确保执行 3. 如果 TTL 正常倒计时但锁未释放 → 业务异常导致 unlock 未执行 → 修复方向:增加锁持有时间上限(强制过期) 4. 如果 Redis 主从切换 → DEL 命令可能丢失 → 修复方向:使用 Redlock 或增加重试机制9.3 场景三:AI 回复超时
排查路径:
1. 确认超时发生在哪一层 → 网关超时?AI 编排超时?LLM 调用超时? 2. 查看 LLM 服务日志 → 是否收到请求?是否开始流式输出? 3. 查看 AI 编排状态机 → 是否触发了降级节点? → 降级话术是否正确返回? 4. 验证超时配置 → WebClient 超时 < AI 编排超时 < 网关超时 → 确保每层都有超时保护,不会无限等待10.2 测试经验
- 响应式代码必须用 StepVerifier:传统
assertEquals无法断言异步流的行为,StepVerifier 是标配 - Redis Stream 是跨实例消息的命脉:Pending 监控、ACK 机制、Stream 修剪缺一不可
- AI 链路必须三级降级:向量 → 小模型 → 大模型,任何一级都要有超时和兜底
- 分布式锁必须有看门狗:没有自动续期的锁在生产环境一定会出问题
- 压测要分档位:短促高压验证极限,长稳低压验证泄漏,两者不可互相替代
- TraceId 是排查的核武器:从网关到 LLM 全链路串联,5 分钟定位问题根因
写在最后:智能客服系统的测试,本质上是在异步、分布式、AI 依赖三重不确定性中,用工程化的手段建立确定性。分层测试是骨架,Mock 是肌肉,监控是可观测的神经——三者缺一不可。希望本文的实战经验能给正在做类似系统的测试同学一些参考。
