微信扫码
添加专属顾问
我要投稿
langchain4j 1.11.0发布,PgVector模块原生支持混合检索,彻底解决RAG检索不准的痛点!核心内容: 1. 纯向量检索的三大硬伤与RAG幻觉问题根源 2. 混合检索技术原理与SQL实现解析 3. PIGAI对主流向量数据库的完整适配方案
你的 RAG 应用明明存进去了正确的文档,回答却驴唇不对马嘴?问题很可能不在大模型,而在检索环节。langchain4j 1.11.0 刚刚发布,PgVector 模块原生支持了混合检索,这篇文章先教你怎么用,再带你看看底层 SQL 是怎么拼的。
前几天一个群友在技术群里吐槽:他搭了一套 RAG 系统,把 Spring Boot 的官方文档全部灌进了向量数据库,结果问"Spring Boot 3.5 有哪些新特性",系统返回的竟然是 Spring Boot 2.7 的迁移指南。
文档明明是对的,模型也没问题,那到底哪个环节出了岔子?
答案是检索。
他用的是纯向量检索,也就是把用户的问题转成一个向量,然后在向量空间里找"最像"的文档片段。问题在于,对向量模型来说,"Spring Boot 3.5"和"Spring Boot 2.7"在语义空间里距离很近——它们都是关于 Spring Boot 版本特性的描述,模型只理解了"Spring Boot + 版本特性"这层语义,但 3.5 和 2.7 这种精确的版本号区分,它做不好。
这不是个别现象。只要你的知识库包含大量相似结构但细节不同的文档(比如多个版本的 API 文档、不同产品线的技术规格、相近日期的会议纪要),纯向量检索几乎必然翻车。
最新版本的PIGAI中已经完全适配了,像milvus 、pgvector、ocenbase 等的混合检索策略大大提升RAG搜索的准确性。
我们先搞清楚向量检索在做什么。它把文本映射到一个高维空间,通过余弦距离或欧氏距离衡量"语义相似度"。这套机制处理"重置密码"和"修改登录凭证"这种同义词替换时表现很好,但它有几个绕不过去的硬伤:
专有名词失灵。产品编号、版本号、错误码这些东西,向量模型处理得并不好。搜"GTX-4090"可能会返回"RTX-3080"的结果,因为它们在向量空间里就是邻居。
过度泛化。问"苹果的营养成分"可能返回 Apple 公司的财报信息,因为向量模型可能没有很好地区分这两个语境。
对短查询不友好。用户输入"CVE-2024-38819"这种查询,向量模型基本是懵的,它倾向于返回"任何跟安全漏洞有关的文档",而不是精确命中那个特定的 CVE 编号。
问题的本质是:向量检索擅长理解"意思",但不擅长匹配"字面量"。而很多真实场景恰恰需要字面量的精确匹配。
既然向量检索擅长语义,传统的关键词检索擅长精确匹配,那把它们拼在一起不就行了?
这就是混合检索(Hybrid Search)的核心思路。一次查询,同时跑两条路:
两条路各自返回一个排序结果,然后用融合算法合并。两路检索的分数量纲完全不一样(一个是余弦距离,一个是词频得分),没法直接相加,所以常见的做法是只看排名位置来融合,比如 RRF 算法。
回到前面那个例子,"Spring Boot 3.5"这几个字会在关键词检索那条路上被精确命中,即使向量检索依然返回了 2.7 的内容,融合之后 3.5 的文档也会排在前面。
langchain4j 发布了 1.11.0 版本。这个版本的 Notable Changes 不少,Agentic 模式支持了流式和多模态,MCP 协议升级到了 2025-11-25 规范,Mistral 推理模型也得到了支持。但我觉得对做 RAG 的同学来说,最实用的一条是这个:
对于大多数 Java 项目来说,PostgreSQL 基本是标配。PgVector 作为 PostgreSQL 的向量扩展,已经是 Java 生态做 RAG 的主流选择。这次直接在 PgVector 模块里加上混合检索,不需要引入额外的搜索引擎,改几行配置就能用。
PgVectorEmbeddingStore 新增了一个 SearchMode 枚举,两个值:VECTOR(纯向量,默认)和 HYBRID(混合搜索)。
第一步,构建 Store 时开启混合搜索:
PgVectorEmbeddingStore store = PgVectorEmbeddingStore.builder()
.host("localhost")
.port(5432)
.database("mydb")
.table("embeddings")
.dimension(384)
.searchMode(SearchMode.HYBRID) // 就这一行
.rrfK(60) // 可选,默认 60
.textSearchConfig("simple") // 可选,默认 simple,英文可改 english,中文需装 zhparser
.build();
第二步,搜索时传入原始文本查询:
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(questionEmbedding) // 向量检索用
.query(question) // 关键词检索用
.maxResults(5)
.build();
EmbeddingSearchResult result = store.search(request);
HYBRID 模式下 query 参数是必填的,不传会抛异常。全文检索没有原始文本查询就没法工作。
第三步,没了。
原有的数据不需要做任何变更,GIN 索引会自动创建。对于已经在用 PgVector 做 RAG 的项目,迁移成本就是加一个 searchMode 配置和一个 query 参数的事。
有个细节要注意:切到 HYBRID 模式之后,返回的 score 含义变了。纯向量模式下 score 是 [0, 1] 的余弦相似度,HYBRID 模式下是 RRF 融合分数,典型范围大概在 0.02 到 0.03 左右(k=60 时最佳情况约 1/61 + 1/61 ≈ 0.0328)。如果你的代码里有基于 score 阈值的过滤逻辑,切换后需要相应调整。
知道了怎么用,再来看看底层做了什么。search() 方法根据 SearchMode 做路由:
return switch (mode) {
case VECTOR -> embeddingOnlySearch(request);
case HYBRID -> hybridSearch(request);
};
纯向量模式下的 SQL 比较简单,用 pgvector 的 <=> 余弦距离运算符排序,(2 - distance) / 2 转成 [0, 1] 的相似度分数。这就是大多数 PgVector 项目目前在用的方式。
混合模式下的 SQL 是一个 CTE 结构,分三段:
WITH vector_search AS (
-- 第一段:向量检索,按余弦距离排序,RANK() 标排名
SELECT embedding_id, text, metadata,
RANK() OVER (ORDER BY embedding <=> :referenceVector) AS rnk
FROM embeddings
ORDER BY embedding <=> :referenceVector
LIMIT :candidateCount
),
keyword_search AS (
-- 第二段:关键词检索,PostgreSQL 原生全文检索
SELECT embedding_id, text, metadata,
RANK() OVER (ORDER BY ts_rank(
to_tsvector(:config, coalesce(text, '')),
plainto_tsquery(:config, :query)
) DESC) AS rnk
FROM embeddings
WHERE to_tsvector(:config, coalesce(text, ''))
@@ plainto_tsquery(:config, :query)
ORDER BY ts_rank(...) DESC
LIMIT :candidateCount
)
-- 第三段:FULL OUTER JOIN 合并,RRF 公式算最终分数
SELECT COALESCE(v.embedding_id, k.embedding_id) AS embedding_id,
COALESCE(1.0 / (:rrfK + v.rnk), 0.0)
+ COALESCE(1.0 / (:rrfK + k.rnk), 0.0) AS score
FROM vector_search v
FULL OUTER JOIN keyword_search k ON v.embedding_id = k.embedding_id
WHERE score >= :minScore
ORDER BY score DESC
LIMIT :maxResults;
几个值得留意的实现细节:
关键词检索用的是 plainto_tsquery 而不是 to_tsquery,好处是不需要用户手动写 & 和 | 布尔运算符,直接丢自然语言进去就行。
FULL OUTER JOIN 保证了两边的结果都不会丢。一条文档只出现在向量结果里、没被关键词命中,关键词那边贡献 0 分,反之亦然。两边都命中的文档得分最高,自然排在前面。
每个子查询的 LIMIT 取的是 Math.max(maxResults, rrfK),保证有足够的候选参与融合。
GIN 索引在 initTable() 里自动创建:
if (searchMode == SearchMode.HYBRID) {
String ftsIndexName = table + "_text_fts_gin_index";
query = String.format(
"CREATE INDEX IF NOT EXISTS %s ON %s "
+ "USING gin (to_tsvector('%s', coalesce(text, '')))",
ftsIndexName, table, textSearchConfig);
statement.executeUpdate(query);
}
整个混合检索 SQL 跑在 PostgreSQL 内部,一次数据库往返就搞定,不需要在应用层做结果合并。
如果你的 RAG 系统在处理版本号、错误码、产品编号这类查询时老是答非所问,大概率不是大模型的锅,而是检索这一环没做好。在 PgVector 上加混合检索是目前改动最小、收益最直接的一步。
当然,混合检索解决的是"召回"层面的问题,让正确的文档进入候选集。如果你对精度要求更高,还可以在混合检索之后再接一个 Reranker 重排模型做精排,那是另一个话题了。
2026-02-09
2026-02-05
2026-02-02
2026-01-30
2026-01-28
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
2026-02-06
探秘 AgentRun丨为什么应该把 LangChain 等框架部署到函数计算 AgentRun
2026-02-04
Agent生态碎片化终结,.agents/skills统一所有工具
2026-01-29
自建一个 Agent 很难吗?一语道破,万语难明
2026-01-28
全球首个Skills Vibe Agents,AtomStorm技术揭秘:我是怎么用Context Engineering让Agent不"变傻"的
2026-01-22
Deepagents落地场景来了:用openwork实现专属办公小管家
2026-01-05
快速上手:LangChain + AgentRun 浏览器沙箱极简集成指南
2026-01-05
为什么大模型企业都在强调可以连续工作XX小时的Agent和模型?长时运行Agent解析(Long-Running Agents)
2025-12-29
单agent落幕,双agent才能解决复杂问题!附LangGraph+Milvus实操
2025-12-21
2025-12-21
2025-11-25
2025-12-08
2025-11-18
2025-11-25
2026-01-05
2025-11-19
2025-12-18
2025-12-17
2025-11-03
2025-10-29
2025-07-14
2025-07-13
2025-07-05
2025-06-26
2025-06-13
2025-05-21