RAG(二)

向量数据库PGVector在这里插入代码片

PGVector 是 PostgreSQL 的向量扩展,让 PostgreSQL 直接支持向量存储和相似度搜索。

PGVector 给 PostgreSQL 加了一个新的数据类型 VECTOR,可以在普通的表里加一列向量,然后对这列向量做相似度搜索。

运算符 距离类型 适用场景

<-> 欧氏距离(L2) 坐标空间,对向量幅度敏感

<=> 余弦距离 文本语义搜索首选,只看方向不看幅度

<#> 内积(负数) 向量已归一化时等效于余弦距离

文本 RAG 场景几乎都用余弦距离。原因是:不同长度的文本,Embedding 向量的幅度可能不同,但语义方向是相似的。余弦距离只看方向,不受幅度影响,更适合语义搜索。

什么是向量索引

没有索引时,向量搜索要把查询向量和库里每一条记录都算一次距离(全量扫描)。100 万条数据,就要算 100 万次距离。这是 O(n) 的复杂度,数据量大了根本跑不动。

向量索引通过构建特殊的数据结构,让搜索时只需要计算一小部分候选向量的距离,大幅降低计算量。代价是:不保证找到"最精确"的结果,只保证找到"足够好"的近似结果------即 ANN(近似最近邻)搜索。

HNSW 构建了一个多层的图结构:

java 复制代码
HNSW 索引结构(简化示意)

层 2(稀疏):  A ←------------→ F ←------→ K
                         ↑
层 1(中等):  A ←------→ D ←------→ F ←------→ H ←------→ K
                    ↑
层 0(密集):  A-B-C-D-E-F-G-H-I-J-K(所有节点)

查询时从高层稀疏图开始找大方向,逐层往下精化,最终在底层找到精确候选。就像在地图上先定位到城市,再找街道,再找门牌号------高效且准确。

HNSW 的优点:

● 查询速度快,延迟低

● 召回率高(精度好)

● 支持增量插入(随时可以插入新数据,不需要重建索引)

HNSW 的缺点:

● 建索引较慢(但通常是一次性操作)

● 内存占用较大(索引结构要常驻内存)

大多数 RAG 场景推荐 HNSW。

IVFFlat(倒排文件 + 扁平量化)

IVFFlat 的思路是:先把向量空间划分成若干个"桶"(Cluster),每次查询只搜最近的几个桶,不搜全部。

IVFFlat 索引结构(简化示意)

java 复制代码
向量空间
  ┌──────────────────────────────────┐
  │  桶1    桶2    桶3    桶4    桶5  │
  │  ···    ···    ···    ···    ··· │
  └──────────────────────────────────┘
                 ↑
  查询向量先判断属于哪个桶附近,只在附近桶里搜

IVFFlat 的优点:

● 建索引快

● 内存占用小

IVFFlat 的缺点:

● 查询速度相对较慢

● 召回率略低(向量被错分到错误桶的情况不可避免)

● 需要先有足够量的数据才能建索引(通常要求 > 1000 条,聚类才有意义)

适合 IVFFlat 的场景: 数据量百万级以上、批量入库(不是实时插入)、对内存比较敏感。

HNSW 的关键参数:m 和 ef_construction

HNSW 有两个建索引时的关键参数,影响精度和资源消耗的权衡。

参数 m:每个节点的最大连接数

m 决定了 HNSW 图的"密度"------每个节点最多连接几个邻居。

m 越大:图越密,搜索越准,但内存占用越大、建索引越慢

m 越小:内存省,但精度降低

通用推荐值:m = 16

什么时候调大 m?当你对召回率要求很高(比如法律文档检索,不能漏),可以考虑 m = 32 甚至 m = 64,代价是内存翻倍。

什么时候调小 m?内存极度紧张时,m = 8,精度会下降但速度更快。

参数 ef_construction:建索引时的搜索宽度

ef_construction 控制建索引时的精度------在往图里插入每个节点时,搜索多少候选邻居来决定最终连接。

ef_construction 越大:索引越精确,但建索引越慢

ef_construction 越小:建索引快,但索引质量略低

通用推荐值:ef_construction = 64

建议:这两个参数入门时不要纠结,16/64 就是很好的默认值。等到真正遇到性能瓶颈,再基于实际测量调整。

查询时的 ef_search:精度和速度的旋钮

建好索引后,查询时还有一个参数 ef_search,控制查询时搜索的候选数量。

ef_search 越大: 查询越准(召回率高),但耗时越长

ef_search 越小: 查询越快,但可能漏掉一些相关结果

默认值通常是 40。

这个参数的意义是:你可以在不重建索引的情况下,灵活地在"查询精度"和"查询速度"之间调整。

java 复制代码
精度优先(比如法律条文检索): ef_search = 100-200
速度精度平衡(大多数场景):  ef_search = 40-100  ← 默认值附近
速度优先(高并发低延迟):    ef_search = 20-40

余弦距离 的范围是 0, 2,0 表示完全相同,2 表示完全相反。

但 RAG 框架里通常展示的是相似度分数,它是对余弦距离的转换:

java 复制代码
相似度 = 1 - 余弦距离

完全相同 → 余弦距离 = 0 → 相似度 = 1.0
不相关   → 余弦距离 ≈ 1 → 相似度 ≈ 0.0
完全相反 → 余弦距离 = 2 → 相似度 = -1.0

PGVector 支持在向量搜索的同时按 Metadata 过滤。这个功能看起来简单,但设计得好可以大幅提升检索精度。

场景举例:

知识库里有:产品手册(category=manual)、FAQ(category=faq)、政策文档(category=policy)

用户提问:"退款要多久?"

普通向量搜索:可能找到来自产品手册的内容(里面也提到了退款)

加 Metadata 过滤:只在 category=faq 里搜,精准命中 FAQ 里的退款条款

Metadata 的设计直接影响过滤的粒度。鸡哥建议入库时至少标注:

{

"source": "文件名或 URL", // 来源

"category": "文档类别", // 分类(用于过滤)

"version": "版本号", // 如果有版本迭代

"upload_date": "入库时间" // 如果有时效要求

}

注意: Metadata 过滤和向量搜索是"先过滤后搜索"------先通过 Metadata 缩小候选集,再在候选集内搜向量。过滤条件越严格,候选集越小,TopK 能找到的结果就越少(甚至可能不足 K 个)。合理设计 Metadata 粒度很重要。

文档加载---多格式处理与 Metadata 设计

RAG 的第一步是把文档读进来。"读进来"听起来很简单,实际上有不少坑

不同格式的加载器选哪个

主流框架(Spring AI、LangChain4j、LlamaIndex)对常见格式都有现成的加载器。原生 PDF 是可以复制文字的,直接用文本提取器就行。扫描件是图片扫进去的,文字提取出来是空的或乱码------这是鸡哥见过的最高频问题之一,上来就说"向量搜不到",结果查半天发现文档本身就没内容。遇到这种情况需要先过一道 OCR。

pdf处理常见的几个陷阱

1.页眉页脚污染内容

处理方法:加载时设置顶部/底部边距(pageTopMargin / pageBottomMargin),跳过这些区域。70pt 是用得比较多的经验值,但具体要看文档的版式------有的文档页眉很高,有的很低,最好打开一两份实际文档量一下。

2.按页分还是按段分

按页加载:每页变成一个 Document,简单粗暴。优点是带页码 Metadata,方便溯源。缺点是一段话跨了两页,会被截断,语义完整性差。

按段落加载:尝试识别文档结构,把完整段落作为一个 Document。对结构清晰的 PDF 效果好;对格式混乱的 PDF(如合同 PDF、扫描转换的 PDF),段落识别不准,反而乱。

搜索的经验:产品手册、操作指南这类格式规整的文档用按段,合同、报告这类篇幅长、格式复杂的用按页,后面再靠分块策略来保证语义完整性。

Word / Excel 加载要知道的事

Word 提取文字内容相对稳定,但有几点要注意:

● 表格:Word 里的表格提取出来是纯文字,行列结构丢了。如果文档里有大量表格,提取后可读性很差,可能需要专门的表格解析处理

● 图片:图片直接忽略,图里的文字不会被提取(除非配 OCR)

● 页眉页脚:Word 的页眉页脚 Tika 一般会提取出来,需要清洗掉

Excel 最尴尬------它本质上是结构化数据,不是文档。Tika 提取出来是把所有单元格的文字拼在一起,表头和数据的对应关系全丢了。

鸡哥的建议:Excel 不要直接用通用文本加载器。如果 Excel 是知识表格(比如产品参数对比表),应该写专门的解析逻辑,把每行转成"字段名:值"的格式,这样向量化后语义更清晰。

如果 Excel 只是文字说明(纯文本填的 Excel),就随便了,Tika 直接读问题不大。

Metadata 设计------这里决定了你后面能不能过滤

java 复制代码
Metadata 是 RAG 的"索引层",决定了:
1. 出了问题能不能追到来源
2. 检索时能不能按条件过滤(只搜这个部门的文档、只搜这个版本)
3. 多租户场景下能不能做数据隔离
java 复制代码
推荐的 Metadata 字段设计
【必填字段】
filename      原始文件名              用于追溯来源,答案引用文件名
category      文档分类                后续检索时按分类过滤(如"产品手册"、"FAQ")
upload_time   入库时间                便于按时间过滤,排查过期内容
status        文档状态 active/inactive 软删除用,下线文档不从向量库删,只改状态

【多租户场景必填】
tenant_id     租户ID                  隔离不同客户的数据,检索时加过滤条件
doc_id        文档唯一标识             同一文档的所有分块共用同一个 doc_id,
                                     方便按文档粒度删除或更新

【可选字段】
department    归属部门
version       文档版本(v1、v2...)
author        文档作者

Metadata 设计的几个原则

原则一:提前想好过滤维度

原则二:doc_id 一定要加

原则三:status 字段做软删除

完整文档加载流程

java 复制代码
文件上传
    │
    ▼
【格式识别】根据文件后缀判断读取器类型
    │
    ▼
【文档加载】调用对应读取器,提取文本内容
    │
    ▼
【文档清洗】去除页眉页脚、多余空行、噪声字符
    │
    ▼
【Metadata 注入】设置 filename/category/tenant_id/doc_id 等字段
    │
    ▼
【分块】按语义或固定大小切分(下一节专讲)
    │
    ▼
【向量化 + 入库】每个分块转成向量,写入向量库
    │
    ▼
记录文档元数据到关系型数据库(方便管理和查询入库状态)

血泪大坑

坑一:上传了扫描件 PDF,向量库里什么都没有

坑二:没有 doc_id,更新文档只能清库重建

坑三:Metadata 字段名拼错,过滤永远不生效

坑四:Excel 产品参数表直接 Tika 读,检索出乱七八糟的结果

分块策略---RAG 效果差异最大的环节

同样的文档,换一种分块策略,检索命中率从 45% 提升到 78%。没有改模型,没有改检索算法,就改了怎么切。分块是 RAG 里性价比最高的优化点。

java 复制代码
块太大(> 1000 Token):
  语义上下文完整,模型理解更准确
  向量语义混杂,检索精度低
  占用更多上下文窗口,Token 成本高

块太小(< 100 Token):
  向量语义聚焦,检索精度高
  语义不完整,单块信息量不足
  同一个问题需要拼多个块才能回答

通用建议:300-600 Token 是甜点区,在语义聚焦和上下文完整之间取得平衡。

这不是死规则。法律文档需要更小的块(精准);技术手册可以稍大(上下文完整)。后面针对不同文档类型会细说。

切块时需要设置相邻块的重叠区:

java 复制代码
原文:  AAAAAABBBBBBCCCCCC

不加重叠:  [AAAAAA] [BBBBBB] [CCCCCC]
加重叠:    [AAAAAABB] [BBBBBBCC] [CCCCCC]
                   ↑ 相邻块有 BB 重叠

为什么需要重叠: 如果一个关键信息恰好在两块的边界处(比如"最高补偿金额为一万元"被切成了"最高补偿金额为"和"一万元,具体以合同为准"),有重叠就能保证至少一个块能完整包含这个信息。

重叠区大小建议:块大小的 10-15%。

不同文档类型的分块策略

技术文档 / 产品手册:按 Token 数量切,在段落边界切,保留上下文。

● 块大小:500 Token

● 重叠区:50-80 Token

● 分块器:TokenTextSplitter(先在段落边界切,超出再按 Token 截)

FAQ 文档(一问一答)

FAQ 是特殊场景------一个问题和对应的答案是一个完整的语义单元,绝对不能被切断。

法律合同 / 政策文档:

法律文件每句话都可能很关键,条款之间高度关联。

● 块大小:300 Token(更小,语义更聚焦)

● 重叠区:50 Token(偏大,防止条款被切断)

● 注意:按条款编号切,不要在条款中间切断

代码文档:代码的天然分割单位是函数和类,不是 Token 数量。

● 按语法边界切,不要在函数中间截断。

● 每个函数(含注释和函数签名)是一个块

● 实在太长的函数可以按逻辑段落切,但不要切在赋值语句中间

● 如果有代码解析能力(如 JavaParser),按 AST 精确分割效果最好

表格 / 结构化内容

表格的每一行通常是一条独立记录,按行切。

● 保留表头:每个块都带上列名,不然检索到的行没有语义

● 格式转换:表格转成自然语言描述后再入库,向量检索效果更好

○ "产品 XPro-3000,最大承载 500kg,工作温度 -20°C 至 80°C"

○ 这比直接存 XPro-3000 | 500kg | -20~80 向量语义更好

父子块检索(Parent-Child Chunking)

这是 Advanced RAG 里的重要技巧,解决块大小的根本矛盾:

小块:向量语义聚焦,检索精度高

大块:上下文完整,模型理解更好

父子块策略:用小块检索,用大块喂给模型。

java 复制代码
索引时:
  大块(父块,500 Token)→ 存到普通数据库(给模型用)
  小块(子块,100 Token)→ 存到向量库(用于检索)
  每个子块记录所属父块的 ID

检索时:
  1. 用用户问题在向量库搜索小块(精准命中)
  2. 通过父块 ID 取出对应的大块(完整上下文)
  3. 把大块内容拼好,传给模型

常见坑:坑一:默认参数不改

很多同学用 TokenTextSplitter(500, 50, ...) 就完事了,从来不看实际切出来的块是什么样。养成习惯:入库之前,打印前 10 个块,看看有没有明显截断或语义破碎的情况。

坑二:忘记设相似度阈值

入库时块切得多,查询时设了 TopK=5 但没设相似度阈值,低质量的块也会被返回。分块和检索参数是一起调的。

坑三:文档更新时只增不删

文档更新了旧版本,但旧块还在向量库里。结果检索会同时返回新旧两个版本的内容,模型搞混。文档更新时要同步删除旧块,按文档 ID 做精确替换。

相关推荐
不爱洗脚的小滕4 小时前
【RAG】Milvus 混合检索参数调优:ef / candidate_k / final_k 详解
网络·langchain·milvus·rag
codefan※8 小时前
RAG 加速指南:Faiss / Milvus / Qdrant 向量库选型与调优
知识图谱·milvus·faiss·向量数据库·rag·qdrant
abigale038 小时前
LangChain 实践4: 7个人AI助手全栈项目:完整拆解+分阶段开发指南
缓存·langchain·prompt·token·rag·lcel
程序员三明治9 小时前
【AI】RAG 数据分块(Chunk)策略与实践
java·人工智能·后端·ai·大模型·llm·rag
咖啡星人k10 小时前
长亭百智云:全新一代AI基础服务平台深度解读
大数据·人工智能·架构·rag·mcp·百智云
不爱洗脚的小滕18 小时前
【RAG】召回(Retrieval)与重排(Rerank)核心技术要点汇总
langchain·aigc·ai编程·rag
deephub1 天前
视频 RAG 中分块策略:基于停顿、滑动窗口与基于 LLM 的方法
人工智能·大语言模型·rag·视频分块
Maiko Star1 天前
理解 RAG 的“为什么”与 Spring AI 实战初体验
人工智能·rag·springai
小何code2 天前
人工智能【第52篇】RAG系统实战:检索增强生成技术详解
embedding·向量数据库·rag·检索增强生成·llm应用