1. 为什么选择 Spring AI 来做企业知识库问答企业知识库问答这个需求这两年我接到的咨询特别多。几乎每一家有点规模的公司内部都堆着成千上万份文档——产品手册、运维手册、合同模板、历史工单、会议纪要散落在 Confluence、语雀、共享盘、甚至个人电脑里。员工想找一条信息要么在群里问一圈没人理要么靠关键词搜索翻半天最后找到的还可能是三年前的过期版本。这个痛点非常真实也非常普遍。传统做法是做一个全文检索系统把文档丢进 Elasticsearch靠关键词匹配。但关键词匹配有个致命问题用户问“服务器磁盘满了怎么处理”文档里写的是“存储空间不足的应急方案”字面完全不重叠搜不出来。用户得自己猜文档里用了什么词这体验就很差。而基于大语言模型的问答系统能理解语义用户怎么问都能找到相关内容还能直接给出总结好的答案这就是 RAG检索增强生成的价值所在。那为什么用 Spring AI 而不是 Python 那一套这是很多 Java 团队最关心的问题。我接触过的企业里后端主力技术栈绝大多数是 Java 和 Spring Boot运维体系、监控体系、发布流程都是围绕 Java 建的。如果为了做个知识库问答单独搭一套 Python 服务意味着要引入新的语言、新的依赖管理、新的部署方式运维成本陡增。Spring AI 的出现让 Java 团队能用自己熟悉的方式把大模型能力接进来这是它最大的意义。Spring AI 本质上是一套抽象层它把不同大模型厂商的 API 差异屏蔽掉提供统一的ChatClient、EmbeddingClient、VectorStore等接口。你今天用 OpenAI明天想换成别的模型改改配置就行业务代码基本不用动。这个抽象设计对企业来说非常重要因为大模型这个领域变化太快了谁也不想被某一家绑死。这篇文章我会完整讲一遍怎么用 Spring AI 搭一个企业知识库问答系统包括整体架构怎么设计、文档怎么切块、向量库怎么选、检索怎么做、多轮对话怎么处理以及我在实际项目里踩过的坑。适合有 Spring Boot 基础、想快速落地一个 RAG 应用的开发者也适合技术负责人做方案选型参考。2. 整体架构设计与技术选型思路2.1 RAG 的核心链路拆解RAG 这个词听起来玄乎拆开看其实就两条链路一条是离线索引链路一条是在线问答链路。离线索引链路干的事是把企业里的各种文档读进来切成小块每块转成一个向量一串浮点数存到向量数据库里。这个过程是一次性的或者文档更新时增量做。在线问答链路干的事是用户提问把问题也转成向量去向量库里找最相似的几个文档块把这些文档块和用户问题一起拼成提示词发给大模型让大模型基于这些文档块生成答案。用生活化的类比离线索引就像给图书馆的每本书做索引卡片卡片上不是写关键词而是写这本书的“语义指纹”。在线问答就是用户来描述他想找什么你拿他的描述去比对所有卡片的指纹找出最接近的几本然后把这几本书的相关段落翻给一个很聪明的助手让他读完告诉你答案。这个链路里每个环节都有讲究。文档切块切多大、向量模型选哪个、检索返回几条、提示词怎么写都会直接影响最终效果。我见过太多团队模型选最贵的向量库选最潮的但切块策略一塌糊涂最后效果惨不忍睹。所以下面我会逐个环节讲清楚。2.2 技术栈选型与理由先把我推荐的技术栈列出来再说为什么。组件选型理由应用框架Spring Boot 3.x Spring AIJava 团队零学习成本生态成熟大模型OpenAI 兼容接口抽象层统一可灵活切换向量模型text-embedding 系列维度适中中文效果可接受向量数据库PGVector复用现有 PostgreSQL运维成本低文档解析Apache Tika支持格式多PDF/Word/Excel 通吃切块策略递归字符切分实现简单效果稳定重点说一下 PGVector 这个选择。很多教程一上来就推荐专用的向量数据库比如 Milvus、Qdrant、Weaviate。这些确实性能强、功能全但对大多数企业来说引入一个新的数据库意味着多一套运维体系、多一份备份策略、多一个故障点。而 PGVector 是 PostgreSQL 的一个扩展你现有的 PostgreSQL 加个扩展就能用数据和应用数据在同一个库里事务、备份、监控全都复用。对于文档量在百万级以下的企业知识库PGVector 的性能完全够用。我实测过单表几百万个向量配上合适的索引查询延迟在几十毫秒级别体验很好。大模型这块Spring AI 支持 OpenAI 兼容的接口。这意味着只要某个模型服务提供了 OpenAI 兼容的 API你就能接进来。这个设计非常实用因为企业往往有合规要求不能直接把数据发给外部服务需要走内部部署的模型。只要内部模型服务包装成 OpenAI 兼容格式Spring AI 就能无缝对接。2.3 项目模块划分我习惯把这类项目拆成三个模块职责清晰方便独立演进。第一个是文档接入模块负责从各种来源读取文档解析成纯文本做清洗和切块。这个模块的输入是文件路径或文档流输出是切好的文本块列表。第二个是向量索引模块负责把文本块转成向量写入向量库同时维护文档元数据来源、标题、更新时间等。这个模块要支持增量更新文档改了要能重新索引。第三个是问答服务模块负责接收用户问题做检索拼提示词调大模型返回答案。这个模块还要处理多轮对话的上下文管理。这三个模块可以放在一个 Spring Boot 应用里也可以拆成微服务。我建议初期放一个应用里用包结构区分就行等量大了再拆。过早拆微服务只会增加复杂度没有实际收益。3. 核心细节解析与实操要点3.1 文档切块RAG 效果的第一道分水岭切块这件事看起来简单实际上是最容易翻车的地方。我见过一个团队把整篇几万字的文档直接转成一个向量结果检索时要么全中要么全不中效果极差。也见过切得太碎一句话一个块检索出来全是碎片大模型拼不出完整答案。切块的核心矛盾是块太大向量表达的信息太杂检索精度下降块太小上下文不完整大模型理解不了。业界比较通用的做法是块大小在 500 到 1000 个字符之间块之间保留 10% 到 20% 的重叠。重叠的目的是防止一个完整的语义被切断比如一句话正好跨在两个块的边界上有重叠就能保证至少有一个块包含完整语义。Spring AI 提供了TokenTextSplitter和基于字符的切分器。我一般用递归字符切分它会优先按段落切段落太长再按句子切句子还长再按字符切。这样能尽量保证语义完整性。配置大概是这样TokenTextSplitter splitter new TokenTextSplitter( 800, // 目标块大小 100, // 最小块大小 50, // 块间重叠 10000, // 最大块数 true // 保留分隔符 );这里有个经验中文和英文的切块策略要区别对待。英文按 token 算比较准中文一个字可能就是一个 token 甚至更多。如果你的文档中英混杂建议按字符数切而不是按 token 数。我一般中文文档用 500 到 800 字符一块英文文档用 1000 到 1500 字符一块。还有一个容易被忽略的点切块前要做文档清洗。PDF 解析出来经常有页眉页脚、页码、乱码Word 解析出来可能有大量空行和格式符号。这些噪音如果不清理会污染向量导致检索不准。我一般会做这几步清洗去掉连续空行、去掉纯数字行页码、去掉重复出现的页眉页脚、统一全角半角标点。3.2 向量模型选择与维度考量向量模型决定了检索的天花板。模型不好后面怎么调都白搭。选向量模型主要看三个指标语义表达能力、维度、推理速度。语义表达能力就是它能不能把语义相近的文本映射到相近的向量。这个一般看公开的评测榜单但榜单只能参考最好还是用自己的业务数据测一下。维度方面常见的有 768 维、1024 维、1536 维、3072 维。维度越高表达能力越强但存储和计算成本也越高。1536 维是个比较平衡的选择大多数场景够用。推理速度也很关键因为索引阶段要把所有文档块都转一遍。如果文档量大模型太慢会导致索引时间不可接受。我一般会先拿一小批文档测一下吞吐估算全量索引时间。Spring AI 里配置向量模型很简单通过application.yml指定就行spring: ai: openai: api-key: ${OPENAI_API_KEY} embedding: options: model: text-embedding-3-small这里有个坑要注意向量模型一旦选定就不能随便换。因为不同模型生成的向量空间不一样换了模型之前索引的所有向量都失效了必须全量重建。所以选型时要慎重考虑清楚未来会不会换。如果预见到可能要换可以在设计上留个字段记录向量模型版本方便后续做灰度迁移。3.3 PGVector 的安装与索引配置PGVector 的安装在 Linux 上相对简单Windows 上稍微麻烦一点。核心就是编译扩展、在数据库里执行CREATE EXTENSION vector。装好之后建表时用vector类型存向量。建表语句大概长这样CREATE TABLE knowledge_chunk ( id BIGSERIAL PRIMARY KEY, content TEXT NOT NULL, embedding vector(1536), doc_id VARCHAR(64), doc_title VARCHAR(255), chunk_index INT, created_at TIMESTAMP DEFAULT NOW() );索引这块是性能关键。PGVector 支持两种索引IVFFlat 和 HNSW。IVFFlat 建索引快、占空间小但查询精度略低HNSW 查询精度高、速度快但建索引慢、占空间大。我一般推荐 HNSW因为查询体验更重要建索引慢一点可以接受。CREATE INDEX ON knowledge_chunk USING hnsw (embedding vector_cosine_ops) WITH (m 16, ef_construction 64);这里的m和ef_construction是两个关键参数。m控制每个节点的连接数越大精度越高但索引越大16 是个常用值。ef_construction控制建索引时的搜索范围越大索引质量越高但建得越慢64 是平衡值。查询时还有个ef_search参数可以在会话级别调整值越大召回越高但越慢。注意HNSW 索引在数据量很大时建索引会占用大量内存建议在业务低峰期建并且监控内存使用。如果内存紧张可以先建 IVFFlat等数据稳定后再换 HNSW。3.4 检索策略不只是向量相似度很多人以为 RAG 的检索就是拿问题向量去向量库查 topK其实远不止。纯向量检索有几个明显问题对专有名词、型号、编号这类精确匹配不敏感对否定语义处理不好topK 固定可能召回不足或召回过多。我的做法是混合检索向量检索加关键词检索两路结果做融合。关键词检索用 PostgreSQL 自带的全文检索就行不需要额外引入 Elasticsearch。融合算法用 RRFReciprocal Rank Fusion它对不同来源的分数尺度不敏感实现简单效果好。-- 向量检索 SELECT id, content, 1 - (embedding :queryVec) AS score FROM knowledge_chunk ORDER BY embedding :queryVec LIMIT 20; -- 关键词检索 SELECT id, content, ts_rank(to_tsvector(content), plainto_tsquery(:query)) AS score FROM knowledge_chunk WHERE to_tsvector(content) plainto_tsquery(:query) ORDER BY score DESC LIMIT 20;两路各取 20 条用 RRF 融合后取前 5 到 8 条送给大模型。这个数量不是拍脑袋定的太少信息不够太多会超出上下文窗口且引入噪音。我一般会做个实验用一批测试问题跑不同 topK看答案质量选最优值。还有一个进阶技巧是重排序。检索回来的文档块用一个专门的重排序模型再排一遍把最相关的放前面。这个能显著提升效果但会增加一次模型调用延迟上升。如果对延迟不敏感强烈建议加。4. 实操过程与核心环节实现4.1 项目初始化与依赖配置先建一个 Spring Boot 3.x 项目Maven 依赖加上 Spring AI 的 starter。注意 Spring AI 的版本要和 Spring Boot 版本匹配不然会有兼容问题。我写这篇文章时用的是 Spring AI 1.0 系列对应 Spring Boot 3.3 以上。dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-pgvector-store-spring-boot-starter/artifactId /dependency dependency groupIdorg.apache.tika/groupId artifactIdtika-core/artifactId version2.9.1/version /dependency配置文件里把数据库和模型相关的都配上spring: datasource: url: jdbc:postgresql://localhost:5432/knowledge username: postgres password: ${DB_PASSWORD} ai: openai: api-key: ${OPENAI_API_KEY} base-url: ${OPENAI_BASE_URL} chat: options: model: gpt-4o-mini temperature: 0.2 embedding: options: model: text-embedding-3-small vectorstore: pgvector: index-type: HNSW distance-type: COSINE_DISTANCE dimensions: 1536temperature设成 0.2 是有意的。知识库问答要的是准确和稳定不是创意温度低一点能让答案更聚焦、更少胡编。base-url单独抽出来是为了方便切换模型服务地址不同环境用不同配置。4.2 文档解析与切块实现文档解析我用 Apache Tika它能自动识别文件类型PDF、Word、Excel、PPT、HTML 都能处理。核心代码就几行public String parseDocument(InputStream inputStream) throws Exception { AutoDetectParser parser new AutoDetectParser(); BodyContentHandler handler new BodyContentHandler(-1); Metadata metadata new Metadata(); parser.parse(inputStream, handler, metadata, new ParseContext()); return handler.toString(); }BodyContentHandler(-1)里的 -1 表示不限制输出长度默认是 100KB文档大了会被截断这个坑我踩过。解析完做清洗然后切块。切块我用 Spring AI 的TokenTextSplitter但前面说了中文要按字符切所以我实际用的是自己封装的递归字符切分器逻辑是按\n\n、\n、。、、、这个优先级递归切直到每块小于目标大小。public ListString split(String text, int chunkSize, int overlap) { ListString chunks new ArrayList(); int start 0; while (start text.length()) { int end Math.min(start chunkSize, text.length()); if (end text.length()) { int lastBreak findLastBreak(text, start, end); if (lastBreak start) end lastBreak; } chunks.add(text.substring(start, end)); start end - overlap; if (start 0) start 0; if (end text.length()) break; } return chunks; }findLastBreak就是从 end 往前找最近的断句符号。这个逻辑简单但有效比直接按固定长度切效果好很多。4.3 向量写入与增量更新向量写入用 Spring AI 的VectorStore接口它封装了写入逻辑ListDocument documents chunks.stream() .map(chunk - new Document(chunk, Map.of( docId, docId, title, title, chunkIndex, index ))) .toList(); vectorStore.add(documents);Document的第二个参数是元数据这个很重要。检索时可以按元数据过滤比如只搜某个部门的文档或者只搜某个时间之后的文档。元数据设计要提前想好后面加字段要重建索引。增量更新是个难点。文档改了怎么知道哪些块要更新我的做法是给每个文档算一个内容哈希存到文档表里。更新时先比对哈希一样就跳过不一样就删掉这个文档的所有旧块重新切块索引。删除用元数据过滤vectorStore.delete(docId docId );注意删除和写入最好放在一个事务里或者至少保证删除成功后再写入。如果删除失败但写入成功会出现重复块检索时同一内容出现多次影响效果。4.4 问答链路完整实现问答链路的入口是一个 REST 接口接收问题和会话 ID。核心流程是问题向量化、混合检索、拼提示词、调大模型、返回答案。public String ask(String question, String sessionId) { // 1. 检索 ListDocument docs hybridSearch(question, 8); // 2. 拼上下文 String context docs.stream() .map(Document::getContent) .collect(Collectors.joining(\n\n---\n\n)); // 3. 取历史对话 ListMessage history sessionStore.getHistory(sessionId); // 4. 拼提示词 String systemPrompt 你是一个企业知识库助手。请严格基于下面提供的资料回答问题。 如果资料中没有相关信息直接说根据现有资料无法回答不要编造。 回答要简洁准确必要时引用资料原文。 资料 context; // 5. 调用模型 String answer chatClient.prompt() .system(systemPrompt) .messages(history) .user(question) .call() .content(); // 6. 存历史 sessionStore.append(sessionId, question, answer); return answer; }提示词里那句“如果资料中没有相关信息直接说无法回答”非常关键。不加这句大模型会倾向于用自己训练时的知识来编答案这在企业场景是灾难。我见过一个案例用户问某个内部流程资料里没有模型编了一套流程出来用户信以为真去执行结果出了问题。所以防幻觉的提示词是必须的。多轮对话的处理我是把最近几轮问答作为历史消息传给模型。但要注意历史不能太长否则会挤占上下文窗口也会让模型分心。我一般保留最近 3 轮更早的做摘要或者直接丢弃。另外检索时最好把当前问题和上一轮问题合并一下再检索这样能处理“那它呢”这种指代性问题。5. 常见问题与排查技巧实录5.1 检索不准的排查思路检索不准是最常见的问题表现是答案答非所问或者明明文档里有却检索不到。排查要按链路一步步来。先看切块是否合理。把检索到的块打印出来看内容是否完整、是否包含答案。如果块切得太碎答案被切散了就要调大块大小。如果块太大包含太多无关信息就要调小。再看向量模型是否合适。拿几个典型问题手动算一下问题和正确文档块的相似度看是否明显高于其他块。如果相似度都差不多说明模型区分度不够考虑换模型。然后看检索数量是否够。topK 太小可能漏掉正确块调大试试。但也不能无限大太大引入噪音。我一般从 5 开始试逐步加到 10、15看效果拐点。最后看是否需要混合检索。如果问题里有专有名词、型号、编号纯向量检索往往不行加上关键词检索通常能解决。5.2 大模型答非所问或编造答案这个问题一般出在提示词上。检查几点提示词有没有明确要求“基于资料回答”有没有明确说“不知道就说不知道”资料和问题的位置是否清晰。我常用的提示词模板是这样的你是一个严谨的知识库助手。请只使用【参考资料】中的信息回答问题。 【参考资料】中没有的内容一律回答根据现有资料无法回答。 不要使用你自己的知识不要推测不要编造。 回答时如果引用了资料请标注来源文档标题。 【参考资料】 {context} 【用户问题】 {question}这个模板的关键是把“只使用参考资料”和“不知道就说不知道”都写死。实测下来加了这两句编造率大幅下降。还有一个技巧是降低 temperature。温度高模型更“发散”更容易编。知识库问答场景温度设 0 到 0.3 之间比较合适。5.3 性能与成本优化性能问题主要在两个地方检索慢和模型调用慢。检索慢一般是索引没建好检查 HNSW 索引是否生效ef_search是否设得太大。模型调用慢是网络和模型本身决定的能优化的就是减少调用次数和 token 数。成本优化有几个方向。一是缓存相同问题直接返回缓存答案不用重新检索和调用模型。二是模型分级简单问题用小模型复杂问题用大模型。三是控制上下文长度检索返回的块数不要太多提示词不要写太长。我做过一个统计一个日活几百人的知识库如果不做缓存光模型调用成本一个月就不少。加了缓存后命中率能到 30% 到 40%成本直接降三分之一。缓存 key 用问题的向量哈希相似问题也能命中。5.4 常见问题速查表现象可能原因排查方向检索不到相关内容切块太大/太小、向量模型不合适打印检索结果调整切块参数答案编造提示词没约束、温度太高加防幻觉提示词降温度答案不完整topK 太小、块被切断调大 topK增加块重叠检索慢索引未生效、ef_search 太大检查索引调小 ef_search重复内容增量更新时旧块未删干净检查删除逻辑加事务专有名词搜不到纯向量检索不敏感加关键词检索做混合多轮对话答非所问历史太长、指代未处理限制历史轮数合并问题检索6. 一些实战中的经验与建议做企业知识库问答技术只是一部分还有不少非技术的坑。我挑几个印象深的说说。文档质量决定效果上限。我接过一个项目客户文档全是扫描件 PDFOCR 出来错字连篇检索效果怎么调都上不去。后来花了两周做文档治理把核心文档重新整理成结构化文本效果立刻好转。所以项目启动前一定要先评估文档质量如果太差先做治理别急着上系统。用户预期管理很重要。知识库问答不是万能的它只能回答文档里有的内容。上线前要跟用户说清楚不然用户问了个文档里没有的问题系统说“无法回答”用户会觉得系统不行。我一般会在界面上加个提示说明系统的能力边界。持续迭代是必须的。上线只是开始后面要根据用户反馈不断调优。我一般会记录用户的提问和系统的回答定期分析哪些问题答得好、哪些答得差针对性优化。差的问题往往是切块或检索的问题找到规律就能批量改进。权限控制别忽略。企业知识库往往有权限要求不同部门能看的文档不一样。这个要在检索层做过滤根据用户身份过滤元数据。别小看这个我见过因为没做权限控制普通员工搜到了高管薪酬文档的事故。最后说个技术选型的心得。Spring AI 这个生态还在快速演进版本之间 API 可能有变化。我的建议是锁定一个稳定版本别追新。等社区验证过、文档齐全了再升级。生产环境稳定比新特性重要得多。这个系统我前后搭过好几套从最初的纯向量检索到后来的混合检索加重排序效果是一点点磨出来的。没有银弹就是不断测试、调整、再测试。希望这些经验能帮你少走点弯路。