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_urlDeepSeek 的接口兼容 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_nameBAAI/bge-large-zh-v1.5, model_kwargs{device: cuda}, # 没显卡改 cpu encode_kwargs{normalize_embeddings: True} # 归一化提升余弦相似度稳定性 ) # 持久化到本地目录重启不丢 vectordb Chroma( collection_namemy_kb, embedding_functionembedding, persist_directory./chroma_db )normalize_embeddingsTrue很关键不归一化时余弦相似度会被向量模长干扰检索排序会飘。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_size500、chunk_overlap50然后根据召回效果微调。from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size500, # 每块目标字符数 chunk_overlap50, # 相邻块重叠防止语义被切断 separators[\n\n, \n, 。, , , , ], # 中文优先按句切 length_functionlen ) 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 太大噪声多还挤占上下文窗口。常见起点是k4配合相似度阈值过滤掉明显不相关的块。retriever vectordb.as_retriever( search_typesimilarity_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(modeldeepseek-r1:7b, temperature0.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 怎么处理))temperature0.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直接报显存不足或者回答一句话要等一分钟。原因是模型参数量超过显存或者没用量化版本。解决换更小的量化 tag7b 起步显存实在紧张就降到 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_fp16True) def rerank(query, docs, top_n3): pairs [[query, d.page_content] for d in docs] scores reranker.compute_score(pairs) ranked sorted(zip(docs, scores), keylambda x: x[1], reverseTrue) return [d for d, _ in ranked[:top_n]]use_fp16True省显存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 直接入库问答阶段怎么调参数都检索不到查了两天才发现库里全是空块。从那以后我养成习惯建库脚本第一步先打印每页文本长度异常的直接拦下来。希望这些能帮你少走点弯路。本文还有配套的精品资源点击获取