SSE 问答和普通接口不一样,用户感受最明显的是两个时间:一个是 排队等待时间,另一个是 模型首包时间。如果用户 30 秒才看到第一个字,不一定是模型慢,也可能是前面排队太久、检索慢、Rerank 慢、Prompt 太长、线程池打满,或者 SSE 写出阻塞。
第一,看 Redis 等待队列。
先看当前队列长度、用户请求的入队时间、等待时长、排队位置。如果队列很长,说明问题出在入口并发控制或下游处理能力不足。
如果用户“一直排队”,我第一步不会看模型,而是先看他有没有拿到执行资格。
我会看:
当前队列长度
用户 requestId 是否还在 ZSET 里
入队时间
等待时长
排队位置
队列里最老请求的时间
是否有大量过期 requestId 残留
如果发现队列很长,说明系统整体处理能力不足,可能是模型慢、线程池满、检索慢导致消费速度下降。
如果队列不长但用户一直排队,就要怀疑队列状态异常,比如队首请求已经断开但没清理,或者信号量 permit 被占满。
排查时,我会先查用户 requestId 的 rank,再看队列中排在前面的请求是否已经超时或取消。
优化上,可以定期清理过期队列项,给等待请求设置最大等待时间,前端展示排队位置和超时提示。
这里的取舍是:排队能保护系统,但不能无限排队。所以我会设置等待上限,超过就明确拒绝,而不是让用户一直等。
第二,看 Redisson 信号量占用。
如果队列不长但请求一直排队,要看执行许可是不是被占满,以及有没有许可没有释放。比如用户关闭页面、SSE 异常、服务重启后,permit 没释放,就会导致后续请求一直拿不到执行资格。
如果 Redis 队列里一直有人排队,但执行中的请求数看起来不高,或者 permit 一直显示被占满,就要怀疑信号量没有释放。
重点看:
当前已占用 permit 数
剩余 permit 数
执行中 requestId 列表
requestId 对应 SSE 是否还活着
permit 是否设置过期时间
onCompletion / onTimeout / onError 是否释放
常见问题是:用户关闭浏览器、刷新页面、网络断开,SSE 连接已经没了,但服务端没有释放 permit。这样后续请求永远排队。
排查时,我会通过 requestId 查对应状态:WAITING、RUNNING、DONE、FAILED、CANCELED。如果状态已经结束但 permit 还占用,就说明释放逻辑有问题。
优化上,要做主动释放和过期兜底:
SSE 正常完成:release permit
SSE 超时:release permit
SSE error:release permit
客户端断开:release permit
服务异常:靠可过期 permit 兜底
这里的取舍是:只靠过期时间会导致资源释放不及时,只靠主动释放又怕异常路径漏掉。所以两层都要做。
第三,看 线程池是否打满。
看问答线程池、检索线程池、模型调用线程池、SSE 推送线程池的 activeCount、queueSize、rejectCount。如果线程池满了,就要看线程栈卡在哪里。
RAG 链路里可能有多个线程池:
问答主流程线程池
多通道检索线程池
模型调用线程池
Rerank 线程池
SSE 推送线程池
Trace 异步落库线程池
文档入库消费线程池
我会看每个线程池的:
activeCount 活跃线程数
poolSize 当前线程总数
queueSize 队列积压数
rejectCount 拒绝任务数
taskDuration 任务执行耗时
largestPoolSize 历史最大线程数
如果队列持续增长,说明任务提交速度超过处理速度。然后用 jstack 看线程到底卡在哪里:
卡在向量库查询
卡在数据库连接池
卡在 Redis
卡在模型 HTTP 调用
卡在 Rerank
卡在 SSE send
实际使用中容易出现的问题是,只看到线程池满就加线程。这样如果根因是模型服务慢,加线程只会让更多请求同时打模型,整体更慢。
优化上,要按任务隔离线程池。入库任务不能和问答任务抢资源,Trace 落库不能拖慢主链路,Rerank 要有短超时。
这里的取舍是:线程池是隔离资源的,不是无限放大并发的。所以我会先看线程卡点,再决定是扩线程、限流还是降级。
第四,看 检索耗时。
RAG Trace 里看多通道检索耗时,是意图定向检索慢,还是全局向量检索慢,还是权限过滤、数据库查询、向量库查询慢。
我会在 Trace 里看:
问题改写耗时
意图识别耗时
权限范围计算耗时
意图定向检索耗时
全局向量检索耗时
多问拆分召回耗时
候选合并去重耗时
如果检索慢,常见原因有:
知识库范围太大
权限过滤条件复杂
TopK 太大
向量库索引不合理
向量库连接池不足
数据库连接池打满
多问拆分导致检索次数过多
排查时,我会先看慢的是哪个通道。如果只有全局向量检索慢,就看向量库;如果权限过滤慢,就看数据库索引和权限模型;如果多问拆分慢,就看子问题数量是否过多。
优化上,可以缩小知识库范围、预过滤权限、控制 TopK、优化向量索引、减少无效通道、给单通道设置超时。
这里的取舍是:检索范围越大,召回可能更全,但延迟和噪声也更高。所以不能所有问题都全局检索,要先按权限和知识库范围收缩。
第五,看 Rerank 耗时。
如果候选 chunk 太多,Rerank 模型慢或超时,会拖慢首包。Rerank 应该有短超时,失败后允许跳过,不能无限等。
如果用户 30 秒才出首字,而检索很快、模型还没开始,那我会重点看 Rerank。
Rerank 慢的常见原因:
进入 Rerank 的候选 chunk 太多
Rerank 模型本身慢
Rerank 服务限流
批量输入过大
没有设置短超时
排查时,我会看:
Rerank 输入候选数量
Rerank 耗时
是否超时
是否触发降级
Rerank 前后排序
如果 Rerank 超过阈值,应该直接降级为原始检索排序,而不是一直等。
优化上,可以限制进入 Rerank 的候选数量,比如先每个通道 topK,再合并去重,再只取前 N 个做 Rerank。
这里的取舍是:Rerank 能提高准确率,但会增加延迟。所以我不会让 Rerank 无限等待,应该短超时、可跳过。
第六,看 模型首包耗时。
首包慢是 30 秒才出字的高频原因。要看 Prompt token 长度、模型路由选择的是哪个模型、是否触发 fallback、模型健康状态、供应商是否限流。
如果 Trace 里显示检索和 Rerank 都很快,但模型调用后很久才有第一个 token,那问题大概率在模型层。
我会看:
选中的模型
Prompt token 长度
上下文 chunk 数量
模型首包耗时
模型总耗时
是否触发 fallback
模型健康状态
是否供应商限流
常见原因:
Prompt 太长
模型本身慢
模型服务排队
供应商限流
网络抖动
主模型健康状态异常
优化上,可以做:
压缩 Prompt
减少无关 chunk
控制最终 topN
首包超时切备用模型
模型健康检查
慢模型降权
按问题类型选择模型
这里的取舍是:强模型效果好但可能慢,快模型质量可能差。所以我不会所有问题都走最强模型,而是按问题复杂度和实时性要求做模型路由。
第七,看 SSE 写出是否阻塞或异常。
如果模型已经返回 token,但前端收不到,要看 SSE send 是否阻塞、客户端是否断开、网络是否异常、是否出现 broken pipe、timeout。
如果模型日志显示 token 已经返回,但前端没有显示,就要看 SSE。
重点看:
SSE send 是否抛异常
客户端是否断开
是否 broken pipe
是否 timeout
是否网络代理缓冲
是否响应头正确
是否 emitter 没有 flush
如果 SSE 写出阻塞或异常,没有正确释放资源,还会影响后续请求排队。
排查时,我会看同一个 traceId 的事件序列:
WAITING
START
MESSAGE
ERROR
DONE
如果只有 START 没有 MESSAGE,要看模型首包。
如果有 MESSAGE 但前端没显示,要看网络和前端 EventSource。
如果 ERROR 后没有 complete,要看资源释放逻辑。
优化上,统一封装 SSE sender,所有异常路径都 completeWithError,并释放 permit、清理上下文、更新请求状态。
这里的取舍是:SSE 提升体验,但长连接状态管理更复杂。所以我不会把它当普通 HTTP 返回处理,而是必须管理断连、超时和资源释放。
第八,看 基础依赖资源。
包括 Redis、向量库、模型服务、数据库连接池、RocketMQ、机器 CPU、内存、GC。如果数据库连接池打满,权限查询和会话查询都会慢;如果 Redis 慢,排队和限流状态也会异常;如果向量库慢,检索节点会变慢。
容易出现的问题是:只看接口总耗时,但不知道时间花在哪一步。RAG 链路长,如果没有 Trace,很容易误判。比如用户说“AI 慢”,但实际可能是排队 25 秒,模型首包只用了 2 秒;也可能是检索 1 秒、Rerank 20 秒、模型 5 秒。
优化上,可以从几个方向处理:
排队慢:调整并发阈值、缩短等待超时、优化 permit 释放、增加用户侧排队提示
线程池满:按任务隔离线程池,限制队列长度,排查线程卡点
检索慢:优化权限过滤、向量索引、TopK、知识库范围
Rerank 慢:限制候选数量,设置短超时,失败跳过
模型首包慢:压缩 Prompt、减少无关 chunk、模型路由、fallback、首包探测
SSE 异常:断连释放资源,统一 onError/onTimeout/onCompletion 清理
依赖慢:数据库连接池、Redis、向量库、模型服务分别做监控和告警
我会分别看:
Redis
队列 ZSET 操作是否慢
连接池是否满
Lua 执行是否耗时
Redis 是否有慢命令
向量库
检索耗时
TopK
过滤条件
collection / table 数据量
连接池
索引状态
模型服务
首包耗时
失败率
超时率
限流错误
当前模型健康状态
数据库
连接池 active / idle / wait
慢 SQL
权限查询耗时
会话查询耗时
Trace 落库耗时
实际使用中容易出现的问题是,数据库连接池打满后,很多节点都会慢,比如会话读取、权限过滤、Trace 写入都会受影响,看起来像整个系统都慢。
排查时,我会把依赖指标和 Trace 节点耗时对齐,看哪个依赖变慢和哪个节点变慢同时发生。
优化上,依赖层都要有超时、连接池监控、慢查询监控和隔离策略。
这里的取舍是:下游慢时不能盲目放大并发,否则会雪崩。所以我会优先保护下游,必要时拒绝或降级。