先说两个真实翻车现场。
第一个:做公司制度知识库的第一周,同事问「请年假的流程是什么」,Agent 检索出来三个结果——第一个是「加班调休制度」,第二个是「差旅费报销标准」,第三个才是「年假申请说明」。前两个完全不相关,但它们包含了「请假」「流程」这些词,向量相似度居然排在正确答案前面。同事看完回复说:「你这检索,还不如 Ctrl+F。」
第二个:文档量从 200 个块涨到 2000 个之后,检索质量断崖式下降。原来是暴力扫描,数据量翻十倍后排序信噪比直接崩了。Top-3 命中率从 85% 掉到不到 50%。
这两次翻车指向同一个问题:检索质量不是调一个参数能解决的。它是在多个维度上同时做工,任何一个环节拉胯,最终结果都会被拖下去。
一、问题的本质:检索质量由什么决定
检索系统的最终输出,可以抽象成四个因子做乘法:
检索质量 ≈ 切块质量 × 检索策略 × 排序质量 × Query 质量
这个乘法关系很关键。把某一个维度从 0.6 调到 0.9,不如把四个维度都从 0.5 调到 0.7。因为 0.6⁴ = 0.13,而 0.7⁴ = 0.24,后者几乎是前者的两倍。
我见过太多项目把 embedding 模型从 text-embedding-ada-002 换成 bge-large-zh,命中率只涨了 3 个百分点;但把切块从固定 500 字改成语义切块,直接涨了 15 个点。所以有时候你花三天调模型和 prompt,真不如花三小时认真想想「这些文档该怎么切」。
下面逐个拆。每个维度我都会给原理、代码、我踩过的坑,以及能直接抄的数据。
二、切块策略:花最多时间做最值钱的事
切块是检索系统里最容易被低估的环节。你切出来的块,决定了 embedding 模型能看到什么、检索时能匹配到什么、LLM 最终能读到什么。切得不好,后面所有优化都是治标不治本。
2.1 四种切块方式,场景完全不同
固定长度切块。按字符或 Token 数一刀切。上一篇用的就是这种,写起来最简单,但问题也最多:一句话被拦腰砍断,一个表格被切成几块,一个函数定义和调用分在两个块里。只适合纯叙述性文本,比如公告、新闻稿。代码文档、结构化文档基本没法用。
递归切块。先按段落切,段落太长就按句子切,句子还长就按字符切。LangChain 的RecursiveCharacterTextSplitter做的是这个。比固定长度强不少,至少在自然段落边界处下刀。但遇到没有自然段落的文档,比如纯文本流水账,跟固定长度没啥区别。
语义切块。让 embedding 模型计算相邻句子的向量相似度,相似度突然下降的地方就是「话题转折点」,在那下刀。这种切法天然按主题分块,检索时每个块的主题更集中,精确度最高。代价是需要额外计算成本,每个文档要多调一次 embedding。不过对离线建库场景来说,这个成本几乎可以忽略。
多模态切块。如果你的知识库里不只有文字,还有表格、图片、代码——那固定三种切法都不够。代码需要按 AST 语法树切,表格按行切后还要在块里标明「这行来自 XX 表格」,图片需要用 OCR 或视觉模型生成文字描述再切。这已经超出「切块」的范畴了,是在做文档解析。
2.2 实测数据
我用两个项目的数据验证过。一个 2000 页的操作手册,另一个 500 个技术 wiki 页面。
| 切块方式 | Top-3 命中率 | Top-5 命中率 | 平均块大小 |
|---|---|---|---|
| 固定 500 字 | 61% | 71% | 492 字 |
| 固定 300 字 | 68% | 77% | 294 字 |
| 递归 500 字 | 74% | 83% | 463 字 |
| 语义切块 | 82% | 91% | 385 字 |
语义切块的 Top-5 命中率能到 91%,比固定长度高 20 个百分点。在新用户首问的场景里,这个差距就是「靠谱」和「浪费感情」的区别。
2.3 几个进阶技巧
Parent-Child Chunking。小块用于检索,大块用于生成。比如把文档切成 100 字的小块做向量索引,但每个小块关联一个 500 字的父块。检索时用小块匹配,返回时把父块喂给 LLM。这样既保证了检索精度,又保证了上下文完整。LangChain 和 LlamaIndex 都有这个模式。
保留标题层级。切块的时候把当前块的文档路径、章节标题、小标题都拼进块里。比如:
[来源:员工手册 > 假期管理 > 年假]
员工每年享有 5 天带薪年假,需提前在 OA 系统提交申请……
这样检索时即使块本身没有「年假」这个词,标题也能补上语义信号。
Late Chunking。先对整个文档或长段落做 embedding,拿到每句话的 token 级表示,再在语义边界处切分。这种方法能保留长距离上下文,Jina AI 的jina-embeddings-v3已经支持。实测在长篇技术文档上比传统语义切块再涨 3-5 个点。
2.4 切块的常见坑
块太大
:embedding 会被稀释,块内主题不集中。一般中文控制在 300-500 字。
块太小
:上下文丢失,LLM 看不懂。比如「点击这里」这种引用在小区块里就是灾难。
overlap 设置不合理
:overlap 能缓解边界截断,但太大会导致重复检索、浪费 token。一般取块大小的 10%-15%。
没有清洗 PDF 换行
:从 PDF 拷出来的文本经常一行就断,直接切块会得到大量半截句子。先用正则把段落拼起来。
三、混合检索:别只用向量检索了
向量检索对「概念相近」敏感,对「关键词匹配」不敏感。你搜「年假申请」,它可能返回「请假流程」这种语义相关但不包含「年假」这个词的结果——对大多数查询来说是好事。但如果你搜的是专有名词「SMTP 配置错误代码 550」,向量检索会把它泛化成「邮件发送错误」,漏掉包含「550」这个精确关键码的文档。
3.1 稀疏检索补的是精确匹配
混合检索的经典做法:稠密检索 + 稀疏检索,取各自的前 K 个结果做融合排序。
稠密检索就是 embedding + 向量相似度,之前已经写过了。稀疏检索的代表是 BM25,一种改进版的 TF-IDF,根据词频和逆文档频率打分。核心公式是:
score(D,Q) = Σ IDF(q_i) · [f(q_i,D) · (k1+1)] / [f(q_i,D) + k1 · (1 - b + b · |D|/avgDl)]
f(q_i,D)是词q_i在文档D中的词频,IDF是逆文档频率,k1和b是超参。默认k1=1.5、b=0.75在多数中文场景够用,但如果你发现短查询的精确匹配被长文档稀释,可以把b调小。
代码实现:
from rank_bm25 import BM25Okapi
import numpy as np
def build_bm25(docs):
tokenized = [jieba.lcut(d) for d in docs] # 中文要先分词
return BM25Okapi(tokenized), tokenized
def hybrid_search(query, vector_store, bm25_index, bm25_docs, top_k=10):
# Dense: 向量检索
dense_results = vector_store.search(query, top_k=top_k)
# Sparse: BM25 关键词检索 tokenized\_query = jieba.lcut(query) bm25\_scores = bm25\_index.get\_scores(tokenized\_query) sparse\_top\_idx = np.argsort(bm25\_scores)[-top\_k:][::-1] sparse\_results = [bm25\_docs[i] for i in sparse\_top\_idx] # 融合:RRF return reciprocal\_rank\_fusion(dense\_results, sparse\_results, k=60)3.2 融合算法是关键
最简单的做法是把两边的分数直接加起来排名,但稠密检索和稀疏检索的分数范围差很多(一个在 0-1,一个可能到几十),直接加相当于稀疏检索完全主导排名。
RRF(倒数排名融合)不用分数,只用排名。每个文档的 RRF 分 = Σ 1/(k + rank_i),rank_i 是该文档在某个检索列表里的排名,k 是平滑常数,通常取 60。这样两边权重相等,不依赖分数分布。
def reciprocal_rank_fusion(list1, list2, k=60):
scores = {}
for rank, doc in enumerate(list1):
scores[doc[‘id’]] = scores.get(doc[‘id’], 0) + 1.0 / (k + rank + 1)
for rank, doc in enumerate(list2):
scores[doc[‘id’]] = scores.get(doc[‘id’], 0) + 1.0 / (k + rank + 1)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
如果你想让向量检索占主导,可以用加权 RRF,给不同列表的倒数排名乘以不同权重。但我的经验是,先保持等权重,调 k 值(常见 20、40、60),通常就够了。
3.3 什么时候必须上混合检索
- 查询里包含专有名词、错误码、型号、人名、API 名称。
- 知识库里有大量短字段,比如配置项、命令行参数。
- 用户对精确匹配有强预期,比如查法律条文、规章制度。
四、重排序:把金子从沙子里捞出来
混合检索能拿回 20 到 30 个候选,但最终只取前 3 个给模型。在 30 个里面挑 3 个,精度要求很高,向量相似度和 BM25 的排序精度都达不到。
这时候专门的重排序模型上场。它们的设计目标就是「精确判断一段话是否回答了某个问题」,作为检索到 LLM 之间的最后一关。
4.1 两个主流选择
BGE-Reranker(BAAI/bge-reranker-base)。开源首选。输入一个问题 + 一段文本,输出一个相关性分数。把混合检索的 30 个候选逐个喂给它,按分数重新排,取前三。单条推理很快,几十毫秒量级,总延迟可控。
Cohere Rerank。商业版本,比开源的强一截,尤其在跨语言场景。rerank-v3 对中英混合查询的表现相当好。缺点是要钱,API 按次收费。
实战经验:在一份 2000 块的中文知识库上,混合检索 Top-3 命中率是 78%。加 BGE-Reranker 重排后到 89%。加 Cohere Rerank 到 93%。78 到 89 看着只差 11 个点,但体验差异很大:前者每 4 次查询就翻车一次,后者每 9 次才翻车一次。
4.2 重排序的两种架构
Pointwise。每个 (query, doc) 单独打分,然后按分排序。BGE-Reranker 就是这类。实现简单,效果已经很好。
Listwise。把 query 和多个 doc 一起喂给模型,让模型直接输出最优排序。代表是 RankGPT 和一些基于 LLM 的 listwise reranker。优点是能考虑文档之间的相对关系;缺点是 token 消耗大、延迟高,一般只用于对延迟不敏感的场景。
4.3 一个完整的重排序流水线
from FlagEmbedding import FlagReranker
reranker = FlagReranker(‘BAAI/bge-reranker-base’, use_fp16=True)
def rerank(query, candidates, top_n=3):
pairs = [[query, doc[‘text’]] for doc in candidates]
scores = reranker.compute_score(pairs, normalize=True)
for doc, score in zip(candidates, scores):
doc[‘rerank_score’] = score
return sorted(candidates, key=lambda x: x[‘rerank_score’], reverse=True)[:top_n]
注意:如果候选太多,比如 100 个,重排序的延迟会线性增长。一般建议先用向量+BM25 召回 30-50 个,再重排序。
五、Query 改写:用户说不好不是用户的错
用户输入的问法越随意,检索越不准。ChatGPT 普及之后这个问题更严重了——很多人习惯了跟 AI 像跟人一样说话,输入的是「那啥,我想问一下,咱们公司那个每个月光租办公室的钱报销政策是什么来着」。这种句子塞进 embedding 模型,向量会被「我想问一下」「那啥」这种噪音词严重稀释。
5.1 Query 压缩
用 LLM 把用户的口水问题压缩成关键词组合。上面的长句改成「办公室租金 报销政策」,检索质量立马拉回来。成本很低——压缩 query 用的 token 可以忽略不计。
COMPRESS_PROMPT = “”“把用户的问题改写成适合向量检索的简短关键词查询,只保留核心实体和意图。
用户:{query}
改写:”“”
def compress_query(query, llm):
return llm(COMPRESS_PROMPT.format(query=query)).strip()
5.2 Query 分解
遇到复杂的多跳问题时用。比如「公司给远程员工的通讯补贴和给本地通勤员工的交通补贴有什么不同」,这是两个子问题。把它拆成「远程员工通讯补贴」和「本地员工交通补贴」,分别检索,分别回答,最后合并。Workflow 稍微复杂一点,但处理多跳问题的效果是碾压式的。
DECOMPOSE_PROMPT = “”“把下面这个问题拆成 1-3 个可以独立检索的子问题,每个子问题占一行。
问题:{query}
子问题:”“”
def decompose_query(query, llm):
text = llm(DECOMPOSE_PROMPT.format(query=query)).strip()
return [line.strip(‘- ‘) for line in text.split(’\n’) if line.strip()]
5.3 HyDE:用假设文档改写
HyDE(Hypothetical Document Embeddings)的做法是:让 LLM 先根据问题生成一个「假设答案」,然后对这个假设答案做 embedding,再去检索。这个方法对「用户问题很短、但答案很长」的场景特别有效。
HYDE_PROMPT = “”“根据以下问题,写一段 100 字左右的参考答案。不需要完全准确,只要覆盖可能相关的关键词和概念。
问题:{query}
答案:”“”
def hyde_search(query, llm, vector_store, top_k=5):
hypo_doc = llm(HYDE_PROMPT.format(query=query))
return vector_store.search(hypo_doc, top_k=top_k)
HyDE 的风险是:如果 LLM 生成的假设答案跑偏了,检索也会跑偏。所以通常只在对答案有大致预期的场景使用,或者和原始 query 的检索结果做融合。
六、怎么评估:不能靠感觉
检索优化有个坑:你改了一个策略,主观觉得「好像好了一点」,上线后用户反馈反而差了。检索质量的评估必须量化。
6.1 三个核心指标
命中率(Hit Rate)。Top-K 结果里至少有一个包含正确答案的比例。最常用,衡量「有没有找到」。
MRR(Mean Reciprocal Rank)。正确答案在结果列表里排第几?排名越靠前 MRR 越高。公式:
MRR = (1/|Q|) · Σ 1/rank_i
正确答案排第一贡献 1,排第二贡献 0.5,没找到贡献 0。
NDCG(Normalized Discounted Cumulative Gain)。综合排名和相关性分数。排名靠前且高度相关的文档获得更高分数。衡量「排序质量」。
6.2 建黄金评估集
做评估集的方法:先建一份「黄金 Q&A」(人工标注的问题 + 标准答案 + 应检索到的文档 ID),然后跑一次检索,用上面的指标打分。
我建议至少准备 100 条标注,覆盖:
- 常见问题(占 50%)
- 边缘/长尾问题(占 30%)
- 故意设计的多跳/模糊问题(占 20%)
每次改动检索策略后都用同一份评估集跑一次,对比分数变化,决定这个改动要不要保留。
6.3 错误分析比指标更重要
指标告诉你「涨了多少」,错误分析告诉你「为什么涨、为什么跌」。建议把检索失败的问题分成几类:
切块失败
:正确答案被切到了两个块里,或块边界截断了关键信息。
语义漂移
:query 和文档用词不同,embedding 没捕捉到同义关系。
关键词缺失
:专有名词、数字、错误码没被向量检索召回。
排序失败
:正确答案在候选里,但重排序把它挤下去了。
Query 噪音
:用户输入太啰嗦,embedding 被无关词稀释。
我每次调优都会先跑一遍错误分类,看哪类错误最多,就优先攻那个维度。这比盲目换模型有效得多。
开源工具 Ragas 可以帮你做这个,它内置了检索评估的整套流程。
七、调优顺序:别同时调四个维度
四个维度优化是有顺序的。一个维度稳定之前调另一个,会让你误判效果。
推荐顺序:
先优化切块。切块在数据准备阶段,最前置,改完不需要动任何下游代码。先把命中率从 60 拉到 80,后面再调检索策略才能看到真实效果。
再开混合检索。在向量检索旁加一条 BM25 管线,融合排序。这一步能把稀奇古怪的专有名词、编码、错误码这些稠密检索抓不住的场景补齐。
然后上重排序。前面两条做完,Top-3 命中率大概在 75-80% 左右。加重排可以把命中率再拉 10 个点。
最后做 Query 改写。前面三个都在后端做工,Query 改写是唯一直接从前端入手的。先让检索本身合格了,再去优化用户的输入质量。
这四步我建议的投入时间分配:切块 50%,混合检索 20%,重排序 20%,Query 改写 10%。我自己复盘过,最值钱的时间就是花在切块策略上——一个下午把切块策略调对,检索质量涨的幅度比其他三项加起来还多。
八、一个完整的调优案例
拿我之前做过的一个企业制度知识库为例,初始状态是:固定 500 字切块 + 纯向量检索 + 无重排。
- 初始 Top-3 命中率:61%
- 第一步:改成语义切块 + Parent-Child,命中率涨到 79%
- 第二步:加上 BM25 混合检索,命中率涨到 84%
- 第三步:加 BGE-Reranker,命中率涨到 91%
- 第四步:加 Query 压缩,命中率涨到 94%
整个过程中,模型还是同一个 bge-large-zh,LLM 也没换。检索质量的提升完全来自「把检索这件事做扎实」。
TL;DR
- 检索质量 = 切块质量 × 检索策略 × 排序质量 × Query 质量。乘法关系,短板拖全盘。
- 切块是回报率最高的优化点:语义切块比固定长度高 20 个点的命中率;Parent-Child 和 Late Chunking 是进一步手段。
- 混合检索 = 稠密语义 + 稀疏 BM25,用 RRF 融合比直接加分数好。
- 重排序在 30 个候选里精确挑 3 个,BGE-Reranker 能再拉 11 个点;listwise 适合延迟不敏感场景。
- Query 改写性价比最高:压缩、分解、HyDE 三种手段覆盖不同场景。
- 评估必须量化,靠 Ragas + 黄金 Q&A 集,每次改策略都跑一次分;错误分类比指标更能指导优化方向。
- 调优顺序:先切块 → 再混合检索 → 然后重排序 → 最后 Query 改写。
学AI大模型的正确顺序,千万不要搞错了
🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!
有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!
就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋
📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇
学习路线:
✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经
以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!
我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~