从零搭建RAG知识库问答系统:DeepSeek+本地向量化实战

发布时间:2026/9/8 10:39:25
从零搭建RAG知识库问答系统:DeepSeek+本地向量化实战 RAG 知识库问答系统是目前把大模型接入私域数据最常用的一套方案。很多团队并不是缺少大模型 API而是模型无法访问内部的文档、线上问题库、合同资料和制度规范。RAG 的难点不在某一个单独环节而在于文档切分、向量检索、Prompt 组织和模型生成如何串成一条可靠链路。这篇文章会从零搭建一个可以本地运行的 RAG 知识库问答系统使用 DeepSeek 作为生成模型使用本地 Embedding 模型完成向量化最终通过一个 Python 脚本完成“提问 - 检索 - 增强 - 生成答案”的完整闭环。读完以后你可以把同样的结构迁移到自己的文档目录逐步改造成服务化知识库。1. 先理解 RAG 为什么能解决“模型不知道你的业务知识”这个问题很多刚接触大模型的开发者会有一个误解把所有业务文档塞进 Prompt 里让模型“读一遍”就能回答。实际工作中企业文档动辄数百页上下文窗口再大也装不下而且每次请求重复发送大量文本成本和时延都会失控。RAG 的思路不是让模型记住全部内容而是每次提问时先从知识库中检索出最相关的一小段内容再把“问题 检索片段”一起交给大模型生成答案。1.1 LLM 为什么不知道你的私有知识大模型的训练数据主要来自公开网页、论文、书籍和开源代码训练完成后参数固化。公司内部的产品说明书、客户工单、线下流程文档模型根本没有见过。这不是模型能力不足而是知识来源缺失。即使模型可以泛化出一些通用回答也无法保证回答能匹配企业内部的事实。想让模型知道私有知识有三种常见路线继续预训练或微调把文档融进模型权重成本高、周期长且知识更新要重新训练。长文本全量注入适合单次小规模知识无法扩展到大规模知识库。检索增强生成RAG每次根据问题动态检索最相关内容低成本、可更新、可追溯。RAG 的核心价值在于知识不要求模型记住而是交给外部存储和检索系统负责。模型只负责在给定片段的基础上组织语言、推理和作答。这种分工让知识更新变得简单只需要更新向量库不需要频繁重新训练模型。1.2 一条完整的 RAG 链路包含哪些环节一个可工作的 RAG 系统通常包含五个阶段文档加载从本地目录读取 txt、pdf、markdown 等文件。文本切分把长文档切成语义完整的 chunk并保留必要的上下文。向量化把每个 chunk 通过 Embedding 模型转成向量。向量存储与检索把向量写入向量数据库提问时把问题转成向量用相似度召回 Top K 片段。增强生成把召回的片段和用户问题组织成 Prompt调用 DeepSeek 生成最终答案。很多人以为 RAG 等于“向量检索 大模型”。从最小演示看确实如此但生产系统还要在切分粒度、检索阈值、重排策略、引用来源和缓存上做很多优化。下面的实现会先把最小闭环跑通再逐步解释每个环节应该怎么调。1.3 为什么选 DeepSeek 作为生成底座DeepSeek 的接入成本相对较低并且在中文理解和指令跟随上有不错的表現。对知识库问答来说生成模型只需要完成两件事根据给定上下文回答问题以及在上下文不足时明确说“不知道”。DeepSeek 的 API 兼容 OpenAI 接口格式使用现成的 openai Python SDK 就能调用服务端返回的也是标准 ChatCompletion 结构。本文示例使用 DeepSeek 的deepseek-chat模型具体模型名和接口地址以你拿到的官方文档为准。如果后续模型版本发生变化只需要修改model参数和base_url整条 RAG 链路不需要重写。1.4 整体架构和你需要准备的资源本项目的架构可以描述为本地文档放置需要问答的业务资料例如data/目录。离线索引文档切分后生成向量写入 Chroma 的本地目录。在线问答用户提问后对问题向量化和检索再调用 DeepSeek 生成回答。需要的资源不多一台普通开发电脑建议内存 8GB 以上。Python 3.10 或更高版本。DeepSeek API Key用于调用生成模型。本地磁盘空间用于存放向量索引。Embedding 模型在 CPU 上也能运行只是文档多时速度会慢。为了学习原理先在小数据集上跑通不需要 GPU。2. 环境准备依赖版本、向量库和 API 配置在写代码之前先把环境固定下来。RAG 链路涉及的依赖比较多如果版本冲突后面会出现各种难排查的问题。2.1 基础环境清单推荐使用 Python 3.10因为常见 Embedding 库和向量库在这个版本上兼容性最稳定。操作系统 Windows、macOS、Linux 都可以注意 Chroma 在 Windows 下需要确保安装 Visual C 运行库否则可能启动报错。依赖作用建议版本Python运行环境3.10 及以上openai调用 DeepSeek API1.xsentence-transformers本地文本向量化3.0 左右chromadb向量存储和检索0.5 左右flask 或 fastapi服务化示例按项目需要不同操作系统下sentence-transformers会自动拉取 PyTorch 相关依赖安装时间会比较长。数据库概念上Chroma 可以选择内存运行也可以持久化到本地目录。本教程采用持久化模式保证重建和重启后索引不丢失。2.2 创建虚拟环境并安装依赖先创建项目目录和虚拟环境避免把依赖装进全局 Python。mkdir rag-demo cd rag-demo python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate新建requirements.txtopenai1.30.0 sentence-transformers3.0.0 chromadb0.5.0 python-dotenv1.0.0安装依赖pip install -r requirements.txt安装完成后可以检查关键包是否可用python -c import chromadb, openai, sentence_transformers; print(ok)这里要注意openai包版本更新较快示例代码使用的是新版客户端写法OpenAI(api_key..., base_url...)如果你项目里同时存在旧版写法需要统一。2.3 向量数据库选型与启动方式Chroma 对初学者友好可以直接使用 Python 内嵌模式不需要单独启动服务。它的数据默认可以保存到指定目录代码里这样声明from chromadb import PersistentClient client PersistentClient(path./chroma_db) collection client.get_or_create_collection(knowledge_base)生产环境如果检索量变大可以换用 Milvus、Qdrant 或 pgvector。选型时重点看三点是否支持持久化、是否支持向量索引、运维成本是否可控。Chroma 适合从零开始因为它把存储和检索封装得足够简单。2.4 获取 DeepSeek API KeyDeepSeek 接口调用需要 API Key。你需要在对应平台注册账号进入控制台创建 API Key然后把 Key 写到.env文件中不要硬编码到代码仓库。DEEPSEEK_API_KEYsk-xxxxxxxxxxxxxxxx DEEPSEEK_BASE_URLhttps://api.deepseek.com DEEPSEEK_MODELdeepseek-chat使用python-dotenv加载.envimport os from dotenv import load_dotenv load_dotenv() DEEPSEEK_API_KEY os.getenv(DEEPSEEK_API_KEY) DEEPSEEK_BASE_URL os.getenv(DEEPSEEK_BASE_URL) DEEPSEEK_MODEL os.getenv(DEEPSEEK_MODEL)注意API Key 属于敏感信息。不要提交到 Git 仓库建议把.env加入.gitignore。如果 Key 泄露要尽快到控制台吊销并重新创建。3. 从零实现最小 RAG文档切分、向量检索、调用 DeepSeek下面进入代码部分。这里不引入过重的框架用原生 Python 把 RAG 每个环节逻辑写清楚。目标是让你知道每一步在做什么之后再引入框架时能看懂封装。3.1 项目目录结构推荐这样组织rag-demo/ ├── data/ # 存放知识库原始文档 │ ├── product.md │ └── employee_handbook.txt ├── src/ │ ├── loader.py # 文档加载 │ ├── splitter.py # 文本切分 │ ├── embedder.py # 向量化封装 │ ├── vectordb.py # 向量库封装 │ ├── retriever.py # 检索逻辑 │ └── llm.py # DeepSeek 调用 ├── scripts/ │ ├── build_index.py # 构建索引 │ └── query.py # 提问测试 ├── .env # API Key 配置 ├── requirements.txt └── README.md这个结构把“索引构建”和“在线查询”分开。索引构建是离线的文档更新后重新执行在线查询只读取已有索引不需要重复切分全部文档。这样设计的好处是文档多以后更新索引不会阻塞线上服务。3.2 文档加载与切分把长文档变成可检索的片段先写一个最简单的文档加载器支持 txt 和 markdown# src/loader.py from pathlib import Path def load_documents(data_dir: str): docs [] data_path Path(data_dir) for file_path in data_path.rglob(*): if file_path.suffix.lower() not in {.txt, .md}: continue with open(file_path, r, encodingutf-8) as f: text f.read() docs.append({ id: str(file_path.relative_to(data_path)), content: text, source: str(file_path) }) return docs实际项目中还经常要处理 PDF、Word、Excel。对于 PDF 可以配合pypdf或pdfplumberWord 可以使用python-docx。加载层的作用是把不同格式统一成“文本 元信息”后续步骤不关心文件原始格式。切分是关键步骤。如果 chunk 太长向量表示会被无关内容稀释如果太短可能丢失上下文。这里使用固定长度加重叠的切分方式# src/splitter.py def split_text_by_size(text: str, chunk_size: int 500, overlap: int 50): if len(text) chunk_size: return [text] chunks [] start 0 while start len(text): chunk text[start:start chunk_size] chunks.append(chunk) start chunk_size - overlap return chunks对于中文文档直接按字符切分可能会把一句话从中间截断。更稳妥的方法是先按段落拆再把过长的段落按句子拆或者按标题层级拆。后续章节会讲如何优化。3.3 Embedding 模型如何把文本变成向量Embedding 的目标是让语义相近的文本在向量空间里距离更近。这里使用sentence-transformers加载本地或 Hugging Face 上的中文模型# src/embedder.py from sentence_transformers import SentenceTransformer _model None def get_embedding_model(model_name: str BAAI/bge-small-zh-v1.5): global _model if _model is None: _model SentenceTransformer(model_name) return _model def embed_texts(texts: list[str]): model get_embedding_model() return model.encode(texts, normalize_embeddingsTrue).tolist()normalize_embeddingsTrue会把向量归一化到单位长度。这样用余弦相似度比较时点积的结果就是余弦相似度分数范围在 -1 到 1 之间检索时更容易设置阈值。如果你不想在本地维护 Embedding 模型也可以使用平台的 Embedding API。不过对私有知识库来说本地 Embedding 在数据隐私和离线能力上更可控。首次使用BAAI/bge-small-zh-v1.5会下载模型文件需要网络。如果公司内网无法访问外网可以提前把模型文件放到本地目录。3.4 构建向量索引写入 Chroma把切分后的片段逐条写入 Chroma# src/vectordb.py import chromadb from chromadb.config import Settings class VectorStore: def __init__(self, persist_dir: str ./chroma_db): self.client chromadb.PersistentClient( pathpersist_dir, settingsSettings(anonymized_telemetryFalse) ) self.collection self.client.get_or_create_collection( nameknowledge_base, metadata{hnsw:space: cosine} ) def add_documents(self, ids, documents, metadatas, embeddings): self.collection.add( idsids, documentsdocuments, metadatasmetadatas, embeddingsembeddings ) def query(self, query_embedding, top_k5): res self.collection.query( query_embeddings[query_embedding], n_resultstop_k, include[documents, metadatas, distances] ) return resChroma 的collection.add要求 ids、documents、metadatas、embeddings 长度一致。id 不能重复否则会覆盖已有数据。这里可以用“文档路径 下划线 chunk 序号”来生成唯一 id。构建索引的脚本# scripts/build_index.py import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from src.loader import load_documents from src.splitter import split_text_by_size from src.embedder import embed_texts from src.vectordb import VectorStore def build_index(data_dir: str data): docs load_documents(data_dir) all_metadata [] all_docs [] all_ids [] for doc in docs: chunks split_text_by_size(doc[content], chunk_size500, overlap50) for idx, chunk in enumerate(chunks): chunk_id f{doc[id]}::{idx} all_ids.append(chunk_id) all_docs.append(chunk) all_metadata.append({source: doc[source], chunk_index: idx}) embeddings embed_texts(all_docs) store VectorStore() store.add_documents(all_ids, all_docs, all_metadata, embeddings) print(f索引完成{len(all_docs)} 个片段) if __name__ __main__: build_index()运行python scripts/build_index.py构建完成后chroma_db目录下会生成向量索引文件。下一次查询时不需要重新加载文档。3.5 检索用问题向量召回相关片段在线查询时先把用户问题转成向量再到 Chroma 里找最相近的 Top K 片段。# src/retriever.py from src.embedder import embed_texts def retrieve(question: str, store, top_k: int 5): q_emb embed_texts([question])[0] res store.query(q_emb, top_ktop_k) documents res[documents][0] metadatas res[metadatas][0] distances res[distances][0] return list(zip(documents, metadatas, distances))返回结果可以包含三个信息片段文本、来源文件、相似度距离。距离越小表示越相似在 cosine 空间里1 - cosine 距离 就是语义相似度。使用时不建议只看排序还要看阈值。如果所有候选的相似度都很低说明知识库里可能没有相关内容此时应该让模型回答“知识库中没有找到相关信息”。3.6 组装 Prompt 并调用 DeepSeek检索到的片段需要放进 Prompt。Prompt 设计有几个原则明确告诉模型只依据提供的上下文回答。给出上下文与问题之间有空行分隔避免模型混淆。要求模型在上下文不足时直接说明不要编造。标注引用来源方便用户核对答案。# src/llm.py import os from openai import OpenAI def build_prompt(question: str, contexts: list[str]) - str: context_block \n\n.join( f[片段 {i1}]\n{ctx} for i, ctx in enumerate(contexts) ) prompt f你是一个企业知识库问答助手。请只根据下面提供的知识片段回答用户问题。 如果知识片段中没有相关信息请明确回答“知识库中未找到相关信息”不要编造。 知识片段 {context_block} 用户问题 {question} return prompt def ask_deepseek(question: str, contexts: list[str]): client OpenAI( api_keyos.getenv(DEEPSEEK_API_KEY), base_urlos.getenv(DEEPSEEK_BASE_URL) ) prompt build_prompt(question, contexts) resp client.chat.completions.create( modelos.getenv(DEEPSEEK_MODEL), messages[ {role: system, content: 你是严谨的企业知识库助手。}, {role: user, content: prompt} ], temperature0.1, max_tokens1024, ) return resp.choices[0].message.contenttemperature0.1会让输出更稳定、更接近原文减少“自由发挥”。知识库问答场景希望回答忠于事实所以温度不要设太高。3.7 把流程串成 query 入口最后写一个查询脚本把检索和生成连起来# scripts/query.py import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from dotenv import load_dotenv load_dotenv() from src.vectordb import VectorStore from src.retriever import retrieve from src.llm import ask_deepseek def main(): question input(请输入问题) store VectorStore() results retrieve(question, store, top_k5) contexts [r[0] for r in results[:5]] print(\n 检索引用的片段 ) for i, (ctx, meta, dist) in enumerate(results[:5]): print(f[{i1}] 来源{meta.get(source)}, distance{dist:.4f}) print(ctx[:120].replace(\n, )) print(\n DeepSeek 回答 ) answer ask_deepseek(question, contexts) print(answer) if __name__ __main__: main()运行python scripts/query.py输入一个问题比如“项目报销流程是什么” 系统会先打印召回的片段再打印模型生成的答案。这时你就拥有了一个最小可运行的 RAG 知识库问答系统。4. 核心参数详解切分粒度、检索数量、生成参数如何影响答案质量最小流程跑通后需要开始调优。RAG 的效果不是由某个参数单方面决定而是由切分、检索和生成共同决定。4.1 切分参数chunk_size 与 overlap 如何影响检索参数含义默认值参考调大影响调小影响chunk_size每个片段的字符数500片段语义完整但向量可能包含多个主题片段更聚焦但上下文容易缺失overlap相邻片段重叠字符数50减少句子被截断导致的丢失重叠太多会造成索引冗余如果知识库是制度文档推荐按章节或标题切分而不是纯按字符切分。纯按字符切分简单但一个片段可能截断在表格中央检索时召回的内容不够完整。更好的方案是先按段落聚合段落过长再按句子边界切分。一个实用的切分判断标准是每个片段能够独立读懂且包含一个完整信息点。比如“报销流程”最好能被一个片段完整覆盖而不是分散在两个片段里。4.2 top_k 与相似度阈值top_k控制每次检索返回多少片段。太小可能漏掉答案太大可能把无关内容混进 Prompt反而干扰模型。推荐先设top_k5然后观察结果相关片段都在前 3 条可以缩小到top_k3。答案内容不够完整适当提高到top_k7。答案前后矛盾查看召回的片段里混入了多少无关内容。相似度阈值更适合用于“拒绝回答”。生产系统不只看排序更要判断“知识库到底有没有相关内容”。如果最相似片段的余弦相似度低于 0.6不同 Embedding 模型阈值有差异应该触发 fallback 逻辑SIMILARITY_THRESHOLD 0.6 if distance is not None: similarity 1 - distance if similarity SIMILARITY_THRESHOLD: print(知识库中没有足够相关的信息无法回答该问题。)注意不同 Embedding 模型输出的分数分布不一样bge 模型和 OpenAI 的 Embedding 模型阈值不能通用。需要在自己的数据集上抽样计算统计值再确定合理阈值。4.3 生成参数 temperature、max_tokens 与 Prompt 模板的关系在知识库问答场景中参数的推荐值如下参数推荐值说明temperature0.1 ~ 0.3越低越稳定过高容易导致模型编造内容max_tokens512 ~ 1024受答案长度影响过长会浪费 tokentop_p0.8 ~ 1.0与 temperature 配合一般不必同时调低presence_penalty / frequency_penalty0知识库问答不需要过度惩罚重复Prompt 模板的稳定性同样重要。不要在线上频繁改动 Prompt 结构任何变更都应该先拿一批测试问题回归比较答案引用率和人工满意度。注意判断 RAG 质量时不能只看“答案是否通顺”。需要看答案是否来自知识库、是否包含模型自己的常识性发挥、是否给出来源。要做到这一点Prompt 里必须要求模型在回答后标注引用片段编号。5. 运行验证如何判断一套 RAG 是否真的可用代码跑通不等于系统可用。还需要用测试问题验证检索和生成两个环节。5.1 准备测试集和预期答案建议准备 20 到 50 个问题覆盖四类可以直接从文档某个片段找到答案的问题。需要从多个片段组合答案的问题。文档里根本没有答案的问题。用户问法比较口语化但意思接近文档内容的问题。例如类型示例问题预期行为单片段年假是怎么计算的答案核心来自文档的某个段落多片段请给出入职后的完整流程答案综合多个片段无答案公司年会具体时间明确回答未找到口语化我请假找谁批能关联到请假制度文档把问题写成一个 JSON 文件保存到eval_data.json[ { question: 项目报销流程是什么, expected: 报销需要先提交申请再经过主管审批 } ]5.2 输出评估指标人工检查是第一步也可以写脚本统计几个简单指标命中率正确答案是否出现在召回的 Top K 片段中。引用率生成的答案是否明确使用了知识库片段。拒绝准确率无关问题是否被正确拒绝。平均延迟从提问到收到完整回答的耗时。示例统计脚本片段def evaluate_retrieval(questions, relevant_doc_ids): hit_count 0 for q, relevant_ids in zip(questions, relevant_doc_ids): results retrieve(q, store, top_k5) retrieved_ids {r[1].get(chunk_id) for r in results} if relevant_ids retrieved_ids: hit_count 1 return hit_count / len(questions)实际评估时命中率只是一个粗略指标。更细的指标可以使用检索领域的 RecallK、MRR生成质量则依赖人工标注。对于小团队第一步先把召回片段打印出来人工判断是否准确比追求复杂指标更快发现问题。5.3 如果答案不好先判断问题在检索还是在生成答案质量不佳时不要直接改 Prompt。先打印检索片段。如果检索片段相关内容已经出现但答案没有使用问题在生成阶段调整 Prompt 或降低 temperature。如果检索片段本身就不相关问题在切分或向量检索阶段需要调整切分策略、换 Embedding 模型或增加重排。如果召回内容相关但不完整问题可能在 chunk_size 太小或多个片段语义分散考虑增加 overlap 或合并段落。这种定位方法能避免无头绪调参。6. 常见问题排查从报错到效果异常的完整路径下面整理 RAG 知识库问答系统最常见的五类问题以及对应的检查顺序。6.1 检索结果为空或只有不相关内容可能原因文档没有被正确加载目录路径不对。切分后 chunks 为空。向量库没有持久化每次启动都在空库状态。Embedding 模型加载失败向量编码格式不对。问题用语和文档用语差异太大。检查顺序确认data目录有文件且扩展名被 loader 支持。运行python scripts/build_index.py确认输出“索引完成N 个片段”。查询 Chroma collection 的 count确认索引条数。对单个片段直接计算与问题的相似度判断检索逻辑是否正常。6.2 DeepSeek 接口超时或鉴权失败常见错误现象返回401 authentication_error。返回404 model_not_found。请求一直超时。现象可能原因检查方式处理建议401API Key 错误或未设置打印.env读取结果确认无空格重新复制 Key 到.env404model 名错误查看官方当前支持的模型列表修改DEEPSEEK_MODEL超时网络不通或接口地址错误用 curl 测试连通性检查 base_url联系管理员确认网络策略和可用地址生产环境还需要在调用 DeepSeek 时设置合理的超时时间和重试策略client OpenAI( api_keyos.getenv(DEEPSEEK_API_KEY), base_urlos.getenv(DEEPSEEK_BASE_URL), timeout60.0, max_retries2 )超时时间不要太短长文本生成可能需要几十秒。max_retries用于网络抖动但注意如果已经返回结果不能盲目重试避免重复扣费。6.3 模型答案没有来自知识库内容如果检索到了正确片段但模型答案写成通用知识通常是因为 Prompt 约束不够强或者上下文格式让模型误以为可以自由发挥。改进方向在 Prompt 中强调“只能依据片段内容作答”。要求模型在回答前先引用片段编号。将 temperature 降到 0.1。增加“不知道时如何回答”的示例。推荐在 Prompt 中加入一个 few-shot 示例知识片段 员工请假需要提前一天在系统中提交申请注明请假类型、日期和原因。 用户问题 请假要提前多久申请 回答 根据知识库员工请假需要提前一天在系统中提交申请。这样模型更容易理解输出约束。6.4 中文切分效果差语义被截断纯字符切分容易把句子切断。优化方案有按段落切分减少跨段落切分。对过长段落调用sentence_splitter或按中文标点。切句。使用 markdown 标题结构切分章节信息作为 metadata 保存。切分后检查每个片段是否以完整句子结束。一个简单的按标点聚合切分思路import re def split_by_sentence(text): parts re.split(r(?[。]), text) return [p for p in parts if p.strip()] def chunk_by_sentences(text, max_chars500, overlap_sentences1): sentences split_by_sentence(text) chunks [] current [] current_len 0 for sent in sentences: if current_len len(sent) max_chars and current: chunks.append(.join(current)) current current[-overlap_sentences:] current_len sum(len(s) for s in current) current.append(sent) current_len len(sent) if current: chunks.append(.join(current)) return chunks这段代码把“以句子为单位”作为切分原则并保留了一个句子的重叠可以有效减少语义断裂。6.5 知识库更新后查询结果没有变化Chroma 持久化后add_documents使用相同 id 会覆盖旧数据。但如果文档路径变了可能生成新 id旧 id 仍然留在库里。更新索引时建议先清空旧 collectionclient.delete_collection(knowledge_base) collection client.get_or_create_collection(knowledge_base)或者维护一个“文档版本”字段每次构建索引时删除该版本以外的数据。生产系统最好采用“重建索引 原子切换”的方式避免索引构建到一半时线上查询读取到不完整数据。7. 生产化从脚本到可用知识库服务RAG 脚本只是起点。实际部署时还要考虑服务接口、并发、安全、增量更新和可观测性。7.1 把 query 逻辑封装成 HTTP 服务用 Flask 或 FastAPI 封装成一个简单的/ask接口# app.py from flask import Flask, request, jsonify from dotenv import load_dotenv load_dotenv() from src.vectordb import VectorStore from src.retriever import retrieve from src.llm import ask_deepseek app Flask(__name__) store VectorStore() app.post(/ask) def ask(): data request.get_json() question data.get(question, ) top_k data.get(top_k, 5) results retrieve(question, store, top_ktop_k) contexts [r[0] for r in results] answer ask_deepseek(question, contexts) return jsonify({ answer: answer, sources: [ {source: meta.get(source), distance: dist} for _, meta, dist in results ] }) if __name__ __main__: app.run(host0.0.0.0, port8000)服务化后要注意并发。Chroma 的客户端在多线程下可能有锁限制生产环境可以提前做压测观察 QPS 和延迟。如果有性能问题考虑引入连接池、缓存或更重型的向量数据库。7.2 增量更新和知识版本管理文档不是静态的。建议在每次构建索引时生成一个版本号并记录每个片段来自哪个文档版本metadata { source: source, version: 2025-01-01, }这样当同一份文档更新后可以定位到旧版本片段并清除。如果使用 Chroma可以在metadatas中记录doc_version然后按 metadata 过滤删除。7.3 缓存、监控和日志对于高频问题可以把“问题 回答 引用片段 id”写入缓存避免重复调用 DeepSeek。缓存 key 可以基于问题的规范化结果。注意不要让缓存掩盖了知识库更新知识库版本变更时要清理相关缓存。日志至少记录用户问题。检索到的片段 id、来源和距离分数。DeepSeek API 调用耗时和 token 数。最终回答文本。错误日志要记录异常类型、堆栈和参数上下文。不要只记录“调用失败”否则很难定位到底失败在检索还是生成阶段。7.4 权限与安全如果知识库包含敏感内部资料需要考虑接口鉴权限制谁可以调用/ask。文档权限不同角色的用户只能检索其有权限的文档子集。越权检查检索时在 metadata 上增加部门或权限标签过滤后再查询。输出审计记录谁在什么时间看到了哪些来源内容。不做权限控制时一个内部知识库很容易变成“所有内部信息的公开窗口”这一点在部署到团队共享环境前一定要想清楚。7.5 可复用发布检查清单发布前按这个清单检查[ ]data目录只包含需要对外问答的文档不包含敏感文件。[ ].env未提交到代码仓库API Key 已正确配置。[ ] 运行构建索引脚本后向量库条数与预期一致。[ ] 设计好的测试问题集全部通过人工验收。[ ] 无关问题能被正确拒绝不会强制输出编造答案。[ ] Prompt、模型、检索 Top K 等参数均已固定。[ ] 接口有鉴权日志有脱敏配置。[ ] 对 DeepSeek API 调用设置超时和重试策略。[ ] 文档更新流程明确不会出现旧索引与新文档混用。[ ] 部署环境已压测单请求延迟在可接受范围。8. 扩展方向多轮对话、重排和 Agentic RAG最小 RAG 系统跑通以后可以从三个方向继续深入。8.1 多轮对话中的上下文改造用户提问往往依赖前文比如先问“公司年假政策是什么”再问“那最多能攒多少天” 第二问单独检索会缺少“年假”这个主语。改进方式是先把多轮历史改写成独立问题再进行检索history [ {role: user, content: 公司年假政策是什么}, {role: assistant, content: 根据知识库新员工第一年有 5 天年假。}, ] current_question 那最多能攒多少天 # 调用大模型进行 query rewriting或者简单拼接历史后检索 rewritten 公司年假最多能攒多少天这一步属于查询改写是 RAG 系统中非常值得优化的部分。8.2 重排从召回到精排向量召回 Top K 可能会有噪音之后可以再接一个重排序模型Reranker。重排模型会针对“问题和候选片段”计算相关性得到更精准的排序。典型流程是向量检索召回 20 条候选。重排模型对 20 条逐条打分。取前 5 条送入 Prompt。重排会额外增加耗时但能有效提高答案质量适合文档量大、检索噪声高的场景。在 Python 中可以使用FlagEmbedding或类似库加载 reranker 模型。8.3 Agentic RAG让模型决定检索策略Agentic RAG 与普通 RAG 的区别在于普通 RAG 每次固定执行“检索 - 生成”而 Agentic RAG 会让模型在生成过程中自主决定是否检索、检索几次、是否需要调用外部工具。比如模型可以先判断“用户问题是否需要最新知识”再决定走检索还是直接回答。这类实现的复杂度更高但更接近真实智能助手的工作方式。对初学者建议先把普通 RAG 的切分、检索、Prompt 和评估做到稳定再研究 Agentic RAG。否则在一个不可靠的检索底座上叠加 Agent 逻辑问题排查会非常困难。RAG 知识库问答系统的价值并不在于代码有多复杂而在于你是否理解每一层数据的流动从文档切分到向量索引从检索片段到大模型生成。希望这篇文章能帮你搭建出第一个可运行的 RAG 系统并让你在遇到效果问题时知道应该从哪一段开始排查。下一步可以把这套代码扩展到你的业务文档把准确率、延迟和权限控制逐步补齐它就能从演示项目长成真正可用的知识库工具。