Spring Boot + Spring AI Alibaba + Redis 企业级向量检索与 RAG 引擎实战
Spring Boot + Spring AI Alibaba + Redis 企业级向量检索与 RAG 引擎实战
一篇面向 Java / Spring 团队的生产级实战文章:不只讲 RAG 能跑通,还系统讲清楚它为什么常在真实业务里失效,以及如何把
Spring Boot + Spring AI Alibaba + Redis组合成一个可扩展、可治理、可观测的企业级知识问答引擎。
一、为什么很多 RAG Demo 一上线就失效
过去两年,RAG 已经成为企业知识问答、智能客服、制度助手、运维助手、研发 Copilot 的标配能力。很多团队第一版系统都做得很快:
- 选一个 Embedding 模型
- 读几篇 PDF / Word / Markdown
- 切块后写进向量库
- 查询时做 TopK 相似度搜索
- 把结果塞进 Prompt 调用大模型
十几分钟就能跑起来,页面效果也往往不错。
但这类 Demo 往往只能说明一件事:
RAG 可以做出来。
却不能说明另一件更关键的事:
RAG 可以稳定上线。
一旦进入真实业务,问题会很快暴露:
- 文档量从几十篇增长到几十万 chunk 后,索引构建、内存占用、召回噪声同时上升
- 用户问题变复杂后,单纯向量召回会漂移,误召回明显增多
- 高并发下,Embedding、重排、LLM 调用相互争抢资源,接口 RT 抖动严重
- 知识更新后,新旧索引混用,导致结果不一致、不可解释
- 多租户过滤如果放在后面做,很容易发生越权召回和串库
- 出现错误答案时,团队无法判断是切块问题、召回问题、重排问题还是 Prompt 问题
根因并不复杂:
RAG 从来不是“向量库 + 大模型”的简单拼装,而是“检索系统 + 生成系统 + 工程治理系统”的组合工程。
所以,真正的企业级 RAG 系统,必须同时解决四类问题:
- 检索质量是否稳
- 在线链路是否快
- 离线入库是否可扩展
- 整体系统是否可治理
本文会围绕Spring Boot + Spring AI Alibaba + Redis,把一个常见 Demo 逐步升级成一套生产级 RAG 架构。
二、本文要解决的核心问题
这篇文章重点回答四个问题:
- 原理层:Embedding、向量检索、RAG Pipeline 的本质是什么
- 架构层:如何把 RAG 拆成离线摄入链路和在线问答链路
- 工程层:如何处理高并发、缓存、限流、熔断、降级、索引演进和多租户隔离
- 代码层:如何给出接近生产可用的 Java 代码,而不是只有几段演示代码
为了让讨论足够具体,本文统一使用一个真实感较强的案例场景。
三、业务场景:电商智能客服知识引擎
假设你负责一个电商平台智能客服系统,业务要求如下:
- 日均会话量:100 万+
- 峰值问答 QPS:2000+
- 业务知识类型:退款规则、售后政策、物流时效、营销活动、商家规范
- 文档来源:运营后台、CMS、PDF 规章、知识库系统、工单沉淀 FAQ
- 目标 RT:P95 小于 800ms,P99 小于 1.5s
- 更新目标:知识变更后 1 到 3 分钟内可查询
- 风险要求:必须支持多租户隔离、来源溯源、故障降级
这类场景的核心难点,不是“模型会不会回答”,而是:
- 回答能不能基于可信证据
- 高峰期能不能稳定服务
- 规则更新后能不能快速生效
- 线上出问题时能不能快速定位
四、先把原理讲透:Embedding 与 RAG 的本质
4.1 Embedding 到底在做什么
Embedding 的本质,是把文本映射到一个高维稠密向量空间,让“语义相近”的文本在空间中距离更近。
示意如下:
"如何申请退款" -> [0.12, -0.38, 0.54, ...] "退款流程是什么" -> [0.10, -0.36, 0.57, ...] "今天天气不错" -> [-0.42, 0.16, -0.33, ...]查询时,我们会把问题也转成向量,然后用相似度算法找最接近的 chunk。
常见相似度度量方式:
Cosine Similarity:最常见,关注方向相似性Inner Product:在归一化向量场景下常用Euclidean Distance:关注向量绝对距离
在工程上,Embedding 模型的三个关键指标是:
- 语义表达能力
- 向量维度
- 成本与吞吐
其中,维度并不是越高越好。维度越高,通常意味着:
- 单条向量占用内存更大
- 索引构建更慢
- 网络传输和序列化成本更高
所以生产环境要在召回质量和系统成本之间做平衡。
4.2 RAG 的本质不是“补知识”,而是“补证据”
很多人把 RAG 理解成“给模型外挂一个知识库”,这个说法不算错,但还不够准确。
更准确的理解是:
RAG 不是给模型补知识,而是给模型补证据。
企业场景里,我们需要模型回答的是:
- 当前版本的制度
- 当前生效的政策
- 你自己企业的私域文档
- 具备权限边界的内部信息
这些内容本质上都不应该依赖模型参数记忆,而应该依赖检索得到的外部上下文。
因此,一个成熟的 RAG Pipeline 往往不是简单的两步,而是五段式甚至七段式链路:
用户问题 -> Query Normalize / Rewrite -> Retrieve -> Rerank -> Context Build -> Generate -> Post Process4.3 企业 RAG 的两条主链路
离线链路:知识入库
文档采集 -> 内容解析 -> 清洗标准化 -> 语义切块 -> 元数据补全 -> 向量化 -> 建索引 -> 发布上线在线链路:问题回答
鉴权 -> 租户识别 -> 查询改写 -> 混合召回 -> 权限过滤 -> 重排 -> Prompt 组装 -> 大模型生成 -> 引用标注 -> 返回结果这两条链路必须解耦。原因很简单:
- 入库是重 CPU、重 I/O、重内存任务
- 在线查询是低延迟任务
如果把它们耦合在一个同步服务里,系统一定在并发时失稳。
五、为什么选择 Spring AI Alibaba + Redis
5.1 Spring AI Alibaba 的价值
对于 Java 团队来说,Spring AI Alibaba 的核心价值不是“能调大模型”,而是它把 AI 能力纳入了 Spring 的工程体系。
它的意义体现在三个层面:
模型抽象统一
ChatModel、EmbeddingModel、VectorStore这样的抽象,让模型与底层实现解耦。与 Spring Boot 生态天然集成
配置、自动装配、健康检查、Micrometer、Actuator 都能自然接上。更适合国内企业云环境
对阿里云模型和周边基础设施的接入更顺手,也更贴近国内企业常见技术栈。
对于一个要长期演进的企业系统来说,“接得上 Spring 工程化体系”比“能跑第一个 Demo”更重要。
5.2 为什么 Redis 适合作为企业 RAG 的第一站
很多团队一提向量检索,就直接想到专用向量数据库。但在大量中等规模场景里,Redis 是一个非常务实的选择。
原因有三点:
一栈多用
Redis 不只可以存向量,还能同时承担缓存、会话、限流、分布式锁、热点结果存储。工程门槛低
大多数 Java 团队本来就有 Redis 运维经验,引入成本更低。适合中小规模到中等规模知识库
对数十万到数百万 chunk 的知识库,Redis Stack 完全可以支撑很多业务场景。
当然,它也有边界:
- 极大规模向量数据下,内存成本较高
- 复杂混合检索和多阶段检索能力不如专业检索引擎灵活
- 当知识规模和过滤条件极其复杂时,可能需要演进到 Elasticsearch + 向量、Milvus、PgVector、OpenSearch 等方案
所以,Redis 在本文里的角色不是“最终答案”,而是:
企业 RAG 体系里一个非常强的起步方案和重要的中间演进节点。
六、企业级 RAG 架构设计
6.1 总体分层架构
┌───────────────────────────────────────────────────────────────┐ │ 接入层 / Gateway │ │ 鉴权、租户识别、限流、灰度、审计、TraceId、渠道适配 │ └───────────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────────────┐ │ RAG Application Service │ │ 会话管理、问题编排、缓存协同、超时控制、降级策略、引用组装 │ └───────────────────────────────────────────────────────────────┘ │ │ │ ▼ ▼ ▼ ┌────────────────┐ ┌──────────────────┐ ┌────────────────────┐ │ Query Service │ │ Retrieval Service│ │ Generation Service │ │ 改写/归一化 │ │ 召回/过滤/重排 │ │ Prompt/模型调用 │ └────────────────┘ └──────────────────┘ └────────────────────┘ │ │ │ └──────────────┬─────┴─────────────┬──────┘ ▼ ▼ ┌─────────────────┐ ┌────────────────────┐ │ Redis Stack │ │ LLM / Embedding │ │ 向量/缓存/会话 │ │ DashScope / 其他模型│ └─────────────────┘ └────────────────────┘ ▲ │ ┌──────────────────────┐ │ Ingestion Pipeline │ │ 解析/切块/向量化/发布 │ └──────────────────────┘6.2 生产系统中的核心边界
企业级 RAG 不应该只有一个RagService包打天下,而应该清晰拆分职责:
Controller
只负责协议适配、参数校验、身份透传和响应封装Application Service
编排一次问答请求,包括缓存命中、召回、重排、生成和降级Retrieval Domain
负责召回逻辑、过滤逻辑、重排策略、上下文选择策略Infrastructure
负责 Redis、模型服务、消息队列、对象存储、配置中心等对接Ingestion Pipeline
独立于在线链路,负责知识解析、切块、索引和发布
如果这些边界不清晰,后续每多一个能力,例如:
- 多租户
- 重排模型
- 灰度索引
- 混合检索
- 知识版本切换
都会导致问答主服务快速膨胀。
七、核心设计原则:从 Demo 升级到生产必须补齐的 12 件事
7.1 检索优先于生成
RAG 场景中,大部分错误回答的根因不在模型,而在召回错误或上下文污染。
7.2 离线摄入与在线查询彻底解耦
入库链路必须异步化,不能占用在线查询资源池。
7.3 元数据是一等公民
每个 chunk 至少要具备:
tenantIdkbIddocumentIddocumentVersionchunkIdtitlesectionPathpermissionTagspublishedAtsourceUri
没有元数据,系统几乎无法做隔离、过滤、追溯和版本演进。
7.4 索引必须版本化
知识更新不能直接覆盖线上索引,必须支持:
- 全量重建
- 增量更新
- 双索引并行
- 灰度验证
- 原子切换
- 快速回滚
7.5 权限过滤必须前置
先过滤,再检索,再生成。不能等答案生成后再做补救。
7.6 模型调用必须被治理
包括:
- 超时
- 重试
- 隔离
- 熔断
- 限流
- 降级
7.7 缓存不是附加优化,而是基础设施
值得缓存的内容包括:
- Query Rewrite 结果
- Embedding 结果
- 热点检索结果
- 最终回答
- 会话摘要
7.8 不要只做向量召回
企业知识里有很多:
- 订单号
- 规则编号
- 错误码
- 接口路径
- 产品名
- 表字段名
这些内容通常需要和关键词、标签过滤、结构化条件联合使用。
7.9 Prompt 预算必须受控
不能把 TopK 全部无脑塞给模型,否则会同时损伤:
- 时延
- 成本
- 生成稳定性
7.10 可观测性必须从第一天开始
至少要能观察:
- 改写前后 query
- 召回候选数
- 重排前后变化
- 上下文 token 数
- 模型耗时
- 引用来源
- 降级原因
7.11 质量评估必须标准化
需要建设离线评测集,对以下指标做持续评估:
- Recall@K
- MRR
- Grounded Rate
- 引用覆盖率
- 人工满意度
7.12 先设计演进路径,再选择组件
不要把某个向量库或某个模型当成“架构本身”,架构重点在可替换和可演进。
八、技术选型建议
本文采用如下选型思路:
| 维度 | 方案 | 说明 |
|---|---|---|
| Web 框架 | Spring Boot 3.x | Java 企业主流,便于接入治理体系 |
| AI 接入 | Spring AI Alibaba | 统一模型抽象,适合 Spring 生态 |
| Embedding 模型 | DashScope Embedding 或同类模型 | 中文语义能力和工程集成更友好 |
| Chat 模型 | 通义千问或同类通用模型 | 负责最终生成与必要的 query rewrite |
| 向量存储 | Redis Stack | 向量检索 + 缓存 + 限流 + 会话可一体化 |
| MQ | Kafka / RocketMQ | 负责知识摄入异步化 |
| 对象存储 | OSS / MinIO | 存原始文档和解析产物 |
| 可观测 | Micrometer + Prometheus + Grafana | 指标、告警、容量评估 |
| 配置中心 | Nacos / Spring Cloud Config | 动态调整 TopK、阈值、模型开关 |
这里要强调一个工程原则:
先把架构接口抽象好,再决定底层组件。
比如,我们在代码里不应把问答逻辑直接写死到 Redis 上,而应抽象出:
EmbeddingGatewayVectorRetrieverRerankServiceKnowledgePublisher
这样后续从 Redis 迁移到别的向量库时,业务层无需大动。
九、项目结构设计
建议的工程结构如下:
rag-engine ├── controller │ ├── ChatController.java │ └── KnowledgeAdminController.java ├── application │ ├── RagQueryApplicationService.java │ ├── KnowledgeIngestionApplicationService.java │ └── model ├── domain │ ├── retrieval │ │ ├── RetrievalCandidate.java │ │ ├── RetrievalContext.java │ │ ├── ContextAssembler.java │ │ └── ScorePolicy.java │ ├── knowledge │ │ ├── KnowledgeChunk.java │ │ ├── KnowledgeDocument.java │ │ └── KnowledgeVersion.java │ └── security │ └── AccessScope.java ├── infrastructure │ ├── ai │ │ ├── DashScopeEmbeddingGateway.java │ │ ├── DashScopeChatGateway.java │ │ └── SimpleRerankService.java │ ├── redis │ │ ├── RedisVectorRepository.java │ │ ├── RedisCacheRepository.java │ │ └── RedisIndexManager.java │ ├── mq │ ├── storage │ └── config └── support ├── trace ├── json └── resilience这套结构最重要的价值是:
- 在线问答和离线摄入分层
- 领域规则和基础设施隔离
- 后续演进到多服务部署时迁移成本低
十、知识摄入架构:离线链路怎么设计才扛得住规模
10.1 不要同步入库
很多 Demo 都会在用户上传文档后,同步完成:
- 解析
- 切块
- Embedding
- 写库
这种做法在文档少时没问题,但在生产环境里会带来三个问题:
- 上传接口 RT 极长
- 重任务和在线查询争抢资源
- 失败重试和幂等控制困难
正确做法是把入库拆成事件驱动链路:
文档上传 -> 持久化原文 -> 投递 ingest task -> 异步解析与切块 -> 异步 embedding -> 写入 staging index -> 质检通过后发布 active version10.2 推荐的知识摄入时序
Admin Upload API -> Object Storage -> Create Ingestion Task -> MQ Publish(document_uploaded) -> Parser Worker -> Chunker Worker -> Embedding Worker -> Redis Staging Index -> Publish Version10.3 为什么必须有 staging index
如果直接把新版本 chunk 写进线上 active 索引,会出现:
- 查询结果新旧混杂
- 部分文档失败导致结果不完整
- 回滚困难
更稳妥的做法是双索引模式:
kb:tenantA:active:v12kb:tenantA:staging:v13
新版本先写staging,通过校验后再原子切换到active。
10.4 文档切块策略
切块质量直接决定召回上限。生产中建议遵循三个原则:
- 按语义边界切,而不是简单按字符数切
- 保留章节标题和路径
- 控制 chunk 长度和 overlap
对于中文文档,一个常见实践是:
- chunk 目标长度:300 到 600 中文字
- overlap:50 到 100 中文字
- 标题单独保留到 metadata 和 chunk content 前缀中
推荐 chunk 示例:
标题:退款申请流程 章节:售后服务 > 退款规则 > 用户发起退款 内容:用户在订单签收后 7 天内,可以在订单详情页发起退款申请...这里有一个常见误区:
chunk 不是为了“切得均匀”,而是为了“被单独召回时仍有足够语义完整性”。
十一、在线问答架构:一次请求在系统里如何流转
11.1 标准处理链路
一条成熟的问答链路建议按下面顺序执行:
- 鉴权与租户识别
- 问题归一化
- 热点缓存检查
- Query Rewrite
- Embedding
- Redis 向量召回
- 元数据过滤
- Rerank
- 上下文压缩与组装
- Prompt 构建
- LLM 生成
- 引用溯源与后处理
- 结果缓存
11.2 为什么 Query Rewrite 值得做
用户原始问题经常非常短,或者上下文模糊,例如:
- “退款怎么弄”
- “发票”
- “为什么不支持”
这类 query 直接做 Embedding 往往语义不足。Rewrite 的价值在于:
- 补充业务语义
- 规范化口语表达
- 对齐知识库中的正式术语
例如:
原问题:退款怎么弄 改写后:用户发起订单退款的条件、入口和审核流程是什么Rewrite 不一定必须走大模型,也可以先做轻量规则归一化。生产里一般建议:
- 高频问题:规则归一化优先
- 长尾复杂问题:再走 LLM Rewrite
11.3 为什么需要重排
向量召回擅长“粗召回”,不擅长最终排序。
典型问题是:
- 召回内容语义相关,但不回答用户核心问题
- 多段内容都相关,但优先级不对
- 标题和正文共同召回时,内容碎片顺序混乱
所以推荐采用两段式检索:
向量召回 TopK=20 -> 过滤与去重 -> Rerank TopN=5 -> 上下文组装在没有专用重排模型时,至少也要做基于业务规则的二次打分,例如:
- 标题命中加分
- 权威来源加分
- 最新版本加分
- FAQ 类短答案适度加分
十二、Redis 向量索引设计
12.1 Redis 中存什么
一个生产级 RAG 系统里,Redis 不应该只存向量,还应该明确区分几类 key:
rag:chunk:{tenant}:{chunkId} -> Hash / JSON,存 chunk 内容与 metadata rag:index:{tenant}:{kbVersion} -> 向量索引名 rag:answer-cache:{tenant}:{queryHash} -> 最终答案缓存 rag:rewrite-cache:{tenant}:{queryHash} -> 改写结果缓存 rag:embedding-cache:{sha256(text)} -> Embedding 缓存 rag:session:{tenant}:{userId}:{sessionId} -> 会话摘要 / 上下文 rag:publish:{tenant}:{kbId} -> 当前生效版本12.2 chunk 元数据建议
每个 chunk 建议至少保存以下字段:
| 字段 | 作用 |
|---|---|
tenantId | 多租户隔离 |
kbId | 知识库标识 |
documentId | 文档定位 |
documentVersion | 版本控制 |
chunkId | chunk 主键 |
title | 提高可解释性和召回质量 |
sectionPath | 引用展示 |
content | 原始正文 |
permissionTags | 权限过滤 |
status | staging / active |
publishedAt | 排序和新鲜度控制 |
12.3 HNSW 还是 FLAT
Redis 向量索引常见有两类:
FLAT
精确召回,适合数据量小、准确率优先的场景HNSW
近似最近邻,适合中大规模数据,查询更快
一般建议:
- 10 万 chunk 以内,可评估
FLAT - 10 万到数百万 chunk,优先
HNSW
HNSW常用参数思路:
M:图连边数,越高精度通常越高,内存也更大EF_CONSTRUCTION:建索引质量相关参数EF_RUNTIME:查询精度与耗时平衡参数
生产中需要通过压测找到业务平衡点,而不是照搬固定值。
12.4 不要忽略过滤字段
很多团队第一次做 Redis 向量检索,只给索引建一个embedding字段,这是远远不够的。
实际业务里通常至少还要建:
tenantId标签kbId标签documentVersion数值status标签publishedAt数值category标签
因为真实查询往往是:
先限定租户、知识库、状态和权限,再做向量搜索。
十三、生产级代码实现
下面给出一套更接近生产的核心代码。为了突出主线,示例省略了部分 import、日志细节和基础 DTO,但保留了关键设计。
13.1 Maven 依赖
<project><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.4</version></parent><properties><java.version>17</java.version><spring-ai-alibaba.version>1.0.0.2</spring-ai-alibaba.version><resilience4j.version>2.2.0</resilience4j.version></properties><dependencyManagement><dependencies><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-ai-alibaba-dependencies</artifactId><version>${spring-ai-alibaba.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency><groupId>org.springframework.kafka</groupId><artifactId>spring-kafka</artifactId></dependency><dependency><groupId>io.micrometer</groupId><artifactId>micrometer-registry-prometheus</artifactId></dependency><dependency><groupId>io.github.resilience4j</groupId><artifactId>resilience4j-spring-boot3</artifactId><version>${resilience4j.version}</version></dependency><dependency><groupId>org.apache.tika</groupId><artifactId>tika-core</artifactId><version>2.9.2</version></dependency><dependency><groupId>com.alibaba.cloud</groupId>