1. 先搞清楚这套东西到底在解决什么问题
1.1 从“搜不到”到“问出来”的转变
很多人第一次接触 AI 知识库,脑子里想的其实是“我有一堆 PDF,能不能直接问它问题”。这个直觉是对的,但中间隔着一整套工程链路。传统做法是把 PDF 丢进文件夹,靠文件名和系统自带的全文搜索找内容,问题是 PDF 里的文字一旦被扫描成图片、或者排版稍微复杂一点,搜索就废了。更麻烦的是,你搜“第三季度营收”只能匹配到包含这几个字的页面,没法回答“第三季度营收同比变化的原因是什么”这种需要跨段落理解的问题。
RAG 要干的事情,就是把“搜索”升级成“问答”。它的全称是 Retrieval-Augmented Generation,检索增强生成。拆开看就三件事:先把你的文档切成小块存起来,用户提问时从库里捞出最相关的几块,再把这几块内容和问题一起交给大模型,让它基于这些材料组织答案。整个过程里,大模型不是凭记忆瞎编,而是拿着你给的“小抄”回答,这就是它比直接问 ChatGPT 靠谱的地方。
我见过太多人一上来就装 LangChain、配向量库、调 embedding 模型,结果卡在 PDF 解析上——扫描件全是乱码,表格被拆得七零八落,后面再怎么调都是白费。所以这篇东西我打算按真实的学习顺序来写:先弄明白每个环节在干嘛,再动手跑通最小闭环,最后才是优化和扩展。零基础也能跟,但前提是你得愿意动手,光看是看不明白的。
1.2 适合谁看,不适合谁看
这套路线适合三类人:一是手里攒了大量 PDF 资料(论文、手册、报告)想快速检索的;二是想给自己产品加个“文档问答”功能的开发者;三是纯粹想搞懂 RAG 是怎么回事、不想被各种框架名词绕晕的学习者。
不适合两类人:指望复制粘贴几行代码就得到一个完美知识库的,以及不愿意花时间理解数据处理的。RAG 的瓶颈从来不在模型,而在你的数据本身。垃圾进,垃圾出,这句话在 RAG 里体现得淋漓尽致。
2. 核心概念拆解:别被名词吓住
2.1 Embedding 到底是什么,为什么它这么关键
Embedding 这个词翻译成“嵌入”其实挺抽象的。你可以把它理解成给每段文字发一个“坐标”。假设我们把所有中文句子都映射到一个三维空间里,“今天天气不错”和“今天阳光很好”这两个句子的坐标会非常接近,而“今天天气不错”和“红烧肉的做法”坐标就离得很远。这个坐标就是一个向量,比如[0.23, -0.11, 0.87],维度可能是 768 维、1024 维甚至更高。
为什么需要这个?因为计算机没法直接比较两段文字“意思像不像”。它只能算数字。有了向量之后,比较两段文字的相关性就变成了算两个向量的余弦相似度,数学上非常成熟。用户问“怎么申请报销”,系统把这个问题也转成向量,然后去库里找坐标最接近的那些文本块,这就是语义检索的底层逻辑。
这里有个坑要提前说:embedding 模型分中文和英文的,也分通用和特定领域的。你拿一个主要用英文语料训练的模型去处理中文法律文书,效果会差得离谱。热词里提到的“embedding模型排行”之所以火,就是因为选错模型后面全白搭。我的建议是,中文场景优先考虑 BGE 系列或者 M3E 系列,它们在中文语义相似度任务上表现稳定,而且有不同大小的版本可以权衡速度和精度。
2.2 Chunk 切分:最不起眼但最容易翻车的一步
Chunk 就是“文本块”。你把一篇 50 页的 PDF 直接丢给大模型,它要么超出上下文限制,要么因为内容太多而抓不住重点。所以必须切。但怎么切,切多大,这里面全是细节。
最粗暴的做法是按固定字数切,比如每 500 个字一块。问题是它可能把一句话从中间切断,“本公司第三季度营收为”在一块里,“5.2 亿元,同比增长 18%”在另一块里。检索的时候只捞到前半句,答案就残了。稍微好一点的做法是按段落切,遇到换行就断开。但 PDF 解析出来的文本经常没有正确的换行,段落边界是乱的。
我实测下来比较稳的策略是“递归切分”:先按段落切,如果某段还是太长,再按句子切,句子还长就按字数硬切,同时设置一个重叠区域(overlap)。比如每块 500 字,相邻两块重叠 50 到 100 字。这个重叠的作用是防止关键信息刚好落在切割线上被一分为二。重叠太多会浪费存储和检索时间,太少又起不到保护作用,50 到 100 字是个比较舒服的区间。
还有一个容易被忽略的点:表格和图片。热词里有人问“rag知识库能存储图片嘛”,答案是能存,但检索逻辑不一样。图片需要先用 OCR 或者多模态模型转成文字描述,再走同样的 embedding 流程。表格则最好在切分时保留表头,否则单独一块数据没有列名,大模型根本看不懂。
2.3 RAG 和 LLM Wiki 的关系
热词里反复出现“llm wiki”和“rag和llm wiki”,这两个概念经常被混着用。简单说,LLM Wiki 更像是一种产品形态——把知识库包装成 wiki 的样子,用户既能浏览条目也能提问。RAG 是背后的技术手段。你可以用 RAG 做一个 LLM Wiki,也可以用 RAG 做一个客服机器人,技术内核是一样的,只是前端交互不同。
至于“ontology rag”和“graphrag”,那是更进阶的玩法。普通 RAG 是把文本切块后平铺存储,检索时只看单块的相关性。GraphRAG 会先抽取出实体和关系,构建一个知识图谱,检索时能沿着关系链找到关联信息。比如问“张三负责的项目有哪些”,普通 RAG 可能只找到提到张三的那一块,而 GraphRAG 能顺着“张三-负责-项目A”这条边找到项目A的详情。代价是构建成本高很多,零基础阶段先不用碰。
3. 从零搭建的最小可行路线
3.1 环境准备:别一上来就搞复杂的
我建议第一版就用 Python 加几个核心库,不要碰 LangChain 这种重框架。原因很简单:框架封装了太多东西,出问题你根本不知道是哪一步挂了。先用最原始的方式跑通,理解每个环节的输入输出,后面再用框架提效。
需要装的东西不多:
pip install pypdf sentence-transformers chromadb openaipypdf负责从 PDF 里抽文字,sentence-transformers提供 embedding 模型,chromadb是一个轻量级向量数据库,openai用来调大模型生成答案。如果你不想用在线 API,可以用ollama在本地跑模型,热词里“ollama + 简易本地 rag 知识库”说的就是这个路子。
注意:chromadb 在 Windows 上偶尔会有编译问题,如果装不上,可以换成
faiss-cpu,功能类似,只是 API 不一样。
3.2 第一步:把 PDF 变成干净的文字
这一步的目标是把 PDF 里的文字提取出来,并且尽量保留段落结构。代码不复杂:
from pypdf import PdfReader def extract_text(pdf_path): reader = PdfReader(pdf_path) full_text = [] for page in reader.pages: text = page.extract_text() if text: full_text.append(text) return "\n".join(full_text)跑完之后你大概率会发现两个问题:一是扫描件提取出来是空的,二是表格变成了乱七八糟的字符。扫描件需要额外做 OCR,可以用pytesseract配合pdf2image,但那是另一个话题了。表格的话,如果 PDF 本身是电子版而非扫描版,可以试试pdfplumber,它对表格的支持比 pypdf 好一些。
我自己的经验是,先拿一份文字版 PDF 跑通全流程,确认没问题了再去处理扫描件和复杂表格。不要一上来就挑战最难的文档,那样容易卡住然后放弃。
3.3 第二步:切块并生成向量
拿到全文之后,按前面说的递归策略切块。这里给一个简化版实现:
def split_text(text, chunk_size=500, overlap=80): chunks = [] start = 0 while start < len(text): end = start + chunk_size chunk = text[start:end] chunks.append(chunk) start = end - overlap return chunks实际用的时候,最好在切之前先按\n\n分段,对每段判断长度,太长再切。这样能最大程度保留语义完整性。
切完之后,用 embedding 模型把每个 chunk 转成向量:
from sentence_transformers import SentenceTransformer model = SentenceTransformer('BAAI/bge-small-zh-v1.5') chunks = split_text(full_text) embeddings = model.encode(chunks)bge-small-zh-v1.5是一个中文小模型,速度快,效果对于入门够用。如果追求更高精度,可以换bge-large-zh-v1.5,但显存占用和计算时间会明显上升。热词里“embedding模型排行”经常提到这两个,选哪个取决于你的硬件和延迟要求。
3.4 第三步:存进向量库并实现检索
把向量和对应的文本块一起存进 chromadb:
import chromadb client = chromadb.Client() collection = client.create_collection("my_knowledge") collection.add( documents=chunks, embeddings=embeddings.tolist(), ids=[f"id_{i}" for i in range(len(chunks))] )检索的时候,把用户问题也转成向量,然后查最相似的几块:
def search(query, top_k=3): query_embedding = model.encode([query]).tolist() results = collection.query( query_embeddings=query_embedding, n_results=top_k ) return results['documents'][0]top_k设多少?一般 3 到 5 比较合适。太少可能漏掉关键信息,太多会引入无关内容干扰大模型。这个参数可以后面根据实际效果调。
3.5 第四步:拼装 prompt 并调用大模型
检索到相关文本块之后,把它们和用户问题拼成一个 prompt:
def ask(query): contexts = search(query) context_text = "\n\n".join(contexts) prompt = f"""基于以下资料回答问题,如果资料中没有相关信息,就说不知道。 资料: {context_text} 问题:{query} 答案:""" # 调用大模型 API response = call_llm(prompt) return response这个 prompt 模板里有一句很关键的话:“如果资料中没有相关信息,就说不知道。” 这是为了防止大模型在检索结果不相关时强行编造答案。我试过不加这句话,模型会非常自信地胡说八道,加了之后至少它会承认自己不知道。
到这里,一个最小可用的 RAG 系统就跑通了。从上传 PDF 到能回答问题,核心代码不超过 100 行。但能跑通和好用之间,还差着大量的调优工作。
4. 效果调优:从“能用”到“好用”
4.1 检索命中率上不去,先查这三个地方
热词里“rag hit rate”被反复提到,说明这是大家共同的痛点。检索命中率低,通常不是模型的问题,而是数据的问题。按我的排查顺序,先看切块大小是否合理,再看 embedding 模型是否匹配语料,最后看检索数量是否够用。
切块太大,一块里混了多个主题,向量表示会变得模糊,检索时反而不容易命中。切块太小,信息不完整,捞到了也没法回答问题。我一般会拿几个典型问题做测试,手动看检索出来的块是否真的相关。如果经常捞到不相关的,就把块调小一点;如果经常捞到相关但信息不全的,就把块调大或者增加重叠。
embedding 模型这块,中文场景下 BGE 系列确实比 OpenAI 的 text-embedding-ada-002 在中文语义相似度上表现更好。但如果你的是中英混合文档,可能需要测试多个模型再决定。热词里“embedding模型排行”之所以受关注,就是因为这个选择对最终效果影响巨大,而且没有万能答案。
4.2 大模型答非所问,问题可能出在 prompt 上
检索没问题但答案不对,八成是 prompt 没写好。我踩过的坑包括:资料块之间没有明确分隔,模型分不清哪段是哪段;没有告诉模型“只基于资料回答”,它就自由发挥了;问题太模糊,模型理解偏了。
改进方法是在 prompt 里加明确的指令和格式。比如:
你是一个严谨的助手。请仅根据下面提供的资料回答问题。 如果资料不足以回答,请直接说“资料中没有相关信息”。 不要编造资料中不存在的内容。 资料片段: [片段1] --- [片段2] --- 问题:xxx用---分隔不同片段,比空行更清晰。加上“不要编造”这种明确禁令,比单纯说“基于资料”有效得多。
4.3 什么时候该上框架,什么时候不该
LangChain、LlamaIndex 这些框架确实能省很多代码,但它们也引入了抽象层。我的建议是:当你已经用原生代码跑通过一遍,清楚每个环节在干嘛之后,再用框架提效。如果一上来就用框架,遇到问题你连从哪查都不知道。
热词里“langchain4j easy rag”和“spring ai rag”是 Java 生态的方案,适合 Java 开发者。Python 生态的话,LlamaIndex 在 RAG 场景下比 LangChain 更专注一些,API 设计也更直观。但核心逻辑是一样的:加载、切分、嵌入、存储、检索、生成。
5. 常见问题速查与避坑指南
5.1 问题排查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| PDF 提取出来是空白 | 扫描件没有文字层 | 需要 OCR 处理 |
| 检索结果完全不相关 | embedding 模型不匹配语料 | 换中文专用模型测试 |
| 答案包含资料中没有的内容 | prompt 没有限制编造 | 加“仅基于资料回答”指令 |
| 回答总是“不知道” | 检索没捞到相关块 | 检查切块大小和 top_k |
| 处理速度特别慢 | 模型太大或块太多 | 换小模型或减少块数量 |
| 表格内容检索不到 | 表格被切散或没保留表头 | 单独处理表格或保留表头 |
5.2 几个我踩过的坑
第一个坑是忽略 PDF 里的页眉页脚。很多 PDF 每页都有相同的页眉,比如书名或章节名。这些内容被重复提取后,会在向量库里产生大量几乎相同的块,检索时经常捞到这些无意义的重复内容。解决办法是在提取后做一次去重,或者用规则过滤掉每页开头结尾的固定文本。
第二个坑是 embedding 模型第一次使用时需要下载,国内网络环境可能很慢甚至失败。可以提前把模型下载到本地,用SentenceTransformer('本地路径')的方式加载。HuggingFace 有镜像站可以用,具体方法搜一下就有。
第三个坑是 chromadb 默认用的是内存存储,程序一关数据就没了。要持久化的话,创建 client 时指定persist_directory参数。这个在官方文档里写得不太显眼,我第一次用的时候重启程序发现库空了,还以为是 bug。
5.3 关于“rag瓶颈”的一些观察
热词里“rag瓶颈”也是个高频词。以我的经验,RAG 系统的瓶颈通常不在生成端,而在检索端。大模型现在都很强,给它正确的资料它就能给出不错的答案。难的是从海量文档里精准捞出那几块真正相关的。
提升检索质量的手段包括:用更好的 embedding 模型、优化切块策略、加 rerank 模型做二次排序、用混合检索(关键词加语义)。其中 rerank 的性价比很高,就是在向量检索捞出 top 20 之后,用一个专门的 rerank 模型对这 20 个结果重新打分,选出最相关的 3 个给大模型。这一步能明显提升最终答案的质量,代价是增加一点延迟。
6. 后续可以往哪些方向扩展
跑通基础版之后,你可以根据实际需求往几个方向走。一是支持更多格式,除了 PDF 还有 Word、Markdown、网页等,每种格式的解析方式不同。二是加多轮对话能力,让用户可以追问,这需要把历史对话也纳入上下文管理。三是做权限控制,不同用户能访问的知识库范围不同。四是接入即时通讯工具或网页前端,让它真正能被用起来。
热词里提到的“agentic rag”和“rag智能体”是更前沿的方向,让 AI 自己决定什么时候检索、检索什么、要不要多轮检索。这些目前还在快速演进中,零基础阶段先把基础 RAG 跑稳,后面再逐步深入。
我个人在实际操作中的体会是,RAG 这件事,百分之七十的功夫花在数据准备上,百分之二十花在检索调优上,真正跟大模型相关的部分可能只占百分之十。很多人本末倒置,一直在换更大的模型,却不肯花时间把 PDF 解析干净、把切块策略调合理。把基础打牢,后面的事会顺很多。