ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring AI 2.0构建生产级RAG知识库:从向量原理到评测闭环

Spring AI 2.0构建生产级RAG知识库:从向量原理到评测闭环 做RAG项目快两年了我发现一个现象很多人第一次接触RAG都是从“把文档扔进知识库然后问AI”开始的。真正做生产级应用时才发现切分、嵌入、检索、评测每一环都能把项目拖垮。这篇内容没有高大上的理论迷阵我用13张图的节奏从向量的直观理解一直讲到Spring AI 2.0的完整落地最后给你一套直接能用的评测体系。如果你是Java技术栈想用Spring AI快速构建RAG知识库或者你在Dify这类工具里画过流程但想回归代码控制这篇文章应该能省你不少弯路。1. 项目整体设计13张图先画给谁看1.1 聊RAG之前先说清我们的目标RAGRetrieval-Augmented Generation检索增强生成要解决的是LLM的两个天然短板一是知识有截止时间公司内部文档它没见过二是它喜欢一本正经地编答案也就是幻觉。RAG的做法简单直接——先根据用户问题去知识库里检索相关资料把找到的资料塞进Prompt再让模型基于这些资料作答。相当于从“闭卷考试”变成“开卷考试”模型手里有了参考书胡说八道的概率自然低很多。做这个项目前我先把13张图规划了出来它们分别对应RAG从原理到落地的13个关键问题。这13张图既是文章的骨架也是我当时给团队做技术分享时用的主线图号主题要解决的疑问图1RAG全景流程RAG整体是怎么串起来的图2向量的二维示意向量为什么能表示语义图3文本嵌入链路从文档到向量的完整过程图4相似度计算RAG检索背后的打分逻辑图5向量数据库索引结构百万级向量为什么还能秒回图6向量数据库选型对比什么场景选什么库图7Spring AI核心组件RAG在Java里的实现骨架图8文档切分策略chunk size与overlap怎么定图9项目目录与依赖一个可跑的工程长什么样图10检索排序调优怎么让正确结果排到前面图11四类RAG瓶颈回答质量差问题出在哪一环图12评测指标体系除了肉眼判断还能怎么测图13自动评测闭环评测如何沉淀成持续流程如果你也准备在公司内部推RAG项目我建议先照着这个框架把图1画出来不要一上来就写代码。RAG看似简单其实文档处理、向量化、检索、模型问答、评测五条链路都有各自的坑先把全局图画清楚后面踩坑时至少知道坑在哪个环节。1.2 为什么选Spring AI而不是别的框架项目选型时有人推荐LangChain4j也有人推荐直接用Python的LlamaIndex。我们团队全是Java后端如果用Python那套后续要单独维护一套Python服务跨语言联调和部署都很痛苦。Spring AI的价值在于它是Spring官方主导的AI抽象层把EmbeddingModel、VectorStore、ChatClient这些概念统一成了Spring风格Java开发者学习成本很低。我当时也对比过Dify这类可视化平台。Dify上手快适合业务同学快速搭原型但到了生产环境发现剂约力不够——你要定制检索逻辑、加权限过滤、接公司内部认证体系、做性能监控可视化节点反而成了瓶颈。所以这次项目定的是“Dify验证可行性Spring AI做最终落地”用代码接管完整链路Dify工作流只用来快速验证Prompt和流程是否合理。2. 从“向量是什么”开始RAG的第一块地基2.1 向量就是一组语义坐标很多人一听“向量”就发怵觉得是线性代数内容。其实你完全可以把它理解成坐标。图2里我画了一个二维坐标系横轴可以理解为“水果程度”纵轴是“事物具体程度”那么“苹果”可能落在(8, 6)“香蕉”在(8, 5)“汽车”却在(1, 3)。坐标越接近表示这两个词语义越接近。真实世界的文档不会只有两个特征所以向量通常是768维、1024维甚至1536维。维度越多能区分的语义维度就越丰富。Embedding模型做的工作就是把“苹果”“香蕉”这样的人类语言映射成高维空间里的一个点。RAG的整个检索逻辑本质上是在这个高维空间里做“找邻居”。理解这一点很重要因为它决定了后面所有调优方向一个Embedding模型不好用往往不是模型本身差而是它表达语义的方式和你业务文档的特征不匹配。比如法律文书和客服问答最好用不同的Embedding模型分别实验。2.2 从文档到向量的完整链路图3展示的是文本嵌入的完整过程原始文档PDF、Word、Markdown先被解析成纯文本然后按规则切成一段段小块每段文本交给Embedding模型转成向量最后连同文本和元数据一起写进向量数据库。这个过程看似简单但“切分”这一步直接决定检索上限。切分切得太粗一块文本里混了好几个主题检索时虽然能命中但上下文里塞了太多无关内容切得太细单个块语义不完整检索到的片段往往答非所问。我在项目里实测的通用规则是通用FAQ类文档用500-800个token合同、法律文件用200-300个token因为合同一个条款就是一个完整语义单元切开语义就碎了。Embedding模型这里也踩过坑。中文场景我推荐两套开源的本地方案用BGE系列比如bge-m3不需要联网成本低线上生产如果有预算用云厂商的text-embedding系列效果通常更好。需要特别强调测试环境和生产环境的Embedding模型必须保持一致一旦换了模型向量空间就变了旧向量会全部失效必须全量重建。这个坑我在第5节评测部分还会展开。2.3 相似度计算点积、余弦与向量范数检索的本质是拿用户提问的向量和库里所有文档向量做相似度计算选最接近的返回。图4里最常用的三种计算方式余弦相似度公式是 cos(A,B) (A·B) / (|A|×|B|)只看方向不考虑长度文本向量场景最常用数值范围在[-1,1]。点积内积A·B Σ(a_i × b_i)同时受方向和长度影响适合向量已经做过归一化的场景。欧氏距离计算的是空间中的直线距离在RAG里不常用因为向量维度很高时距离反差不明显。这里涉及“向量范数”的概念通俗理解就是向量的长度模长。我见过不少人在代码里直接比较点积结果却不看向量是否做过归一化。Spring AI的多数Embedding模型返回的向量默认是归一化的此时余弦相似度和点积是等价的你选哪个都行但如果模型没做归一化直接比点积就会出现“长向量天然得分高”的问题结果会很奇怪。从工程角度我建议在项目里统一用余弦相似度并且在写入向量库前确认向量是否L2归一化过。多数向量数据库在构建索引时也有默认的相似度度量选型和建索引时就要定好中途改metric很可能要重建全量索引耗时很长。2.4 复向量、多向量什么时候才需要搜索热词里有个“复向量”很容易让人误解成数学里的复数。这里大家真正搜的其实是有两套方案向量数据库处理多向量以及多向量检索。简单说就是一段文本不再是只生成一个向量而是生成多个向量。典型代表是ColBERT这种late interaction模型它给文本里每个token都生成一个向量检索时拿查询向量和文档的token级向量做细粒度匹配能捕捉“苹果公司”里的“苹果”到底是水果还是公司这种上下文差异。这种方案对小段精确匹配效果更好但存储量会放大10倍以上计算也更重。我做项目的经验是如果你的文档是短文本问答比如客服FAQ单向量完全够用如果你是长文档、法律条款、论文这类需要“在长文里定位关键句子”的场景才值得认真考虑多向量。切勿一上来就上多向量先跑通单向量基线评测发现检索精准度不够再考虑升级。3. 向量数据库集成与优化检索层才是工程重心3.1 向量数据库选型别只看跑分图5我画的是向量数据库索引的简化结构一棵多层图HNSW或倒排文件IVF排在前面的是“粗略索引”后面再精排。图6则是选型对比。你会发现向量数据库的领域非常热闹pgvector、Milvus、Qdrant、Redis、Elasticsearch都在做选型标准其实不在“谁能跑100万分”而在“谁和你现有架构匹配”。方案适合场景规模能力运维成本混合检索备注pgvector已有PostgreSQL的小团队千万级以内低支持无需新引入中间件Milvus大规模独立向量服务亿级高支持功能全面、社区活跃QdrantRust实现的独立向量库千万到亿级中支持性能不错、API友好Redis低延迟缓存型检索百万级以内低弱适合轻量场景Elasticsearch需要全文检索强耦合千万级高强已有ES团队才推荐我在生产里优先选了pgvector原因是团队太多中间件了能少一个就少一个。向量量级在千万级以内、每天增量不大时pgvector完全顶得住而且可以和业务数据放在同一个库里做事务和权限过滤。等到数据量真涨到需要独立向量服务再平移到Milvus也不迟因为Spring AI的VectorStore接口是抽象封装的替换实现只需要改配置。3.2 HNSW与IVF索引参数到底怎么调图5里HNSW的结构像一个多层跳表越上层节点越稀疏检索时从顶层往下走快速定位到近似最近邻。HNSW的核心参数有三个M每个节点的最大连接数默认16。M越大检索精度越高但内存和构建时间也越高。efConstruction构建索引时的动态列表大小默认100。越大索引质量越高构建越慢。efSearch查询时的动态列表大小默认40。越大检索越准但延迟越高。我一般建议从M16、efConstruction100、efSearch40起步然后用评测集把efSearch从40调到200看在hit rate的提升和P99延迟之间找平衡点。IVF则是“先聚类再搜索”nlist决定聚成多少个桶nprobe决定查询时搜多少个桶。nlist设为数据量的sqrt左右nprobe从1开始往上调。有一个容易被忽略的点索引参数的作用范围和你的数据分布高度相关。如果你的知识库文本类型差异很大既有短标题又有长正文tuning一个通用参数不如先给文档打类型标签然后在查询时用元数据过滤缩小搜索范围。3.3 元数据过滤与混合检索别把整个库都拿来排序纯向量检索的痛点是语义相近不代表业务相关。比如用户问“退货政策”向量库里可能有“商品退货说明”也有“员工离职退电脑流程”语义都沾边但你只想要商品中心的数据。解决办法是给每个Document写入元数据检索时用过滤条件把范围圈起来。Spring AI的SearchRequest支持类似filterExpression(docType product status active)的表达。这个才是我认为向量检索工程化的核心真正上生产时RAG的检索一定不是“全库语义搜索”而是“有权限、有时间范围、有业务类型的语义搜索”。数据权限甚至可以映射到查询过滤条件里不同角色的用户检索到不同的数据集。混合检索BM25全文检索 向量检索也值得关注。某些情况下用户输入的是产品型号“A-100-X”向量检索可能找不到但BM25的精确关键词匹配效果反而稳定。Spring AI 2.0的VectorStore接口里可以同时保留多个存储实现手动合并两套结果再重排这是RAG进阶必须掌握的技巧。3.4 RAG知识库能存图片吗搜索里高频出现的“rag知识库能存储图片嘛”答案是可以但要区分清楚。图片进RAG有两种路线第一多模态Embedding。用CLIP、SigLIP这类多模态模型把图片和文本映射到同一个向量空间查询时拿文字向量直接检索图片向量。适合“给一张图找类似图”“用文字描述找设计稿”的场景。第二先用多模态大模型把图片转成文字描述再走常规文本RAG。比如给产品截图生成一段说明文字入库后用户问“那个截图里的按钮功能是什么”纯文本检索也能命中。这种方式实现简单成本低适合绝大多数企业内部知识库场景。我给业务方做技术方案时通常推荐第二条路线。原因很现实企业内部图片资产大多是产品截图、流程图、单据扫描件用户要的是“根据图片内容回答问题”而不是“按图搜图”。用图片描述文本入库流程可复用现有链路也方便人工审核。只有真正需要图像检索的业务比如以图搜图的设计素材库才值得上多模态向量。4. Spring AI 2.0实战把RAG跑在Java服务里4.1 Spring AI核心组件长什么样图7是Spring AI 2.0里和RAG相关的核心组件本质上就四个EmbeddingModel负责向量化VectorStore负责存取向量DocumentReader负责读文档TextSplitter负责切块。把这些配好之后问答链路就是用户问题 → 向量化 → 检索 → 组装Prompt → LLM回答。Spring AI 2.0相比1.x最大的变化是把很多模型接入方式改成了spring.ai.model.*自动配置配置简化了不少。比如OpenAI兼容协议的模型在application.yml里配好base-url和api-keyChatModel和EmbeddingModel的Bean就自动生成。这对国内云厂商的模型服务特别友好因为大多数云服务都暴露了兼容OpenAI协议的接口。依赖方面一个典型的Spring Boot 3项目只需要引入spring-ai-starter-model-openai、spring-ai-starter-vector-store-pgvector这类starter就行。我强烈建议用Spring Initializr或者Maven仓库里明确标注的版本Spring AI版本迭代很快不同版本之间的API偶尔会有调整不要直接照抄网上的旧代码。4.2 接入通义模型或本地Ollama对接通义大模型或者本地Ollama配置方式几乎相同。下面这套配置在Spring AI 2.0里实测可用spring: ai: model: chat: openai: api-key: ${DASHSCOPE_API_KEY} base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 options: model: qwen-plus embedding: openai: api-key: ${DASHSCOPE_API_KEY} base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 options: model: text-embedding-v3如果团队偏好本地部署直接用Ollama也行配置文件改成spring: ai: model: chat: ollama: base-url: http://localhost:11434 options: model: qwen2.5:7b embedding: ollama: base-url: http://localhost:11434 options: model: bge-m3:latest这里有一个值得注意的兼容性问题模型服务的model名要以实际开通为准。云厂商的模型ID经常更新比如qwen-plus、qwen3.x系列在控制台上以实际开通值为准。本地Ollama则要看拉取过的模型列表代码里写错模型名启动不会报错但运行时调用就会出现模型不存在之类的错误排查起来容易绕圈子。4.3 文档切分策略实操图8的主题是切分。我一直觉得切分是整个RAG里最“看着简单、做起来全是坑”的环节。Spring AI里常用的TokenTextSplitter实现如下TextSplitter splitter TokenTextSplitter.builder() .withDefaultChunkSize(500) .withDefaultOverlap(50) .build(); ListDocument chunks splitter.apply(reader.get());chunk size我建议结合你的模型最大上下文来定。如果模型上下文是128kchunk设大一点没太大问题因为可以多塞几个段落到Prompt里如果模型上下文只有8kchunk设500但检索返回4个段落再加上系统提示词和用户问题上下文就被塞满了。overlap重叠的作用是避免“一句话被从中间切开”导致语义断裂。50-100个token的overlap比较稳妥。但重叠也带来重复内容会稍微增加存储和Prompt成本具体表现为检索结果里相邻块经常同时命中。这个现象不算bug但如果重复过多说明chunk size设小了。我做合同类项目时会配合一个自定义切分先按章节标题定位再按段落切最后用TokenTextSplitter兜底。这样做的好处是不会把不同法律条款混在一个chunk里。Spring AI 2.0支持自定义DocumentReader和TextSplitter团队里有Java功底写个扩展并不难。4.4 一个最简可跑的RAG Pipeline图9是一个最简可运行的项目结构src/main/java/com/example/ragdemo ├── RagDemoApplication.java ├── config/VectorStoreConfig.java ├── service/RagService.java └── controller/RagController.java先说初始化。EmbeddingModel的Bean由Spring Boot自动注入VectorStore这里用pgvectorBean VectorStore vectorStore(EmbeddingModel embeddingModel, DataSource dataSource) { PgVectorStore store new PgVectorStore(EmbeddingModel converter, dataSource); return store; }写入文档的Service方法public void ingest(String filePath) { DocumentReader reader new PagePdfDocumentReader(filePath); ListDocument docs splitter.apply(reader.get()); vectorStore.add(docs); }查询链路是Spring AI里最有价值的部分用QuestionAnswerAdvisor实现了“检索增强生成”的整个包装RestController public class RagController { private final ChatClient chatClient; public RagController(ChatClient.Builder builder, VectorStore vectorStore) { this.chatClient builder .defaultAdvisors(QuestionAnswerAdvisor.builder() .vectorStore(vectorStore) .build()) .build(); } PostMapping(/chat) public String chat(RequestBody String question) { return chatClient.prompt() .user(question) .call() .content(); } }这段代码写到这一步一个RAG服务就能跑了。用户提问后QuestionAnswerAdvisor自动完成向量化、检索、把上下文塞进Prompt最后让模型基于检索内容回答。我特别欣赏Spring AI这一点它不像别的框架把RAG流程写在框架内部黑盒里而是通过Advisor机制暴露出来你可以随时加一个新的Advisor做权限过滤、对话历史、Prompt模板改写组合起来非常灵活。我再贴一段更精细的检索参数控制SearchRequest request SearchRequest.builder() .query(question) .topK(4) .similarityThreshold(0.5) .filterExpression(docType contract) .build(); ListDocument docs vectorStore.similaritySearch(request);这里topK决定取多少片段similarityThreshold决定最低相似度门槛。如果检索结果里有一堆低分内容混进来先检查threshold是不是没设但threshold设太高又会漏检一般0.4-0.6之间需要反复试。4.5 从Dify工作流到Spring AI代码一张映射表很多团队先用Dify把RAG原型做出来后面希望用Java代码接管。这时候你会发现Dify里每个可视化节点和Spring AI的组件都有对应关系。图10我用映射表把它们对应起来照着迁移就行Dify节点Spring AI对应组件说明知识库检索节点VectorStore.similaritySearch配置过滤条件、topK问题分类节点ChatClient LLM Router写成switch分支或单独Model调用Prompt模板节点Advisor PromptTemplate定制System PromptAgent节点ChatClient ToolCallingSpring AI的Function Calling知识库添加DocumentReader TextSplitter导入方向由节点改成代码任务从Dify迁到Spring AI本质是把可视化流程翻译成代码流程。迁移过程中建议不要一次性全搬先把“知识检索LLM回答”的最小链路跑通再逐步添加工作流里那些条件分支。Spring AI对复杂流程的支持是足够的只不过它不提供拖拽画布你得接受用代码来表达流程这个事实。5. RAG评测体系从“感觉不错”到“可量化”5.1 先诊断瓶颈四类问题对应四个环节搜索里“rag瓶颈”出现频率很高说明不少项目都卡在“效果不够好但不知道哪里不好”。图11给出了四类典型瓶颈正好对应RAG的四个环节文档处理环节PDF解析乱码、表格丢失、扫描件没有OCR。表现是所有问题答非所问。切分环节chunk太小或太大语义被切断检索返回内容不完整。检索环节该命中的没命中或者命中了但排序靠后。生成环节检索结果都是对的但模型没按检索内容回答或者原样复述了一堆废话。诊断方法是把RAG基座拆开第一步单独打印检索结果用“带检索结果的离线测试集”直接看top5里有没有正确答案第二步把正确答案硬塞进Prompt看生成质量。如果第一步过了第二步没过问题在生成层如果第一步就没过问题在检索层或文档处理层。5.2 评测指标hit rate、MRR与NDCG怎么算图12是评测指标体系。肉眼判断问题不好量化我们需要一组可计算、可对比的指标。RAG评测里最常用的三个检索指标hit rate命中率用户提问后top-N检索结果里是否包含目标文档。计算方式是命中数量 / 总提问数。这指标最简单适合先卡基线。MRR平均倒数排名只关心第一个正确答案排在哪个位置。公式是 MRR (1/N) × Σ(1 / rank_i)rank_i是第i个问题第一个正确答案的排名。如果没命中该问题计0。NDCG归一化折损累计增益考虑整个排序质量。公式是 DCG Σ(rel_i / log2(i1))NDCG DCG / IDCG。rel_i是相关性打分比如正确答案给1相关但不完全匹配给0.5无关给0。对应的Java实现并不复杂public double hitRate(ListBoolean hitFlags) { long hit hitFlags.stream().filter(Boolean::booleanValue).count(); return (double) hit / hitFlags.size(); } public double mrr(ListInteger ranks) { double sum 0.0; for (int rank : ranks) { if (rank 0) sum 1.0 / rank; } return sum / ranks.size(); }生成侧还有一个“忠实度”指标用来判断模型回答是否忠于检索上下文这要和LLM-as-Judge结合。做法是把“用户问题、检索上下文、模型回答”一起发给裁判模型让裁判打1-5分并输出理由。打分脚本我在下游评测时常用Python跑因为统计分析方便def faithfulness_score(context, answer): prompt f判断回答是否完全由给定上下文支撑。 上下文{context} 回答{answer} 只输出 0 或 1并给出理由。 # 调用裁判LLM解析分数 return parse_score(llm(prompt))5.3 搭一套可落地的自动评测闭环图13是自动评测闭环这套流程在我项目里稳定跑了好几个月。步骤如下构造测试集从真实用户问题里挑20-50条每条标注它对应的标准文档ID和一份标准答案。测试集要覆盖常见问题、模糊问题、长尾问题规模不在大而在于每一条都能暴露问题。批量跑RAG写个离线脚本逐条调用检索接口记录检索到的文档ID、排序、最终生成的答案。自动计算检索指标对照标注的文档ID直接算出hit rate、MRR、NDCG。生成质量打分用裁判LLM给忠实度、答案相关性打分。回归对比把每次改动前后的指标记录下来形成一个简单趋势表。改切分参数、换Embedding模型、加过滤条件都靠这个表拍板。我的经验是第一次跑这个闭环时指标难看是正常的千万别慌。你只需要记录基线然后每次只改一个变量。比如这周改chunk size从300到500下周再改overlap每次对比hit rate和MRR的变化你很快就能找到当前瓶颈。还要提醒一个常被忽略的坑不要用生产环境的同一个LLM既做生成又做裁判。生成和裁判如果共用同一个模型会在“是否认可自己回答”这件事上产生主观偏向。最好裁判模型用能力更强或者至少不同温度参数的大模型把temperature调低到0.1以下打分结果才稳定。5.4 评测之后向Agentic RAG与GraphRAG进阶当你把上面这套评测指标跑出来后可能会发现一些单纯调参解决不了的问题。比如单轮RAG无法回答“对比A部门和B部门去年Q3的报销差异”这类多跳问题因为信息分布在多个文档块一次检索很难覆盖全。这时候就要考虑Agentic RAG让模型具备多轮检索能力先检索出A部门数据再检索B部门数据最后汇总对比。如果业务问题强依赖实体关系比如“这个供应商和哪些项目有关联”GraphRAG或Ontology RAG值得调研。GraphRAG把文档里的实体和关系抽取成知识图谱Ontology RAG更进一步用业务本体约束图谱结构。它们的共同点是前期建设成本高但评测数据显示在关系密集场景下回答准确率提升非常明显。回到热门问题“rag和llm wiki”以及“解决了知识割裂”其实很多团队的知识库不是没有资料而是资料之间缺乏关联导致检索时只能召回孤立片段。GraphRAG就是针对这种“知识割裂”的解法之一。但我的建议很直接先把单轮RAG的评测跑好确认瓶颈确实在“跨文档推理”后再上图谱。不要因为热词就盲目引入重方案成本会高到你怀疑人生。最后说一个我个人的实操心得。做这个项目时踩过最大的坑就是中途换了一次Embedding模型却没有重建向量库结果线上检索返回了一堆风马牛不相及的片段。从那以后所有模型升级都强制走“离线评测 全量重建向量”流程。还有一个实际的小技巧评测集一开始只有5条也没关系先把整个闭环跑通再去扩充到50条。评测体系的建立比评测集的规模重要得多。
返回列表