☰
基于Spring AI与RAG的企业内部知识库问答系统实践
2026/9/28 15:11:40 网站建设 项目流程

做企业内部知识库问答系统,我最初的动机非常具体:团队文档越来越多,消息群里每天被同样的问题刷屏,而通用大模型对内部业务一无所知。带着 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

方案评审前我确实试过 LangChain(Python 版)。它的 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 坐标到 PGVector

2.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> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-zhipuai</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-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"); List<Document> 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(); List<Document> 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 四组参数的四次体检

我固定 topK=5,只调整 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 的花活,你会少走很多弯路。

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

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

立即咨询