资讯动态

基于Spring AI与RAG的企业内部知识库问答系统实践

发布时间:2026/9/28 15:11:44 来源:尧图企业网站定制
做企业内部知识库问答系统我最初的动机非常具体团队文档越来越多消息群里每天被同样的问题刷屏而通用大模型对内部业务一无所知。带着 Spring AI 和 RAG 这两个关键词我花了三个星期从零搭起了一套能用的知识库问答系统把散落在 Markdown、PDF 和 Confluence 里的内容统一变成可检索、可追溯、可对话的入口。下面我按选型、搭建、入库、问答、调优、踩坑的顺序把完整过程复盘一遍内容包括每一步的配置代码、参数依据、实测数据以及几个我觉得值得写进组内 wiki 的坑。适合所有想用纯 Java 技术栈落地 RAG 的团队参考。1. 为什么选 Spring AI把 RAG 从原型拉进生产1.1 这类项目真正的痛点在哪先说需求本身。我所在团队维护的系统和大量文档脱不开关系接口定义散在 Swagger 和 Markdown 里运维手册在 Confluence历史故障复盘在飞书还有一部分知识只在个人脑子和聊天记录里。信息割裂的直接后果是新人入职第一周反复问同样的问题老员工被 到烦。市面上的搜索工具多少能缓解找到文档的问题但解决不了给出答案的问题——你拿到三份文档还是要自己读、自己归纳才能回答这个报错码怎么处理。RAG 恰好补了这个缺口。它的核心思路是先检索、再回答用户提问时先从向量库里捞出最相关的几段原文拼到提示词里让大模型基于这些原文生成回答。模型不需要事先记住你的业务知识知识以可检索的文本形式放在外部存储里改文档就能更新答案不用重新训练。这对企业内部场景是量身定做的——我们最怕的就是模型一本正经地编造接口参数而 RAG 至少能把回答约束在给定的资料范围内。1.2 为什么不用 LangChain / LangChain4j而是 Spring AI方案评审前我确实试过 LangChainPython 版。它的 RAG 生态成熟各种 loader、splitter、vector store 集成应有尽有社区里踩坑记录也好搜。但摆在面前的问题很现实团队是 Java 背景服务跑在 Spring Boot 上为问答功能额外维护一个 Python 服务等于多养一套部署、日志、监控和埋点体系。LangChain 的 API 迭代也快按教程写的代码隔半年再打开经常已经过时。LangChain4j 是 JVM 侧移植方案RAG 组件很全我也认真对比过但当时它的版本节奏和 Spring 生态的融合度不如 Spring AI 顺手。Spring AI 是 Spring 官方支持的项目最大优势在于自动配置引入 starter、填好 api-key聊天模型、嵌入模型、向量库这些 Bean 全部由框架装配好写代码的方式跟在 Spring Boot 里连数据库、配缓存几乎没有差别。加上 1.0 GA 之后 ChatClient 和 Advisor 这套 API 基本定型做 RAG 需要写的胶水代码比我预想少得多。最终我的选型是Spring AI 智谱 GLM PGVector 向量库本地原型阶段也验证过嵌入式方案。2. 环境搭建与最小依赖从 Maven 坐标到 PGVector2.1 版本怎么选这是我进项目的第一道坎Spring AI 在 0.8.x 以及 1.0.0 M1-M3 期间的 API 变动非常频繁网上很多教程用的还是旧写法照着抄大概率启动就报错。我的建议是用 1.0 GA 之后的版本配合 Spring Boot 3.4.x用 BOM 统一管理所有 AI 相关依赖的版本避免各 starter 之间版本不齐导致的神秘问题。pom 里这样引dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-zhipuai/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-vector-store-pgvector/artifactId /dependency /dependencies注意 starter 本身不写版本号版本统一吃 BOM。如果你不在智谱生态里而是用阿里云的通义系列可以参考 spring-ai-alibaba 的 dashscope starter接入思路完全一样。另一个备选方案是走 OpenAI 兼容模式引入spring-ai-starter-model-openai把 base-url 指到智谱的 v4 兼容接口同样能跑适合你已经有 OpenAI 封装代码的情况。2.2 配置文件和模型选型application.yml 里分三块聊天模型、嵌入模型、向量库。这是我的最小可用配置spring: ai: zhipuai: api-key: ${ZHIPU_API_KEY} chat: options: model: glm-4-flash embedding: options: model: embedding-2 vectorstore: pgvector: initialize-schema: always聊天模型我选了 glm-4-flash成本低、响应快做知识库问答够用如果后续对复杂推理要求高再换更强型号就行。嵌入模型用 embedding-2输出 1024 维向量。这里有个项目级的关键点先确定嵌入模型再确定向量库的维度。Embedding 模型一旦更换向量库表结构就得跟着改后面踩坑章节我会专门讲这个连锁问题。2.3 PGVector 建库以及原型阶段的偷懒方案PGVector 需要 PostgreSQL 12并且先执行扩展创建CREATE EXTENSION IF NOT EXISTS vector;Spring AI 的 PGVector starter 默认会帮你创建vector_store表就是上面配置里的initialize-schema: always表结构大致是 id、content、metadata、embedding 几列embedding 列向量的维度必须和嵌入模型的输出维度一致。生产环境我建议把自动建表关掉改用 Flyway 或 Liquibase 管理 schema数据库变更流程会干净很多。原型阶段不想碰 PostgreSQL 的话可以换SimpleVectorStore本地内存版或 Redis 向量库业务代码几乎不用改——这正是 Spring AI 抽象层的价值VectorStore 是个接口换实现就是换 starter 和配置不至于被某个存储方案绑死。3. 入库链路从原始文档到可检索的向量3.1 文档加载Reader 只负责把文件变成文本Spring AI 的 DocumentReader 接口就是读文件 → 产出 List 。Document 由 content正文文本和 metadata元数据两部分组成。我用得比较多的是MarkdownDocumentReader和TikaDocumentReader前者解析 Markdown 干净利落后者靠 Apache Tika 处理 PDF、Word 等富格式文档PDF 场景还可以用PagePdfDocumentReader按页读取方便后续保留页码引用。这里我想重点强调 metadata 的价值。入库时我会把来源文件名、文档分类、更新时间写进 metadata后面检索可以按 metadata 做过滤——比如用户只想查部署文档时检索只扫这一类文档既提升准确率也节省 token。这段逻辑写起来很简单但能显著改变线上问答体验属于改动最小、收益最大的投资。File folder new File(docs); ListDocument docs new ArrayList(); for (File file : folder.listFiles(f - f.getName().endsWith(.md))) { var readDocs new MarkdownDocumentReader(file.toPath()).get(); readDocs.forEach(d - d.getMetadata().put(source, file.getName())); docs.addAll(readDocs); }3.2 切分别让一句话死在分块线上切分是 RAG 的第一道质量关口标题里的分块调优讲的就是这一步。我一开始用默认参数直接跑结果经常出现答案只有半截就是因为相关的一句话恰好被分块线切断了。Spring AI 的TokenTextSplitter按 token 数量切分支持重叠窗口TextSplitter splitter TokenTextSplitter.builder() .chunkSize(800) .chunkOverlap(200) .build(); ListDocument chunks splitter.apply(docs); vectorStore.add(chunks);chunkSize 和 chunkOverlap 的单位是 token不是字符。chunkSize 决定每个片段多长chunkOverlap 决定相邻片段重叠多少。重叠的目的是让横跨分块线的语义单元一句完整的话、一个列表项、一个表格行不至于被截断丢失。默认参数我记得是 chunkSize 1200、chunkOverlap 0对英文长文档还行对中文知识库偏粗我后面专门做了调优对比。3.3 向量化入库一句话代码背后的两件事切完后直接vectorStore.add(chunks)就行。这一步实际上做了两件事先调用嵌入模型把每个 chunk 转成向量再写入 PGVector。嵌入模型和向量库的维度必须匹配前面说过这是最容易出连锁问题的地方。整条入库管道我建议封装成一个可重复执行的方法读文件 → 切分 → 加 metadata → 嵌入 → 写入。文档更新时只对变更的文件重跑一遍即可不用每次全量重建。4. 问答链路检索、增强、生成如何串起来4.1 QuestionAnswerAdvisor 帮你把三步变成一行调用手工实现 RAG 问答要写四步向量化问题、查向量库、拼 prompt、调模型。Spring AI 的 Advisor 机制把查询前处理、检索、查询后增强、最终调用模型这个流程抽象了出来其中QuestionAnswerAdvisor就是专为 RAG 设计的开箱即用组件。配置方式很直接Configuration public class RagConfig { Bean QuestionAnswerAdvisor questionAnswerAdvisor(VectorStore vectorStore) { return new QuestionAnswerAdvisor(vectorStore, SearchRequest.builder() .topK(5) .similarityThreshold(0.5) .build()); } Bean ChatClient chatClient(ChatClient.Builder builder, QuestionAnswerAdvisor advisor) { return builder.defaultAdvisors(advisor).build(); } }问答调用就一句话public String ask(String question) { return chatClient.prompt() .user(question) .call() .content(); }QuestionAnswerAdvisor 内部会自动完成检索并把检索结果和原始问题一起组装进系统提示词。提示词内部结构效果类似下面这样先放检索出来的若干段落再放原问题要求模型只依据资料回答。这个默认模板在英文场景没问题但在中文企业场景我会换一版自定义模板强调资料里没有答案就明确说不知道下面马上讲。4.2 检索参数topK 和相似度阈值怎么定面试官式地把参数列给你没用关键要理解它们各自控制什么。topK一次检索返回的候选 chunk 数。topK 太小容易漏答案太大容易把不相关的内容灌进 prompt既稀释模型注意力又浪费 token。我的经验是在 3~5 的范围内做微调先用 5 起步。similarityThreshold相似度阈值低于这个分数的 chunk 会被丢弃。Spring AI 的默认值我记得是 0.5但在中文场景下偏低容易把不太相关的内容也捞进来。这个值我最后是靠测试集试出来的不是拍脑袋定的。filterExpression按 metadata 过滤比如.filterExpression(category 部署手册)。实测对精确率提升非常明显尤其当知识库涵盖多个业务域时。4.3 关键让模型学会说不知道这是我从线上反馈里学到的。如果不明确限制大模型会顺着检索来的只言片语硬编下去看起来自信满满其实在编。我在自定义提示词模板里加了三条硬约束只依据提供的上下文作答上下文没有答案时直接回复资料中未找到相关信息请尝试换一个关键词回答末尾标注引用来源。第一条和第二条能大幅减少幻觉第三条在企业场景尤其重要——知识库问答和通用聊天不一样用户要的是有依据、敢采信的回答来源标注直接决定了同事敢不敢用这个系统。String template 你是一个企业知识库助手。请只依据下面的资料回答用户问题。 如果资料中没有答案请直接回复资料中未找到相关信息。 回答结尾注明引用来源。 资料 {context} 问题 {question} ; PromptTemplate promptTemplate new PromptTemplate(template); QuestionAnswerAdvisor advisor new QuestionAnswerAdvisor(vectorStore, searchRequest, promptTemplate);构造函数的具体签名以你当前依赖的版本文档为准不同小版本可能有出入但思路是一致的替换默认 prompt把不编造、不硬答的态度写进去。5. 分块调优实测用 20 道题把命中率从 60% 拉到 88%5.1 先建评测集再谈调参分块调优最怕没有标准就瞎试。我第一步是从真实高频问题里选了 20 个每个问题人工标注出期望命中的段落一句话或一段然后写个小脚本跑检索统计 20 个问题中有几个在 top5 结果里命中了期望段落——这个指标就是 RAG 社区常说的 hit rate。hit rate 高不一定代表回答质量好但 hit rate 低几乎一定质量差所以先拿它做筛选器再人工抽检回答质量。评测集不用很大20~30 个有代表性的问题就足以暴露趋势。关键是这些问题必须来自真实用户和高频场景而不是自己拍脑袋编的。5.2 四组参数的四次体检我固定 topK5只调整 chunkSize 和 chunkOverlap用同一批文档、同一组问题跑了一遍结果如下chunkSize / chunkOverlaphit rate观察到的现象300 / 3060%答案碎片化严重长段落经常只召回其中一小段500 / 5070%中规中矩多步骤说明类问题仍会漏800 / 20088%单点答案和多步骤说明都能完整召回本次最优1200 / 30082%检索精确率下降大 chunk 混入次要信息稀释了相似度可以看到不是 chunk 越大越好。1200 token 的 chunk 看起来覆盖内容多但一个 chunk 里往往包含不止一个主题向量化后多个主题互相中和跟问题的相似度反而被拉低检索回来之后 prompt 里也混着无关语句模型容易被带偏。300 token 的小 chunk 召回精准但遇到回答需要跨三个段落的问题就抓瞎因为答案散在多个 chunk 里top5 不一定能把它们凑齐。800/200 在本项目里是平衡点。5.3 中文场景的额外注意调试中我注意到几个中文特有的问题。TokenTextSplitter的分隔符默认偏英文习惯遇到中文文本时会在部门和供应链之间硬切一刀。我的处理是给 splitter 传入自定义分隔符集合把中文句号、问号、顿号和反引号都加进去让切分线尽量落在语义边界上。另外中文里的缩写和专有名词对嵌入模型不友好实测对包含缩写的问题先做一次 query rewrite比如把缩写补全成完整名称再进向量检索命中率提升明显。这也是 agentic RAG 里 query rewrite 思路的价值先让模型把问题规范化再检索。5.4 治本之策结构感知切分与父子分块固定大小切分永远是兜底方案。文档本身有结构时按结构切分效果明显更好。比如 Markdown 文档按标题层级切开每一节作为一个独立 chunk元数据里记录该节所在的章节路径检索命中后还能给用户展示完整上下文路径。Spring AI 里可以自己写一个简单的 DocumentTransformer 实现这个逻辑核心就是解析标题层级在标题处打断点。想再上一个台阶就用父子分块small-to-big策略先把文档切成大块作为父块用于提供完整上下文再把父块切成小块作为子块用于检索。检索时命中子块但把子块所属的父块整体喂给模型。这样兼顾了小 chunk 的召回精度和大 chunk 的上下文完整性。Spring AI 的 Advisor 机制是天然扩展点自己写一个自定义 Advisor 很容易几十行代码的事这也是 RAG 越用越顺的关键路径。6. 踩坑记录我替你先趟过的雷6.1 启动就挂VectorStore Bean 找不到现象启动时NoSuchBeanDefinitionException报 VectorStore 或 EmbeddingModel 找不到。排查链路建议这样走先确认 pom 里是否引入了 vector store starter再确认 api-key 是否真的配置了环境变量缺少 api-key 时自动配置可能静默失效最后确认配置文件的 namespace 是不是写错了比如把 zhipuai 的配置写到了 openai 下面。我那次就是 copy 配置时漏了 api-key 环境变量启动时不报错等真正调用vectorStore才抛异常。所以建议在启动阶段加一个健康检查显式注入 VectorStore执行一次空的相似度搜索确认整个链路通畅后再对外提供服务。6.2 向量维度不匹配插进去就报错换过嵌入模型之后insert 时抛出类似column embedding is of type vector(1024) but expression is of type vector(768)的异常。本质就是 embedding 输出维度和表结构不一致。如果开了自动建表直接把vector_store表 drop 掉重启让它重建如果用了 Flyway 管理就要写一个修改列类型的迁移脚本。这是容易忽视的连锁问题换 chat 模型大家都会记得改配置但换了 embedding 模型很容易忘记还有维度这件事。项目里最好把 embedding 模型名和向量维度显式写进配置文件的注释里团队协作时能少踩很多坑。6.3 答案串味垃圾进到 prompt 里了现象问订单超时怎么排查回答里出现了营销活动的优惠券规则。排查路径是先打印每次检索返回的 chunk 和相似度分数看垃圾是不是被检索回来的。如果是要么提高 similarityThreshold要么给文档打分类标签、查询时按标签过滤。如果检回来的 chunk 本身是相关的但模型还是答串味那就是 prompt 约束不够把只依据上下文回答、不明确就拒绝的指令写强一点。我的实测结论是大部分串味问题不是模型笨而是垃圾进到了 prompt 里。所以排查顺序永远是先看检索结果再改 prompt不要一上来就怪大模型。另外打印检索日志这个习惯非常值得培养线上问题排查全靠它。6.4 版本升级与 API 变动Spring AI 从 0.x 到 1.0 的 API 变化很大网上搜到的代码经常是旧版。我迁移时遇到过 ChatClient 返回类型变化、Advisor 从构造注入变成 Bean 装配、TextSplitter 的 builder 重新设计等一堆坑。应对办法很简单锁死在 BOM 版本以官方文档和当前版本的 javadoc 为准不要照抄旧博客。团队内维护一份当前版本 API 备忘遇到网上代码先对照版本再使用。6.5 成本与限流上线前就要想好RAG 系统有两处 API 调用向量化和生成。向量化在入库时调用文档量大时费用和耗时都不小生成在每次问答时调用是日常成本大头。智谱这类接口要注意并发限制线上服务我加了一个简单的信号量限流加队列削峰避免突发流量把配额打爆。同时把每次问答的 token 数和耗时记到日志里后续优化成本、评估效果都有数据支撑。7. 后续还能往哪些方向扩展7.1 先把单轮 RAG 跑稳再谈 Agentic最近社区里 agentic RAG、GraphRAG、nl2sql 这些概念很热AgentScope 和各类 RAG 框架也在快速迭代。我的体感是知识库问答这个场景先把单轮 RAG 的分块、检索阈值、metadata 过滤调明白已经能覆盖 80% 的需求。在此之上值得加的第一个智能是 query rewrite问题进向量库之前先让模型补全缩写、拆解复合问题再分拆检索。这一步简单、风险低、效果好是性价比最高的演进路径也不会把系统复杂度一下子抬上去。7.2 增量索引、多轮记忆和重排序当文档总量起来之后全量重跑入库管道就不划算了。我现在维护一张文档哈希表入库前对比文件内容是否变化只有变化的文件才重新切分和向量化。回答的引用出处和 chunk 内容也会落库方便做可溯源审计。想再进一步的话可以研究重排序先粗召回 20 个 chunk再用专门的 rerank 模型精排取前 5。多轮对话记忆则可以利用 Advisor 链把 ChatMemory 接进去让后续提问能引用上文这些都是在现有框架上渐进叠加的不必推翻重来。我个人做完这个项目最大的体会是RAG 的难点从来不在模型而在知识能不能被准确找到、找到之后能不能被干净地喂给模型。分块参数、metadata 设计、检索阈值这些看似琐碎的东西恰恰决定了系统效果的上限。先把这些基本功打扎实再去追 agentic 的花活你会少走很多弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑