拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DeepSeek + RAG 本地知识库实战:从 PDF 解析到检索生成全链路

DeepSeek + RAG 本地知识库实战:从 PDF 解析到检索生成全链路

简介:这份PDF面向希望将大模型落地到垂直业务的技术人员与AI应用开发者,系统讲解如何用DeepSeek大模型结合RAG技术搭建本地知识库,并以CST/ABAQUS官方文档为例,构建“虚拟技术支持工程师”智能体来验证实际业务效果。资源包共1个PDF文件,约2.9MB,内容涵盖整体架构设计、RAG检索增强生成流程、Embedding向量化与向量数据库、RAGFlow智能检索、Ollama容器化本地部署及DeepSeek-R1模型选型等关键环节,并对比了在线与本地部署的差异。读者可从中获得从知识库解析、分块嵌入到检索生成的完整方法论,理解RAG如何以低于全参数微调的成本扩展专业认知边界,同时掌握全链路本地化部署保障数据安全的思路。目前已有897人学习,适合需要构建行业知识库、关注私有化部署与AI技术支持定制化的读者参考。

1. 从一份 PDF 到能对话的知识库:DeepSeek + RAG 到底解决什么问题

手里有一份 300 页的产品手册 PDF,想让它变成能直接问答的助手,这是很多人找到「DeepSeek 模型 + RAG 技术构建本地知识库」这个方向的真实起点。RAG 是检索增强生成,核心思路是先把文档切块、向量化存进本地库,提问时先检索出最相关的几段原文,再连同问题一起喂给 DeepSeek 生成答案。它解决的是大模型不知道你私有资料、又容易一本正经胡说的问题。适合手里有内部文档、产品手册、技术规范,又不想把数据传到外部接口的开发者。整条链路可以全跑在本地:DeepSeek 负责生成,RAG 负责把资料喂到它嘴边,向量库负责记住这些资料。下面按选型、切分、检索、生成、排错的顺序,把这条链路拆成能照着复现的步骤。

2. 选型先定死:DeepSeek 走本地还是走 API,向量库用哪个

2.1 本地部署和 API 调用的取舍

DeepSeek 有两种用法,选错了后面全是返工。本地部署用 Ollama 拉起量化模型,数据不出机器,适合涉密文档,代价是显存和速度。API 调用走官方接口,省显存、响应快,适合文档不敏感、想快速验证的场景。判断标准很简单:文档能不能出内网。不能出,就本地;能出,就 API 先跑通流程再决定要不要迁本地。

本地部署的常见做法是用 Ollama 拉 DeepSeek 的蒸馏或量化版本,命令如下:

# 拉取 DeepSeek 模型(以常见量化版本为例,具体 tag 以本地 ollama list 为准) ollama pull deepseek-r1:7b # 启动服务,默认监听 11434 ollama serve # 验证模型能正常对话 ollama run deepseek-r1:7b "用一句话解释什么是向量检索"

ollama pull的 tag 决定模型大小,7b 在 8G 显存能跑,32b 起步要 24G。ollama serve是常驻服务,后面 LangChain 通过http://localhost:11434访问。验证这一步别省,模型没拉全就往下走,后面报错会误导你以为是 RAG 的问题。

API 方式则是在代码里配 key 和 base_url,DeepSeek 的接口兼容 OpenAI 格式,所以 LangChain 里可以直接用ChatOpenAI指向 DeepSeek 的地址。这种方式不用管显存,但每次调用都走网络,批量建库时要注意限流。

2.2 向量库和 Embedding 模型怎么配

向量库常见选择是 Chroma 和 FAISS。Chroma 自带持久化、支持元数据过滤,适合中小规模知识库,落地最快;FAISS 是纯索引库,速度快但要自己管存储和元数据。第一次搭,我一般直接上 Chroma,省掉一堆文件管理代码。

Embedding 模型决定检索质量,中文场景别用纯英文模型。常见做法是用bge-large-zh或m3e这类中文向量模型,本地跑用 sentence-transformers 加载。Embedding 模型和生成模型是两回事,DeepSeek 负责生成答案,Embedding 负责把文本变成向量,两个都要配。

from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma # 中文 Embedding 模型,本地加载 embedding = HuggingFaceEmbeddings( model_name="BAAI/bge-large-zh-v1.5", model_kwargs={"device": "cuda"}, # 没显卡改 "cpu" encode_kwargs={"normalize_embeddings": True} # 归一化,提升余弦相似度稳定性 ) # 持久化到本地目录,重启不丢 vectordb = Chroma( collection_name="my_kb", embedding_function=embedding, persist_directory="./chroma_db" )

normalize_embeddings=True很关键,不归一化时余弦相似度会被向量模长干扰,检索排序会飘。persist_directory指定后 Chroma 会把索引落盘,下次直接 load 不用重建。Embedding 模型第一次加载会下载权重,内网环境要提前把模型文件放好。

3. 文档切分与入库:PDF 解析和 chunk 参数怎么定

3.1 PDF 解析的坑比想象中多

PDF 不是纯文本,是排版指令的集合。直接读出来经常是乱序、断行、表格错位。常见做法是先用PyPDFLoader或pdfplumber抽文本,表格多的文档用pdfplumber更稳。扫描件必须先 OCR,否则抽出来是空字符串,这一步不做后面全白搭。

from langchain_community.document_loaders import PyPDFLoader loader = PyPDFLoader("./manual.pdf") pages = loader.load() # 每页一个 Document 对象 # 检查抽取结果,空页说明是扫描件,需要 OCR for i, p in enumerate(pages): if len(p.page_content.strip()) < 20: print(f"第 {i} 页疑似扫描件或空白,需 OCR")

PyPDFLoader返回的每个 Document 带metadata,里面有页码,这个页码后面做引用溯源要用,别丢。检查空页这步是血泪经验,扫描件不 OCR 直接入库,检索永远命中不了,你还以为是模型问题。

3.2 chunk_size 和 overlap 怎么设

切分是把长文档切成小块,每块单独向量化。块太大,检索出来的内容太杂,噪声多;块太小,语义不完整,检索不到。中文场景常见起点是chunk_size=500、chunk_overlap=50,然后根据召回效果微调。

from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=500, # 每块目标字符数 chunk_overlap=50, # 相邻块重叠,防止语义被切断 separators=["\n\n", "\n", "。", "!", "?", ";", " "], # 中文优先按句切 length_function=len ) chunks = splitter.split_documents(pages) print(f"切出 {len(chunks)} 块")

separators的顺序决定切分优先级,先按段落,再按句号,最后才按空格。中文文档如果沿用英文默认分隔符,会把句子从中间切断,检索出来的片段读不通。chunk_overlap是后悔药,防止关键句正好卡在两块边界上被切散。切完打印块数,几百页文档切出几千块是正常的,块数太少说明切太大。

入库就是把 chunks 灌进向量库:

vectordb.add_documents(chunks) vectordb.persist() # 落盘

add_documents会逐块调 Embedding 再写入,几千块会跑几分钟,别以为卡死了。persist之后./chroma_db目录里会有索引文件,下次直接Chroma(persist_directory=...)加载即可。

4. 检索与生成:把 DeepSeek 接进 RAG 链路

4.1 检索器参数怎么调

检索就是从向量库里找和问题最像的几块。核心参数是k,即返回几块。k 太小,上下文不够,模型答不全;k 太大,噪声多,还挤占上下文窗口。常见起点是k=4,配合相似度阈值过滤掉明显不相关的块。

retriever = vectordb.as_retriever( search_type="similarity_score_threshold", search_kwargs={"k": 4, "score_threshold": 0.5} ) # 单独测检索,先看召回内容再谈生成 docs = retriever.invoke("设备报错 E05 怎么处理") for d in docs: print(d.page_content[:100], d.metadata.get("page"))

similarity_score_threshold比纯similarity多一层过滤,低于阈值的块直接丢,能显著减少答非所问。score_threshold设多少取决于 Embedding 模型,bge 系列一般 0.4 到 0.6 之间试。单独测检索这步很重要,检索召回的内容不对,后面换再强的生成模型也救不回来。

4.2 拼 Prompt 和调用 DeepSeek

检索到上下文后,把它和问题拼成一个 Prompt 交给 DeepSeek。Prompt 里要明确约束:只根据给定资料回答,资料里没有就说不知道。这条约束是防幻觉的关键。

from langchain_community.chat_models import ChatOllama from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser llm = ChatOllama(model="deepseek-r1:7b", temperature=0.1) prompt = ChatPromptTemplate.from_template( "只根据下面的资料回答问题,资料中没有的信息就回答「资料中未提及」。\n" "资料:\n{context}\n\n问题:{question}" ) def format_docs(docs): return "\n\n".join(d.page_content for d in docs) chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() ) print(chain.invoke("设备报错 E05 怎么处理"))

temperature=0.1让输出更稳定,知识库问答不需要创造力。format_docs把检索到的多块拼成一段,块之间用空行隔开,方便模型区分。整条 chain 是 LangChain 的 LCEL 写法,|是管道,数据从左往右流。如果换成 API 版 DeepSeek,把ChatOllama换成指向 DeepSeek 地址的ChatOpenAI即可,其余不变。

5. 避坑与排查:RAG 上线前必须过的几道坎

5.1 检索命中率低,答非所问

现象:问 E05 报错,检索出来的却是安装步骤。原因通常是 Embedding 模型不匹配中文,或者 chunk 切得太碎导致语义丢失。解决:先换中文 Embedding 模型,再把chunk_size调大试试,同时用score_threshold把低分块过滤掉。排查时把检索到的原文打印出来,一眼就能看出是检索问题还是生成问题。

5.2 模型无视资料,自己编答案

现象:资料里明明没有这个功能,模型却答得头头是道。原因是 Prompt 约束不够强,或者 temperature 太高。解决:Prompt 里加死约束「资料中没有就回答未提及」,temperature 压到 0.1 以下,必要时在生成后加一层校验,检查答案里的关键实体是否出现在检索资料中。

5.3 扫描件 PDF 入库后检索永远为空

现象:库建好了,问什么都检索不到内容。原因是 PDF 是扫描件,文本抽取出来是空的,入库的全是空块。解决:入库前检查每页文本长度,低于阈值就判定为扫描件,走 OCR 流程重新抽取。这个坑在建库阶段不暴露,等到问答阶段才发现,返工成本很高。

5.4 本地模型显存爆了或响应极慢

现象:ollama run直接报显存不足,或者回答一句话要等一分钟。原因是模型参数量超过显存,或者没用量化版本。解决:换更小的量化 tag,7b 起步,显存实在紧张就降到 3b 级别,或者改用 API 方式。本地部署不是越大越好,知识库问答对模型参数量要求没那么高,7b 量化版通常够用。

5.5 重复入库导致检索结果重复

现象:同一份文档入库两次,检索出来的四块里三块是重复内容。原因是add_documents不去重,重复调用就重复写。解决:建库脚本里加文档指纹,入库前先查 collection 里有没有相同来源,或者干脆每次重建库时清空目录。Chroma 支持按 metadata 过滤删除,可以按文件名先删后加。

6. 进阶技巧:用重排序和引用溯源把答案质量再抬一档

基础链路跑通后,最值得加的两个东西是重排序和引用溯源。重排序是在向量检索之后再加一个精排模型,把召回的块按和问题的真实相关度重新排一遍,只留最相关的几块喂给 DeepSeek。向量检索是粗筛,重排序是精筛,两者配合能把命中率明显抬上去。常见做法是用bge-reranker系列,本地加载后对召回的 k 块打分重排。

from FlagEmbedding import FlagReranker reranker = FlagReranker("BAAI/bge-reranker-large", use_fp16=True) def rerank(query, docs, top_n=3): pairs = [[query, d.page_content] for d in docs] scores = reranker.compute_score(pairs) ranked = sorted(zip(docs, scores), key=lambda x: x[1], reverse=True) return [d for d, _ in ranked[:top_n]]

use_fp16=True省显存,compute_score返回每块和问题的相关分,按分排序取前几块。重排序模型比 Embedding 模型慢,但只对召回的几块算,开销可控。加了这一步,检索阶段可以先把 k 放大到 10,再重排取 3,召回率和精度兼顾。

引用溯源是另一个提升可信度的技巧。每个 chunk 入库时都带了页码 metadata,生成答案时让模型在句末标注来源页码,用户能点回去核对原文。实现方式是在 Prompt 里要求模型输出[页码]标记,或者生成后用检索资料反查。这个功能在内部知识库场景特别有用,用户看到答案能溯源才敢信。

prompt = ChatPromptTemplate.from_template( "只根据资料回答,并在每句话末尾用 [页码] 标注来源。\n" "资料:\n{context}\n\n问题:{question}" )

页码来自d.metadata["page"],拼 context 时把页码一起带上,模型才有依据标注。这一步做完,知识库从「能答」变成「答得可信」,是内部落地和玩具 demo 的分界线。

我自己踩得最狠的一次,是扫描件没做 OCR 直接入库,问答阶段怎么调参数都检索不到,查了两天才发现库里全是空块。从那以后我养成习惯:建库脚本第一步先打印每页文本长度,异常的直接拦下来。希望这些能帮你少走点弯路。

本文还有配套的精品资源,点击获取

返回列表