在业务中接入大模型时,很多同学都会走到同一步:希望大模型能回答自己企业内部文档里的问题。但直接提问会发现,模型既不了解你的业务,也容易一本正经地编造答案。要解决这个问题,RAG 是目前最实用、成本最低的方案。本文将基于一套完整可运行的代码,带你从零搭建一个RAG知识库问答系统,并把 Deepseek 作为最终生成模型接入其中,从原理到落地,一次性讲透。
先说一下本文适合谁:对大模型感兴趣但还没系统接触过 RAG 的开发者、正在做企业知识库需求的后端工程师、想在自己电脑上跑通一套完整问答系统的学习者。读完本文,你会理解 RAG 的完整处理链路,掌握文档加载、文本切分、向量化、向量检索、Prompt 组装、大模型生成这几个关键环节,并拿到一套可以扩展成真实项目的基础源码。
1. RAG 到底是什么,为什么大模型需要它
1.1 从大模型的“知识缺陷”说起
大模型虽然能写文章、写代码、做翻译,但有一个天然缺陷:它的知识来自训练数据,存在明显的“知识截止时间”。如果你的问题涉及企业最新制度、某个产品的使用手册、某个系统的内部操作说明,大模型很可能会回答一个看似合理、实则错误的内容,这就是常说的“幻觉”问题。
举个例子,你问“公司请休假制度是什么”,模型可能会编造一套看似标准的制度,而它根本没有见过你公司的文档。此时你不可能为了这个问题重新训练一个大模型,成本太高,也不现实。
RAG 的解决思路很直接:与其让模型“背”下所有知识,不如在它回答问题之前,先帮它找到相关资料。这就好比考试时允许翻书,模型不用死记硬背,只需要根据“查到的那几页”来组织答案。
1.2 RAG 的完整定义与核心流程
RAG,全称 Retrieval-Augmented Generation,检索增强生成。它是一种将信息检索与文本生成相结合的架构:先从外部知识库中检索出与用户问题相关的文档片段,再将片段与问题一起交给大模型,让模型基于这些片段生成回答。
标准 RAG 的处理流程可以拆成八个环节:
- 文档加载:读取内部文档,支持 txt、PDF、Word、Markdown 等格式。
- 文本切分:长文档不能整体向量化,需要按一定策略切分成 chunk(文本块)。
- 向量化:使用 embedding 模型将每个 chunk 转换为向量,向量可以理解为一段文本的语义坐标。
- 向量存储:将向量和原文一起存入向量数据库,例如 Chroma、FAISS、Milvus。
- 问题向量化:用户提问时,将问题也转换为向量。
- 相似度检索:计算问题向量与知识库向量的相似度,返回最相关的 Top K 个片段。
- Prompt 组装:将系统提示词、检索到的片段、用户问题拼接成一个完整的 Prompt。
- 生成回答:将 Prompt 发送给大模型,模型基于片段内容生成最终答案。
1.3 RAG 与模型微调的区别
很多初学者会把 RAG 和微调搞混。简单来说,微调是修改模型的“记忆”,RAG 是给模型“查资料”。两者各有适用场景:
| 对比维度 | RAG | 微调 |
|---|---|---|
| 成本 | 较低,无需训练显卡 | 较高,需要训练资源和数据准备 |
| 知识更新 | 替换文档即可,实时生效 | 需要重新训练,周期长 |
| 可解释性 | 回答可溯源到具体文档片段 | 相对难以解释模型参考了什么 |
| 幻觉控制 | 通过限定参考片段,效果明显 | 降低有限,模型仍可能生成非训练知识 |
| 适用场景 | 企业知识库、文档问答、实时信息 | 改变模型语气风格、专业术语理解、特定格式输出 |
真实项目中,RAG 和微调也经常配合使用。如果模型本身对你所在行业的术语理解较弱,可以先用部分数据微调模型,再叠加 RAG 让模型获取最新知识。但对大多数知识库问答需求来说,RAG 是首选中低成本方案。
2. 系统方案设计与技术选型
2.1 方案目标
我们要搭建的这套系统需要满足以下需求:
- 支持把本地文档导入知识库。
- 文档必须支持持续追加和更新,不需要重训练模型。
- 用户提问后,系统能检索到相关文档片段,并在回答中给出依据。
- 最终生成模型使用 Deepseek API,保证中文效果且调用简单。
- 整个系统可以本地运行,不依赖大型 GPU 环境。
2.2 总体架构
根据需求,系统的整体架构可以这样设计:
用户提问(Web界面 / 命令行) ↓ 中文问题向量化(本地Embedding模型) ↓ Chroma向量库相似度检索 ↓ 返回 Top-K 相关文档片段 ↓ 组装 Prompt:系统提示词 + 参考片段 + 用户问题 ↓ 调用 Deepseek Chat API ↓ 生成最终回答并返回给用户在这个架构中,文档离线处理链路把原始知识库转换为向量索引,在线问答链路负责检索与生成。两条链路合起来就是一套完整的 RAG 系统。
2.3 技术栈说明
本方案涉及的主要技术组件如下:
| 模块 | 技术选型 | 选择原因 |
|---|---|---|
| 文档加载 | Python 内置文件读取 | 轻量,零依赖,适合 txt 文档入门 |
| 文本切分 | LangChain Text Splitters | 成熟的切分策略,支持重叠窗口 |
| 向量化模型 | sentence-transformers + bge-small-zh-v1.5 | 中文效果好,本地运行,免费 |
| 向量数据库 | Chroma | 轻量级,无需独立服务,适合快速开发 |
| 生成模型 | Deepseek Chat API | 中文能力强,兼容 OpenAI 协议,接入成本低 |
| Web 展示层 | Streamlit | 用 Python 快速搭建交互界面 |
为什么使用 Deepseek 作为生成模型,这是很多读者关心的问题。Deepseek 的 API 调用方式与 OpenAI 协议兼容,只需要安装 openai SDK 并修改 base_url 和 api_key 就能完成接入,非常省事。同时,Deepseek 在中文理解与生成上的表现足够优秀,适合中文知识库问答场景。相比本地部署一个十几B或几十B的大模型,API 方式对电脑性能要求极低,个人开发者和中小规模应用都能低成本使用。
为什么 embedding 模型选用本地部署,原因也很实际。如果每次写文档和提问都调用外部 embedding 接口,会产生持续费用,同时还会把文档内容送到第三方服务。使用本地 bge-small-zh-v1.5 模型,只需要首次运行下载模型文件,之后便可以在完全离线的状态下完成向量化,成本低、隐私性也更好。
3. 环境准备与项目结构
3.1 安装 Python 与依赖
本文示例以 Python 3.9 及以上版本为例。老规矩,先创建一个独立的虚拟环境,避免依赖污染系统环境:
python -m venv rag-demo-env source rag-demo-env/bin/activate # Windows 下为 rag-demo-env\Scripts\activate然后安装依赖包:
pip install openai sentence-transformers chromadb langchain-text-splitters streamlit python-dotenv如果你的环境安装速度较慢,可以更换为国内 pip 镜像源:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple openai sentence-transformers chromadb langchain-text-splitters streamlit python-dotenv这里不锁定具体版本号,因为这些库的迭代速度较快,建议使用当前最新稳定版即可。如果后续遇到某个库升级导致接口变动,可以回到对应官方文档确认最新用法。
3.2 项目目录结构
完整源码建议按照下面的目录结构保存:
rag-demo/ ├── .env # API Key 等敏感配置(不提交到仓库) ├── requirements.txt # 依赖清单 ├── config.py # 全局配置 ├── ingest.py # 文档加载与向量库构建 ├── retriever.py # 检索模块 ├── rag.py # RAG 问答主流程 ├── app.py # Streamlit Web 界面 └── data/ # 存放私有文档 ├── 公司介绍.txt └── product_faq.txtrequirements.txt 文件内容如下:
openai>=1.0.0 sentence-transformers>=2.2.0 chromadb>=0.4.0 langchain-text-splitters>=0.0.1 streamlit>=1.30.0 python-dotenv>=1.0.04. 核心代码实现:从文档到知识库
4.1 全局配置 config.py
代码的第一步是写一个配置模块,把路径、模型名称、检索参数等集中管理起来。这样后续修改参数时不需要翻遍每个文件。
# 文件路径:rag-demo/config.py import os from dotenv import load_dotenv # 加载 .env 文件中的环境变量 load_dotenv() # 知识库文档目录 KNOWLEDGE_BASE_DIR = "data" # Chroma 向量库持久化目录 CHROMA_DIR = "./chroma_db" # 向量库集合名称 COLLECTION_NAME = "rag_demo" # 本地 Embedding 模型名称 EMBEDDING_MODEL_NAME = "BAAI/bge-small-zh-v1.5" # 检索返回的片段数量 TOP_K = 3 # 文档切分参数 CHUNK_SIZE = 500 CHUNK_OVERLAP = 50 # Deepseek API 配置 DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com") DEEPSEEK_MODEL = os.getenv("DEEPSEEK_MODEL", "deepseek-chat")有一处要重点说明:DEEPSEEK_API_KEY 不建议硬编码在代码文件中,应该写在项目根目录的 .env 文件里,并确保 .env 被加入 .gitignore。这样可以避免 API Key 意外提交到公开仓库,造成安全风险。.env 文件的格式如下:
DEEPSEEK_API_KEY=你的Deepseek_API_Key关于 Deepseek API 的使用,请前往 Deepseek 官方开放平台注册并创建 API Key。使用 API 会产生少量费用,具体计费标准以官方页面为准,建议先小额充值并查看接口文档中的价格说明。
4.2 文档加载与向量化入库 ingest.py
ingest.py 的作用是读取 data 目录下的文档,进行切分、向量化,然后写入向量数据库。这是整个 RAG 系统的离线构建阶段。
# 文件路径:rag-demo/ingest.py import os from typing import List from langchain_text_splitters import CharacterTextSplitter from sentence_transformers import SentenceTransformer import chromadb import config def load_documents(data_dir: str) -> List[str]: """读取 data 目录下的所有 txt 文件,返回文档内容列表。""" docs = [] for filename in os.listdir(data_dir): if filename.endswith(".txt"): filepath = os.path.join(data_dir, filename) with open(filepath, "r", encoding="utf-8") as f: content = f.read() if content.strip(): docs.append(content) print(f"已加载文档: {filename}") return docs def split_text(documents: List[str]) -> List[str]: """将长文档切分成指定大小的文本块 chunk。""" splitter = CharacterTextSplitter( separator="\n", chunk_size=config.CHUNK_SIZE, chunk_overlap=config.CHUNK_OVERLAP, length_function=len, ) chunks = [] for doc in documents: chunks.extend(splitter.split_text(doc)) return chunks def build_vector_store(chunks: List[str]): """将文本块向量化并写入 Chroma 向量库。""" # 加载本地 embedding 模型,首次运行会自动下载 print("正在加载 embedding 模型...") embedder = SentenceTransformer(config.EMBEDDING_MODEL_NAME) # 生成向量 print("正在生成向量...") embeddings = embedder.encode(chunks, show_progress_bar=True).tolist() # 创建 Chroma 客户端,持久化到本地目录 client = chromadb.PersistentClient(path=config.CHROMA_DIR) # 如果集合已存在,先删除,避免重复插入 existing_collections = client.list_collections() for col in existing_collections: if col.name == config.COLLECTION_NAME: client.delete_collection(config.COLLECTION_NAME) print("已删除旧的向量库集合") collection = client.create_collection( name=config.COLLECTION_NAME, metadata={"hnsw:space": "cosine"}, # 使用余弦相似度 ) # 写入向量库,id 使用序号,方便管理 ids = [str(i) for i in range(len(chunks))] # 这里直接把原始文本和向量一起存入 collection.add( ids=ids, embeddings=embeddings, documents=chunks, ) print(f"向量库构建完成,共写入 {len(chunks)} 个文本块") if __name__ == "__main__": all_docs = load_documents(config.KNOWLEDGE_BASE_DIR) if not all_docs: raise ValueError(f"data 目录下没有找到可用文档,请检查目录: {config.KNOWLEDGE_BASE_DIR}") all_chunks = split_text(all_docs) print(f"文档切分完成,共得到 {len(all_chunks)} 个文本块") build_vector_store(all_chunks)这段代码的重点有几个。
文本切分参数值得专门调试。CHUNK_SIZE 表示每个文本块的最大字符数,CHUNK_OVERLAP 表示相邻块之间重叠的字符数。重叠的部分可以让切块边界处的语义不丢失,尤其是当问题和答案分别跨越两个块边界时,重叠能明显提升召回效果。实际项目中,500 到 800 字是比较常见的块大小,过小会导致检索上下文不足,过大则会让单块包含太多无关信息,降低检索精度。
Chroma 的持久化方式在代码中使用了 PersistentClient,向量库数据会保存到 ./chroma_db 目录。这样下次启动系统时不需要重新构建向量库,直接读取即可。如果知识库文档发生了更新,重新运行一次 ingest.py 即可,不必重启任何服务。
4.3 检索模块 retriever.py
retriever.py 负责接收用户问题,将其向量化后在 Chroma 中检索最相关的 Top K 个片段。
# 文件路径:rag-demo/retriever.py import chromadb from sentence_transformers import SentenceTransformer import config def get_embedder(): """获取共享的 embedding 模型实例,避免多次重复加载。""" return SentenceTransformer(config.EMBEDDING_MODEL_NAME) def search(query: str, top_k: int = None): """根据用户问题检索知识库,返回相关片段列表。""" if top_k is None: top_k = config.TOP_K # 加载向量库 client = chromadb.PersistentClient(path=config.CHROMA_DIR) collection = client.get_collection(config.COLLECTION_NAME) # 对用户问题进行向量化 embedder = get_embedder() query_embedding = embedder.encode([query]).tolist() # 执行相似度检索 results = collection.query( query_embeddings=query_embedding, n_results=top_k, ) documents = results.get("documents", [[]])[0] distances = results.get("distances", [[]])[0] return documents, distances这里需要留意一个问题:每次调用 get_embedder 都会重新加载一次模型。在 Streamlit 这种 Web 服务中,频繁加载模型会拖慢速度。实际开发时可以把 embedder 设计成全局单例,或者放到内存缓存中。我在这里保留简单写法,是为了让流程更清晰,后面工程优化部分再讨论如何改进。
Chroma 的 query 接口返回结果中,documents 是对应的原始文本内容,distances 是余弦距离。距离越小表示相似度越高,后续可以把距离信息一并展示,帮助用户判断答案的可信度。
4.4 RAG 问答主流程 rag.py
rag.py 是系统的核心,负责把检索结果和用户问题组装成 Prompt,然后调用 Deepseek 生成回答。
# 文件路径:rag-demo/rag.py from openai import OpenAI import config from retriever import search # 初始化 Deepseek 客户端,兼容 OpenAI SDK client = OpenAI( api_key=config.DEEPSEEK_API_KEY, base_url=config.DEEPSEEK_BASE_URL, ) def build_prompt(query: str, context_docs: list) -> str: """组装发送给大模型的 Prompt。""" system_prompt = ( "你是一个专业的知识库问答助手。" "请根据提供的参考资料回答用户问题。" "必须优先参考参考资料中的内容,不要臆造答案。" "如果参考资料无法回答该问题,请直接说明知识库中暂未找到相关信息。" ) context = "\n\n".join(context_docs) user_prompt = f"""请根据以下参考资料回答用户问题。 【参考资料】 {context} 【用户问题】 {query} 请用中文回答,如果引用了参考资料,可以说明信息来源于哪个部分。""" return system_prompt, user_prompt def ask(query: str) -> dict: """Retrieve + Generate 完整流程。""" # 检索相关片段 docs, distances = search(query) # 组装 Prompt system_prompt, user_prompt = build_prompt(query, docs) # 调用 Deepseek 生成回答 response = client.chat.completions.create( model=config.DEEPSEEK_MODEL, temperature=0.3, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], ) answer = response.choices[0].message.content return { "answer": answer, "source_documents": docs, "distances": distances, } if __name__ == "__main__": question = input("请输入你的问题:") result = ask(question) print("\n==== 回答 ====") print(result["answer"]) print("\n==== 引用片段 ====") for i, doc in enumerate(result["source_documents"]): print(f"\n[{i + 1}] {doc[:200]}")Deepseek 的接入方式与 OpenAI 几乎一致,核心代码只有这么几行。这里给出的 model 名称是 deepseek-chat,对应 Deepseek 的对话模型。temperature 设置为 0.3,会让回答更加稳定、更贴近参考资料内容。如果你想获得更有创造性的回答,可以调高到 0.7 左右,但在知识库问答场景下,推荐保持低 temperature,以减少胡说八道的概率。
Prompt 的设计值得仔细思考。系统提示词明确要求“必须优先参考参考资料中的内容,不要臆造答案”,这一步是抑制幻觉的关键。如果直接把问题和检索片段丢给模型,而不加任何约束,模型仍然可能倾向使用自己的知识回答。所以在 RAG 系统中,Prompt 不是随便拼一段文字,而是需要反复打磨的工程质量项。
4.5 Streamlit Web 界面 app.py
最后写一个简单的 Web 界面,让系统可以被同事或用户直接使用。
# 文件路径:rag-demo/app.py import streamlit as st import config from rag import ask st.set_page_config(page_title="RAG 知识库问答系统", page_icon="📚", layout="wide") st.title("📚 RAG 知识库问答系统") st.caption("基于 Deepseek + Chroma 构建的私有知识库问答系统") query = st.text_area("请输入你的问题", height=100) if st.button("获取答案", type="primary"): if not query.strip(): st.warning("请输入问题内容") elif not config.DEEPSEEK_API_KEY: st.error("未检测到 DEEPSEEK_API_KEY,请检查 .env 文件配置") else: with st.spinner("正在检索并生成回答..."): result = ask(query.strip()) st.subheader("回答") st.write(result["answer"]) with st.expander("查看参考文档片段"): for i, (doc, dist) in enumerate(zip(result["source_documents"], result["distances"])): st.markdown(f"**片段 {i + 1}**(距离:{dist:.4f})") st.text(doc[:500])这个界面就是典型的“输入框 + 按钮 + 结果展示”结构,没有复杂的前端依赖。st.expander 折叠展示参考片段,让用户既能看答案,也能核对答案的出处,符合 RAG 可溯源的优势。
5. 运行系统与效果验证
5.1 准备示例文档
在 data 目录下创建两个测试文档,这里以一个虚构的公司场景为例:
# 文件路径:rag-demo/data/公司介绍.txt 某某科技有限公司成立于2018年,总部位于上海,主营业务是企业级人工智能解决方案定制服务。 公司核心技术团队来自国内一线互联网公司与高校实验室,在自然语言处理、知识图谱、大模型应用方向有丰富经验。 公司的主要产品包括智能客服系统、知识库问答平台、数据中台建设服务。 截至2025年,公司已服务超过200家中小企业客户,覆盖零售、金融、教育等行业。# 文件路径:rag-demo/data/product_faq.txt 问:知识库问答平台支持哪些文档格式? 答:平台支持 txt、PDF、Word、Markdown 等常见格式,推荐使用 Markdown 或 txt 以获得最佳解析效果。 问:平台的数据是否安全? 答:平台支持私有化部署,文档向量化和检索过程可以在企业内部网络独立完成,只有最终生成回答时会调用大模型 API。 问:知识库更新需要重新训练模型吗? 答:不需要。知识库问答系统采用 RAG 架构,只需要更新向量库,即可让模型基于最新文档回答问题。5.2 初始化知识库
在项目根目录执行:
python ingest.py正常情况下会看到类似输出:
已加载文档: 公司介绍.txt 已加载文档: product_faq.txt 文档切分完成,共得到 6 个文本块 正在加载 embedding 模型... 正在生成向量... 向量库构建完成,共写入 6 个文本块首次运行会下载 bge-small-zh-v1.5 模型,文件大小在 100MB 左右,需要保持网络畅通。下载完成后,模型会缓存在本地,之后运行不会再重复下载。
5.3 启动 Web 界面
streamlit run app.py浏览器会自动打开 Streamlit 提供的本地地址,默认是 http://localhost:8501。可以看到一个简洁的问答界面,输入问题后点击“获取答案”。
尝试提问:“公司知识库平台支持哪些文档格式?”
预期回答会参考 product_faq.txt 中的内容,并明确指出支持 txt、PDF、Word、Markdown 等格式。如果提问“公司成立于哪一年”,系统应该从公司介绍文档中检索到对应信息并给出答案。
在“查看参考文档片段”区域,你会看到检索到的原始片段和对应的相似度距离,这能帮助你判断系统是否真正用上了知识库内容。
6. 常见问题与排查思路
在实际运行中,很多问题其实是共通的。下面把高频问题整理成表格,再逐个展开。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 首次运行下载模型失败 | 网络不稳定或 HuggingFace 地址无法访问 | 使用镜像地址或手动下载模型到本地 |
| 向量库查询报错 Collection not found | 未执行 ingest.py 或目录不一致 | 执行 ingest.py 构建向量库;检查 CHROMA_DIR |
| 检索结果与问题无关 | chunk 过大、top_k 过小、文档格式混乱 | 调整 CHUNK_SIZE、增大 TOP_K,清洗文档 |
| Deepseek API 返回 401 错误 | API Key 不正确或未加载成功 | 检查 .env 文件,确认 load_dotenv 生效 |
| 回答没有引用知识库内容 | Prompt 约束不够或检索片段为空 | 加强 system prompt,检查检索结果 |
| Chroma old database version 报错 | 向量库版本升级不兼容 | 删除 chroma_db 目录后重新 ingest |
6.1 首次运行模型下载失败
bge-small-zh-v1.5 模型默认从 HuggingFace 下载。如果你的网络环境无法稳定访问,可以设置镜像环境变量:
export HF_ENDPOINT=https://hf-mirror.com然后在终端重新运行 ingest.py。设置镜像环境变量的目的是解决下载通道问题,属于开发过程中的常规操作。模型下载完成后,可以不再依赖外网。
另一种方式是从其他方式下载模型文件后,把模型目录放到本地,然后在 config.py 中将 EMBEDDING_MODEL_NAME 改为模型所在的本地路径。例如:
EMBEDDING_MODEL_NAME = "./models/bge-small-zh-v1.5"6.2 检索结果不理想
检索结果差通常不是单一原因造成的,需要按照顺序排查几个关键参数。
先检查 TOP_K。如果设置为 1,结果可能只覆盖一个片段,信息量不足;如果设置为 5,又可能混入大量不相关内容。对于短文档,3 是合理的起点。
再检查 CHUNK_SIZE。如果文档本身是 FAQ 格式,每条问答很短,那么把 CHUNK_SIZE 设置成 500,会把多个问答切进同一个块,导致检索时指向内容不准确。对这种结构化文档,建议将 chunk_size 调小,比如 200 到 300,并去掉过大的 overlap。
最重要的还是文档清洗。如果原始文档包含大量页眉页脚、特殊符号、乱码,向量化效果会明显变差。在真实项目中,文档预处理往往占用整个 RAG 项目一半以上的工作量,这是很正常的现象。
6.3 Deepseek API 接入问题
如果调用时报 401,最直接的排查方式是打印 config.DEEPSEEK_API_KEY,确认 .env 是否被正确加载。.env 文件必须和运行命令所在目录一致,也就是项目根目录。
如果报连接超时,先确认网络环境能够访问 Deepseek API。同时,可以在代码中打印 DEEPSEEK_BASE_URL,确认没有因为之前的项目设置而覆盖成了其他地址。
6.4 向量库版本兼容问题
Chroma 版本迭代较快,不同版本生成的数据库文件可能不兼容,出现类似“old database version”的报错时,最简单的处理是备份并删除 chroma_db 目录,然后重新运行 ingest.py。这只是开发阶段的做法,生产环境中应该提前规划向量库的升级策略,例如使用独立的向量数据库服务。
7. 最佳实践与工程化建议
文章到这里,整套代码已经能跑通了。但如果你想把这个 Demo 变成真正可用的系统,还需要注意下面几个工程问题。
7.1 把 Prompt 单独管理
不要把 Prompt 字符串散落在业务代码里。随着项目迭代,Prompt 的调整频率很高,应该单独提取为一个配置文件或 Prompt 管理模块。某些团队还会为不同场景编写多套 Prompt,比如“摘要模式”“对比模式”“严格引用模式”,然后用模版变量动态切换。
7.2 增加检索结果的引用溯源
RAG 的核心优势之一就是可解释性。在 Web 界面和 API 返回中,都应该保留 source_documents 字段,最好把文档名、页码、片段位置一起返回。这样用户能够点击查看原文,运营人员也能快速发现错误片段并及时修正知识库。
7.3 完善文档更新机制
目前 ingest.py 每次都会删除旧集合重新写入。当知识库规模变大后,应该改成增量更新:根据文档的 hash 值或更新时间,只处理变更部分。向量库中也要增加元数据字段,比如来源文件名、更新时间、部门标签,方便按条件过滤。
7.4 引入重排(Rerank)提升精度
向量检索拿到的 Top K 片段里,可能顺序并不完全符合语义相关度。更成熟的方案是在向量检索之后增加一个 rerank 模型,对候选片段重新打分。比如本地部署 bge-reranker-base 模型,先用向量召回 20 个候选,再重排取前 3 个送给大模型。实践表明,加一层 rerank 后,回答准确率往往会有明显提升。
7.5 从 RAG 走向 Agentic RAG
目前实现的是标准 RAG,每次提问只做一次检索。当问题复杂时,例如“对比公司产品 A 和产品 B 的区别”,或者“找出去年所有投诉工单中的高频问题”,单次检索可能不够。
Agentic RAG 的核心思想是让大模型具备“工具调用”能力:它可以决定先搜索什么、搜索几次、是否改写问题、是否需要查看多个文档后再综合回答。这个方向可以作为下一步的进阶学习目标。本质上是把 RAG 的“搜索链路”从固定流程升级成由模型驱动的动态流程,对 Prompt 设计和工具调用的工程质量要求更高。
7.6 安全与合规注意事项
涉及到 API 调用、内部文档时,有几个安全底线需要坚持。第一,API Key 绝不硬编码在源码中,.env 文件必须加入 .gitignore;第二,生产环境建议通过独立的密钥管理服务接收 API Key,由后端服务统一调用,不要把密钥直接暴露给前端;第三,企业内部敏感文档如果涉及数据保密要求,需要确认是否允许调用外部大模型 API。如果严格禁止外部传输,则应该采用本地部署的模型替换 Deepseek API,把整个链路都收敛在内网环境中完成。
8. 从入门到落地,下一步可以做什么
至此,你已经亲手搭完了一套完整的 RAG 知识库问答系统:从文档加载、切分、向量化、检索,到 Prompt 组装和 Deepseek 生成回答,所有环节都是可运行、可扩展的。相比直接调用大模型接口“硬问”,这套方案让模型真正学会利用你的私有资料来回答问题。
如果你打算继续深入,有两条推荐路线。一条是优化检索质量,尝试引入 rerank、多路召回、元数据过滤,让召回结果更精准;另一条是走向 Agentic RAG,让模型能自主规划检索步骤,处理更复杂的多跳问答任务。两条路线都不需要重新训练模型,完全符合 RAG 低成本的核心理念。
构建知识库本身也是一门细活。文档清洗、文本切分、领域词表、Prompt 调优,每一个环节都会影响最终回答质量。不要指望运行一遍代码就得到完美效果,真正的项目落地就是在这些细节里不断打磨的过程。希望这份手把手教程能帮你迈出扎实的第一步,也欢迎把你在搭建过程中遇到的问题留在评论区,大家互相交流排错经验。