关键在于文档切分保语义、嵌入模型匹配业务、检索后过滤无关段落;PDF用PyPDFLoader+pdfminer,中文优选bge-small-zh-v1.5,FAISS需保存映射并验证检索结果。

直接用 LangChain + FAISS 搭本地 RAG 问答,关键不在“能不能跑通”,而在于文档切分是否保留语义、嵌入模型是否匹配业务场景、以及检索后如何避免把无关段落当答案——这三处出错,问答结果基本不可用。
文档加载与切分:别用默认 RecursiveCharacterTextSplitter 硬切
默认按字符长度切(比如 chunk_size=1000),容易把表格、代码块、标题和正文硬生生劈开,导致后续嵌入向量丢失上下文。尤其 PDF 或 Markdown 中的层级结构,一割就废。
实操建议:
- PDF 优先用
PyPDFLoader+pdfminer后端(比pymupdf更准识别段落),再交给MarkdownTextSplitter或自定义按"\n## "/"\n### "切分 - 纯文本或日志类内容,改用
CharacterTextSplitter并设置separator="\n\n",以空行作为自然段边界 - 务必开启
add_start_index=True,否则后续定位原文位置会失效 - 切完立刻打印前两段的
len(doc.page_content)和doc.metadata,确认没丢标题、没截断列表项
嵌入模型选型:别迷信 text-embedding-ada-002,本地请用 all-MiniLM-L6-v2 或 bge-small-zh-v1.5
OpenAI 的 API 嵌入虽好,但本地 RAG 失去实时性、有网络依赖、且中文效果一般;而随便下个 HuggingFace 上标着 “Chinese” 的模型,也可能因训练数据陈旧或未对齐 QA 场景,导致检索召回率低。
立即学习“Python免费学习笔记(深入)”;
实操建议:
- 中文知识库优先试
bge-small-zh-v1.5(langchain-community内置支持),它在小样本 QA 任务上比text2vec类模型更稳 - 若用
HuggingFaceEmbeddings,必须显式指定model_kwargs={"device": "cuda"},否则默认走 CPU,1000 条文档嵌入要等几分钟 - 嵌入前先
normalize_embeddings=True,FAISS 内积检索才等价于余弦相似度 - 嵌入后快速验证:挑一个关键词(如“发票报销流程”),用
embeddings.embed_query()得到向量,再手动算它和几个含该词文档向量的余弦值,确认 top3 确实相关
FAISS 初始化与检索:别跳过 index_to_text_mapping,也别忽略 k 值的实际影响
FAISS 本身只存向量,不存原文。LangChain 封装的 FAISS.from_documents() 会自动建映射,但如果你中途 reload index 或跨进程使用,这个映射极易丢失——结果就是 retriever.get_relevant_documents() 返回空内容或乱码。
实操建议:
- 保存时务必同时 dump
faiss_index和原始docs列表(用pickle或joblib),不要只存 index 文件 - 加载时用
FAISS.load_local(..., allow_dangerous_deserialization=True),并立即用index.docstore._dict检查是否非空 -
k=4不代表返回 4 段最相关文字——FAISS 默认返回最近邻,但 LangChain 的similarity_search_with_score()可能因重复向量或归一化问题让第 3 名得分反超第 2 名;建议始终用search_type="similarity_score_threshold"并设score_threshold=0.3 - 检索后立刻检查每条结果的
metadata["source"]和page_content[:100],防止 PDF 加载时把页眉页脚混进正文
问答链组装:别把 stuff 当万能模板,警惕 prompt 中的幻觉放大器
LangChain 的 create_stuff_documents_chain 会把所有检索结果拼进 prompt,一旦 k 设大或文档质量差,LLM 容易从噪声里“编造”答案。更麻烦的是,很多中文 LLM 对 context 区域的指令敏感度远低于英文模型。
实操建议:
- 改用
create_stuff_documents_chain+ 自定义 prompt,开头必须加硬约束:"请严格依据以下<context>中的内容回答,不得添加任何外部知识。若<context>中未提及,请回答“未找到相关信息”。"</context></context> - 在
RunnablePassthrough.assign()中注入retrieved_docs前,先做一次简单过滤:[d for d in docs if len(d.page_content.strip()) > 30],剔除空段、页码、分隔线 - 本地小模型(如
Qwen2-0.5B)推理时,把max_tokens控制在 256 以内,否则容易把答案拖成复述整个 chunk - 首次调试务必关闭流式输出,用
invoke({"input": "xxx"})看完整 prompt 输入,确认 context 没被截断、没混入 metadata 字段
最常被跳过的环节是:没验证检索结果是否真包含答案关键词。拿几个已知答案的问题(比如“服务器重启命令是什么?”),先人工查原文位置,再跑一遍检索,对比返回的 page_content 是否真有那行命令——这一步省掉,后面调 prompt、换模型都是在修假问题。


















