尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

SpringAI+通义千问+PostgreSQL构建企业知识库RAG问答系统实践

SpringAI+通义千问+PostgreSQL构建企业知识库RAG问答系统实践 简介随着企业智能化转型构建基于大语言模型的知识库问答系统成为热点。RAG检索增强生成技术通过结合向量检索与生成模型有效解决传统关键词搜索不精准的问题。SpringAI作为Java生态的AI开发框架提供统一模型调用与向量存储抽象通义千问提供对话与Embedding能力PostgreSQL配合pgvector插件实现结构化数据与向量相似度检索的融合。这套方案借助Spring Boot生态大幅简化开发部署成本适用于企业内部知识库、智能客服、内部问答等场景。文章从工程实践角度出发分享从技术选型、环境搭建到核心链路实现和问题排查的完整路径为Java技术栈开发者提供一套可快速落地、可运维的RAG问答系统参考方案。 最近半年我一直在做企业知识库问答方向的落地前后对比了好几套技术方案。从LlamaIndex到LangChain4j再到SpringAI最后选择了SpringAI 通义千问 PostgreSQL这套组合把RAG知识库问答系统完整跑通并且已经支撑了内部几个业务线的智能问答场景。这套方案的构建过程踩了不少坑也沉淀了不少经验今天就围绕这个项目把从技术选型到核心实现再到问题排查的完整路径分享出来。这个项目解决的核心问题很明确企业内部文档散落在各种系统里业务人员想快速找到答案传统的关键词搜索要么搜不准、要么搜出来一堆无关内容。基于RAG实现的知识库问答系统先把文档向量化存入数据库再把用户问题向量化去检索最相关的文档片段最后把检索结果交给大语言模型生成回答兼顾了语义理解和内容溯源非常适合企业知识库、客服辅助、内部问答这类场景。如果你也在用Java技术栈想快速搭建一个能落地的RAG知识库这套方案可以直接参考。1. 这套技术组合怎么选型SpringAI、通义千问、PostgreSQL各担什么角色1.1 为什么是SpringAI而不是LangChain4j或自己封装HTTP调用选型的时候我其实纠结过一阵子。Java生态里做LLM应用无非几条路自己用HttpClient封装大模型API、用LangChain4j、用SpringAI。自己封装太原始RAG链路里涉及Embedding、向量检索、结构化输出、Token统计这些琐碎环节全手写要维护的东西太多。LangChain4j功能确实全但社区和Spring生态的整合深度不如SpringAI。SpringAI最核心的优势是它把“模型调用”和“向量存储”这两件事抽象成了统一的接口底层对接阿里云通义千问也好、OpenAI也好切换的时候只需要改配置和依赖业务代码基本不用动。对于做企业级应用的人来说这非常重要因为你不知道未来会不会换模型供应商也不知道客户要求私有化部署还是走公有云API。SpringAI的AutoConfiguration机制做得比较成熟Spring Boot项目引入依赖后配置好API Key就能直接注入ChatClient、EmbeddingModel这些Bean使用。我实际用下来SpringAI的抽象设计是够用的尤其是它对Prompt模板、ChatMemory、结构化输出转换的支持让RAG里的检索增强生成可以很自然地组合出来。如果你本身就在用Spring Boot选SpringAI几乎零学习成本。1.2 通义千问在项目里承担什么任务Embedding模型和对话模型怎么分工通义千问在这个项目里其实是两个角色一个负责把文本转成向量一个负责根据检索结果生成最终回答。这两个角色对应的模型完全不同不能混用。对话模型我选的是qwen-plus这个模型在中文场景下的语义理解、概括能力和指令遵循表现都够用而且价格相对合理。qwen-max更强但成本高一些对于企业内部知识库问答这种场景qwen-plus性价比更高。如果你处理的文档比较专业、需要更强的推理能力比如法律文书解读、复杂技术方案对比可以考虑qwen-max但对多数企业内部知识QA来说qwen-plus已经绰绰有余。Embedding模型用的是text-embedding-v2这个模型的向量维度是1536语义匹配效果在中文场景下表现不错。这里有个注意点模型切换的时候要注意向量维度是否一致因为向量数据库里的索引是基于维度建的如果换了Embedding模型导致向量维度变化原来的索引会直接失效需要重建。这也是我后面在排查问题时踩过的坑。需要说明的是SpringAI官方对阿里云通义千问的支持是通过spring-ai-alibaba或spring-ai-starter-model-qwen这类扩展包提供的不同版本适配情况不同。我当时用的是SpringAI 1.0.0版本搭配对应的qwen starter整体兼容性已经很稳定了。1.3 为什么用PostgreSQL做向量存储而不单独部署Milvus或Chroma这个问题是项目选型时讨论最多的。团队里有人建议用Milvus有人推荐Chroma也有人想上Elasticsearch。最后我们选了PostgreSQL pgvector理由很务实架构越简单越好维护。企业里本身大概率已经有PostgreSQL在跑了DBA熟悉运维备份恢复、权限管理这些机制成熟完善没必要为了一个向量检索功能再引入一套分布式系统。Milvus确实在千万级向量规模下性能很强但对多数企业内部知识库来说百万级以下的向量规模pgvector加HNSW索引完全扛得住。Chroma更适合本地开发和原型验证生产环境数据一致性、权限控制这些方面还差一些意思。PostgreSQL自带的能力也让开发省了很多事。比如知识库文件除了向量还有原文内容、元数据、文件来源、创建时间这些结构化信息联合查询的时候一条SQL把向量相似度检索和结构化条件过滤同时完成这比纯向量数据库要方便得多。另外企业数据敏感度高的场景PostgreSQL的行级安全策略可以直接用在知识库表上实现用户级数据隔离这也是我选择它的重要原因。pgvector是PostgreSQL的扩展插件不是独立数据库安装后给数据库增加vector类型和几个索引方法简单直接。网上不少文章喜欢拿PostgreSQL和MySQL对比说PostgreSQL适合复杂查询和扩展能力这正好吻合RAG场景既能存结构化业务数据又能做向量相似度检索一个数据库解决两件事。2. 搭建环境时最容易踩坑的三个前置条件2.1 让PostgreSQL具备向量能力pgvector插件安装不成功的常见原因先讲pgvector的安装。多数情况下直接执行CREATE EXTENSION vector就可以了前提是你装PG的时候选了包含扩展包的版本或者编译时带了相关支持。但我实测下来有几个场景会让这一步报错一是PostgreSQL版本和pgvector版本不匹配。pgvector 0.5.0以上版本要求PostgreSQL 11以上如果你还在跑PG 9.6或10要么先升级数据库要么选旧版pgvector。二是权限不够执行CREATE EXTENSION需要数据库超级用户权限如果你用的是普通业务账号会提示permission denied。三是greenplum这些基于PG内核改造的数据库加载pgvector的兼容性很不稳定建议直接用原生PostgreSQL。我是在Ubuntu服务器上装好PG 15之后用apt install postgresql-15-pgvector装的插件执行很顺利。Windows环境稍微麻烦一些需要下载对应版本的安装包或者在Docker里跑pgvector/pgvector:pg15镜像推荐直接用Docker省去一堆编译依赖的麻烦。装好之后验证一下版本SELECT extversion FROM pg_extension WHERE extname vector;。看到版本号就说明可用。2.2 SpringAI工程依赖引入版本为什么必须保持一致SpringAI的依赖管理有一个坑它对Spring Boot版本有强依赖不是随便哪个版本都能配。我用的是Spring Boot 3.2.x SpringAI 1.0.0如果Spring Boot版本太低启动时会出现类找不到或者Bean初始化失败的问题。pom.xml里的依赖大致是这样的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-qwen/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-vector-store-pgvector/artifactId /dependency /dependenciesSpringAI的仓库地址比较特殊早期版本不在Maven Central而是在Spring的里程碑仓库里需要额外配置repository。1.0.0版本已经发布了正式版Central上可以拉到了但如果用0.8.x或RC版本必须在settings.xml或pom里加repositories repository idspring-milestones/id nameSpring Milestones/name urlhttps://repo.spring.io/milestone/url /repository /repositories这里要特别提醒通义千问的starter和OpenAI的starter不要同时引入如果工程里还有旧的其他AI框架依赖可能产生Bean冲突。我遇到过ChatClient注入出来的是OpenAI实现结果请求打到OpenAI接口上报错排查半天发现是依赖冲突把多余的starter排除掉就好了。2.3 配置文件里的关键参数API Key、向量维度和索引参数别乱填配置文件是第一个容易埋坑的地方。我当时在application.yml里配置时有几个参数折腾了很久spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.2 embedding: options: model: text-embedding-v2 vectorstore: pgvector: index-type: hnsw distance-type: cosine dimensions: 1536几个关键参数的含义我解释一下index-type有hnsw和ivfflat两种。HNSW是图索引查询速度快、召回准确率高但建索引耗时和内存占用偏大。ivfflat是倒排索引建索引快但查询精度略低。生产环境我建议直接用HNSW百万级向量规模下性能差异非常明显。distance-type有cosine、euclidean、inner_product三种文本语义检索选cosine最合适因为Embedding模型的相似度计算通常基于余弦相似度。dimensions必须和Embedding模型输出维度严格一致text-embedding-v2是1536维如果填错了插入数据直接报维度不匹配。API Key建议用环境变量注入别硬编码在配置文件和代码里。这个Key走的是阿里云百炼平台去百炼控制台创建API Key然后配置好模型权限。实际操作中我发现百炼平台的模型开通和API Key授权是两回事开通了模型不一定代表Key有该模型的调用权限需要在控制台确认是否已授权。3. 核心链路实现从文档切块到检索生成的完整代码落地3.1 文档加载和切块策略为什么推荐按段落语义切而不是固定长度硬切RAG效果好坏一半取决于切块策略。一开始我图省事用固定长度切块每500个字符切一段相邻重叠50个字符。实测下来效果很差一个完整的技术方案被拦腰切断检索时只召回其中一小段信息不完整大模型生成回答时缺上下文明显能感觉到回答“缺东西”。后面改成按文档结构切Markdown按标题层级分块docx按段落分块再对太长的段落做二次切分。比如一个标题下的内容超过1000字就按语义边界再切成多个块尽量保证每个块是一个相对完整的信息单元。SpringAI的TikaDocumentReader可以自动读取PDF、Word、Markdown、TXT等常见格式使用很方便var reader new TikaDocumentReader(new FileSystemResource(filePath)); ListDocument documents reader.get();切块的核心代码我用的是自定义策略按段落后按长度二次切分public ListDocument split(Document document) { ListDocument chunks new ArrayList(); for (String paragraph : document.getContent().split(\\n\\s*\\n)) { paragraph paragraph.trim(); if (paragraph.length() 50) continue; if (paragraph.length() 800) { chunks.addAll(splitByLength(paragraph, 800, 100)); } else { chunks.add(new Document(paragraph, document.getMetadata())); } } return chunks; }切块参数里有两个值比较关键chunkSize和overlap。chunkSize太大检索返回的内容多但可能包含噪声太小信息碎片化召回片段不完整。我测试了很多组800字加上100字重叠在中文场景下效果比较均衡。overlap的作用是让上下文在切分处有衔接避免关键句恰好被切开导致语义断掉。具体数值建议根据你文档的特点调整如果文档专业术语多、句子长可以适当调大。3.2 向量化入库批量写入和去重校验文档切好块之后需要逐个调用Embedding模型转成向量然后写入PostgreSQL。代码实现上SpringAI已经封装了向量化操作不需要手动调接口。核心流程是这样的Service public class KnowledgeBaseService { private final VectorStore vectorStore; public void addDocuments(String filePath) { // 读取文档 var reader new TikaDocumentReader(new FileSystemResource(filePath)); ListDocument documents reader.get(); // 切块 ListDocument chunks documentSplitter.apply(documents); // 补充元数据 chunks.forEach(doc - { doc.getMetadata().put(source, filePath); doc.getMetadata().put(timestamp, LocalDateTime.now().toString()); }); // 向量化并入库 vectorStore.add(chunks); } }这里要注意的是vectorStore.add()是同步的文档量大时比较耗时。我第一次往库里写一个200MB的项目文档集跑了将近15分钟而且还容易超时。后面改成批量写入每次批量提交50到100个文档速度快了不少。另外建议加一个文档去重逻辑用文件内容的MD5作为唯一标识已经入库的文件不能重复写入否则会积累大量重复向量检索时同义内容被重复召回回答质量会下降。PostgreSQL这边向量表的结构由SpringAI自动创建表名默认是vector_store。我手动创建的索引是这么写的CREATE INDEX ON vector_store USING hnsw (embedding vector_cosine_ops) WITH (m 16, ef_construction 64);这里的m表示每个节点的最大连接数m越大索引精度越高但内存和构建时间也越大。ef_construction是构建索引时的动态候选列表大小值越大索引质量越好构建越慢。我测试场景下m16、ef_construction64已经够用如果数据量到千万级再考虑调大。3.3 检索生成链路怎么让大模型“只看该看的”检索增强生成的核心是用户提问后先从向量库检索出TopK相关的文档片段再把这些片段拼装成Prompt上下文让大模型基于这些内容回答。SpringAI的QuestionAnswerAdvisor就是干这个的。Service public class QaService { private final ChatClient chatClient; private final VectorStore vectorStore; public String answer(String question) { return chatClient.prompt() .advisors(QuestionAnswerAdvisor.builder() .vectorStore(vectorStore) .topK(5) .build()) .user(question) .call() .content(); } }topK的设置很关键。我刚开始设成10发现回答经常“超纲”因为召回片段里有不相关内容大模型也会强行编入答案。后面调成5效果明显变好。问题在于topK值太小可能漏掉关键信息太大则引入噪声。建议结合你的文档情况测试文档质量高、切块规范的话topK可以适当调小文档碎片化严重可能需要调大一些。SpringAI还支持在prompt里注入系统指令让模型知道自己是知识库问答助手只根据给定内容回答不知道的内容要明确说不知道。这个对减少幻觉很重要。我用的系统指令是你是企业知识库智能助手。请根据【参考资料】中的内容回答用户问题。如果参考资料中没有相关信息请直接回答“根据当前知识库我无法回答这个问题”。回答时请保持简洁、准确不要编造信息。另外值得说的是为了保留回答的溯源能力我给每个文档片段都加了元数据包括来源文件名、页码、创建时间。这样回答生成后可以把命中的文档片段ID和来源信息返回给前端展示类似“参考文档xxx.md 第3段”的提示这在企业场景里非常重要因为使用者需要确认答案的出处。4. 实测记录常见报错与检索效果调优4.1 五个绕不开的经典问题逐个排雷项目上线前后我整理了一份问题排查清单挑几个典型讲一下。第一个是“vector type not found”或“extension vector does not exist”。原因很直接数据库里没装pgvector扩展。解决办法是在对应的数据库实例里执行CREATE EXTENSION vector;注意不是在某个schema下装而是装到你要用的库上。第二个是“expected 1536 dimensions, not 1024”这类维度报错。这个问题特别隐蔽我遇到过一种情况配置文件的dimensions写的是1536但代码里手动创建了一个维度不对的测试数据导致插入时直接报错。保证所有入口写入的数据维度一致同时把SpringAI的dimensions配置项和实际Embedding模型的输出维度对齐是最基本的检查项。第三个是连接池被打满。批量写入大量向量时如果用了默认连接池配置很容易把连接耗尽。给HikariCP配置合理的最大连接数同时控制批量写入的并发度能有效规避。我实测下来批量写入时并发数设为3到5数据库CPU和内存表现都比较平稳。第四个是HNSW索引建不起来。有些版本在表里已经有大量数据时建HNSW索引会非常慢甚至内存溢出。解决办法是先建表后建索引并且把maintenance_work_mem调大一点比如SET maintenance_work_mem 2GB索引构建会快很多。第五个是“Answer is not grounded in the provided context”这类回答质量评估问题。本质是检索到的内容和问题不匹配原因可能是切块太碎、topK太小、Embedding模型不贴场景。我的排查顺序是先看检索出的原始片段是否语义相关如果不相关问题在Embedding或切块如果相关内容确实被召回了但回答不用问题在Prompt或生成模型参数。4.2 检索效果不如预期的调优思路别急着换模型在知识库问答场景里用户问“报销流程是什么”你返回的片段必须真的包含报销流程的步骤说明而不是只有“报销”两个字。相似度阈值控制可以和topK配合使用先按相似度过滤再做topK截断召回质量会更稳定。其次可以考虑对文档元数据做过滤。比如多类文档混在一个知识库里用户提问时可以指定只看某类文档。pgvector支持带where条件的ANN检索这种场景用起来非常方便SQL类似SELECT * FROM vector_store WHERE metadata-category 人事制度 ORDER BY embedding $1 LIMIT 10;这样不仅减少跨域噪声还让检索效率更高。另外我建议把“召回率”这个指标纳入日常监控。统计每轮问答命中的文档片段如果经常出现0命中说明问题连不上任何文档如果每次都命中但答案质量差说明Prompt或生成策略需要调整。靠指标驱动优化比拍脑袋调参靠谱得多。最后提一个进阶玩法就是Agentic RAG。普通RAG是“检索一次、生成一次”的线性流程遇到复杂问题容易顾此失彼。Agentic RAG让大模型先判断需要查哪几个子问题、按什么顺序查询、是否值得调用多轮检索灵活性和答案完整度都有提升。SpringAI的ChatClient结合工具调用机制也能实现这种模式但目前还在比较早期的阶段。如果你做的是试验性质的项目可以研究一下这条路径生产环境建议先把基础的RAG链路打磨扎实。5. 企业级落地的一些补充思考5.1 权限和审计不能忽略的两个环节知识库问答在企业内部上线不能只看技术指标。权限隔离是第一个要处理的。不同角色能访问的文档可能不一样比如HR政策只有HR和部门负责人能看到财务制度只有财务线能看到。pgvector支持在检索SQL中拼接权限条件但SpringAI自带的QuestionAnswerAdvisor没有直接暴露这个能力需要继承或自定义advisor在检索前把用户的权限数据塞进查询条件里。审计日志是第二个要补上的环节。每次提问、命中了哪些文档、回答了哪些内容都应该记录日志方便追溯。大模型偶尔会出现输出偏差有审计兜底才能在问题发生后快速定位是检索环节的问题还是生成环节的问题。5.2 回答风格的工程化温度、系统指令、输出格式一起调很多初学者只把温度调到0以为模型就会“确定性地”回答。经验是温度确实要低但系统指令和输出格式约束的作用甚至超过温度。比如我希望回答要先给结论、再列依据在系统指令里写明比靠模型自觉靠谱得多。SpringAI可以用OutputConverter来实现结构化输出比如要求模型返回JSON格式的答案和引用来源列表record Answer(String message, ListString references) {} Answer answer chatClient.prompt() .system(systemPrompt) .advisors(QuestionAnswerAdvisor.builder() .vectorStore(vectorStore) .topK(5) .build()) .user(question) .call() .entity(Answer.class);结构化输出很适合对接前端页面展示也方便后续做答案的二次处理和归档。5.3 这个方案的扩展边界什么情况下应该换架构最后聊聊什么场景不需要升级架构。如果知识库文档量超过千万级单一PostgreSQL节点的pgvector检索延迟会明显上升这时候就需要考虑分布式的向量数据库或者将热数据拆分到专门的向量服务。如果并发QPS很高比如面向全公司上万人同时提问PostgreSQL读压力会比较大可以加读写分离或者Redis做结果缓存。但如果只是几百上千份文档、日活几百人的企业内部知识库PostgreSQL pgvector这套方案的运维成本和效果都是最优的。项目上线到现在数据库服务器内存占用稳定在4GB左右查询响应在200毫秒以内完全满足业务需要。我在实际项目中还有一个体会这套方案最大的价值不是技术多新而是把“能落地的工程化”做透了。Java工程师熟门熟路Spring Boot一套走完数据库直接用现成的运维也轻松。后续如果要扩展知识库范围无非是加数据源、补切块规则、调Prompt不需要动地基。如果你也在评估Java技术栈的RAG方案这个组合值得优先考虑。本文还有配套的精品资源点击获取
返回列表