从 Demo 到生产:基于 Spring AI + PGVector 构建企业级 RAG 系统全链路实战 从 Demo 到生产:基于 Spring AI + PGVector 构建企业级 RAG 系统全链路实战摘要:很多 RAG 文章停留在“能跑通”的阶段,而企业真正关心的是准确率、延迟、吞吐、成本、治理和可演进性。本文以“企业知识问答平台”为主线,系统讲解如何使用 Spring AI 与 PostgreSQL PGVector 构建生产级 RAG 系统,覆盖架构设计、检索原理、索引构建、混合召回、重排、缓存、限流、异步化、可观测、安全治理、容器化部署与高并发优化,并给出接近生产可用的代码骨架与实战方案。一、为什么企业需要的不是“一个 Chat 接口”,而是一套 RAG 系统企业落地大模型时,最常见的第一步往往是把 LLM 接到一个聊天页面上。但只要进入真实业务,很快就会遇到三类问题:知识不在模型里企业制度、售后 SOP、产品文档、架构设计、工单沉淀、运维手册都属于私域知识,基础模型既不知道,也不应该直接记住。直接问大模型会产生幻觉模型会“合理地胡说”,尤其是在制度解释、故障归因、价格条款、操作流程等高风险场景中。企业场景要求的不只是回答,还要可解释、可追踪、可治理谁上传了文档、哪些文档参与了回答、为什么召回这几段、是否命中缓存、失败后如何降级、延迟为什么抖动,这些都必须可观测。所以企业真正需要的不是一个“AI 聊天框”,而是一套完整的检索增强生成系统,也就是 RAG。RAG 的本质可以概括为一句话:先用检索把“相关事实”找出来,再让大模型在事实约束下完成生成。它解决的是“模型能力”与“企业知识”之间的连接问题。二、企业级 RAG 的目标,不只是准确回答一个真正可上线的 RAG 系统,通常要同时满足以下目标:维度目标准确性降低幻觉,回答可溯源实时性检索与生成延迟可控吞吐支撑高并发问答与批量入库成本控制 embedding、推理、存储和缓存成本可扩展文档量、用户量、租户量增长时可平滑扩容可治理支持权限、审计、灰度、评估、回放可运维有指标、有日志、有链路追踪、有告警这意味着企业级 RAG 不应只包含向量库 + ChatModel两个组件,而应该是一个分层清晰、链路闭环的系统。三、企业级 RAG 的整体架构3.1 分层架构图3.2 两条核心链路企业级 RAG 一般由两条链路组成:写入链路文档上传、解析、清洗、切块、向量化、落库、建索引、版本切换。查询链路问题理解、召回、重排、Prompt 组装、LLM 生成、引用返回、会话记录、指标采集。很多 Demo 只关注第二条链路,但生产系统往往更容易在第一条链路出问题,例如:大 PDF 解析失败OCR 文本噪声严重切块不合理导致上下文断裂文档更新后索引未及时刷新重复入库导致结果污染所以设计上必须把“索引构建”和“在线问答”分离。四、为什么选择 Spring AI + PGVector4.1 Spring AI 的价值Spring AI 的优势不在于“比别人多一个 SDK”,而在于它把 AI 能力纳入了 Spring 体系:统一接入 ChatModel、EmbeddingModel、VectorStore天然兼容 Spring Boot 配置、生命周期、监控、AOP、事务更容易与企业已有的认证、缓存、消息队列、数据库、限流体系集成对 Java 团队友好,学习和维护成本低4.2 为什么选择 PostgreSQL + PGVector很多团队会纠结:到底用专门的向量数据库,还是直接用 PGVector?对大量中大型企业来说,PGVector 是一个很有现实价值的选择,尤其适合以下场景:已经有成熟 PostgreSQL 运维体系需要向量检索与业务元数据做联合过滤希望一个库里同时维护文档、权限、版本和向量数据规模处于可控区间,优先考虑一致性与工程复杂度对比核心点如下:能力PGVectorACID 事务强SQL 联合查询强元数据过滤强运维复杂度低到中向量检索性能中到高,取决于索引与参数适合场景企业知识库、内部搜索、业务融合检索如果你的场景是“数十亿级纯向量搜索”,专用向量数据库可能更合适;但如果你做的是企业 RAG,多数时候数据模型、权限模型和混合检索能力更关键,PGVector 很有性价比。五、RAG 的核心技术原理5.1 Embedding 不是“把文本转数组”这么简单Embedding 的作用,是把文本映射到语义空间中的稠密向量。语义接近的句子,在向量空间中距离也更近。例如:“怎么申请年假”“员工休假流程是什么”这两句话关键词并不完全重合,但在 embedding 空间中会比较接近,因此向量检索能弥补关键词搜索的不足。但实际工程中,embedding 质量受很多因素影响:模型是否适合中文/多语种文档切块是否保留完整语义单元噪声是否过多,例如页眉页脚、乱码、导航菜单查询是否经过改写和归一化所以 RAG 准确率不只是模型问题,前处理同样关键。5.2 为什么要切块企业文档通常很长,不能整篇直接向量化后检索,因为:粒度太粗,召回不精准上下文窗口有限单块包含多个主题,语义中心不稳定因此需要切块。切块策略常见三类:固定长度切块实现简单,但容易把语义切断。递归切块按标题、段落、句子逐级切分,是多数场景下的优选。语义切块按语义边界切分,效果更好,但实现和成本更高。企业实践里,通常建议:FAQ:小块,便于精确命中制度文档:中块,保留条款上下文技术文档:按标题层级切块工单与日志:按事件片段切块5.3 PGVector 的相似度与索引原理PGVector 常见距离计算:-- 余弦距离,最常用 SELECT id, content FROM document_chunks ORDER BY embedding = CAST(:queryEmbedding AS vector) LIMIT 5; -- 欧氏距离 SELECT id, content FROM document_chunks ORDER BY embedding - CAST(:queryEmbedding AS vector) LIMIT 5; -- 内积 SELECT id, content FROM document_chunks ORDER BY embedding # CAST(:queryEmbedding AS vector) LIMIT 5;企业知识问答里,通常优先使用余弦距离。PGVector 常见索引:IVFFlat先聚类,再在部分桶里搜索,速度快,适合大规模数据,但需要训练,召回率受参数影响较大。HNSW近似最近邻图索引,召回率高,查询表现稳定,通常是企业 RAG 首选,但会占用更多内存和建索引时间。经验建议:10 万到 500 万 chunk:优先尝试 HNSW极大规模且写入频繁:评估 IVFFlat检索准确率优先:先 HNSW,再做参数调优六、企业级数据模型设计RAG 的数据建模,不要只建一张“向量表”。至少应区分:原始文档表文档块表索引任务表对话会话表检索日志表评估反馈表6.1 推荐数据库 DDLCREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE TABLE kb_document ( id UUID PRIMARY KEY, tenant_id VARCHAR(64) NOT NULL, knowledge_base_id VARCHAR(64) NOT NULL, title VARCHAR(512) NOT NULL, source_type VARCHAR(32) NOT NULL, source_uri VARCHAR(1024), doc_type VARCHAR(64) NOT NULL, content_text TEXT, content_hash VARCHAR(64) NOT NULL, version_no BIGINT NOT NULL DEFAULT 1, status VARCHAR(32) NOT NULL, language VARCHAR(16) DEFAULT 'zh', metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_by VARCHAR(64), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted BOOLEAN NOT NULL DEFAULT FALSE ); CREATE UNIQUE INDEX uk_kb_document_hash ON kb_document (tenant_id, knowledge_base_id, content_hash, version_no); CREATE INDEX idx_kb_document_query ON kb_document (tenant_id, knowledge_base_id, doc_type, status, updated_at DESC); CREATE INDEX idx_kb_document_metadata ON kb_document USING gin (metadata); CREATE TABLE kb_document_chunk ( id UUID PRIMARY KEY, tenant_id VARCHAR(64) NOT NULL, knowledge_base_id VARCHAR(64) NOT NULL, document_id UUID NOT NULL REFERENCES kb_document(id) ON DELETE CASCADE, chunk_no INT NOT NULL, chunk_type VARCHAR(32) NOT NULL, heading_path VARCHAR(1024), content TEXT NOT NULL, content_tsv tsvector, token_count INT NOT NULL, char_count INT NOT NULL, embedding vector(768), enabled BOOLEAN NOT NULL DEFAULT TRUE, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX uk_kb_document_chunk_no ON kb_document_chunk (document_id, chunk_no); CREATE INDEX idx_kb_document_chunk_doc ON kb_document_chunk (tenant_id, knowledge_base_id, document_id, enabled); CREATE INDEX idx_kb_document_chunk_tsv ON kb_document_chunk USING gin (content_tsv); CREATE INDEX idx_kb_document_chunk_embedding_hnsw ON kb_document_chunk USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64); CREATE TABLE kb_index_job ( id UUID PRIMARY KEY, tenant_id VARCHAR(64) NOT NULL, knowledge_base_id VARCHAR(64) NOT NULL, document_id UUID NOT NULL, job_type VARCHAR(32) NOT NULL, status VARCHAR(32) NOT NULL, error_message TEXT, retry_count INT NOT NULL DEFAULT 0, started_at TIMESTAMP, finished_at TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE rag_query_log ( id BIGSERIAL PRIMARY KEY, trace_id VARCHAR(64), tenant_id VARCHAR(64) NOT NULL, knowledge_base_id VARCHAR(64) NOT NULL, session_id VARCHAR(64), user_id VARCHAR(64), query_text TEXT NOT NULL, rewritten_query TEXT, top_k INT NOT NULL, hit_count INT NOT NULL, llm_model VARCHAR(64), prompt_tokens INT, completion_tokens INT, latency_ms INT, cache_hit BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP );6.2 为什么要区分文档表和 chunk 表因为文档是业务对象,chunk 是检索对象。文档层需要管理:标题、来源、版本、权限、租户、状态是否已发布上传人和更新时间chunk 层需要管理:分块序号embeddingtoken 数标题路径是否参与检索这两个层次混在一张表里,后续做版本切换、局部重建索引、A/B 实验时会很痛苦。七、生产级 RAG 的查询链路设计一个成熟的查询链路通常不是“问题进来直接 similaritySearch”。更合理的链路如下:原始问题 - 查询标准化 - 查询改写 / 意图识别 - 向量召回 - 关键词召回 - 元数据过滤 - 融合排序 - 重排 - Prompt 组装 - LLM 生成 - 引用注入 - 结果缓存 / 日志记录7.1 为什么企业需要混合检索纯向量检索并不能解决所有问题。它在以下场景容易失手:产品型号、错误码、合同编号、接口名等精确字符串表格型制度条款时间、版本、组织架构等强过滤场景因此企业级检索通常采用:向量召回解决语义匹配问题。全文召回解决关键字精确匹配问题。元数据过滤限制租户、知识库、文档类型、时间范围、权限标签。结果融合常用 RRF 或加权排序。交叉编码器重排对 TopN 结果再精排,提升最终准确率。7.2 RRF 融合为什么有效RRF,Reciprocal Rank Fusion,本质上不是取两个列表的加权平均分,而是基于排名位置融合结果:score(d) = Σ 1 / (k + rank_i(d))优点在于:不需要不同检索器的分值完全同尺度实现简单在线效果通常比较稳这非常适合“向量召回 + BM25/全文召回”的组合。八、Spring AI 项目工程结构设计建议把项目拆成下面这几个层次:enterprise-rag/ ├── pom.xml ├── docker-compose.yml ├── k8s/ ├── sql/ ├── src/main/java/com/example/rag/ │ ├── RagApplication.java │ ├── config/ │ ├── controller/ │ ├── application/ │ │ ├── command/ │ │ ├── query/ │ │ └── service/ │ ├── domain/ │ │ ├── model/ │ │ ├── service/ │ │ └── repository/ │ ├── infrastructure/ │ │ ├── ai/ │ │ ├── persistence/ │ │ ├── cache/ │ │ ├── mq/ │ │ └── storage/ │ └── interfaces/ │ ├── rest/ │ └── scheduler/ └── src/main/resources/ ├── application.yml ├── application-pro