☰
Spring AI接入向量数据库:RAG知识库问答实战指南
2026/10/17 19:13:19 网站建设 项目流程

上篇把Spring AI接入对话模型的链路聊完了,这篇接着啃搜索扩展这块硬骨头:向量数据库和RAG。先别急着写代码,我先把一个很常见的认知误区摆出来——好多人以为RAG就是把文档切碎塞给大模型再让它总结,其实这一步就已经跑偏了。RAG的核心在于“检索”,不是在“生成”那里做文章。检索这步能不能把真正有信息量的片段捞出来,直接决定了后面生成的质量上限。而这背后撑起检索的,就是向量数据库和一套设计得当的索引、召回链路。

这篇文章会从原理、选型、实操到调优,把我自己把Spring AI接到向量数据库做知识库问答的完整过程拉一遍,包括踩过的坑和一些常规文档里不会写的情况。适合两类人看:一类是已经在Spring项目里接了对话模型、想进一步做私有知识库问答的开发者;另一类是知道RAG概念但不知道从哪里下手,想找一个能直接落地的Java实现路径的同学。看完之后,你至少能独立搭出一条“文档入库、向量召回、增强生成、问题排查”的完整链路。

1. 先厘清一个核心问题:为什么搜索扩展要走向向量检索

1.1 关键词搜索的天花板在哪里

传统搜索引擎用倒排索引,按字面匹配。你搜“怎么退款”,它只能命中包含“退款”这两个字的文档。但知识库里如果写的全是“退货流程”“费用返还”这种变体表达,字面匹配就抓瞎了。更麻烦的是,很多问题本身就不是靠字面能解决的,比如“我收到的东西坏了怎么办”,这个词组跟“商品瑕疵”“售后处理”没有任何一个字重叠,关键词检索直接失效。

向量检索解决的就是这个“语义鸿沟”。它先把文档和查询都映射到同一个高维向量空间,然后在空间里找“挨得最近”的向量。关键词不重叠没关系,只要意思相近,向量距离就一定近。这个转变的本质,是从匹配字面符号变成匹配语义坐标,检索的泛化能力会有一个质的提升。

1.2 向量相似度的两个核心指标

有了向量之后,怎么判断谁离谁近?最常用的是余弦相似度。它计算两个向量夹角的余弦值,值越接近1说明方向越一致。计算逻辑其实就是点积除以模长乘积,在一个已经归一化的向量库里,点积就等价于余弦相似度。欧氏距离也在用,但它对向量长度敏感,嵌入门控模型很多默认输出高维稀疏表示,用欧氏距离容易让长向量占便宜,所以实际落地里我基本都优先余弦相似度。

Spring AI里的相关度判断最终会落到一个相似度阈值(threshold)上。检索阶段先取topK个候选,再按阈值过滤,低于阈值的直接丢弃。阈值设太低了无关内容混进来,设太高了召回的片段太少,这个尺度在后面调优部分我详细说。

1.3 RAG的完整链路不是只有“检索”和“生成”

标准RAG流程可以拆成三块:索引阶段(Ingestion)、检索阶段(Retrieval)、生成阶段(Synthesis)。索引阶段要完成文档加载、清理、切分、嵌入、入库;检索阶段把用户问题嵌入成向量,去向量库召回相关片段;生成阶段把召回的片段拼进提示词模板,交给大模型生成回答。

很多人只关注中间的检索和最后的生成,忽略了索引阶段对结果的影响其实更大。嵌入质量、切分粒度、元数据设计,全部决定向量库里存的是什么。存进去的东西本身是脏的、碎的,后面检索再努力也变不出花来。所以我在下面实操部分会特别强调索引阶段的设计。

1.4 Spring AI在这个链路里扮演的角色

Spring AI在RAG链路里做得比较聪明的一点,是把基础设施抽象成三个稳定接口:EmbeddingModel负责文本向量化,VectorStore负责向量存储与相似度检索,ChatClient负责对话,Advisor负责把检索结果与用户问题整合进提示词。你后续无论换底层数据库还是换模型,上层业务代码基本不用动。

2. 选型与准备:向量数据库和嵌入模型的Spring AI搭配方案

2.1 主流向量数据库对比与选型决策

Spring AI官方支持的向量数据库不少,我没法每个都实测,但把主流的放在一起比较后,选型逻辑会清晰很多。

数据库方案部署方式适用数据量运维成本特点与适用场景
PgVector基于PostgreSQL插件千万级以内低复用现有关系库,事务一致性友好
Milvus独立集群亿级以上高专为向量计算设计,分片能力强
Redis Stack基于Redis模块百万到千万级低缓存近热,检索性能高
Elasticsearch独立集群亿级高天然支持全文+向量混合检索
Chroma嵌入式本地十万级以内极低本地开发调试方便
Qdrant独立容器亿级中Rust实现,过滤条件丰富

我自己的经验是,千万级以内、团队又已经有PostgreSQL的场景,无脑选PgVector最稳。理由很简单:不用引入新的运维组件,关系数据与向量数据可以放一起做联合过滤,事务和权限都能复用。Spring AI对PgVector的支持也算成熟,Schema自动初始化,接入成本很低。如果你的业务消息量巨大且需要海量向量计算,再考虑Milvus或Elasticsearch那套重方案。

2.2 接入PgVector的依赖与配置

我用的是Spring AI 1.0版本系列,Spring Boot 3.3以上。pgvector的依赖里需要同时引入向量数据库相关包和PG JDBC驱动。

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-vector-store-pgvector</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId> </dependency>

嵌入模型我用的某云端文本嵌入模型,API Key通过环境变量注入。配置文件里把PgVector的初始化模式设成create,这样第一次启动时会自动建好vector扩展和向量表,省去手工执行的步骤。

spring: ai: azure: openai: api-key: ${AZURE_OPENAI_API_KEY} endpoint: ${AZURE_OPENAI_ENDPOINT} embedding: options: deployment-name: text-embedding-3-small vectorstore: pgvector: initialize-schema: create index-type: HNSW distance-type: COSINE_DISTANCE dimensions: 1536

这里有个容易踩坑的地方,dimensions必须跟嵌入模型输出的维度保持一致。text-embedding-3-small输出的是1536维,你把配置写成1024或者默认值,入库和检索时都会因维度不一致直接报错。维度这件事,后面问题排查里我还会再提。

2.3 嵌入模型的选择:不能只看价格和维度

嵌入模型直接决定了向量空间里“语义拉得开拉不开”。商业云服务方便,但数据要到云上,内部知识库要评估合规成本;开源模型如BGE、M3E可以本地部署,中文效果也不错,只是需要自己扛推理资源。我自己的建议是,如果知识库内容以中文为主,优先选对中文支持好的模型,别拿一个面向英文优化的接口直接糊上;做英文场景,商业模型的英文底座会更成熟。

一个较容易被忽略的点是嵌入模型与生成模型最好来自同一个体系家族,它们的语义对齐度通常更好。我在早期阶段混搭过不同厂商的嵌入与生成模型,发现召回的片段有时答非所问,换成同体系的模型后效果明显稳定。

2.4 Spring AI里的VectorStore和EmbeddingModel自动装配

引入stater之后,Spring Boot的自动装配会帮你把EmbeddingModel和VectorStore的Bean都建好。直接在代码里注入即可,底层用的什么向量库和模型,业务代码完全不感知。

@Service public class KnowledgeService { private final VectorStore vectorStore; private final EmbeddingModel embeddingModel; public KnowledgeService(VectorStore vectorStore, EmbeddingModel embeddingModel) { this.vectorStore = vectorStore; this.embeddingModel = embeddingModel; } }

这个设计有个很大的好处:测试环境里你可以配一个本地文件或内存版VectorStore,生产环境切到PgVector,代码零改动。

3. 完整实操:从知识库入库到检索增强生成的上手记录

3.1 场景设定:一个电商售后知识库

为了把链路讲具体,我拿一个电商售后客服问答场景举例。知识库里存的是一些售后政策、退货流程、物流时效、常见纠纷处理规则,目标是让客服系统能根据用户的自然语言提问,准确回答“退货运费谁承担”“七天无理由怎么算时间”“物流十天没动怎么办”这一类问题。

3.2 文档加载与清洗:这一层直接决定垃圾进垃圾出

入库流程的第一步不是切分,而是清洗。我的做法是先做文档标准化:把PDF、Word里的格式残留(页眉、页脚、目录编号)去掉,把全角半角统一,对明显重复的段落去重。这一步很多人嫌麻烦跳过,实际效果差距非常明显——脏文本的嵌入向量也会偏离正常语义位置,检索时同一份文档的不同片段互相干扰。

加载这一步,Spring AI基于接口抽象做得比较好,你可以直接用内置的文件读取器,也可以从数据库或对象存储读文本。我用过从数据库读取情况,核心就是拿到纯文本内容并保留文档ID作为元数据,后面更新和删除都好办。

3.3 切分策略:为什么chunk_size和overlap要一起调

切分是索引阶段技术含量最高的一环。Spring AI里通过DocumentReader和TokenTextSplitter实现文本切分。简单按字符切会切断语义,简单按句切又会产生过多碎块。我的经验是两个参数要配合着调:

  • chunk_size:每个片段的目标长度,单位不是字符而是token。设太小语义不完整,设太大检索粒度太粗。
  • overlap:相邻片段重叠的token数量,用来缓解跨片段语义断裂。通常设置为chunk_size的10%-20%。
TokenTextSplitter splitter = new TokenTextSplitter(500, 80, 5, 500, true); List<Document> docs = splitter.apply(textLoader.load());

我举个例子,售后政策里常见一句话把“适用条件”和“不适用条件”放在同一段,但两个语义其实分属不同问答场景。如果没有overlap,恰好这一句在切分时被一刀砍断,那后半句的“不适用情况”就落到了下一片段,检索时前半片段被召回但信息不完整。设置了overlap后,下一片段带着上一片段的尾巴,能保证这个语义节点完整出现在至少一个片段里。

3.4 数据入库:构造Document、设置元数据、调用VectorStore

切分完之后,把清洗好的文本和元数据组装成Document,然后调用VectorStore.add把向量写进库。这一步我会非常注意元数据字段的设计,因为后面检索过滤全靠它。

List<Document> documents = splitDocs.stream() .map(text -> new Document(text, Map.of( "docId", docId, "category", category, "source", fileName, "seq", seq++ ))) .collect(Collectors.toList()); vectorStore.add(documents);

元数据里我固定放了docId、category、source、seq四个字段。docId用于链路追踪和整文档删除,category用于业务域过滤,source记录来源文件,seq记录片段在原文中的顺序。之所以保留seq,是因为后面如果要把多个片段按原文顺序重新拼接给模型,或者做引用溯源,它能拼回完整上下文。

入库后我一般会做一次随机抽检:取几个真实用户问题跑检索,人工看看召回的前三条是否合理。这个环节在开发早期能发现大量索引问题,别跳过去。

3.5 问答链路:从ChatClient到QuestionAnswerAdvisor

Spring AI实现RAG问答最省力的方式是直接装配一个QuestionAnswerAdvisor,传入VectorStore作为检索器。它的工作方式是:把用户输入自动嵌入,去VectorStore检索TopK个相关片段,把片段作为上下文注入Prompt,然后调用ChatClient生成回答。

ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.builder() .query("") .topK(4) .similarityThreshold(0.5) .build())) .defaultSystem("你是一个售后客服助手,请严格根据提供的知识库内容回答,如果知识库没有相关信息,请明确告知不知道,不要编造。") .build();

之后业务侧调用方式变得很简单,把用户问题传进去,回答和引用信息一起返回。

String question = userMessage; String answer = chatClient.prompt() .user(question) .call() .content();

这套接口用起来像拼积木,但有一点要清楚:QuestionAnswerAdvisor内部的上下文窗口和Prompt模板是相对固定的。如果要做更精细的检索逻辑,比如多路召回、重排或者人工干预上下文候选,Advisor就不够用了,得自己实现Advisor接口或者直接在业务层写检索逻辑后拼Prompt。多数中小业务场景里,默认Advisor完全够用。

3.6 检索日志:把召回的片段和相似度分数打出来

开发过程中,我强烈建议把召回明细打到日志里。Spring AI的Advisor接口允许在检索前后介入,加一个自定义Advisor就能把Recalled Documents的片段内容和分数全部打出来。这一步对定位回答质量问题的帮助是巨大的:答得不好,你也能立刻区分是检索没召回对,还是模型生成时没用好召回内容。

@Override public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) { SearchRequest searchRequest = SearchRequest.builder() .query(request.userText()) .topK(5) .similarityThreshold(0.5) .build(); List<Document> docs = vectorStore.similaritySearch(searchRequest); docs.forEach(d -> log.info("召回片段,分数={},来源={},内容={}", d.getScore(), d.getMetadata().get("source"), d.getContent())); return chain.nextAroundCall(request); }

看到日志里召回内容和用户问题根本不在一个话题,基本可以判断是嵌入模型或切分策略出了问题,而不是生成阶段的问题。这个排查顺序很关键。

4. 效果分析与调优:让RAG回答得更准的几个阀门

4.1 topK和similarityThreshold的正确组合姿势

topK控制召回数量,similarityThreshold控制质量下限。这两个参数单独调都有局限,topK太高会把低质量内容塞进上下文,尤其在中文场景里嵌入模型对短问题的区分能力有限,topK一高就容易出现语义漂移的噪声片段;阈值太高又可能召回不到足够信息。

我从实践里总结的参数起步方案是:topK=4,threshold=0.5,然后在这个组合之上微调。如果你发现回答总缺信息,先降阈值或升topK;如果发现模型容易被无关片段带偏,提升阈值或降topK。调参不是一次到位的事,通常看20到30个真实问题才能有一个相对靠谱的结论。

注意:不同嵌入模型的分数分布差异很大,同一个0.5在不同模型上可能一个是“严格过滤”,一个是“几乎全放行”。最好先抽样看一批真实问题的相似度分数分布,再定阈值刻度。

4.2 切分粒度如何搭配问答场景去调整

售后政策类文档,平均一个完整知识点通常200到300个token,我会把chunk_size设在350到500之间,配合60到80个token的overlap。这样绝大多数条款能被完整包裹在一个片段里。大模型的上下文窗口越来越大,很多人认为可以把超大文档整段丢进去,但向量检索的核心逻辑决定了,粒度过大时,片段内混合了多个主题,距离计算会被拉偏,检索精度反而下降。

这里我踩过的坑是:早期阶段我追求“一次性把整章塞进去避免切碎语义”,chunk_size设置到了2000,结果检索噪音非常明显。比如用户问无理由退货时限,召回片段里除了时限条款,还夹杂了运费险和保价条款,回答内容就变得混杂。后来我把片段控制在500以内,每个片段核心信息更聚焦,回答准确率提升很多。

4.3 混合检索与重排:要不要上,什么阶段上

向量检索解决语义匹配问题,但字面搜索在某些场景依然有优势,比如产品型号、订单号、政策编号这些带强符号特征的查询。混合检索就是向量检索和关键词检索并行,再做结果融合或重排。Spring AI生态里没有内置的融合方案,需要自己实现一个Advisor或Service来做多路召回和合并。我的建议是:先评估业务查询类型,如果售前问答里大量出现具体型号、编号,再考虑上BM25或ES的混合检索;纯语义问答场景,向量单路召回做扎实已经够用。

关于重排,如果上下文窗口允许,可以把召回返回的topK适当放大到10到20个片段,交给重排模型精排后取前4个。重排对效果提升是实打实的,但引入额外模型调用和延迟,这在Spring AI里也意味着要自己实现一层处理逻辑。我的经验是,知识库规模不大时,重排提升有限,先把切分和阈值调明白更划算。

4.4 数据更新与增量入库策略

知识库不是一封不动,我遇到过政策条款更新后旧向量还留在库里,新旧内容混合,回答自相矛盾。解决办法是利用元数据做版本控制:入库时带上docId和更新批次号,更新时先按docId删除旧Document,再写入新的。Spring AI的VectorStore提供了delete方法,按元数据过滤删除。

vectorStore.delete(FilterExpressionBuilder.builder() .in("docId", docId) .build()); vectorStore.add(newDocuments);

如果一套文档被替换的频率很高、数据量又大,更适合引入离线重建任务的方案:先把全量知识库向量算好写入临时表,重建成功后切表。在线删除加写入容易在中间状态时检索到半套旧数据,离线重建能规避这个问题。

4.5 评估闭环:别靠感觉优化,建一个小测试集

RAG优化最忌讳拍脑袋。我自己的习惯是准备40到50条真实业务问题,每个问题标注期望答案的知识片段或回答要点,形成一个小测试集。调整任何参数后,用这个测试集跑一遍,统计“召回命中率”和“回答准确率”两个指标。人工评虽然费时,但比感觉靠谱得多。

我还尝试过用大模型对回答做A/B评估,给它两条回答让它按相关性、完整性、忠实度打分,但大模型评估自己生成的同类结果,有时会给出虚高的分。所以自动评估结果只能作为初筛,核心决策还是要过人工评审,尤其在上线前的验收阶段。

5. 典型问题与排查实录:从日志到语义事故的处理思路

表现常见原因排查思路
回答明显答非所问召回内容与问题无关打开检索日志,看召回的片段与用户问题是否同一主题
回答里出现编造内容上下文没有可用信息但未做知识缺失判断降低阈值,或修改System Prompt明确禁止编造
入库报错维度不匹配配置dimensions与模型输出维度不一致核对嵌入模型输出的维度值,同步配置
中文检索效果差嵌入模型对中文支持弱更换为中文优化过的嵌入模型
回答总是缺一部分信息切分粒度过粗导致知识点被切开调小chunk_size,增大overlap
数据更新后回答仍旧改版内容旧向量未被删除,新旧版本共存按docId删除旧数据或用重建机制
调用时报依赖冲突Spring AI版本与Spring Boot版本不兼容对齐版本,参考官方版本矩阵

5.1 为什么阈值和topK都调了,回答还是很差

常见问题倒不是参数,而是元数据过滤和检索逻辑打架。比如我在代码里按category过滤了售后类,但某些问题跨了品类,过滤条件把相关信息全部挡掉了,回答自然就差。排查时先检查Advisor配置里是否带了过滤条件,再看检索日志里实际召回了什么。单纯的参数调优无法弥补检索范围本身被限制过死的问题。

5.2 中文场景下片段质量不稳定,Word文档导出的文本自带一堆换行和序号

从Word导出的文档,段落内部经常是每条条款单独一行。直接把整段文本切分,模型会把换行符当作特征学习,同一条政策被拆得七零八落。我建议在清洗阶段做一次段落合并:先按句号、问号分割,再把同一语义块的句子拼回同一段落,最后丢给切分器。顺序不能反,先拼句再切块比直接切块效果好非常多。

5.3 嵌入维度与数据库不匹配,报错信息容易误导人

Spring AI里报的错比较直白,基本会指向向量维度不对。但容易忽视的是,如果你换过嵌入模型,旧表里的历史数据维度仍然是旧值。这时候不是仅仅修改配置就能解决的,需要建新表或用ALTER语句变更列类型,再把历史数据重新算向量。我在一次迭代里把嵌入模型从768维换到1536维,忘了历史表,结果上线后老数据检索全部异常。

5.4 内存型向量库撑不住真实流量,启动缓存越来越多

本地调试用内存版VectorStore没问题,但部署到生产后千万别省事继续用。一个常见的线上故障是服务重启后向量库直接清空,缓存未命中的问题越来越多。改用PgVector这样持久化的方案之后,问题消失。所以建议流程设计上就区分环境:开发环境可以用内存库,测试与生产环境必须接持久化向量库。

5.5 提问太短,召回结果不稳定

用户问题如果只有“运费”两个字,嵌入模型很难把语义稳定映射到一个准确的向量区域。实测里这种短问题的召回来回飘。比较实用的策略是在检索前做一次查询改写,把短问题扩写成完整问句后再做向量检索。比如“运费”改成“退货运费由谁承担?”,召回效果立刻不一样。Spring AI里可以自定义Advisor做查询改写,也可以用一个轻量模型先做改写,再走后续检索链路。

一些实操上的体会

最后聊一点我在实践过程中的体会。这套Spring AI + 向量数据库 + RAG的链路,真正拉开效果差距的往往不在加了多少新特性,而在对每个基础环节的精雕细琢:清洗是否干净、切分是否恰当、元数据是否有效、阈值是否贴合模型分布。先别急着追求复杂的重排和多路召回,把一个简单的向量召回链路打磨稳定,数据质量做扎实,效果已经能超过大多数粗略堆功能的方案。

如果你现在正要开始做第一个RAG模块,建议从最小闭环入手:挑一个领域范围可控的知识库,接PgVector和默认Advisor,把日志和分析工具都打好,先跑通再优化。很多坑,只有自己把数据喂进去跑一跑才能暴露出来。另外提醒一句,文本嵌入模型上线前一定要做抽样验证,中文场景尤其不能只看英文评测指标,真实业务的问题风格跟标准数据集差别很大。这套方案后续能扩展的方向还有不少,比如结合函数调用做自动化操作,或者引入查询改写提高短问题召回率,这些都可以跑在同一个链路上,扩展性已经够用了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询