第14章 Sentinel 限流与 AI 降级
本章目标
第 13 章我们讲了 Redis。
Redis 解决的是高频读取、短期状态和任务并发协调问题。
这一章继续讲系统稳定性。
KnowHub 中最需要稳定性保护的接口,是知识库问答接口。
原因很简单:一次问答不是一次普通数据库查询。
它背后可能包含:
用户鉴权
知识库归属校验
问题 Embedding
pgvector 检索
Prompt 构造
聊天模型调用
qa_log 写入
qa_reference 写入
其中聊天模型还是外部依赖。
外部依赖可能变慢、可能失败、可能被限流,也可能产生较高费用。
所以一个企业级 RAG 平台不能只关心“模型能不能回答”,还要关心:
请求太多时怎么办?
模型超时时怎么办?
模型异常时怎么办?
用户应该看到什么?
后台应该记录什么?
本章要讲清楚:
- 为什么 AI 问答接口必须限流。
- Sentinel 在 KnowHub 中保护哪个入口。
- Sentinel 的资源名、QPS 和 blockHandler 是什么。
- 本地限流规则和生产动态规则有什么区别。
- AI 模型调用为什么要设置 timeout。
KnowledgeChatFallbackProperties如何配置降级。CompletableFuture如何配合线程池做超时控制。modelSuccess、modelFallback、modelErrorMessage分别表示什么。- 限流、降级、无检索结果和业务异常有什么区别。
- 常见限流和降级问题如何排查。
下面是本章的整体结构图:
14.1 为什么 AI 问答接口必须保护
普通业务接口通常只访问本地数据库。
例如:
查询知识库列表
查询文档列表
查询问答日志
这些接口虽然也需要性能优化,但它们的成本和不确定性相对可控。
AI 问答接口不同。
它至少有三个特点。
14.1.1 单次请求成本高
一次问答可能会调用:
Embedding 模型
聊天模型
向量数据库
MySQL
Redis
其中模型调用通常比普通 SQL 更慢,也可能产生调用费用。
如果没有控制,用户连续点击、脚本刷接口或前端重试,都可能快速消耗模型额度。
14.1.2 外部依赖不稳定
聊天模型服务可能出现:
网络波动
接口超时
模型限流
供应商故障
返回空内容
如果问答接口一直等待模型返回,线程会被长期占用。
请求堆积后,可能拖慢整个 knowledge-service。
14.1.3 用户需要明确反馈
当系统繁忙或模型不可用时,用户不能一直等待。
更合理的方式是快速返回明确提示。
例如:
当前 AI 问答服务繁忙,请稍后重试。
或者:
当前 AI 服务暂时不可用,请稍后重试。
前者是入口限流。
后者是模型调用降级。
这两者不能混淆。
14.2 两道稳定性保护线
Sentinel 入口保护和 AI fallback 模型调用保护的基本区分,已经在第 12 章 12.9 节讲过。本章不再重复展开概念,只强调落地时要关注的配置、默认行为和调参边界:Sentinel 管“请求能不能进入问答入口”,fallback 管“模型调用失败或超时时怎么快速返回”。
在 KnowHub 中,这两道保护线分别对应下面几组配置:
Sentinel 入口限流
配置类:KnowledgeChatFlowRuleProperties
YAML 前缀:rag.sentinel.chat-flow
默认行为:enabled=true,resource=knowledgeChat,qps=1,便于本地演示限流
AI fallback 降级
配置类:KnowledgeChatFallbackProperties
YAML 前缀:rag.ai.chat.fallback
默认行为:enabled=true,timeout=5s,模型超时或异常时返回降级答案
两者都关闭时,问答接口仍然可以运行,但稳定性会明显下降:
关闭 Sentinel:入口请求不会被 QPS 保护,可能瞬时打爆模型调用链路。
关闭 fallback:模型慢或失败时,接口会一直等待或直接抛出异常。
所以第 12 章解决“代码怎么串起来”,本章解决“这些参数怎么调、出问题怎么判断、生产环境怎么演进”。
下面是两道保护线的分工示意图:
14.3 Sentinel 的基本概念
Sentinel 的资源名和 @SentinelResource 注解写法,第 12 章 12.3.2 已经给出。本章只保留和调参直接相关的部分:资源名必须稳定,QPS 是入口放行速率,blockHandler 是限流后的友好返回。
当前 KnowHub 用到的是最基础、最容易理解的一种:
QPS 限流
QPS 是 Queries Per Second,每秒请求数。
如果设置:
qps = 1
表示某个资源每秒最多通过 1 个请求。超过的请求会被 Sentinel 拦截,并进入 blockHandler。
14.3.1 资源名
本章只建议把资源名抽成常量,避免 Controller 注解、YAML 配置和规则初始化类中写出多个不同字符串:
public final class SentinelResourceNames {
private SentinelResourceNames() {
}
public static final String KNOWLEDGE_CHAT = "knowledgeChat";
}
然后在配置中保持一致:
rag:
sentinel:
chat-flow:
resource: knowledgeChat
资源名不一致,是 Sentinel 限流不生效最常见的原因。
14.3.2 blockHandler
blockHandler 的完整写法详见第 12 章 12.3.2。本章只强调两点:
第一,blockHandler 只处理 Sentinel 拦截,不处理模型超时。
第二,blockHandler 的方法签名必须和原方法匹配,并额外接收 BlockException。
限流返回文案应该和模型 fallback 文案区分开:
入口限流:当前 AI 问答服务繁忙,请稍后重试。
模型降级:当前 AI 服务暂时不可用,请稍后重试。
这能帮助前端和管理员快速判断问题发生在入口还是模型调用阶段。
14.4 KnowHub 的 Sentinel 本地规则
Sentinel 规则配置类是:
KnowledgeChatFlowRuleProperties
配置前缀是:
rag:
sentinel:
chat-flow:
enabled: true
resource: knowledgeChat
qps: 1
这三个配置分别表示:
`enabled`:是否启用问答限流
`resource`:资源名
`qps`:每秒允许通过的请求数
规则初始化类是:
KnowledgeChatSentinelRuleConfig
它实现了:
ApplicationRunner
应用启动后会执行:
FlowRule rule = new FlowRule();
rule.setResource(properties.getResource());
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(properties.getQps());
FlowRuleManager.loadRules(List.of(rule));
翻译成业务语言就是:
应用启动时,为 `knowledgeChat` 资源加载一条 QPS 限流规则。
补充 KnowledgeChatFlowRuleProperties 完整代码:
package com.luo.ragknowledge.qa.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 知识库问答 Sentinel 限流规则配置。
*
* 三个字段最终会映射到 Sentinel FlowRule:
* enabled 控制是否加载规则,resource 对应 FlowRule.resource,qps 对应 FlowRule.count。
*/
@ConfigurationProperties(prefix = "rag.sentinel.chat-flow")
public class KnowledgeChatFlowRuleProperties {
/**
* 是否启用问答接口限流。
*/
private boolean enabled = true;
/**
* Sentinel 资源名,必须和 @SentinelResource(value = "...") 保持一致。
*/
private String resource = "knowledgeChat";
/**
* 每秒允许通过的请求数,对应 FlowRule.count。
* 学习项目默认 1,便于手工触发限流。
*/
private int qps = 1;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getResource() {
return resource;
}
public void setResource(String resource) {
this.resource = resource;
}
public int getQps() {
return qps;
}
public void setQps(int qps) {
this.qps = qps;
}
}
补充 KnowledgeChatSentinelRuleConfig 完整代码:
package com.luo.ragknowledge.qa.config;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 知识库问答 Sentinel 本地限流规则初始化器。
*
* ApplicationRunner 会在 Spring Boot 启动完成后执行,
* 这样规则能在服务正式接收流量前加载完成。
*/
@Component
public class KnowledgeChatSentinelRuleConfig implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(KnowledgeChatSentinelRuleConfig.class);
private final KnowledgeChatFlowRuleProperties properties;
public KnowledgeChatSentinelRuleConfig(KnowledgeChatFlowRuleProperties properties) {
this.properties = properties;
}
@Override
public void run(ApplicationArguments args) {
if (!properties.isEnabled()) {
log.info("Sentinel 问答限流规则未启用,跳过加载");
return;
}
FlowRule rule = new FlowRule();
rule.setResource(properties.getResource());
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(properties.getQps());
FlowRuleManager.loadRules(List.of(rule));
log.info("Sentinel 问答限流规则已加载,resource={},qps={}",
properties.getResource(), properties.getQps());
}
}
14.4.1 为什么默认 1 QPS
项目默认:
qps = 1
这不是生产建议值。
它主要适合本地学习和演示。
因为 1 QPS 很容易触发限流,读者可以快速看到效果。
例如短时间连续点击两次问答按钮,第二次就可能被限流。
生产环境中,需要根据实际情况调整。
参考因素包括:
服务器线程数
模型服务额度
模型平均响应时间
pgvector 查询性能
用户并发量
预算成本
14.4.2 本地规则和生产规则
当前项目使用代码本地加载规则。
这适合学习和演示。
但生产环境通常不会把规则写死在应用启动逻辑中。
更常见的是:
Sentinel Dashboard
Nacos 动态数据源
配置中心动态推送
这样可以在不重启服务的情况下调整限流规则。
教材中先使用本地规则,是为了让读者更容易理解最小闭环。
下面是 Sentinel 本地规则加载流程:
14.5 AI Fallback 配置
fallback 的基础用途、YAML 配置和完整调用流程,已经在第 12 章 12.8 节讲过。本章只从调参角度补充两点:timeout 应该略高于模型正常 P95 耗时,enabled 默认建议为 true,因为外部模型服务不可控,关闭 fallback 等于把模型不稳定性直接暴露给用户。
如果模型平均 1 到 2 秒返回,timeout = 5s 通常足够。如果模型平均已经接近 5 秒,继续把 timeout 拉长并不是首选方案,应该优先检查 Prompt 是否过长、TopK 是否过大、模型服务是否稳定。
配置仍然是:
rag:
ai:
chat:
fallback:
enabled: true
timeout: 5s
answer: 当前 AI 服务暂时不可用,请稍后重试。
14.5.1 fallback 不是正常答案
完整 fallback 流程和调用代码详见第 12 章 12.8 节。本章只强调一个工程约束:降级答案必须通过 modelFallback=true 标记出来,不能当成模型正常回答,否则问答质量统计和用户界面都会失真。
14.6 模型调用超时控制
KnowledgeChatServiceImpl 中的模型调用方法是:
callModelWithFallback(...)
如果 fallback 没有启用,代码会直接同步调用模型:
String answer = callModel(prompt);
如果 fallback 启用,则走异步调用和超时控制。
核心代码是:
CompletableFuture<String> future = CompletableFuture.supplyAsync(
() -> callModel(prompt), knowledgeChatExecutor);
String answer = future.get(
fallbackProperties.getTimeout().toMillis(),
TimeUnit.MILLISECONDS);
这段代码可以分成两步理解。
第一步,把模型调用放到独立线程池中执行。
第二步,主线程最多等待 timeout 时间。
如果模型按时返回,就采用正常答案。
如果超时,就返回 fallback。
补充 callModelWithFallback 完整代码:
private ModelCallResult callModelWithFallback(Long kbId, Long userId, String prompt) {
// fallback 关闭时,同步调用模型。失败时返回 failure,由上层记录 model_success=0。
if (!fallbackProperties.isEnabled()) {
try {
String answer = callModel(prompt);
return ModelCallResult.success(answer);
} catch (Exception ex) {
String errorMessage = shortErrorMessage(ex);
log.warn("AI 模型调用失败,fallback 未启用:kbId={}, userId={}, error={}",
kbId, userId, errorMessage);
return ModelCallResult.failure(errorMessage);
}
}
CompletableFuture<String> future = CompletableFuture.supplyAsync(
() -> callModel(prompt),
knowledgeChatExecutor
);
try {
String answer = future.get(fallbackProperties.getTimeout().toMillis(), TimeUnit.MILLISECONDS);
return ModelCallResult.success(answer);
} catch (TimeoutException ex) {
// 超时后尝试取消任务,避免后台继续占用线程。
future.cancel(true);
String errorMessage = "AI 模型调用超时,timeout=" + fallbackProperties.getTimeout();
log.warn("AI 模型调用超时,timeout={},kbId={},userId={}",
fallbackProperties.getTimeout(), kbId, userId);
return ModelCallResult.fallback(fallbackProperties.getAnswer(), errorMessage);
} catch (InterruptedException ex) {
// 恢复中断标记,这是 Java 并发中的基本习惯。
Thread.currentThread().interrupt();
String errorMessage = "AI 模型调用线程被中断";
log.warn("AI 模型调用线程被中断,kbId={},userId={}", kbId, userId);
return ModelCallResult.fallback(fallbackProperties.getAnswer(), errorMessage);
} catch (ExecutionException ex) {
Throwable cause = ex.getCause();
String errorMessage = shortErrorMessage(cause);
log.warn("AI 模型调用执行异常,kbId={},userId={},error={}",
kbId, userId, errorMessage);
return ModelCallResult.failure(errorMessage);
}
}
这段代码体现了三层判断:
fallback 未启用:同步调用,异常返回 failure
fallback 已启用且超时:返回 fallbackAnswer
fallback 已启用但模型内部异常:记录短错误信息,返回 failure
14.6.1 为什么需要独立线程池
补充 AsyncConfig 完整代码:
package com.luo.ragknowledge.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("knowledgeChatExecutor")
public Executor knowledgeChatExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("knowledge-chat-");
// 队列满时由调用线程执行,形成天然背压,避免任务被静默丢弃。
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
线程池大小不能孤立设置。它必须和模型平均耗时、期望 QPS、模型服务额度一起看。CallerRunsPolicy 的意义是:当线程池和队列都满了,调用方线程会自己执行任务,接口自然变慢,从而形成背压。
问答模型调用使用的线程池是:
knowledgeChatExecutor
配置在 AsyncConfig 中:
executor.setCorePoolSize(2);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("knowledge-chat-");
这表示模型调用不会无限制创建线程。
线程池有核心线程、最大线程和队列容量。
如果模型服务变慢,线程池能限制并发规模,避免系统无限堆积模型调用线程。
14.6.2 future.cancel(true) 的作用
超时时代码会执行:
future.cancel(true);
它表示尝试取消这次异步任务。
需要注意,取消不一定能立刻中断底层 HTTP 调用。
这取决于底层客户端是否响应中断。
但对业务接口来说,已经可以先返回降级答案,避免用户一直等待。
下面是 callModelWithFallback 的完整调用流程:
14.7 异常分类处理
模型调用失败并不只有一种情况。
项目中主要处理三类异常。
14.7.1 TimeoutException
超时异常表示模型在指定时间内没有返回。
项目会记录:
AI 模型调用超时,timeout=5s
并返回 fallback。
这种情况通常要排查:
模型服务是否慢
网络是否慢
Prompt 是否过长
timeout 是否设置过短
14.7.2 InterruptedException
线程被中断时,项目会先恢复中断标记:
Thread.currentThread().interrupt();
然后返回 fallback。
恢复中断标记是良好的 Java 并发习惯。
它告诉上层:这个线程曾经被中断过。
14.7.3 ExecutionException
ExecutionException 表示异步任务执行过程中抛出了异常。
比如:
模型接口返回错误
网络连接失败
认证失败
HTTP 客户端异常
项目会提取错误信息,并截断到 500 字以内:
shortErrorMessage(ex.getCause())
这样可以避免超长异常写入日志或数据库。
补充 shortErrorMessage 工具方法:
private String shortErrorMessage(Throwable cause) {
if (cause == null) {
return "未知错误";
}
String message = cause.getMessage();
if (!StringUtils.hasText(message)) {
message = cause.getClass().getSimpleName();
}
return message.length() > 500 ? message.substring(0, 500) + "..." : message;
}
这个方法不要直接返回完整堆栈。堆栈应该写应用日志,qa_log.model_error_message 只保存便于后台筛选的短错误原因。
14.8 模型状态字段
modelCalled、modelSuccess、modelFallback 三个布尔字段的组合含义,已经在第 12 章 12.12.4 节讲过。本章只强调排查视角:这三个字段要和 modelErrorMessage 一起看,才能区分“没有资料”“模型失败”“入口限流”和“业务异常”。
14.8.4 modelErrorMessage
modelErrorMessage 记录模型失败原因。
例如:
AI 模型调用超时,timeout=PT5S
或者:
Connection reset
项目会把错误信息截断到 500 字以内。
原因有两个:
第一,避免超长异常把 `qa_log` 表撑得过大。
第二,避免把第三方接口返回的冗长错误直接暴露给后台页面。
完整堆栈应该写应用日志,数据库只保存排查所需的短原因。
14.9 qa_log 中的稳定性指标
retrieval_cost_time_ms、model_cost_time_ms、cost_time_ms 三种耗时的基础含义和排查价值,已经在第 12 章 12.10 节讲过。本章只保留管理员排查时最常用的筛选方式。
14.9.4 model_success 和 model_fallback
这两个字段能快速筛选问题。
查询最近发生过 AI 降级的问答:
SELECT id, user_id, kb_id, question, model_error_message, created_at
FROM qa_log
WHERE model_fallback = 1
ORDER BY created_at DESC
LIMIT 20;
查询模型调用失败但没有走 fallback 的记录:
SELECT id, user_id, kb_id, question, model_error_message, created_at
FROM qa_log
WHERE model_success = 0
AND model_fallback = 0
AND model_error_message IS NOT NULL
ORDER BY created_at DESC
LIMIT 20;
查询模型耗时异常高的记录:
SELECT id, user_id, kb_id, model_cost_time_ms, question
FROM qa_log
WHERE model_cost_time_ms > 5000
ORDER BY model_cost_time_ms DESC
LIMIT 20;
这就是把稳定性问题从“用户说慢”变成“数据库里可以查”的基础。
14.10 限流、降级、无检索结果和业务异常
这四类情况很容易混在一起。
必须分清楚。
14.10.1 限流
限流发生在入口。
请求没有进入完整问答流程。
典型返回:
当前 AI 问答服务繁忙,请稍后重试。
它对应 Sentinel 的 blockHandler。
14.10.2 降级
降级发生在模型调用阶段。
请求已经进入问答流程,也已经检索到了资料,但模型调用超时或失败。
典型返回:
当前 AI 服务暂时不可用,请稍后重试。
响应中:
modelFallback = true
14.10.3 无检索结果
无检索结果不是限流,也不是降级。
它表示知识库里没有召回相关资料。
典型返回:
知识库中未检索到相关内容,无法确定。
响应中:
modelCalled = false
modelFallback = false
14.10.4 业务异常
业务异常包括:
未登录
无权限
知识库不存在
参数错误
向量库未启用
这类问题应该通过统一异常处理返回业务错误。
它们和限流、模型降级不是一类问题。
下面是四类情况的区分决策图:
14.11 参数调优
补充一个经验型调优决策表:
模型平均耗时 1 到 2 秒
QPS:5 到 10
timeout:3 到 5 秒
线程池 corePoolSize:QPS 的 1 到 2 倍
模型平均耗时 3 到 5 秒
QPS:2 到 5
timeout:5 到 8 秒
线程池 corePoolSize:QPS 的 2 倍
模型平均耗时超过 5 秒
不建议先盲目提高 timeout
应优先优化 Prompt 长度、TopK、chunkSize 或更换更快的模型服务
这只是学习项目的经验参考值。生产环境必须通过压测和真实 qa_log 指标确定,重点看平均耗时、P95 耗时、错误率和模型服务限额。
稳定性保护不是配置一次就永远不用管。
随着用户量、文档量和模型速度变化,参数也要调整。
14.11.1 QPS 怎么调
本地演示默认:
qps = 1
生产环境要结合:
服务器 CPU 和内存
模型服务额度
模型平均耗时
线程池大小
用户并发量
成本预算
如果模型平均 3 秒返回,QPS 配太高,很容易堆积。
如果模型额度充足、服务稳定、机器资源足够,可以适当提高。
14.11.2 timeout 怎么调
默认 timeout 是:
5s
如果 timeout 太短,模型可能还没来得及返回就被降级。
如果 timeout 太长,用户等待时间会变长,接口线程占用也会增加。
调参时要看:
模型平均耗时
P95 耗时
用户可接受等待时间
Prompt 长度
网络延迟
14.11.3 线程池怎么调
knowledgeChatExecutor 当前配置:
corePoolSize = 2
maxPoolSize = 4
queueCapacity = 50
线程池太小,模型调用容易排队。
线程池太大,可能同时打太多模型请求,导致外部服务限流或本机资源紧张。
队列太长,用户可能排队等待很久。
这些都需要根据实际压力测试调整。
14.11.4 Prompt 长度也会影响稳定性
补充一个具体估算。
如果配置是:
TopK = 5
chunkSize = 800
overlap = 100
那么每个 chunk 的有效新增内容大约是 700 字符,5 个 chunk 的上下文大约是:
5 * 700 = 3500 字符
再加上系统 Prompt 规则、引用编号、来源文档名和用户问题,总 Prompt 很容易达到:
4500 到 5000 字符
如果模型在这个长度下明显变慢,可以按下面顺序优化:
第一,把 TopK 从 5 降到 3。
第二,把 chunkSize 从 800 降到 500。
第三,对 context 做总长度截断,例如超过 3000 字符只取前 3000 字符。
不要只调大 timeout。timeout 变长只是让用户等更久,不会让模型变快。
Prompt 越长,模型调用通常越慢,成本也越高。
Prompt 长度受这些因素影响:
TopK
chunkSize
引用片段长度
Prompt 规则文本
所以限流和降级不是孤立优化。
它们和前面章节的切片、TopK、阈值都有关。
14.12 常见问题排查
14.12.1 Sentinel 限流不生效
排查顺序:
rag.sentinel.chat-flow.enabled是否为 true。@SentinelResource的 value 是否是knowledgeChat。- 配置中的
resource是否也是knowledgeChat。 KnowledgeChatSentinelRuleConfig是否执行。FlowRuleManager.loadRules(...)是否加载规则。- 请求是否真的打到了
POST /kb/{kbId}/chat。
资源名不一致是最常见问题。
14.12.2 一直被限流
排查顺序:
- QPS 是否设置太低。
- 是否本地连续快速点击。
- 前端是否自动重试。
- 是否有多个页面同时请求。
- 压测脚本是否没有限速。
本地默认 1 QPS 很容易触发限流。
14.12.3 blockHandler 不执行
排查顺序:
- 方法名是否和注解中的
blockHandler一致。 - 方法参数是否匹配原方法,并额外接收
BlockException。 - 方法是否在同一个类中。
- Sentinel 依赖是否正常。
- 资源是否真的触发限流。
14.12.4 fallback 不返回
排查顺序:
rag.ai.chat.fallback.enabled是否为 true。- 模型调用是否进入
callModelWithFallback。 - 是否有检索结果。没有检索结果不会调用模型,也不会 fallback。
- timeout 是否设置过长。
- 异常是否发生在 fallback 外层流程。
14.12.5 明明超时但接口仍然等很久
排查顺序:
timeout配置是否被正确读取。- 时间单位是否正确,例如
5s。 - 模型调用是否走了异步分支。
- fallback 是否被关闭。
- 线程池是否已经排队导致等待位置和预期不同。
14.12.6 modelFallback=true 但没有错误原因
排查:
model_error_message是否写入qa_log。shortErrorMessage是否拿到了异常 message。- 是否是 InterruptedException 或 TimeoutException。
- 数据库字段长度是否足够。
项目中错误信息会截断到 500 字,避免超长异常写入数据库。
14.12.7 降级答案被当成正常答案展示
前端应该根据:
modelFallback
区分正常答案和降级答案。
如果 modelFallback=true,可以用提示样式告诉用户:
本次 AI 服务不可用,系统返回了临时提示。
不能把降级答案当成知识库回答。
14.12.8 模型调用首次很慢但后续变快(或相反)
现象:
第一次问答的 model_cost_time_ms 明显高于后续请求,
或者刚启动时正常,运行一段时间后逐渐变慢。
排查顺序:
第一,确认是否是模型服务端首次加载模型导致的预热延迟。首次调用很慢很常见,可以在服务启动后主动发起一次预热请求。
第二,检查 knowledgeChatExecutor 是否被占满。重点看活跃线程数和队列深度。
第三,检查是否有慢任务长期占用线程没有释放,例如 Prompt 过长导致模型生成时间远超预期。
第四,检查 JVM 是否触发 Full GC,导致 Stop The World 延迟。
14.12.9 Sentinel 规则在生产环境重启后丢失
现象:
服务重启后,限流规则恢复为代码中的默认值,
之前通过 Dashboard 或动态配置调整的规则丢失。
排查顺序:
第一,确认规则加载方式是代码本地加载,还是从配置中心读取。如果是代码本地加载,重启后一定恢复默认值。
第二,确认是否配置了 Sentinel Dashboard 或 Nacos 作为动态数据源。
第三,检查动态数据源连接配置是否在服务重启后正确加载。
学习项目使用代码本地加载足够直观。生产环境建议把 Sentinel 规则迁移到配置中心管理,避免重启后规则丢失,也方便运行时调整。
14.13 本章和前面章节的关系
第 14 章建立在前面几章之上。
第 11 章的 pgvector 检索如果很慢,会影响问答接口整体耗时。
第 12 章的 Prompt 如果太长,会影响模型调用耗时。
第 13 章的 Redis owner 缓存可以减少问答前置权限校验压力。
第 14 章的 Sentinel 和 fallback,则是在这些链路外再加稳定性保护。
可以这样理解:
Redis:减少重复访问。
Sentinel:限制入口流量。
fallback:模型不可用时快速返回。
qa_log:记录发生了什么。
这几块组合起来,才是一个更接近企业项目的 RAG 平台。
本章小结
这一章我们讲了 Sentinel 限流与 AI 降级。
AI 问答接口成本高、耗时长、依赖外部模型服务,所以必须做稳定性保护。KnowHub 使用 Sentinel 对 knowledgeChat 资源做 QPS 限流,超过阈值时通过 chatBlockHandler 返回“当前 AI 问答服务繁忙,请稍后重试”。
Sentinel 保护的是接口入口。项目通过 KnowledgeChatSentinelRuleConfig 在应用启动时加载本地 QPS 规则,默认 1 QPS,适合本地演示。生产环境通常会把规则交给 Sentinel Dashboard 或 Nacos 动态管理。
AI fallback 保护的是模型调用阶段。项目通过 KnowledgeChatFallbackProperties 配置是否启用降级、模型最长等待时间和降级答案。KnowledgeChatServiceImpl 使用 CompletableFuture 和 knowledgeChatExecutor 调用模型,并通过 timeout 控制最长等待时间。
模型超时、线程中断或执行异常时,系统会返回降级答案,并记录 modelSuccess=false、modelFallback=true 和 modelErrorMessage。无检索结果、入口限流、模型降级和业务异常是四类不同情况,不能混为一谈。
下一章,我们会进入调用日志、异常演练与问题排查,把前面章节中的日志字段、错误场景和排查路径系统化整理出来。
本章涉及的关键类与文件
knowledge-service/src/main/java/.../
config/
sentinel/
KnowledgeChatFlowRuleProperties.java (Sentinel QPS 限流配置)
KnowledgeChatSentinelRuleConfig.java (启动时加载 Sentinel 规则)
async/
AsyncConfig.java (模型调用专用线程池)
fallback/
KnowledgeChatFallbackProperties.java (AI 降级配置)
qa/service/impl/
KnowledgeChatServiceImpl.java (callModelWithFallback、shortErrorMessage)
resources/
application.yml (rag.sentinel.chat-flow、rag.ai.chat.fallback 配置)
动手验证:限流与降级行为确认
步骤一,对应 14.4 节和配置前缀 rag.sentinel.chat-flow:启动 knowledge-service,查看启动日志中是否有:
Sentinel 问答限流规则已加载,resource=knowledgeChat,qps=1
步骤二,对应 14.3 和 14.4 节:快速连续发送两次:
POST /kb/{kbId}/chat
两次间隔小于 1 秒。预期第二次请求返回:
当前 AI 问答服务繁忙,请稍后重试。
HTTP 状态码可以是 429,也可以是项目统一业务错误码,关键是语义要明确。
步骤三,对应配置前缀 rag.sentinel.chat-flow:把 qps 临时调高到 10,或者把 enabled 改成 false,重启服务后再次连续发送两次请求。预期第二次不再被限流。
步骤四,对应 14.5 和 14.6 节,以及配置前缀 rag.ai.chat.fallback:把 fallback timeout 临时改为:
rag:
ai:
chat:
fallback:
timeout: 1s
发送一个正常问答请求。如果模型调用通常超过 1 秒,预期收到:
当前 AI 服务暂时不可用,请稍后重试。
并且响应中:
modelFallback = true
步骤五,对应 14.5 节:把 timeout 改回 5s,再次问答。预期 modelSuccess=true,不再走降级。
步骤六,对应 14.9 节:查询最近一条被降级的记录:
SELECT id, model_success, model_fallback, model_error_message
FROM qa_log
ORDER BY created_at DESC
LIMIT 1;
预期:
model_success = 0
model_fallback = 1
model_error_message 包含“超时”或 “timeout”
步骤七,对应 14.10 节:验证无检索结果不是降级。传入一个和所有文档都无关的问题,预期返回:
知识库中未检索到相关内容,无法确定。
modelCalled = false
modelFallback = false
如果 Sentinel 限流不生效,优先检查 @SentinelResource 注解中的 value 和 YAML 中的 resource 是否都是 knowledgeChat。资源名不一致是最常见的问题。
思考题
-
为什么 AI 问答接口比普通查询接口更需要限流?
因为一次问答不是一次普通数据库查询,它背后包含用户鉴权、知识库归属校验、问题 Embedding、pgvector 检索、Prompt 构造、聊天模型调用、qa_log 写入等多个步骤,其中聊天模型还是外部依赖。模型调用通常比普通 SQL 更慢,也可能产生调用费用。如果没有控制,用户连续点击、脚本刷接口或前端重试,都可能快速消耗模型额度,甚至打爆模型调用链路。
-
Sentinel 的 resource 名称为什么必须和配置中的 resource 保持一致?
因为 Sentinel 是通过资源名来匹配限流规则的。
@SentinelResource注解中的 value、YAML 配置中的 resource、规则初始化类中FlowRule.setResource(...)设置的值,三者必须指向同一个字符串。只要有一处不一致,请求进入的资源名就匹配不到已加载的规则,限流就不会生效。资源名不一致是 Sentinel 限流不生效最常见的原因。 -
blockHandler的作用是什么?blockHandler是 Sentinel 限流后的友好返回方法。当请求超过 QPS 阈值被 Sentinel 拦截时,会进入blockHandler而不是抛出异常。它只处理 Sentinel 拦截,不处理模型超时。方法签名必须和原方法匹配,并额外接收BlockException。限流返回文案应该和模型 fallback 文案区分开,帮助前端和管理员快速判断问题发生在入口还是模型调用阶段。 -
本地 Sentinel 规则和生产动态规则有什么区别?
本地规则是在应用启动时通过
KnowledgeChatSentinelRuleConfig用代码加载的,规则写死在启动逻辑中,适合学习和演示,但调整规则需要重启服务。生产动态规则通常使用 Sentinel Dashboard、Nacos 动态数据源或配置中心动态推送,可以在不重启服务的情况下调整限流规则。学习项目先使用本地规则,是为了让读者更容易理解最小闭环。 -
fallback 解决的是入口请求过多,还是模型调用失败?
fallback 解决的是模型调用失败或超时。它发生在模型调用阶段,请求已经进入问答流程、也已经检索到了资料,但模型调用超时或异常时,快速返回降级答案。入口请求过多由 Sentinel 限流解决,两者是两道不同的保护线,不能混淆。
-
为什么模型调用需要 timeout?
因为聊天模型是外部依赖,可能慢、可能失败、可能限流。如果问答接口一直等待模型返回,线程会被长期占用,请求堆积后可能拖慢整个 knowledge-service。设置 timeout 后,主线程最多等待指定时间,超时就返回降级答案,避免用户一直等待,也避免线程被无限期占用。
-
CompletableFuture在本项目的模型调用中起什么作用?CompletableFuture配合独立线程池knowledgeChatExecutor实现异步调用和超时控制。CompletableFuture.supplyAsync(() -> callModel(prompt), knowledgeChatExecutor)把模型调用放到独立线程池中执行,主线程通过future.get(timeout, TimeUnit.MILLISECONDS)最多等待 timeout 时间。如果模型按时返回就采用正常答案,超时就返回 fallback。 -
modelCalled=false和modelFallback=true分别代表什么?modelCalled=false表示没有调用模型,典型场景是无检索结果——知识库里没有召回相关资料,不会调用模型,也不会 fallback。modelFallback=true表示模型调用超时或失败后走了降级,返回了降级答案。两者是不同情况:前者是没到模型调用这一步,后者是模型调用失败后的降级。 -
modelErrorMessage为什么不应该无限长?有两个原因:第一,避免超长异常把
qa_log表撑得过大;第二,避免把第三方接口返回的冗长错误直接暴露给后台页面。项目通过shortErrorMessage把错误信息截断到 500 字以内,完整堆栈写应用日志,数据库只保存便于后台筛选的短错误原因。 -
如果接口一直被限流,你会先检查哪些配置?
排查顺序:第一,QPS 是否设置太低,本地默认 1 QPS 很容易触发限流;第二,是否本地连续快速点击;第三,前端是否自动重试;第四,是否有多个页面同时请求;第五,压测脚本是否没有限速。如果确认是正常流量被限流,再结合模型平均耗时、服务器资源、模型服务额度等调整 QPS。
-
如果模型超时但没有返回降级答案,你会如何排查?
排查顺序:第一,
rag.ai.chat.fallback.enabled是否为 true,fallback 是否被关闭;第二,模型调用是否真的进入了callModelWithFallback的异步分支;第三,是否有检索结果,没有检索结果不会调用模型也不会 fallback;第四,timeout 配置是否被正确读取、时间单位是否正确;第五,异常是否发生在 fallback 外层流程,导致降级答案没有返回。 -
为什么降级答案不能当成正常知识库答案?
因为降级答案不是模型基于检索资料生成的回答,而是模型超时或失败后的临时提示。如果把它当成正常答案展示,问答质量统计和用户界面都会失真。前端应该根据
modelFallback区分正常答案和降级答案,当modelFallback=true时用提示样式告诉用户“本次 AI 服务不可用,系统返回了临时提示”,不能把降级答案当成知识库回答。
更多推荐


所有评论(0)