LangChain4j集成PGVector 17,Java后端实现RAG全流程实战
先把标题里的手滑纠正一下LangChian4j正主应该是 LangChain4j。这名字一看就是拼音和英文混着敲出来的但别被拼写带偏它是 Java 生态里最有分量的 LangChain 移植版。今天这篇就是围绕 LangChain4j 集成 PGVector 17 完整跑通 RAG检索增强生成的实战记录从 Docker 起库、依赖选型、代码装配到检索问答和排坑一条龙捋完。文章适合正在调研 Java 技术栈怎么做 RAG、或者被“教程全是 Python”劝退的开发者看完可以直接照着抄作业。我为什么特意选 PGVector 17 而不是单独部署一套 Milvus 或 Weaviate因为很多业务团队压根没有专门的向量数据库运维能力但 PostgreSQL 基本家家都有。把向量检索能力塞进 PG等于用一套已经养熟的基础设施顺手把 RAG 的存储、检索、事务问题全解决了。下面直接从方案选型讲起。1. 方案选型Java项目落地RAG凭什么选这套组合1.1 LangChain4j和Spring AI、手写方案怎么权衡Java 生态做 RAG绕不开三个选择LangChain4j、Spring AI以及完全手写。很多人在 Spring AI 和 LangChain4j 之间反复摇摆我一开始也是这样后来两边各写了一个 demo才摸清它们的脾气。Spring AI 的优势是和 Spring Boot 生态贴合极好官方维护、起步快如果你项目里本来就全是 Spring 家族用 Spring AI 确实省心。但它的短板也很明显模块成熟度和工具链没有 LangChain4j 丰富尤其向量存储适配器的覆盖范围、和对各种 Embedding 模型的开箱支持目前还是 LangChain4j 更全。Spring AI 更像是“框架”而 LangChain4j 更像是“工具箱”。手写方案其实也没那么可怕。说白了 RAG 核心就是三件事文本切分、向量化、相似度检索。如果团队里没有现成框架洁癖手写反而更好控制流程。但问题在于手写会踩很多轮子重复造的坑比如 embedding 模型调用失败的重试策略、metadata 过滤的组合条件、token 超限时的截断策略这些框架都已经帮你处理过了。我最终选 LangChain4j图的就是它抽象层干净、组件可替换而且对 PGVector 的支持是原生维护的版本跟随也及时。1.2 PGVector 17把向量检索塞进关系型数据库到底香不香PGVector 是 PostgreSQL 的向量存储扩展它可以让你在 PG 里直接创建 vector 类型的列然后基于这个列做相似度搜索。PGVector 17 这个说法通常包含两层意思一是 PostgreSQL 主版本跑到 17二是 pgvector 扩展版本跟进到对应的主流版本。标题里的“PGVector 17”在实际落地中通常指 PostgreSQL 17 pgvector 扩展的组合。这套组合最大的价值就是省掉了一套独立向量数据库。你的业务数据、元数据、向量数据可以放在同一个库里应用层不需要同时维护两套数据源的连接和事务。很多对一致性有要求的场景比如用户上传文档后立刻要做权限过滤向量和业务数据在同一个库、同一个事务里操作逻辑一下就简单了。PGVector 在性能上也并非网上说的那么弱。pgvector 0.5 以后引入了 HNSW 索引0.7 以后进一步优化了索引构建速度。对于千万级以下的向量规模配合 HNSW 索引完全够用。我这里选的就是 HNSW 余弦距离的组合具体后面会讲。2. 环境准备5分钟在Docker上跑起PGVector 172.1 docker run启动前的三个关键参数环境准备这步看起来一条 docker run 就完事但里面有三个参数必须提前想明白。第一是镜像标签。pgvector 官方镜像的标签格式是pgvector/pgvector:pg17注意这里明明白白写着 pg17表示内置 PostgreSQL 17 和对应版本的 pgvector 扩展。千万别乱拉 latestlatest 不一定是你想要的 PG 主版本真到排查问题的时候版本对不上很抓狂。第二是数据卷。我见过太多人演示 RAG 的时候不挂 volume容器一删之前灌进去的向量数据全部蒸发。第一次跑就必须把数据卷挂好否则后面重建容器就是从头再来。第三是端口映射。PG 默认 5432 端口本机如果装了原生 PG端口冲突是大概率事件。我一般习惯映射到 15432 这种非常规端口避免和本地已有服务打架也方便区分哪一个是容器实例。我实际使用的启动命令如下docker run -d \ --name pgvector-demo \ -e POSTGRES_USERpostgres \ -e POSTGRES_PASSWORDpostgres \ -e POSTGRES_DBragdb \ -p 15432:5432 \ -v pgvector_data:/var/lib/postgresql/data \ pgvector/pgvector:pg17启动后先进容器确认扩展可用docker exec -it pgvector-demo psql -U postgres -d ragdbCREATE EXTENSION IF NOT EXISTS vector; SELECT extversion FROM pg_extension WHERE extname vector;2.2 Maven依赖和配置文件环境就绪后开始搭建 Spring Boot 工程。我用的是 Spring Boot 3.2.x配合 LangChain4j 0.36.2。这个版本组合我踩过一轮坑稳定性是可以的。如果你们用的是 Boot 2.xLangChain4j 的版本选择就要更谨慎因为 0.36.x 对 Jakarta 的依赖要求摆在那里。Maven 坐标上最核心的三个依赖分别是 langchain4j 主模块、open-ai 模块和 pgvector 模块。注意这里有一个很多人忽略的点langchain4j-pgvector 是独立 artifact不会跟着主模块自动带出来必须单独声明。dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.36.2/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.36.2/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-pgvector/artifactId version0.36.2/version /dependency配置文件里我把 OpenAI 的 key、模型名、PG 连接串都放在 application.yml 中。这里提醒一句真实项目 key 一定走环境变量或配置中心不要硬编码。spring: datasource: url: jdbc:postgresql://localhost:15432/ragdb username: postgres password: postgres langchain4j: open-ai: api-key: ${OPENAI_API_KEY} chat-model: model-name: gpt-4o-mini embedding-model: model-name: text-embedding-3-small这里有个容易混淆的地方spring.datasource 和 LangChain4j 的 PGVector 连接是两个独立的东西。spring datasource 是给 Spring 管理事务用的而 PgVectorEmbeddingStore 内部用自己的连接参数去建向量表两套配置都要写对缺一个后面都会出诡异问题。3. RAG链路拆解从文档到答案的完整流转3.1 文档切分chunk_size和overlap不是随便填的RAG 的效果上限一半由切分策略决定。很多人第一次跑通 demo 后兴奋地把整本文档丢进去然后发现问答效果稀烂十有八九是切分没做对。切分的核心矛盾是chunk 太大单段文本信息密度高但检索出来塞进 prompt 容易超 token而且混入的无关噪声也多chunk 太小语义完整性被切断一个完整知识点被腰斩成两半检索时谁也匹配不全。LangChain4j 里比较顺手的是DocumentSplitters.recursive(maxSegmentSize, maxOverlapSize)它会按段落、句子、单词的优先级逐层尝试切分尽量保住语义边界。我在 demo 里设置的是 500 字符的块、50 字符的重叠对中文技术文档来说是个比较稳的起点。DocumentSplitter splitter DocumentSplitters.recursive(500, 50);overlap 的意义很多人不理解。简单说如果一段文本在 500 字符处恰好把某个关键结论切断重叠部分可以让下一段带上结尾那几十个字检索时上下文衔接就自然很多。实践中 overlap 一般取 chunk_size 的 10% 到 20%太小等于没有太大则产生大量冗余 chunk白白增加存储和检索耗时。3.2 向量化Embedding模型选型的两条路线向量化的本质是把文本映射成一个固定维度的浮点数组让语义相近的文本在向量空间里距离更近。Embedding 模型的选择直接影响 RAG 效果这个问题上没有“免费午餐”。第一条路线是调用托管 API比如 OpenAI 的 text-embedding-3-small。优点是不需要本地 GPU代码简单效果好尤其英文场景非常能打。缺点是数据要出网对部分企业内部知识库场景不友好而且长期调用是有成本。我这篇 demo 用 small 模型维度 1536足够说明问题。第二条路线是本地模型。常见做法是部署 Ollama 后拉取 nomic-embed-text或者用 LangChain4j 的本地 ONNX 模型 all-minilm-l6-v2。本地方案的优点是数据不出内网、无调用费用缺点是中文效果普遍不如商业 API需要专门找针对中文优化的模型。这里必须强调一条硬约束向量维度必须对齐。text-embedding-3-small 输出 1536 维nomic-embed-text 输出 768 维all-minilm-l6-v2 输出 384 维。你在创建 PgVectorEmbeddingStore 时配置的 dimension 必须和模型输出维度完全一致否则插入数据时直接报错。这个坑我后面专门列了一条。3.3 检索dense vector search和HNSW索引检索阶段做的事是用用户问题生成一个查询向量然后在向量表里找最相似的 TopK 个 chunk。这种基于稠密向量的相似度搜索在热词里叫 dense vector search它是 RAG 检索的主流方式和基于关键词的稀疏检索比如 BM25正好互补。PGVector 支持多种距离算法最常用的是余弦距离。余弦距离看的是向量方向而不是长度对文本这种受长度影响大的数据特别友好。LangChain4j 默认也是基于余弦相似度来找最近邻。索引方面数据量小的时候全表扫描也能出结果但一旦数据过万线性扫描的延迟就会让你怀疑人生。PGVector 从 0.5.0 开始支持 HNSW 索引这是一种基于图的近似最近邻索引检索效率极高精度损失却很小。我后来手动在库里补建了索引CREATE INDEX ON document_embeddings USING hnsw (embedding vector_cosine_ops);如果是数据量大的生产环境建议建索引时把m每个节点的最大连接数和ef_construction调大一点虽然建索引会慢一些但查询召回率更稳。数据量小的时候保持默认就好。4. 代码落地一步步把RAG跑起来4.1 配置类分清楚EmbeddingModel和ChatModel代码部分我从一个统一配置类说起。新手最容易搞混的就是把 EmbeddingModel 和 ChatLanguageModel 当成同一个东西。这两个模型各司其职EmbeddingModel 负责把文本变成向量用于入库和检索ChatLanguageModel 负责根据 prompt 生成回答也就是真正跟你对话的模型。在配置类里我会把这两个 model 分别声明成独立 Bean同时注册向量存储和内容检索器。Configuration public class RAGConfig { Value(${langchain4j.open-ai.api-key}) private String apiKey; Bean public ChatLanguageModel chatLanguageModel() { return OpenAiChatModel.builder() .apiKey(apiKey) .modelName(gpt-4o-mini) .build(); } Bean public EmbeddingModel embeddingModel() { return OpenAiEmbeddingModel.builder() .apiKey(apiKey) .modelName(text-embedding-3-small) .build(); } Bean public EmbeddingStoreTextSegment embeddingStore() { return PgVectorEmbeddingStore.builder() .host(localhost) .port(15432) .database(ragdb) .user(postgres) .password(postgres) .table(rag_demo) .dimension(1536) .build(); } }这段代码里有几个细节值得展开。第一table 参数我特意起了rag_demoLangChain4j 在首次启动时会自动建表不需要你手动建但建表逻辑依赖扩展存在扩展没建好就会报错。第二port 是 15432和前面 Docker 映射保持一致如果你也遇到连接失败先检查这里是不是用了本机版的 5432。4.2 知识库导入从Markdown文本到向量落库配置好 Bean 之后导入文档就很简单了。LangChain4j 提供了一个组合器EmbeddingStoreIngestor一口气完成切分、向量化、入库三步。我在实际项目里通常把上传的知识库文件存到本地临时目录然后遍历导入。这里演示的是导入单篇 Markdown 文件的场景Service public class KnowledgeBaseService { private final EmbeddingStoreIngestor ingestor; public KnowledgeBaseService(EmbeddingStoreIngestor ingestor) { this.ingestor ingestor; } public void ingestMarkdown(String filePath) { Document document loadDocument(filePath); ingestor.ingest(document); } private Document loadDocument(String filePath) { return new Document(Paths.get(filePath), 知识库文档); } }EmbeddingStoreIngestor的构建我放在配置类里它会把前面定义的 splitter、embeddingModel、embeddingStore 组合起来Bean public EmbeddingStoreIngestor embeddingStoreIngestor( EmbeddingModel embeddingModel, EmbeddingStoreTextSegment embeddingStore) { return EmbeddingStoreIngestor.builder() .documentSplitter(DocumentSplitters.recursive(500, 50)) .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .build(); }导入完成后可以到库里确认一下数据是否落库SELECT count(*) FROM rag_demo_embedding;count 大于 0 说明切分和向量化都成功了。如果你看到结果是 0优先检查文件的编码和解析是否正常常见原因是 FileDocument 解析异常导致生成了空文本。4.3 问答链路ContentRetriever和AiServices问答链路是 RAG 真正发挥作用的地方。LangChain4j 的做法是先定义一个对外的接口然后用 AiServices 动态生成实现类这个模式比 Spring AI 的封装更直白。先定义 Assistant 接口public interface Assistant { String answer(String query); }再配置 ContentRetriever把检索器的检索数量和相关度阈值调好Bean public ContentRetriever contentRetriever( EmbeddingStoreTextSegment embeddingStore, EmbeddingModel embeddingModel) { return EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(4) .minScore(0.5) .build(); }最后让 AiServices 把对话模型、检索器、接口类组装到一起Bean public Assistant assistant(ChatLanguageModel chatLanguageModel, ContentRetriever contentRetriever) { return AiServices.builder(Assistant.class) .chatLanguageModel(chatLanguageModel) .contentRetriever(contentRetriever) .build(); }这样每次调用assistant.answer(query)时框架会先把 query 转成向量从 PGVector 里检索出最相关的 4 段文本拼接到 prompt 里送给大模型最终返回基于知识库的答案。如果你想看内部到底发生了什么可以手动跑一次检索链路Embedding queryEmbedding embeddingModel.embed(你的问题).content(); ListEmbeddingMatchTextSegment matches embeddingStore.findRelevant(queryEmbedding, 4); for (EmbeddingMatchTextSegment match : matches) { System.out.println(match.embedded().text()); System.out.println(match.score()); }这段代码是理解 RAG 内部机制的关键也是调试的好帮手。当你觉得答案不对的时候先看检索出来的文本对不对再判断是大模型的问题还是检索的问题。5. 踩坑记录我实际遇到的问题和排查过程5.1 “extension vector does not exist”八成是镜像问题很多人在第一步就卡死启动应用时报ERROR: extension vector is not available。这个问题的根源几乎都是你连的 PG 实例里根本没有 pgvector 扩展。排查思路很简单。先确认自己连的是不是pgvector/pgvector镜像的容器如果是普通 PostgreSQL 官方镜像里面不会有 vector 扩展。其次执行CREATE EXTENSION vector如果提示不存在直接换镜像重来不要浪费时间去找替代方案。我自己就栽过一次图省事用了本机自带的 PG结果绕了半天最后还是老老实实换到了容器。5.2 查询不生效先看HNSW索引用没用到RAG 跑通后我一度发现查询时间随着数据量增长急剧变差于是手动建了 HNSW 索引但奇怪的是执行时间并没有明显改善。后来用 explain analyze 查了一下执行计划才发现 PG 根本没用上我建的索引。原因在于我建的索引列名或操作符与查询语句不匹配。PGVector 的 HNSW 索引必须指定正确的向量操作符余弦距离就用vector_cosine_ops欧氏距离就用vector_l2_ops。如果建索引时用了 l2查询时用余弦索引就是废的。排查方式EXPLAIN ANALYZE SELECT * FROM rag_demo_embedding ORDER BY embedding [0.1, 0.2, ...] LIMIT 5;执行计划里如果出现Index Scan using rag_demo_embedding_embedding_idx说明索引生效了。如果没有检查索引操作符和查询距离是否匹配。5.3 Docker重启后向量数据还在吗Docker 容器重建后数据丢失是 RAG demo 里最高频的事故。我在 2.1 节特意强调了 volume 挂载这里再强调一次容器删除前先确认-v pgvector_data:/var/lib/postgresql/data是否正确。验证方式也很直接docker volume inspect pgvector_data如果 volume 存在重建容器时只要保证 volume 名称不变数据就不会丢。我见过有同事重建容器时报端口占用一气之下 docker rm -f 结果数据全没了就是因为没挂 volume。类似的坑我不想你再踩一遍。5.4 中文问答效果差问题可能出在Embedding另一个高频问题是用 OpenAI 的 text-embedding-3-small检索英文文档效果还行一换中文就明显拉胯。这个和模型对中文语义的理解能力有关不是代码写错了。我实测下来中文场景可以考虑两条路。第一换用支持多语言的 embedding 模型比如 OpenAI 的 text-embedding-3-large它对中文支持稍微好一些但维度翻倍、成本更高。第二走本地模型路线用专门针对中文优化的开源 embedding 模型比如基于 BGE 系列或者 m3e 系列的模型通过 Ollama 或 ONNX 接入 LangChain4j。这里再提醒一次维度对齐。如果你从 text-embedding-3-small 换成 local 的 384 维模型PgVectorEmbeddingStore 的 dimension 也要从 1536 改成 384否则报错信息会告诉你维度不匹配。6. 说几句实在话这套方案我从调研到跑通前后折腾了小半天真正写代码的时间其实很短大量时间都花在版本对齐、索引调优和排查环境问题上。但跑通那一刻的爽感是直接把 Java 项目接上 RAG 的确定性带来的。我个人在实际项目中的体会是PGVector LangChain4j 这套组合最适合数据量在百万级以下、不想引入额外基础设施、但又需要快速上线知识库问答的团队。它最大的优势不是性能天花板高而是把复杂度控制在了两套技术栈内一套 Java一套 PostgreSQL。真要到了千万级向量规模再考虑独立向量数据库也不迟前期没必要为不存在的规模提前买单。最后再分享一个扩展方向LangChain4j 的 ContentRetriever 支持 metadata 过滤条件实际业务场景里几乎都需要按用户、部门、文档类型做权限隔离。我目前的做法是在文档入库时把权限字段写入 metadata检索时通过MetadataFilter先过滤再排序效果很稳。这一步做完你手里的 RAG 就不是 demo而是能见生产的版本了。

相关新闻

最新新闻

日新闻

周新闻

月新闻