RAG 检索增强完全指南:让大模型回答你的私有知识

接了个看起来简单的活:给公司做一个能回答内部资料问题的 AI 助手。第一版方案简单粗暴到自己都觉得聪明:把几百页文档全部拼进 prompt 和问题一起丢给大模型,结果第一次运行就报 context length exceeded 文档太长把上下文窗口撑爆;退而求其次只塞一部分这下不报错但塞进去的恰好是不相干的资料模型答非所问甚至一本正经胡编;每问一次都要把一大坨文档重发一遍 token 烧得飞快账单肉疼。卡了很久直到一个问题点醒:为什么非要把整本书背给模型听,考生去考试也不会把整个图书馆搬进考场只会带那一本最相关的参考书甚至只翻到那一页,缺的不是更大的上下文窗口而是在海量资料里精准找到那一页的能力。梳理:RAG 检索增强生成不微调模型不把全部资料塞进去而是先检索出和问题最相关的几小段资料再把这几小段拼进 prompt 让模型回答,等于给大模型配一场开卷考试答题前先翻书只翻相关的那几页,四步流水线切分向量化检索生成。让模型会私有知识两条路:微调把知识焊进参数贵慢知识更新要重训仍会幻觉,RAG 不动模型一个参数另建资料库知识更新只需更新库答案有出处可控便宜;RAG 系统分离线建库和在线问答两阶段。第一步切分把长文档切成小块,整篇当一块检索要么整篇命中要么不命中捞回只有一段相关其余全噪声,但切太碎会把完整句子概念拦腰斩断核心矛盾是块要足够小检索精准又足够完整语义不断裂,优先按段落标题等天然结构切并让相邻块留 overlap 重叠让骑在边界的关键句两块都完整出现。第二步向量化与检索,找相关块不能用关键词搜索它匹配字面而报销差旅费和出差费用核销字面全不同却是一件事,核心武器 Embedding 把文字变成向量且意思相近的向量空间里也挨得近于是按语义找资料变成算两个向量距离的数学题,离线把每块向量化灌进向量数据库在线把问题用同一个 Embedding 向量化取最近的 K 个块,问题和文档块必须用同一个 Embedding 模型否则向量不在一个空间。第三步重排与生成,向量检索快但准有上限 Top-K 常混着看着相关其实跑题的块,重排用更精细的 Cross-Encoder 把问题和每个候选成对细比,标准做法向量库先粗捞 20到30 个再 Rerank 精挑 3到5 个,生成时 prompt 必须写死只依据所给资料回答没有就说无法回答严禁编造这是压制幻觉的命门,检索回来相似度都很低就直接答不知道设阈值兜底。工程选型:chunk_size 是权衡经验起点 500 字重叠 10%到20%、Embedding 模型是地基中文文档选对中文友好的建库和查询必须同一个、向量库量小用 FAISS pgvector 量大才上 Milvus Qdrant、混合检索向量加 BM25 两路融合、Rerank 噪声多时上。正确做法是答不好先把检索回的原始块打印出来人眼看,答案在块里是生成差改 prompt 不在块里是检索差,以及让模型变强不是把更多塞进它内部而是让它内部保持轻盈接上一个又大又准的外部。

2024 年,我接了个看起来很简单的活:给公司做一个"能回答内部资料问题"的 AI 助手——员工问"我们的报销流程是什么",它能照着公司那几百页的规章制度,准确答出来。我的第一版方案,简单粗暴到我自己都觉得聪明:把那几百页文档,【全部】拼进 prompt 里,后面跟上用户的问题,一起丢给大模型。我心想,大模型这么强,资料都给它了,它还能答不出来?结果第一次运行就报错:context length exceeded——文档太长,把模型的上下文窗口撑爆了。我退而求其次,只塞一部分文档进去,这下不报错了,但新问题来了:用户问"报销流程",我塞进去的恰好是"考勤制度"那几页,模型对着不相干的资料,要么答非所问,要么干脆开始一本正经地胡编。我又试着把文档压缩、做摘要再塞,效果还是飘忽不定。而且每问一次,都要把一大坨文档重新发一遍,token 烧得飞快,一个月账单看得我肉疼。我卡在这里很久,直到一个问题点醒了我:我为什么非要把【整本书】都背给模型听?一个人去考试,也不会把整个图书馆都搬进考场——他只会带【那一本】最相关的参考书,甚至只翻到【那一页】。我缺的,不是一个更大的上下文窗口,而是一个"在海量资料里,精准找到那一页"的能力。这件事逼着我把 RAG(检索增强生成)到底是什么、文档为什么要切分、向量检索怎么靠"语义"找资料、重排和生成怎么做、以及 RAG 的工程选型,彻底理清了一遍。本文是这份梳理的完整复盘。

问题背景:为什么不能把文档全塞进 prompt

需求:做一个 AI 助手,能照着公司内部文档(几百页)准确回答问题

我的第一版(错误)方案:把全部文档拼进 prompt,和问题一起发给模型
踩到的三堵墙:
- ★ 墙 1:上下文窗口装不下
  几百页文档 token 数远超模型上下文上限 -> context length exceeded
- ★ 墙 2:塞一部分进去,塞的常常是"不相关"的那部分
  用户问报销,塞进去的是考勤制度 -> 模型答非所问 / 开始胡编
- ★ 墙 3:每次提问都重发一大坨文档
  token 烧得飞快,账单肉疼,响应还慢

★★ 三堵墙背后是同一个错误认知:
   我以为"让模型回答私有知识"= "把私有知识全部喂给它"。
   错。真正要做的是:在海量资料里,只【找出那几段最相关的】,
   只把这几段喂给模型。—— 这就是 RAG 要解决的事。

★ RAG(Retrieval-Augmented Generation,检索增强生成)一句话:
  不微调模型、不把全部资料塞进去,而是【先检索】出和问题
  最相关的几小段资料,再把【这几小段】拼进 prompt 让模型回答。
  = 给大模型配一场"开卷考试":答题前先翻书,只翻相关的那几页。

★ RAG 的四步流水线(本文逐步拆解):
  ① 切分(Chunking)  :把长文档切成一小块一小块
  ② 向量化(Embedding):把每一块变成一个能比较"语义"的向量
  ③ 检索(Retrieval) :问题来了,找出语义最相近的几块
  ④ 生成(Generation):把这几块 + 问题拼成 prompt,交给模型答

★★ 一个关键认知:RAG 不改模型一个参数。它改变的不是
   "模型会什么",而是"模型答题前,手边摆着哪几页纸"。

RAG 是什么:给大模型配一场"开卷考试"

# === ★ 先想清楚:让模型"会"私有知识,到底有几条路 ===

# === ★ 路线对比:微调 vs RAG ===
# ★ 路线 A —— 微调(Fine-tuning):拿你的私有数据去【再训练】
#   模型,把知识"焊"进模型的参数里。
#  - 代价:贵、慢、要会调;知识一更新就得重新训练;
#    模型还是会"忘"和"串"(没法保证它精确复述某条规定)。
# ★ 路线 B —— RAG:模型【一个参数都不动】。你另外建一个
#   "资料库",每次提问前,先从库里捞出相关资料,塞进 prompt。
#  - 代价:要搭一套检索;但知识更新只需更新资料库(秒级)、
#    答案有出处、可控、便宜。

# === ★★ "开卷考试"这个比喻,要吃透 ===
# ★ 闭卷考试 = 纯大模型:它只能凭脑子里"记得"的答。私有
#   知识它从没"学"过,自然答不出,或者瞎编(幻觉)。
# ★ ★ 开卷考试 = RAG:答题前,允许你翻一本【你自己带的
#   参考书】。但考场不让你把整个图书馆搬进去 —— 你得在
#   翻书前,先知道"翻哪一本、翻到哪一页"。
# ★ ★★ RAG 的全部技术含量,就在这"精准翻到那一页"上:
#   它不是"把书给模型",而是"把书里【那一页】给模型"。

# === ★ RAG 为什么能压住"幻觉" ===
# ★ 大模型胡编(幻觉),很多时候是因为它"不知道,但又不肯
#   说不知道",于是顺着语感编一个。
# ★ ★ RAG 把【真实资料】明明白白摆在 prompt 里,并要求
#   模型"只依据给你的资料回答" —— 模型有据可依,就不必
#   编;检索不到资料时,还能让它直接答"我不知道"。
# ★ ★ 附带好处:答案能【标注出处】(来自哪份文档第几段),
#   用户可核对,可信度天差地别。

# === ★ RAG 系统分两个阶段,别混了 ===
# ★ ★ 阶段一 —— 离线建库(Indexing):把你的文档,切分、
#   向量化,灌进一个"向量数据库"。这步【提前做一次】,
#   文档变了才重做。
# ★ ★ 阶段二 —— 在线问答(Querying):用户每来一个问题,
#   就走"检索 -> 拼 prompt -> 模型生成"。这步【每次提问
#   都做】。
# ★ 把这两个阶段分清,后面的工程选型才不会乱。

# === 小结 ===
# ★ 让模型"会"私有知识有两条路:微调把知识焊进参数(贵、
#   慢、知识更新要重训、仍会幻觉),RAG 不动模型一个参数、
#   另建资料库每次提问前检索相关资料塞进 prompt(知识更新
#   只需更新库、答案有出处、可控便宜)。★★ RAG = 给模型
#   配一场开卷考试:不让你把整个图书馆搬进考场,要先精准
#   翻到那一页 —— 全部技术含量就在"精准翻到那一页"。★ 它
#   靠把真实资料摆进 prompt 压住幻觉、还能标出处。系统分
#   两阶段:离线建库(切分+向量化,提前做一次)、在线问答
#   (检索+拼 prompt+生成,每次提问都做)。

第一步:文档切分——把知识切成"合适大小"的块

# === ★ RAG 的第一步,也是最被低估的一步:切分(Chunking) ===

# === ★ 为什么必须切,不能整篇存 ===
# ★ ★ 原因 1 —— 检索精度:如果你把一整篇万字长文当成一个
#   "块"存进库,那检索时,要么整篇命中、要么整篇不命中。
#   用户问一个具体小问题,你却捞回来一整篇 —— 相关的只有
#   一段,其余全是噪声,还白占 prompt 空间。
# ★ ★ 原因 2 —— prompt 容量:检索出来的块,是要拼进
#   prompt 的。块太大,塞两三个就把窗口占满了。

# === ★★ 但切得太碎,也是灾难 ===
# ★ 把文档按"每 50 字一刀"硬切,会把一句完整的话、一个
#   完整的概念,拦腰斩断。检索时捞回半句话,模型看着这
#   半句没头没尾的东西,照样答不好。
# ★ ★ 所以切分的核心矛盾:★ 块要【足够小】(检索才精准)、
#   又要【足够完整】(语义才不断裂)。这是个权衡。

# === ★ 切分的几种常见策略,由糙到精 ===
# ★ ① 定长切分:每 N 个字符一刀。最简单,但最容易切断
#   句子。基本只用于"实在没有结构"的纯文本。
# ★ ② ★ 按结构切分:顺着文档天然的结构 —— 段落、标题、
#   Markdown 的 #、列表项 —— 来切。一段是一块,一节是
#   一块。这是【最常用】的,因为天然的结构边界,通常
#   就是语义边界。
# ★ ③ 按句子切分:用标点把文本切成句子,再把相邻句子
#   攒成一块,凑到目标大小为止。

# === ★★ 关键技巧:块与块之间要"重叠"(overlap) ===
# ★ 就算按段落切,也难免有一句关键的话,正好骑在两块的
#   交界处。解法:★ 让相邻的块,【互相重叠一小段】。
# ★ ★ 比如块大小 500 字,重叠设 80 字 —— 那第二块的
#   开头,会把第一块结尾的 80 字【再抄一遍】。这样,
#   骑在边界上的那句话,在两块里都【完整地】出现过,
#   不会被切丢。重叠是低成本、高收益的一招。

# === 小结 ===
# ★ 切分是 RAG 第一步也最被低估。必须切不能整篇存:
#   ① 整篇当一块检索要么整篇命中要么不命中,捞回一整篇
#   只有一段相关其余全噪声;② 检索出的块要拼进 prompt,
#   块太大塞两三个就满。★★ 但切太碎也是灾难 —— 按固定
#   字数硬切会把完整句子概念拦腰斩断。核心矛盾:块要
#   足够小(检索精准)又足够完整(语义不断裂)。策略由
#   糙到精:定长切分、★按结构切分(顺段落标题列表,最
#   常用,结构边界通常即语义边界)、按句子攒。★★ 关键
#   技巧是块间留 overlap 重叠一小段,让骑在边界的关键句
#   在相邻两块里都完整出现,不被切丢。
# ★ 文档切分:按段落切,并让相邻块重叠一小段(overlap)
def split_into_chunks(text, chunk_size=500, overlap=80):
    # ★ 先按段落(双换行)粗分 —— 顺着文档天然结构
    paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]

    chunks, buf = [], ''
    for para in paragraphs:
        # ★ 攒到一段,加上去还不超 chunk_size,就继续攒
        if len(buf) + len(para) <= chunk_size:
            buf += para + '\n\n'
        else:
            if buf:
                chunks.append(buf.strip())
            # ★★ 关键:新块的开头,带上一块结尾的 overlap 个字
            #    骑在边界上的句子,就在两块里都完整出现了
            tail = buf[-overlap:] if buf else ''
            buf = tail + para + '\n\n'
    if buf.strip():
        chunks.append(buf.strip())
    return chunks

# ★ 每个块,连同它的来源信息一起存 —— 答案要能标出处
def build_chunk_records(doc_name, text):
    return [
        {'doc': doc_name, 'chunk_id': i, 'text': c}
        for i, c in enumerate(split_into_chunks(text))
    ]

第二步:向量化与检索——靠"语义相似"找到相关块

# === ★ 切好块了,怎么"找出和问题相关的块" ===

# === ★ 为什么不用关键词搜索(像 Ctrl+F) ===
# ★ ★ 关键词搜索,匹配的是【字面】。用户问"怎么报销
#   差旅费",而文档里写的是"出差费用的核销办法" ——
#   一个关键词都对不上,但它俩说的【是同一件事】。
# ★ 私有知识问答,用户的问法和文档的写法,几乎永远
#   对不上字面。要的是按【意思】找,不是按【字】找。

# === ★★ 核心武器:Embedding(向量化)===
# ★ Embedding 模型,能把一段文字,变成一串数字(一个
#   "向量",比如 1536 个浮点数)。
# ★ ★★ 它最神奇的性质:【意思相近的文字,变出来的
#   向量,在空间里也挨得近】。"报销差旅费"和"出差
#   费用核销",字面天差地别,但它俩的向量,会离得
#   很近。意思无关的两段话,向量就离得远。
# ★ ★ 于是"按语义找资料"这件玄乎的事,被 Embedding
#   变成了一道实打实的数学题:【算两个向量之间的距离】。

# === ★ 离线阶段:把所有块,向量化、灌进向量库 ===
# ★ 建库时,对每一个文档块,调 Embedding 模型,算出
#   它的向量,然后把(向量 + 块原文 + 出处)一起,
#   存进一个【向量数据库】。
# ★ ★ 向量数据库(如 FAISS、Milvus、pgvector)的专长,
#   就是干一件事:给它一个向量,它能极快地从几百万个
#   向量里,找出【离它最近的 K 个】。

# === ★ 在线阶段:检索就是"算距离、取最近的 K 个" ===
# ★ 用户问题来了,流程是:
#  ① 把【用户的问题】,也用【同一个】Embedding 模型,
#     向量化成一个向量;
#  ② 拿这个"问题向量",去向量库里查:和它距离最近的
#     K 个块向量是哪些(K 一般取 3~5);
#  ③ 这 K 个块,就是"和问题最相关的资料"。
# ★ ★ 注意:问题和文档块,【必须用同一个 Embedding
#   模型】向量化 —— 否则两组向量不在一个空间,距离
#   没有意义。这是新手最常翻的车。

# === ★ 衡量"近"的标准:余弦相似度 ===
# ★ 最常用的距离衡量,是【余弦相似度】 —— 看两个向量
#   "方向"有多一致。值域 -1~1,越接近 1 越相似。
# ★ 大多数向量库,你不用自己算,指定度量方式即可。

# === 小结 ===
# ★ 找相关块不能用关键词搜索:它匹配字面,而"报销
#   差旅费"和"出差费用核销办法"字面全不同却是一件事,
#   用户问法和文档写法几乎永远对不上字面。★★ 核心
#   武器是 Embedding —— 把文字变成向量,且【意思相近
#   的文字向量在空间里也挨得近】,于是"按语义找资料"
#   变成"算两个向量的距离"这道数学题。离线阶段把每个
#   块向量化连同原文出处灌进向量数据库(FAISS/Milvus/
#   pgvector,专长是从百万向量里极快找出最近的 K 个);
#   在线阶段把问题用【同一个】Embedding 模型向量化,
#   去库里取距离最近的 K 个块(K 取 3~5)。★★ 问题和
#   文档块必须用同一个 Embedding 模型,否则向量不在
#   一个空间距离无意义 —— 新手最常翻的车。
# ★ 离线建库:把每个文档块向量化,存进向量库
import numpy as np

def embed(text):
    # ★ 调 Embedding 模型,把一段文字变成一个向量
    resp = client.embeddings.create(model='text-embedding-3-small', input=text)
    return np.array(resp.data[0].embedding, dtype='float32')

def build_index(chunk_records):
    index = []
    for rec in chunk_records:
        rec['vector'] = embed(rec['text'])   # ★ 每块算一个向量
        index.append(rec)
    return index                              # 真实项目里换成 FAISS / pgvector

# ★ 在线检索:问题向量化,取距离最近的 K 个块
def cosine(a, b):
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

def retrieve(index, question, top_k=4):
    # ★★ 关键:问题用【同一个】 embed() 向量化,才和块在同一空间
    q_vec = embed(question)
    scored = [(cosine(q_vec, rec['vector']), rec) for rec in index]
    scored.sort(key=lambda x: x[0], reverse=True)   # ★ 按相似度降序
    return scored[:top_k]                            # ★ 取最相关的 K 个

第三步:重排与生成——把检索到的资料喂给模型

# === ★ 检索拿到 K 个块之后,还差两步:重排、生成 ===

# === ★ 为什么检索完还要"重排"(Rerank) ===
# ★ ★ 向量检索快,但它的"准",是有上限的:Embedding
#   把一整段话压成一个向量,细节难免丢失。它给出的
#   Top-K,里面常混着"看着相关、其实跑题"的块。
# ★ ★ 重排(Rerank)是补这一刀:用一个更精细、但更慢
#   的模型(Cross-Encoder / Rerank 模型),把"问题"
#   和"每一个候选块"成对地、仔细地比一遍,重新打分。
# ★ ★ 标准做法:★ 检索时先用向量库【粗捞】出 20~30 个
#   候选(快、宽);★ 再用 Rerank 模型把这 20~30 个
#   【精挑】出最好的 3~5 个。粗一道、精一道,又快又准。

# === ★★ 生成:prompt 怎么拼,决定答案质量 ===
# ★ 最后一步,是把"精挑出的几个块" + "用户问题",拼成
#   一个 prompt,交给大模型生成答案。这个 prompt 的写法,
#   极其关键,几条铁律:
# ★ ① ★★ 明确指令:"只依据下面提供的资料回答,资料里
#   没有的,就说'根据现有资料无法回答',不要编。"
#   —— 这一句,是 RAG 压制幻觉的命门。
# ★ ② ★ 把检索到的资料,清清楚楚地、带着出处地,放进
#   prompt(用分隔线、编号标清楚每段来自哪)。
# ★ ③ ★ 要求模型在答案里【标注引用来源】 —— 它答的
#   每个点,来自第几段资料,让用户可核对。
# ★ ④ 资料放前、问题放后,中间用明显的分隔。

# === ★ 一个常被忽略的点:检索不到,要敢说"不知道" ===
# ★ ★ 如果检索回来的块,相似度分数都很低(压根没相关
#   资料),正确的做法【不是】硬把这些不相关的块塞给
#   模型,而是:直接返回"知识库里没有相关内容"。
# ★ 设一个相似度阈值,低于它,就走"答不了"的兜底分支。
#   宁可答"不知道",也别让模型对着烂资料硬编。

# === 小结 ===
# ★ 检索拿到 K 个块后还差两步。① 重排(Rerank):向量
#   检索快但"准"有上限 —— Embedding 把整段压成一个向量
#   丢细节,Top-K 常混着看着相关其实跑题的块;重排用
#   更精细更慢的 Cross-Encoder 把问题和每个候选块成对
#   细比重新打分。★★ 标准做法:向量库先粗捞 20~30 个
#   (快、宽),再用 Rerank 精挑 3~5 个(准),粗一道
#   精一道。② 生成:prompt 写法决定答案质量 —— ★★ 明确
#   指令"只依据所给资料回答,没有就说无法回答不要编"
#   (压制幻觉的命门)、资料带出处放进 prompt、要求标注
#   引用来源、资料放前问题放后。★ 还要敢说不知道:检索
#   回来相似度都很低就直接返回"知识库没有相关内容",
#   设阈值兜底,别让模型对着烂资料硬编。
# ★ 重排 + 生成:粗捞 -> 精排 -> 拼 prompt -> 让模型作答
SIM_THRESHOLD = 0.35    # ★ 相似度阈值:低于它,判定"没有相关资料"

def answer(index, question):
    # ① ★ 粗捞:向量检索宽宽地取 20 个候选
    candidates = retrieve(index, question, top_k=20)

    # ★★ 检索不到相关资料 —— 敢说不知道,别硬编
    if not candidates or candidates[0][0] < SIM_THRESHOLD:
        return '根据现有知识库,无法回答这个问题。'

    # ② ★ 精排:用 Rerank 模型把 20 个精挑成最好的 4 个
    reranked = rerank_model.rank(question, [c[1] for c in candidates])
    top = reranked[:4]

    # ③ ★ 拼 prompt:资料带出处放前,问题放后
    context = '\n\n'.join(
        f"[资料{i+1}] (来源:{c['doc']} 第{c['chunk_id']}段)\n{c['text']}"
        for i, c in enumerate(top)
    )
    prompt = (
        "你是企业知识库助手。★ 只依据下面提供的资料回答,"
        "资料里没有的内容,直接说'根据现有资料无法回答',严禁编造。"
        "★ 回答时请标注你引用了哪几段资料。\n\n"
        f"=== 资料 ===\n{context}\n\n=== 问题 ===\n{question}"
    )
    resp = client.chat.completions.create(
        model='gpt-4o-mini',
        messages=[{'role': 'user', 'content': prompt}],
    )
    return resp.choices[0].message.content

工程选型:RAG 的几个关键决策

# === ★ 把 RAG 跑通容易,跑好,全在这几个选型上 ===

# === ★ 决策 1:块切多大(chunk_size)===
# ★ ★ 没有标准答案,是个权衡:
#  - 块【偏小】(200~300 字):检索精准,命中的噪声少;
#    但单块信息不全,可能要凑好几块才说清一件事。
#  - 块【偏大】(800~1000 字):单块上下文完整;但检索
#    精度下降,且容易把 prompt 占满。
# ★ ★ 经验起点:500 字左右、重叠 10%~20%,再按你的
#   文档类型(FAQ 短、技术手册长)和实测效果去调。

# === ★ 决策 2:Embedding 模型怎么选 ===
# ★ ★ 它决定了"语义找得准不准",是 RAG 的地基。看三点:
#  - ★ 语言:你的文档是中文,就必须选【对中文友好】的
#    Embedding 模型,别用纯英文语料训出来的。
#  - ★ 维度与成本:维度高(如 1536/3072)通常更准,但
#    存储和计算更贵。按规模权衡。
#  - ★★ 一致性铁律:建库用的 Embedding 模型,和线上
#    查询用的,【必须是同一个】。换模型 = 整个库重灌。

# === ★ 决策 3:向量数据库怎么选 ===
# ★ - 数据量小(几万块)/ 想最快上手:FAISS(库,进程内)、
#    或直接用 pgvector(给你现成的 Postgres 加个扩展)。
# ★ - 数据量大、要分布式、要高可用:Milvus、Qdrant 这类
#    专业向量数据库。
# ★ 别一上来就上重型方案 —— 多数内部知识库,pgvector 够用。

# === ★★ 决策 4:要不要做"混合检索"(Hybrid Search)===
# ★ 纯向量检索,有个短板:它擅长"语义",却可能对【精确
#   关键词】不敏感 —— 比如用户问的是一个【确切的产品
#   型号、错误码、人名】,这种地方,字面精确匹配反而更靠谱。
# ★ ★ 混合检索 = 向量检索(管语义)+ 关键词检索(BM25,
#   管字面),两路结果融合。对"既有概念问题、又有精确
#   术语"的知识库,混合检索通常明显更稳。

# === ★ 决策 5:要不要 Rerank ===
# ★ 数据量小、对延迟极敏感 -> 可以先不上 Rerank,直接用
#   向量 Top-K。★ 但只要你发现"检索回来的块里噪声多、
#   答非所问",上 Rerank(粗捞宽、精排准)几乎总是值得的。

# === 认知 ===
# ★ RAG 跑通容易跑好全在选型:① chunk_size 是权衡 ——
#   偏小检索精准但单块信息不全,偏大上下文完整但检索精度
#   降,经验起点 500 字、重叠 10%~20% 再按文档类型实测调;
#   ② Embedding 模型是地基 —— 中文文档必选对中文友好的、
#   维度高通常更准但更贵、★★建库和查询必须用同一个模型
#   换了就整库重灌;③ 向量库 —— 数据量小用 FAISS/pgvector
#   最快上手,量大要分布式才上 Milvus/Qdrant,别一上来上
#   重型;④★★ 混合检索 —— 纯向量擅长语义但对精确关键词
#   (型号/错误码/人名)不敏感,向量+BM25 两路融合对又有
#   概念又有术语的库更稳;⑤ Rerank —— 量小对延迟敏感可
#   先不上,一旦发现噪声多答非所问就上,粗捞宽精排准。

RAG 避坑与排查纪律

# === RAG 答得不好,九成不是模型的锅,是检索的锅 ===

# === ★ 排查铁律:答案差,先分清是"检索差"还是"生成差" ===
# ★ ★ RAG 答错,有两种完全不同的病因,先定位是哪种:
#  - ★ 病因 A —— 检索差:捞回来的几个块,本身就【不
#    包含】答案。这时模型再强也没用 —— 巧妇难为无米。
#  - ★ 病因 B —— 生成差:块里【明明有】答案,但模型
#    没答好(漏了、答偏了、还是编了)。
# ★ ★★ 怎么分:把每次检索【捞回来的原始块,打印出来,
#   人眼看一遍】。答案在块里 -> 是生成差(改 prompt);
#   答案不在块里 -> 是检索差(改切分 / Embedding / 检索)。
#   不做这一步,你就是在瞎调。

# === ★ 坑 1:检索差 —— 多半是切分或 Embedding 的问题 ===
# ★ - 块切太大 -> 一块里混了好几个主题,检索抓不准;
# ★ - 块切太碎 -> 答案被切散在好几块,一次捞不全;
# ★ - Embedding 模型对中文/专业术语不友好 -> 语义找偏。

# === ★ 坑 2:建库和查询用了不同的 Embedding 模型 ===
# ★ ★ 前面强调过,这里再列一次 —— 因为它太致命且太隐蔽:
#   两组向量不在一个空间,检索结果完全是噪声,但程序
#   【不报错】,你只会觉得"RAG 怎么这么蠢"。换过模型,
#   一定整库重灌。

# === ★ 坑 3:文档更新了,索引忘了重建 ===
# ★ RAG 的知识,在【向量库】里,不在模型里。文档改了、
#   新增了,必须重新切分、向量化、更新索引 —— 否则用户
#   拿到的永远是旧知识。要有一套索引更新机制。

# === ★ 坑 4:把"检索不到"硬答成"看似有理" ===
# ★ ★ 知识库里没有的问题,一定要让它走兜底、答"不知道"。
#   最危险的 RAG,是它"对没有依据的问题,也答得头头是道"
#   —— 用户会信,然后被坑。设相似度阈值,守住这条线。

# === ★ 坑 5:prompt 里没有"严禁编造"的硬指令 ===
# ★ 只是把资料贴进去、不加约束,模型仍会"参考资料 +
#   自由发挥"。必须明确写死:只依据资料、没有就说没有。

# === ★ 坑 6:只看一两个 case 就下结论 ===
# ★ ★ RAG 效果要靠【一批测试问题】来评估:准备一组
#   "标准问题 + 标准答案",每次调完参数,整批跑一遍,
#   看命中率有没有真的变好。别凭手感调。

# === 认知 ===
# ★ RAG 答不好九成不是模型锅是检索锅。★★ 排查铁律:
#   先分清"检索差"还是"生成差" —— 把每次检索捞回的
#   原始块打印出来人眼看,答案在块里就是生成差(改
#   prompt),不在块里就是检索差(改切分/Embedding/
#   检索),不做这步就是瞎调。其余坑:检索差多半是切分
#   太大/太碎或 Embedding 对中文术语不友好;★★ 建库和
#   查询用了不同 Embedding 模型 —— 致命且隐蔽,程序不
#   报错只是结果全是噪声;文档更新了忘重建索引,知识在
#   向量库不在模型;把"检索不到"硬答成看似有理最危险,
#   设阈值走兜底;prompt 必须写死"严禁编造";效果要靠
#   一批标准问答整批评估,别凭手感调。

命令速查

RAG 四步流水线
=============================================================
① 切分 Chunking    把长文档切成小块,块间留 overlap 重叠
② 向量化 Embedding 每块算一个向量,意思相近的向量空间里也近
③ 检索 Retrieval   问题向量化,取距离最近的 K 个块
④ 生成 Generation  块+问题拼 prompt,要求只依据资料、标出处

关键参数 / 选型速查
-------------------------------------------------------------
chunk_size       块大小,经验起点 ≈500 字,按文档类型调
overlap          块间重叠,经验 10%~20%,防关键句被切断
top_k            检索取几块,一般 3~5(粗捞可先取 20~30)
Embedding 模型   中文文档选对中文友好的;建库/查询必须同一个
向量数据库       量小 FAISS/pgvector,量大 Milvus/Qdrant
混合检索         向量(语义)+ BM25(精确关键词)两路融合
Rerank           粗捞宽 + 精排准,噪声多/答非所问时上
相似度阈值       低于它判定"无相关资料",走兜底答"不知道"

排查口诀
-------------------------------------------------------------
答得不好,先把检索回来的块打印出来人眼看一遍
答案在块里  -> 生成差,改 prompt(加严禁编造、调指令)
答案不在块里 -> 检索差,调切分 / 换 Embedding / 上混合检索

口诀:RAG 不改模型一个参数,改的是模型答题前手边摆着哪几页纸
      答不好九成是检索的锅,不是模型的锅

避坑清单

  1. 别把整本文档全塞进 prompt,上下文窗口装不下、塞进的常是不相关部分、token 还烧得飞快
  2. RAG 不微调模型一个参数,它改变的不是模型会什么,而是模型答题前手边摆着哪几页纸
  3. 文档必须切分,整篇当一块检索要么整篇命中要么不命中,捞回一整篇只有一段相关其余全噪声
  4. 但也别切太碎,按固定字数硬切会把完整句子和概念拦腰斩断,优先顺着段落标题等天然结构切
  5. 块与块之间一定要留 overlap 重叠一小段,让骑在边界上的关键句在相邻两块里都完整出现
  6. 找相关块靠语义不靠关键词,报销差旅费和出差费用核销字面全不同却是一件事,要用 Embedding
  7. 建库和线上查询必须用同一个 Embedding 模型,用了不同的两组向量不在一个空间检索全是噪声还不报错
  8. 检索回来相似度都很低就直接答不知道,最危险的 RAG 是对没有依据的问题也答得头头是道
  9. prompt 里必须写死只依据所给资料回答、没有就说没有、严禁编造,这是 RAG 压制幻觉的命门
  10. RAG 答不好先把检索回的原始块打印出来人眼看,答案在块里是生成差改 prompt,不在块里是检索差

总结

这一趟把 RAG 彻底理清的过程,纠正了我一个关于"让 AI 变聪明"的、藏得很深的错觉。在我做第一版方案的时候,我脑子里那个等式是天经地义的:想让大模型回答我的私有知识,那我就得想办法,把这些私有知识【装进它脑子里】——要么把文档全塞进 prompt(我以为这是"把书递给它"),要么去微调它(我以为这是"让它把书背下来")。我所有的努力,方向都是同一个:【往模型里灌】。灌不进去(窗口爆了),我就想办法压缩了再灌;灌进去的不对,我就想办法换一批再灌。我从来没怀疑过"灌"这个动作本身——在我看来,模型要"会"一个知识,这个知识就【必须在模型里面】。直到我被那三堵墙撞得头破血流,直到那个"考生不会把整个图书馆搬进考场"的念头击中我,我才忽然看清:我一直在解一道根本不必解的题。我费尽心思想把图书馆塞进模型脑子,可真正聪明的做法,是【让模型脑子保持空的,但给它一个随时能查的图书馆】。模型不需要"记住"我的知识,它只需要在【被问到的那一刻】,手边正好摆着【那一页纸】。复盘到最深,我意识到我混淆了两种截然不同的"拥有知识"的方式。一种是【记忆】——把知识焊进自己的脑子里,这是微调干的事,代价是昂贵、僵硬、知识一变就得重来。另一种是【检索】——脑子里不存,但建立一套"在需要时精准找到它"的能力,这是 RAG 干的事。而我过去,下意识地以为只有第一种才算"真的会"。可仔细想想,人类专家又何尝不是如此?一个顶尖的律师,不是把所有法条都背得滚瓜烂熟,而是他知道"这个问题该查哪部法、翻到哪一条";一个好医生,手边永远摆着最新的指南。他们的"专业",从来不全是"记忆量",更多是一种"精准检索"的能力。我一直想把我的 AI 训练成一个"什么都背下来的学霸",而 RAG 告诉我,我真正该做的,是把它训练成一个"知道去哪里查、并且查得又快又准"的专家。这个认知的转弯,影响了我后来看很多问题的角度:解决一个问题,我不再下意识地问"我要把多少东西准备好、装进去",而是先问"这东西,有没有可能不必装进去,而是用的时候再去取";一个系统要"知道"很多状态,我不再总想着把状态都塞进内存常驻,而会想"哪些可以是按需查询的"。"全部装进来"是一种很有诱惑力的笨办法——它直观,它让你觉得踏实,但它几乎总是最贵、最不灵活的那一个。这次最大的收获,是我给自己换了一道思考的起手式:当我要让一个系统"具备"某种知识或能力时,我不再先问"怎么把它灌进去",我先问"它到底需不需要被灌进去——还是说,我只要给它一条'用时能精准取到'的路就够了"。RAG 这三个字母教给我的,从来不是一套检索的技术拼装,而是一个朴素到我绕了一大圈才真正服气的道理:让一个东西变强,办法不一定是"把更多塞进它内部";更高明的那条路,常常是"让它内部保持轻盈,却为它接上一个又大又准的外部"。模型不必是图书馆,模型只需要,会查图书馆。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

SSR / SSG / ISR 完全指南:现代渲染策略的工程选型

2026-5-21 12:04:27

技术教程

JWT 与 Session 完全指南:Web 登录态方案的工程选型

2026-5-21 12:14:48

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索