前阵在帮一个内部团队调知识库问答的时候,我遇到了一个很典型的场景:资料已经整整齐齐传到了文档平台,也能用关键词搜到标题,但员工真正问题抛过来时就傻眼了。比如有人问“报销卡在审批流程了怎么办”,文档里写的是“财务审批长期未处理”,关键词完全不重合,传统搜索引擎就是找不出来。后来我们把方案改成Spring AI + 向量数据库做搜索扩展,按RAG的链路口径重做了一版检索服务,问题才真正解决。
这次实操涉及的东西很多:向量化、切片、存储、相似度召回、prompt组装。考虑到内容较多,我把它拆成上、下两篇来写。上篇主线是把“搜索扩展”这件事讲透——为什么传统检索失效、向量数据库怎么选、RAG检索链路里的细节怎么调,再顺手给出一个Spring Boot + PGVector的最小可运行工程。适合给那些想在现有Spring体系里引入AI能力,但又不是从零做大模型平台的后端团队参考。
1. 搜索扩展本质上要解决什么问题
1.1 传统关键词搜索的“天花板”
传统搜索看着成熟,但它的核心假设是:用户输入的关键词、文档里出现的词、以及语义背后的概念,三者必须能对上。倒排索引和BM25这类算法再强,也只是在“字面重合度”上做文章。你搜“工资发放延迟”,文档里写的是“薪酬到账时间超过约定日”,字面几乎不重合,经典检索基本无能为力。这不是引擎的问题,而是查的词与存的词不是同一套表达体系。
在实际的业务系统里,这种“字面不匹配”比我们想象中普遍得多。用户会口语化提问、会打错字、会用岗位术语或行业黑话,文档作者又习惯用书面语境来写。两边没有共享同一词表,传统检索召回效果自然差。还有一个更隐蔽的问题:传统检索只能告诉你“哪些文档命中关键词”,不能告诉你“哪一段话确实回答了这个问题”。它返回的是文档列表,需要用户自己再打开、翻阅、判断。放在知识库问答场景里,这个体验其实是非常原始的。
1.2 RAG:给大模型再接一块“外接记忆”
RAG的全称是Retrieval-Augmented Generation,翻译过来就是“检索增强生成”。思路可以理解成开卷考试:大模型本身像是一个已经接受过通识训练的学生,知识面广但不够新、不够细,也没法记住你公司内部成百上千份制度文档。开卷考试的核心动作是允许带资料,而RAG要解决的问题就是:在它作答之前,先帮它找到与当前题目最相关的几页参考资料,再让它结合这些资料回答。
这里的关键点是,找参考资料不能再靠关键词了,而要语义相似。用户的句子和文档里的段落虽然用词不同,但意思相近,我们希望系统能感知到这种“近义”。实现方式是先把文档切成小块,用Embedding模型把每一小块转成一个高维向量,存入向量数据库。查询问题时,也把问题转成向量,然后在数据库里找出与这个提问向量最接近的若干个文档块。在大模型生成答案之前,把检索出来的文档块拼接在prompt中作为上下文,这样模型就有了可参考的事实依据。
这套流程可以拆成两个阶段:离线阶段负责文档解析、切片、向量化、写库;在线阶段负责问题向量化、相似度检索、拼接上下文、调用大模型。多数Spring项目改造的着力点其实就在这两个阶段上,业务代码不需要大动,新增一套检索服务即可。
1.3 什么业务场景才值得上这套方案
项目聊到一定阶段,我一般会让团队先停下来,认真做一个“值不值得上”的判断。如果你的搜索场景只要求精确匹配,比如查订单号、查身份证、查商品ID,用传统数据库查询就行,别引入向量化和RAG,那是给系统平白增加复杂度。
真正适合搜索扩展的场景有这些共性:业务需要理解自然语言;知识内容是长期累积的、非结构化的文档;答案需要引用具体来源;文档更新频率较高,不适合每次重训模型。典型落地包括企业制度问答、售后知识库、研发Wiki检索、客服话术推荐、法规和合规文件问答等。
不适合的情况也有:纯结构化数据查询,比如“本月销售额多少”这类需要精确汇总的问题,RAG并不擅长,应交给BI或数据库;对实时性要求极高的交易场景,也不适合把链路拉这么长。所以筛选标准不是“RAG热门就上”,而是“你的海量文档里确实藏着业务答案,但用户找不到”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 在Spring项目里如何组装“向量数据库 + RAG”这套东西
2.1 Spring AI提供的三个核心抽象
如果自己直接调大模型SDK和向量库客户端,也能把RAG链路写出来,但会发现代码非常向具体厂商倾斜:换个Embedding模型要改一段,换个向量库又要改一段。Spring AI的价值在于提供了三个抽象,让这套逻辑可插拔。
Document是对待入库文本的统一封装,里面除了正文text之外,还有metadata。metadata简单理解就是一串key-value,可以挂来源文件名、部门、页码、文档类型、权限维度等信息。EmbeddingModel负责把文本向量化,不同厂商的embedding能力在这里被统一成同一个接口,你可以在OpenAI、Ollama、Azure等实现间切换,而调用方不需要改动。VectorStore则是对向量数据库的抽象,Chroma、Milvus、PGVector、Redis等都会提供对应的实现,上层代码只面对add、similaritySearch这样的方法。
这三个抽象组合起来,RAG链路就变成了一种搭积木式的开发。你在配置里选好一家Embedding模型和一种向量库,Spring AI会自动装配好对应的Bean,剩下的Java代码就是面向接口写,将来换底层组件时业务代码基本不用动。
2.2 离线和在线两个阶段分别该做什么
如果你画过那种组件框图,会发现RAG并不神秘,本质就是两条“数据流水线”。离线流水线负责导入:从原始数据源读取文档,使用DocumentReader或手工读取把非结构化文本解析出来,然后按策略切片,再调用EmbeddingModel把每个切片向量化,最后把结果存入VectorStore。在线流水线负责问答:拿到用户问题后,对问题做一次embedding,到VectorStore执行相似度检索,取出topK个候选切片,拼入prompt,送给ChatClient生成最终回答。
很多人第一次实现时容易把两条流水线写成同步强耦合,比如在文档上传接口里既解析又向量化又查询。生产环境更推荐拆成独立的索引服务和问答服务,离线导入放在后台异步任务中执行,在线部分才需要低延迟响应。Spring项目里可以用ApplicationEvent、消息队列甚至定时任务分别承载,关键点是不要让文档导入拖垮了在线链路。
2.3 向量数据库是必需的,但别依赖它解决所有事
有一种误解是,引入了向量数据库就等于实现了AI搜索。实际上向量库只负责“存储向量 + 按距离召回”,它不负责语义理解,理解文本含义的是Embedding模型;它也不负责回答质量,回答的是大模型;它更不负责准确率,准确率主要受切片和召回策略影响。我经常和团队做一个类比:向量数据库像一个按“语义位置”排列的书架,Embedding决定了书放在哪里,检索只是把离你最近的那几本抽出来。书架本身并没有理解内容。
所以我们在架构里始终把向量库当成基础设施,而不是核心逻辑。凡是涉及内容治理、切片、权限过滤、阈值判断的策略,都应该沉淀在Spring服务层里,这样将来底层替换向量库时,业务策略不会丢。
3. 向量数据库选型与实际落地细节
3.1 主流向量数据库横向对比
做搜索扩展前,团队问得最多的一个问题是:到底该选哪个向量数据库。这类问题没有标准答案,但有几个可对比的维度。我把常见选择整理成一张表,方便大家直接从项目条件反推。
| 方案 | 部署方式 | 运维成本 | 大体量支持 | Spring AI支持 | 适合场景 |
|---|---|---|---|---|---|
| Chroma | 嵌入式/本地服务 | 低 | 一般 | 有 | 开发学习、中小知识库 |
| PGVector | 基于PostgreSQL插件 | 中低 | 中等 | 有 | 已有PostgreSQL的Spring项目 |
| Milvus | 独立集群 | 高 | 很高 | 有 | 大规模检索、毫秒级高并发 |
| Qdrant | 独立服务/云 | 中 | 高 | 有 | 通用生产环境 |
| Redis | 模块加载 | 低 | 中 | 有 | 已在用Redis且向量规模不大 |
| Elasticsearch | 独立集群 | 高 | 高 | 有 | 已有ES,需要混合检索 |
真实选型逻辑其实不复杂。如果团队已经有PostgreSQL且数据量在几百万条左右,PGVector是最平滑的选择,不需要额外引入一套新组件;如果数据量达到千万级、并发要求非常高,再考虑Milvus或Qdrant这类专用向量库;如果只是个人学习、搭建Demo或快速验证,Chroma的嵌入式模式最省事。在我的经验里,绝大多数企业内部知识库的语料规模远没有到必须上专用集群的地步,很多团队是过度设计。
3.2 为什么不用单独的专用向量库
我在多数Spring Boot项目里优先推荐PGVector,最大的理由是“减少组件种类”。一个业务系统如果本来就有PostgreSQL,加上pgvector插件后,原来所有数据库运维、备份、权限体系都能继续复用,学习成本低。很多小团队对独立向量库的运维并不熟悉,引入Chroma、Milvus等于给团队增加一个新物种,出问题时排查链路也变长。
但PGVector也确实有它的边界。向量检索性能取决于两个因素,一是表里已有的向量规模,二是索引方式。Spring AI里可以配置HNSW或IVFFlat,HNSW在召回质量上更稳,但写入和内存占用也会更高。单纯几万、几十万条向量场景下,压测差异都不明显,所以先跑通流程比预先调优重要。等真正遇到性能瓶颈时,再评估迁移专用向量库也不迟。
3.3 用Chroma时“一个集合会拉出一堆表”是怎么回事
这个题目里提到的热搜词挺有意思,现实中确实有团队用Chroma时发现,明明创建了一个集合,但查看底层SQLite文件时,却出现了好多张表,一时分不清各自作用。我在实际排障中也翻过Chroma的持久化文件,下面这张表关系能帮助你建立认知。
Chroma在持久化模式下会把元数据和元信息落到SQLite中,一般的版本里会让你看到若干系统表和业务存储表。核心的写入逻辑大致是:collections是集合主表,记录集合名称和配置;embeddings是核心表,保存向量记录,每条对应一个文本块;embedding_metadata保存对应向量记录附加的元数据信息;segments记录索引分区,Chroma内部是按segment来组织HNSW索引的。除此之外还会有一些序列号或队列类系统表,用来维护id分配和写入顺序。
有一个重要认识是:向量本体并不完全“可读地”躺在表里,真正的相似度索引是在外部HNSW索引文件或表中的专用结构里实现的。SQLite只是负责把文本、metadata和段归属管理好。因此,如果直接手改SQLite表,很可能破坏索引与数据的对应关系,导致检索结果异常。排查时观察表结构没问题,但千万不要绕过Chroma API直接改数据。对于应用层来说,理解成“一个集合 = 一批文本块 + 对应的向量索引 + 一套元数据旁路”即可。
4. 检索效果从源头把控:Embedding、分块、Metadata、阈值
4.1 Embedding模型决定“语义坐标系”
向量检索不是把文本压缩成一个数字,而是把文本转换成一组向量。每个Embedding模型都在自己的“语义坐标系”下工作,同一个句子在两个模型下的向量表达可能完全不同。这产生了一个非常常见的坑:入库时用A模型,查询时误操作成B模型。A模型生成的是1024维向量,B模型生成的是768维向量,要么查询报维度错误,要么虽然维度恰好一致但语义坐标系不同,检索结果基本不可信。
所以我的习惯是把Embedding模型名固化在配置中心里,并且要求索引服务与检索服务必须读取同一份配置。模型本身的选型,要看你的语料语言分布、领域专有词比例、向量维度约束。常见开源模型如nomic-embed-text维度为768,bge系列能覆盖中英文场景,具体选用哪个,建议拿一份自己业务里真实的问题集做离线评测,不要只看benchmark榜单。Embedding模型更新后,已经入库的历史向量一般不会自动重算,需要做全量重灌,这是上线时容易遗漏的一个环节。
4.2 Chunk分块策略是“最容易调但最容易被忽略”的环节
很多人把RAG效果不好归咎于大模型或向量库,但查到最后,问题往往出在分块上。分块大小如果过大,一个向量里塞了太多主题,检索时虽能命中这个块,但其中与问题相关的可能只有几个句子,其余一堆不相关内容会稀释大模型对答案的判断。分块过小,又会丢失上下文,比如一句话没有主语、缺少前因后果,即便被检索到,模型也无法正确回答。
我通常建议从500到800个token的块大小起步,重叠量设置为100到200个token。这个区间适合大多数制度文档和知识问答场景。但“通用区间”不等于最优解,像技术文档这类结构清晰的资料,优先按章节、标题、段落边界去切,而不是机械数token硬切。Spring AI里有TokenTextSplitter,但要注意不同版本对构造参数支持有差别,最新的可以用构建器或默认构造函数来获得合适参数,再用split方法处理Document列表。分割后的块最好带上原始的源文件标识和章节路径,这些metadata后续在召回结果展示和引用溯源时会很有价值。
4.3 Metadata过滤:防止“语义相近但隔离不彻底”
如果你做过一个稍微像样的生产系统,很快就会遇到一个残酷现实:单靠向量相似度没法保证数据的“语义隔离”。两个不同项目团队的知识文档,在一些通用描述上可能语义高度接近,如果不对检索范围做过滤,A团队的提问很可能会把B团队的内容召回出来。
解决这件事不能只靠向量距离,要在写入和查询两层都加上metadata过滤。写入时给文档打好标签,比如项目ID、可见范围、部门编码、文档类型;查询时在SearchRequest里加filterExpression条件,让检索在限定范围内进行。Spring AI在这块抽象得比较清楚,只要底层VectorStore支持元数据过滤,业务层就能统一使用。实际项目中我强烈建议把所有权限相关条件都放到过滤器里,不要把权限判断放到召回之后再做,否则等于让用户看到了他本不该看到的内容,只是最后回答时被挡了一下,这个隐患在知识库场景里不可接受。
4.4 TopK和相似度阈值的“手感”
TopK和相似度阈值是检索服务最外层的旋钮,也是上线后最容易被反复调节的参数。TopK决定了最终拼进prompt的文档块数量,设得太大,模型会被大量无关内容干扰;设得太小,相关答案可能压根没被召回。我一般是从4到6开始试,再根据准确率上下浮动。类似地,相似度阈值设置过高,系统会经常找不到任何资料;设置过低,又会返回一堆语义牵强的片段。
具体阈值没有一个通用的“标准答案”,因为它依赖你对Embedding模型的分布认识。比如某些模型返回的余弦相似度天然偏高,常见相关内容的相似度都在0.7以上,那阈值选0.5可能就形同虚设。更稳妥的做法是先用一段真实问答集跑一遍,观察“真正相关的文档块”分数落在哪个区间,再以略低于这个区间为阈值。宁可让阈值低一点、多召回几个候选,也别因为阈值过高导致召回为空。在我的实践里,检索为空是比检索不准更让用户体验糟糕的问题,因为模型彻底没法回答了。
5. 端到端实操:用Spring Boot + PGVector构建最小RAG搜索
5.1 前置环境准备
为了能照着走通而不依赖团队内部组件,我这里选PGVector做演示。它只需要一个Docker容器,不引入额外复杂度。前置条件如下:
- JDK 17+
- Spring Boot 3.4.x(示例用了Spring AI 1.0.0)
- Docker,用来启动PostgreSQL + pgvector
- Ollama,用来本地提供embedding模型和chat模型,免去了申请外部API key的等待时间
依次启动服务和拉取模型即可。用Docker运行PostgreSQL是比较干净的方案:
bash复制docker run --name pgvector-demo \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=knowledge \
-p 5432:5432 \
-d pgvector/pgvector:pg16
接着本地安装Ollama并拉取模型。我这里embedding用nomic-embed-text,问答用qwen2.5,都是本地模型,网络依赖小,适合快速验证。
bash复制ollama pull nomic-embed-text
ollama pull qwen2.5
如果你的团队已经接入了云厂商模型,那更省事,直接配置对应的API Key就行,后面Java代码可以完全复用。
5.2 引入Spring AI依赖
Spring AI 1.0.0正式版已经发布在Maven Central,不需要额外配置仓库。在pom.xml中加入spring-ai-bom来做统一版本管理,然后声明以下核心依赖:
xml复制<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
实际需要的starter包括:
xml复制<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-st
