4. RAG 检索增强生成特训指南
本指南结合您简历中 "sky-ai RAG 检索器实现 FAQ 知识库问答"、"PgvectorVectorStore 向量数据库 Bean 自动注入"、"RagAdvisor 检索上下文注入" 和 "离线文档处理与分块策略" 场景进行深度定制,全面采用 Why - What - How - Deep 极简源码/原理速成结构。
一、 RAG 全链路核心架构
Why(幻觉治理与开卷考试的必要性)
- 幻觉与时效黑洞:大模型的参数知识在预训练完成后就完全锁定,对于企业私有的内部 FAQ、实时订单数据、或是最新行业规范完全无知。若直接提问,模型会基于自回归采样编造虚假信息。
- 高昂的微调成本:通过 Full Fine-Tuning(全参数微调)注入私有知识,需要昂贵的算力和极高的数据集清洗开销,且一旦知识更新(如菜品价格变动),就必须重新微调,时效性极差。
- RAG 的降维打击:RAG 将问题转化为“检索事实”,在生成前将事实以“开卷考试”形式喂给大模型,模型仅负责阅读理解与润色,从根本上攻克了幻觉和时效性缺陷。
What(离线索引与在线生成双管道)
- RAG 架构物理上拆分为两条独立并行的管道:
【离线索引 Pipeline】 原始 FAQ 原始文件 ──> Parse (解析清洗) ──> Chunk (切分分块) │ ▼ 向量库 (Pgvector) <── Vector Store <── Embedding (高维向量化) 【在线生成 Pipeline】 用户提问 ──> Query 重写 ──> 向量相似度检索 ──> Cross-Encoder 重排 (Rerank) │ ▼ 最终回复 <── 大模型阅读生成 <── 织入 SystemMessage (RagAdvisor) <── Top-N 截断
How(sky-ai 中的检索注入流程)
sky-ai在在线管道中,使用RagAdvisor(优先级设为HIGHEST_PRECEDENCE + 3)在工具链过滤前强插拦截。RagAdvisor从用户请求中提取提问,调用OnlineRetrievalService触发相似度搜索与重排,获取精选候选块。- 将召回文本打包,以
"使用以下检索上下文回答问题:\n"前缀拼入SystemMessage强插在 Prompt 首位,最后 mutate 组装成不可变的 Request 向后传递。 - 对线重点:👉 跳至:sky-ai RAG 管道大厂对线
Deep(三方 Rerank API 线上超时崩溃 NullPointerException Bug 治理实战)
- 线上事故成因:早期版本中,我们为了提升重排精度,自研了
SiliconFlowRerankerClient对接三方 SiliconFlow Rerank 服务。然而,在面对高并发大流量时,三方 API 时常因为触发 429 限流或网络抖动响应超时。 - NPE 物理崩溃:我们在解析 JSON 响应时,由于把保底的重排分数值(Score)错写为了局部变量的自我赋值,导致当网络抛出异常时,返回给重排服务的是一个未被正确赋值分数的 Null 实体。当下游执行
chunk.score()时,直接触发NullPointerException崩毁整个 AI 客服请求链路。 - 自愈熔断降级重构:我们在
RerankingService中设计了完备的自愈拦截。一旦捕获到三方 Rerank API 超时或连接异常,立即激活 Fallback 降级开关,就地放弃三方重排,直接回退并退化到向量库检索时自带的相似度打分(embeddingScore)进行 Top-N 重新排序与截断。该自愈设计在完全不阻断核心业务响应的前提下,达成了 99.99% 的高可用性。 - 对线重点:👉 跳至:重排异常降级自愈大厂对线
二、 向量数据库选型与 HNSW / IVFFlat 索引底层
Why(高维向量检索 $\mathcal{O}(N)$ 的计算诅咒)
- 线性搜索计算灾难:要在一亿条向量知识库中寻找与当前用户 Query 相似度最高的一条,如果采用暴力搜索(Brute Force),必须用 Query 向量去逐一计算余弦距离,计算复杂度为 $\mathcal{O}(N \times D)$($N$ 是向量个数,$D$ 是维度,如 1536 维)。在高并发下,这会导致 CPU 瞬间被打满,查询延迟达到秒级。
- 近似最近邻(ANN)加速:为了打破这个屏障,向量数据库引入空间分区索引算法,将线性搜索降维,以牺牲极其微弱的精度为代价换取对数级 $\mathcal{O}(\log N)$ 的超快响应。
What(Pgvector 选型考量与工业界对比)
- 向量库选型物理格局:
数据库 物理形态 分布式架构 支持索引 适合数据量级 运维特征 Pgvector PG 原生扩展 依托 PG 本身 HNSW, IVFFlat 100万级以内 极简,无需引入额外中间件,事务一致 Milvus 独立专用向量库 微服务多节点分布式 HNSW, IVF, ScaNN 亿级以上 极其臃肿,存在 ZooKeeper/MinIO 运维开销 Qdrant Rust 专用向量库 分布式高可用 HNSW 千万级 性能极高,内存占用极大 - Pgvector 的绝对性价比:在 sky-ai 项目中,业务数据本就存储于 PostgreSQL。通过开启 pgvector 扩展,业务字段(如
dish_id)可以与向量字段(embedding)存放在同一张表里。单条 SQL 语句即可无缝支持混合过滤(如:先where status = 1过滤营业菜品,再计算向量距离),且天然支持 ACID 事务,完美解决向量数据与业务数据的强一致性冲突。 - 对线重点:👉 跳至:向量数据库工程选型大厂对线
How(Spring AI 自动装配原理)
- 在
pom.xml中引入spring-ai-pgvector-store-spring-boot-starter。 - 底层原理利用
@ConditionalOnClass(PgvectorVectorStore.class)条件注解,一旦类路径存在该类,且 YAML 中配置了正确的spring.datasource,Spring Boot 就会自动实例化并向 Spring 容器注入PgvectorVectorStore的全局 Singleton Bean,实现就地免代码配置装配。
Deep(HNSW 与 IVFFlat 底层索引数学机理)
- HNSW (Hierarchical Navigable Small World, 分层导航小世界):
【HNSW 导航跳跃搜索机理】 Layer 2 (稀疏粗层) ─────●─────────────────────────────● (大步跳跃寻优) │ │ Layer 1 (过渡层) ─────●─────────────●───────────────● │ │ │ Layer 0 (密集底层) ─────●──────●──────●──────●──────●──────● (精确滑行锁定)- 数学机理:借鉴了跳表(SkipList)的分层设计理念,构建一个多层图结构。最高层图结构极其稀疏,只保留极少数的采样向量;越往下层,节点和连接线越稠密,底层(Layer 0)包含全部物理向量。
- 跳跃寻优:检索时,从最高层的入口节点开始,进行局部贪婪梯度搜索。当在当前层找不到更近的邻居时,通过层间通路跳跃下降到下一层继续搜寻,在底层完成精准锁定。这种设计规避了全局遍历,检索时间复杂度暴降至对数级 $\mathcal{O}(\log N)$。
- 核心控制参数:
M(每个节点的最大邻居数,范围 4-64):设定值越大,高维空间关联密度越高,召回精度越强,但显存占用与建索引时间成平方级上升。efConstruction(构建图结构时的搜索候选集深度):值越大,建图越慢,但能产出完美的图拓扑结构。efSearch(查询时的动态扩展边界):查询时动态保留的最邻近候选集大小。设定 efSearch 偏大可以有效抑制“图断裂”导致的漏召回,但会增加耗时。
- IVFFlat (Inverted File Flat, 倒排文件扁平量化):
- 数学机理:基于传统的 K-Means 均值聚类。第一步,在构建索引时,通过聚类算法将全体高维向量划分并聚类为 $N$ 个虚拟的群簇中心(即参数
nlist)。 - 分组倒排:将每一个物理向量的 ID 挂载到其最接近的聚类中心的倒排表下。
- 探针查询:检索时,首先计算 Query 向量与所有聚类中心的余弦距离,仅挑出最接近的几个聚类中心(参数
nprobe,即探针深度),然后只在这些中心的倒排表内进行精确相似度暴力计算,将其余绝大多数无关类簇直接过滤,节省 90% 以上的计算开销。
- 数学机理:基于传统的 K-Means 均值聚类。第一步,在构建索引时,通过聚类算法将全体高维向量划分并聚类为 $N$ 个虚拟的群簇中心(即参数
三、 Hybrid Search(混合检索)与 RRF 融合打分
Why(纯向量检索与关键词检索的互补自救)
- 纯语义向量检索的盲区:Embedding 向量极其擅长“模糊语义匹配”,但在完全精确匹配场景表现极其差劲。例如,当用户提问:“帮我查下我的订单号是 sky-10023908 的进度”,Embedding 模型无法从“sky-10023908”这个无规律的代码哈希中提取出合理的语义向量,检索时极易漏召回正确的订单文档块。
- 关键词检索的盲区:传统的倒排索引关键词匹配(如 Elasticsearch 的 BM25 算法)对“同义词、语境、概念”完全瞎眼。如果用户提问:“这家店是否有过敏保护?”,但 FAQ 中记录的是“本店配备饮食禁忌防御机制”,因为关键词不重合,BM25 会完全无法检索。
- 混合检索的物理自救:必须将两路检索(语义向量 + 精准关键词)并联运行,取长补短,才能支撑生产级的业务诉求。
What(双路检索物理特征对比)
- 密集向量 (Dense Embedding) 检索:用浮点型密集矩阵表示,捕捉整段话的高维抽象语义(表达“想吃甜食”)。
- 稀疏向量 (Sparse Vector / BM25) 检索:捕捉极高频的特定字符匹配,进行高置信度物理精确打击(表达“sky-10023908”)。
How(RRF 合并打分去重服务)
sky-ai通过RetrievalFusionService实现了高性能的 RRF (Reciprocal Rank Fusion, 倒数排名融合) 算法:- 并行拉取向量语义检索与 BM25 关键词检索候选集。
- 利用多级哈希算法防重清洗:首先比对
chunkHash,再比对元数据chunkId,最后比对内容 MD5 值。 - 利用经典 RRF 倒数公式累加打分,并进行排序和 Top-N 过滤截断。
- 对线重点:👉 跳至:混合检索去重融合与 RRF 算法对线
Deep(RRF 公式推导与 uniqueKey 去重设计)
- RRF 倒数排名融合数学公式: $$RRF_Score(d \in D) = \sum_{m \in M} \frac{1}{k + r_m(d)}$$
- $M$ 代表检索管道的数量(这里是 2,即向量路和 BM25 路)。
- $r_m(d)$ 是文档 $d$ 在第 $m$ 个检索管道中被召回时的物理排名字次(例如排在第 1 位的 $rank = 1$,排在第 10 位的 $rank = 10$)。
- $k$(通常默认设为 60)是平滑因子(Hyperparameter)。引入 $k$ 的目的是防止排在第一二名的高名次文档获得过大的爆炸级分值,平衡低排名文档的合理贡献,拉平整体的归一化曲线。
- uniqueKey 多级去重的防污染哲学:
- 在高频检索或多文档重叠切分下,同一个 FAQ 文档块可能会因为不同的匹配路径(比如带空格和不带空格)被向量路和关键词路同时召回。
- 若直接无脑丢给 LLM,会在上下文中产生严重的信息冗余污染(Prompt Padding),挤占有限的 Context 窗口。
- 处理机制:我们在
uniqueKey()方法中,建立了严密的物理去重键生成链: $$\text{UniqueKey} = \text{Coalesce}(\text{Metadata.chunkHash}, \text{Metadata.chunkId}, \text{Hash(Chunk.content)})$$ 在向合并 Map 中添加元素时,利用该唯一键做就地合并。一旦发生 key 碰撞,就将两路检索的 RRF 贡献分叠加到同一个Accumulator中,既完成了物理去重,又合理体现了该文档块“在两路检索中名次均靠前”的极高相关度。
四、 高阶检索优化与知识库热更新策略
Why(数据生命周期维护与语义对齐)
- 静态知识库的老化死区:随着业务推进,菜品下线、价格调整、配送范围变化频繁发生。如果不做热更新,RAG 检索出的过期信息会直接误导模型,造成严重的交易合规风险。
- 自回归检索的结构不匹配:用户的 Query 通常是疑问句(“我怎么退款?”),而知识库中存的 Chunk 是陈述句(“退款流程为...”)。两者的特征空间向量存在物理夹角,导致检索召回率天然受阻。
What(重排、改写与增量热更新)
- 重排序 (Rerank):
- 在第一路相似度召回(如 HNSW)时,使用的是 Bi-Encoder (双塔架构)。 Query 和 Document 相互独立进行 Embedding 计算,丢失了交叉 Attention 信息。
- Rerank 采用 Cross-Encoder 架构。将 Query 和 Candidate Document 拼成一条长文本一次性输入大模型/重排模型,让它们在自注意力机制中发生深度的“全交互式两两 Attention 计算”,输出一个极高精度的语义相关性 Score,完成精准二次排序。
- 知识库更新策略:
- 增量更新 (MD5 碰撞):计算新增文件的 MD5 摘要与向量库存储的 MD5 元数据比对,若未发生变化直接跳过;若变化,则物理删除旧的 Chunk 向量并重构入库。
- 蓝绿部署全量重建:当更替底层 Embedding 模型(如从 768 维升级为 1024 维)时,必须进行全量数据重建。此时新建一个 shadow vector index 并在后台灌满数据,灌满后原子切换读写指针(Alias),实现 0-Downtime 无感知部署。
How(Reranking 流程编排)
sky-ai的OnlineRetrievalService在获取混合去重候选集后,优雅触发RerankingService的重排。- 重排完成后提取高精度 Top-N 传递给
ContextAssemblyService组装,并织入 AI 请求。 - 对线重点:👉 跳至:在线检索编排与重排异常降级自愈对线
Deep(Cross-Encoder vs Bi-Encoder 性能/精度博弈与 GraphRAG 机制)
- 计算开销与精度对比:
【Bi-Encoder 双塔检索 (速度极快,精度中等)】 Query ──> [Embedding] ──┐ ├──> [余弦相似度计算 (O(D) 极低开销)] ──> 候选集 Doc ──> [Embedding] ──┘ 【Cross-Encoder 单塔重排 (速度较慢,精度极高)】 [ Query + Doc ] ──> [ 深度 Transformer 交互 Attention ] ──> 精准 Score- Bi-Encoder:可以将所有 Doc 的向量提前预计算存入 Pgvector,在线仅需计算一次 Query Embedding 加上毫秒级的向量距离,适合第一阶段召回大规模候选集。
- Cross-Encoder:由于不能提前预计算,每次请求必须对
Top-50的每一个候选 Doc 都进行一次深度的 Transformer 前向推理,计算开销极其恐怖。如果在第一阶段直接用 Cross-Encoder 对百万文档直接打分,检索耗时将达数分钟。
- GraphRAG 构建精髓(Leiden 算法聚类):
- 传统 RAG 的致命短板是无法回答全局概括性问题(如:“帮我总结下这个月所有的客诉痛点有哪几类?”)。因为这些信息零散分布在成百上千个不同的 Chunks 中,Bi-Encoder 无法一次性把它们全部召回。
- GraphRAG 破局:第一步,利用大模型提取文档实体与关系,构建全局知识图谱。
- 第二步,利用 Leiden 社区发现算法,从拓扑结构上对关联紧密的实体网进行聚类,划分出多层级的“社区(Communities)”。
- 第三步,使用大模型为每个社区生成独立的“概括性摘要(Summary)”并存储。
- 全局搜索(Global Search)机制:当遭遇全局概括性提问时,直接在“社区摘要”这一层级进行 Map-Reduce 式的高层检索,完美解决了传统 RAG 面对宏观概括问题时的彻底漏招回和断裂痛点。
五、 简历亮点与大厂面试对线(袁志刚专属)
1. sky-ai RAG 管道与【RAG 全链路工程】
- 面试官切入点:
"你的 AI 客服项目中集成了 RAG 检索增强,请完整描述一下你的 RAG 链路——从文档入库到检索到生成的全过程。"
- 袁志刚专属特训回答模版:
- 离线索引管道 (Offline Pipeline):我们将原始知识库(支持 PDF/Markdown/FAQ 结构化文本)打入解析层,采用滑动窗口递归切分策略(Chunk 大小定为 500 Token,前置重叠 50 Token),避免关键信息因边缘切分而丢失。分块后的 Chunks 调用 Embedding 模型向量化,最后存入
PgvectorVectorStore向量存储中,其中我们利用 Spring AI 的 Auto-Configuration 机制实现 IoC 自动装载。 - 在线检索管道 (Online Pipeline):当用户发出提问,请求被 AI 核心过滤链拦截,触发
RagAdvisor。检索器首先发起混合检索(Hybrid Search),在向量路和关键词路召回候选集后执行 RRF 打分融合和去重。去重后的前 50 条文本送入 Cross-Encoder 重排服务精筛,截取最相关的 Top-5 知识块。 - 上下文动态注入防御失忆:
RagAdvisor将这 Top-5 重排知识块进行安全和非空校验,格式化为"使用以下检索上下文回答问题:\n" + retrievalResult.context(),通过 SystemMessage 强行插在整个 Message 数组的第 0 位(最顶部),随后调用chatClientRequest.mutate()替换 Prompt 整体向下游传递。这样既保证了大模型拥有绝对的业务背景知识,又利用位置首尾效应,杜绝了模型长文本失忆与幻觉的可能。
- 离线索引管道 (Offline Pipeline):我们将原始知识库(支持 PDF/Markdown/FAQ 结构化文本)打入解析层,采用滑动窗口递归切分策略(Chunk 大小定为 500 Token,前置重叠 50 Token),避免关键信息因边缘切分而丢失。分块后的 Chunks 调用 Embedding 模型向量化,最后存入
2. PgvectorVectorStore 与【向量数据库工程选型】
- 面试官切入点:
"你为什么选择 Pgvector 而不是 Milvus、Pinecone 或 Qdrant?Pgvector 的优势和局限性是什么?"
- 袁志刚专属特训回答模版:
- 中小型知识库下的性价比与运维决策:我们立项时核心评估了知识库文档规模(目前系统内 FAQ 和业务文档总量在 10 万条以内,对应向量 Chunk 数在 100 万左右)。在这种量级下,如果强行引入 Milvus 等独立专用向量数据库,不仅需要额外部署 Zookeeper、MinIO 以及专用的物理索引集群,还会带来极高的网络往返耗时和运维成本。
- 混合过滤与事务一致性(核心痛点):在 AI 客服场景下,检索必须带有业务过滤条件(例如:仅检索当前商铺
shop_id = 123且处于上架状态status = 1的菜品知识)。如果使用独立向量数据库,必须先在 Milvus 里根据向量相似度召回 Top-N,再在 Java 服务端根据 SQL 进行业务过滤,极易导致“业务过滤后候选集所剩无几”的预过滤断裂问题。而Pgvector可以在一张表内同时存储业务字段和向量字段,利用 PostgreSQL 强大的查询规划器,在一条 SQL 语句内完成混合预过滤(Pre-Filtering)与 HNSW 索引检索,极大地提升了系统的吞吐量与开发效率。同时,它支持 ACID 事务,业务数据更新时,向量索引同步写入,保证了 100% 的数据一致性。 - 架构局限与高阶规划:当然,Pgvector 在大规模分布式横向扩展(如千万级/亿级向量)时的查询延迟和并发支撑确实不如 Milvus,且对内存吃得极狠。对于我们目前的量级,Pgvector 配合 HNSW 索引是极致性价比、稳定性最高的最优解。
3. 混合检索去重融合与 RRF 算法对线
- 面试官切入点:
"你在混合检索中使用了 RRF 算法,能详细说说为什么要用它,以及你们在 Java 端是如何具体去重并融合打分的吗?"
- 袁志刚专属特训回答模版:
- 双路互补的工程必要性:纯语义向量检索擅长处理口语化同义词(如“有没有不放海鲜的饭”与“避开海鲜禁忌”匹配),但在精确的特定代码(如查订单号“order_001”或菜名“鱼香肉丝”)场景漏招回率极高;而传统基于 TF-IDF 演进的 BM25 关键词检索正好相反。我们将两路并联检索,获取各自召回的文档候选集。
- 去重融合的实现逻辑:我们使用
RetrievalFusionService开展融合。因为同一文档分块常在两路同时被召回,直接拼入 Prompt 会引发信息冗余。我们以uniqueKey()方法为基座,按Metadata.chunkHash -> Metadata.chunkId -> Hash(Content)的次序降级生成唯一哈希。 - RRF 倒数公式的应用:在遍历两路文档时,一旦发生 Key 碰撞,我们就使用公式: $$Score = \sum \frac{1}{60 + \text{Rank}}$$ 将该 Chunks 在两路各自的名次 Rank 代入求倒数后累加,最终重新降序排序。这样,如果一个 Chunks 在语义向量路排第一,BM25 关键词路也排第二,它的分值将极为突出;而仅在某一路靠后排名的 Chunks 分数会被迅速衰减。这套算法彻底保障了召回上下文的高相关度与干净度。
🖥️ 核心支撑源码:混合检索去重融合与 RRF 算法实现
java
// RetrievalFusionService.java - RRF 混合打分去重服务
@Service
@RequiredArgsConstructor
public class RetrievalFusionService {
private final OnlineRetrievalProperties properties;
public List<RetrievedChunk> fuse(List<RetrievedChunk> embeddingCandidates, List<RetrievedChunk> keywordCandidates) {
// 核心:基于 uniqueKey 去重,并累计两路检索的 RRF 贡献分数
Map<String, Accumulator> merged = new LinkedHashMap<>();
addRankedCandidates(merged, embeddingCandidates);
addRankedCandidates(merged, keywordCandidates);
int limit = Math.max(1, properties.getFusion().getMaxCandidates());
return merged.values().stream()
.map(Accumulator::toChunk)
.sorted(Comparator
// 优先按 RRF 融合分(fusionScore)降序,相同时按向量分(embeddingScore)降序
.comparingDouble((RetrievedChunk chunk) -> metadataDouble(chunk, "fusionScore")).reversed()
.thenComparing(Comparator.comparingDouble(RetrievedChunk::embeddingScore).reversed()))
.limit(limit)
.toList();
}
private void addRankedCandidates(Map<String, Accumulator> merged, List<RetrievedChunk> candidates) {
for (int i = 0; i < candidates.size(); i++) {
RetrievedChunk candidate = candidates.get(i);
String key = uniqueKey(candidate);
// 经典 RRF 打分公式:score = 1.0 / (K + rank)
double contribution = 1.0d / (properties.getFusion().getRrfK() + i + 1.0d);
merged.computeIfAbsent(key, ignored -> new Accumulator(candidate)).add(candidate, contribution);
}
}
private String uniqueKey(RetrievedChunk chunk) {
// 多级哈希校验去重:主键 -> 文档分块坐标 -> 内容哈希,防范内容重复污染 Prompt
Object chunkHash = chunk.metadata().get("chunkHash");
if (chunkHash != null && !chunkHash.toString().isBlank()) return "chunkHash:" + chunkHash;
Object chunkId = chunk.metadata().get("chunkId");
if (chunkId != null && !chunkId.toString().isBlank()) return "chunkId:" + chunkId;
return "content:" + chunk.content();
}
}4. 在线检索编排与重排异常降级自愈对线
- 面试官切入点:
"在高并发或者网络波动下,你们的在线重排序(Rerank)服务如果挂了,你们是怎么做故障自愈的?"
- 袁志刚专属特训回答模版:
- 三方依赖的天然脆弱性:在线重排是我们整个 RAG 检索精度的最后一公里,但因为其底层需要调用外部重排 API(如 SiliconFlow 提供的 Cohere-Rerank 接口),在生产环境的网络抖动、网络超时、或三方算力拥堵限流下,该接口有着极高比例的失败概率。如果直接放任崩溃,会导致用户的所有 AI 客服问答直接报错挂掉。
- 高可用重排器设计与降级拦截:我们在
RerankingService中进行了严密的异常包装设计。核心的rerank管道在调用三方 Client 时套入try-catch保护。一旦遭遇任何超时、50x 或 429 限流异常,控制台立刻捕获警报,同时依据配置参数判断是否开启自愈 fallback。 - 无缝降级为 Embedding Score 排序:若 fallback 开启,我们会将原本传入的 RRF 去重召回候选集,直接按照它们在第一阶段被向量库检索出的
embeddingScore(相似度分数)进行倒序排序,直接截取前 Top-N 条返回给组装器,彻底跳过三方 API 重试阻塞。这套无缝退避降级方案仅使重排精度受到极其微弱的偏角影响,但挽救了整条在线生成链路,为 AI 系统带来了极致的抗网络动荡弹性。
🖥️ 核心支撑源码:在线检索编排与重排异常降级自愈
java
// OnlineRetrievalService.java - 在线检索流程编排
@Service
@RequiredArgsConstructor
public class OnlineRetrievalService {
private final CandidateRetrievalService candidateRetrievalService;
private final RerankingService rerankingService;
private final ContextAssemblyService contextAssemblyService;
public RetrievalResult retrieve(String query) {
// Step 1: 召回候选文本块(包含 RRF 混合检索去重)
List<RetrievedChunk> candidates = candidateRetrievalService.retrieveCandidates(query);
// Step 2: 重排序(包含 SiliconFlow API 超时熔断自愈降级)
List<RetrievedChunk> finalChunks = rerankingService.rerank(query, candidates);
// Step 3: 上下文组装
return contextAssemblyService.assemble(query, finalChunks);
}
}
// RerankingService.java - 重排高可用兜底自愈服务
@Service
@RequiredArgsConstructor
public class RerankingService {
private final RerankerClient rerankerClient;
private final OnlineRetrievalProperties properties;
public List<RetrievedChunk> rerank(String query, List<RetrievedChunk> candidates) {
if (candidates.isEmpty()) return List.of();
try {
List<RetrievedChunk> reranked = rerankerClient.rerank(query, candidates);
int limit = Math.min(properties.getTopN(), reranked.size());
return reranked.subList(0, limit);
} catch (Exception ex) {
log.error("Reranker 重排 API 异常或超时,检查自愈配置...", ex);
// 核心自愈机制:如果开启了 fallback,则直接退化为向量检索打分排序,保障链路可用性
if (properties.isFallbackToEmbeddingResultsOnRerankFailure()) {
int limit = Math.min(properties.getTopN(), candidates.size());
return candidates.stream()
.sorted(Comparator.comparingDouble(RetrievedChunk::embeddingScore).reversed())
.limit(limit)
.toList();
}
throw ex; // 若未配置则向上传递异常
}
}
}