ARTICLE DETAIL

资讯详情

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

RAG知识库问答系统实战:从文本切分到部署优化

RAG知识库问答系统实战:从文本切分到部署优化 之前接手过一个基于宗教经典文献的 AI 问答项目需求是从一部体量很大的古籍中快速检索原文并生成有依据的回答。最初想法很简单把全文丢给大模型让它直接回答。结果发现两个棘手问题一是经典文本里同一个概念在不同章节有完全不同的阐述语境二是直接“整书投喂”既不现实回答也经常张冠李戴。后来换成 RAG检索增强生成架构把知识库、向量检索和生成模型组合起来才真正把问题解决。这篇教程就把这套完整方案拆开讲清楚包含环境搭建、代码实现、部署踩坑和工程优化适合想用大模型做垂直领域问答开发的同学参考。1. 知识库问答与 RAG 架构1.1 为什么不能直接把文档丢给大模型很多同学第一次做“AI 问答”时第一反应是我有一本 PDF直接让 ChatGPT 帮我回答不就行了吗简单场景确实可以比如把一段文本粘贴到对话框中提问。但在真实知识库场景下这套做法会遇到三个明显瓶颈上下文窗口有限。市面主流大模型的上下文长度虽然在不断增长但一部宗教经典、法律条文、企业手册动辄几十万字甚至上百万字根本无法一次性放入上下文。回答缺少依据。大模型是概率生成模型对于知识库中的冷门内容如果训练语料里没有覆盖就可能出现“一本正经地胡说八道”也就是常说的幻觉问题。更新成本高。如果每次知识库内容变化都要重新训练或微调模型时间和成本都不可接受也不符合知识快速迭代的业务需求。既然“整书丢给模型”不可行那有没有更聪明的方案答案是采用 RAG 架构。1.2 RAG 的核心思想RAG全称 Retrieval-Augmented Generation即检索增强生成。简单来说它把“检索”和“生成”两个步骤串起来先把知识库文档切分成小块做向量化Embedding存入向量数据库。用户提问时先把问题向量化到向量数据库中检索出最相关的若干文本块。将检索到的文本块作为上下文连同用户问题一起组装成 Prompt交给大模型生成最终答案。这样做的好处很明显模型不需要“记住”全部知识只需要根据检索到的片段作答显著降低幻觉。知识库更新时只需要重新向量化新增或修改的部分不需要重新训练模型。回答可以附带引用来源便于用户溯源。以 Sikhbani.ai 这类“向古籍提问”的项目为例它的产品形态是让用户用自然语言查询 Sri Guru Granth Sahib 中的内容。技术上最核心的部分就是把整部经典先做切分、向量化再基于检索结果让大模型生成回答。整个过程对任何长文档知识库问答系统都是通用的。1.3 RAG 应用的完整链路一个标准 RAG 应用可以拆成离线索引和在线问答两条链路离线索引链路原始文档 - 文本清洗 - 分块 - Embedding 向量化 - 写入向量数据库在线问答链路用户提问 - 问题向量化 - 向量检索 TopK - 组装 Prompt - 大模型生成 - 返回答案与引用这两条链路并不复杂但每步都有大量工程细节。接下来按真实项目开发顺序从环境准备、代码实现到部署优化一步一步展开。2. 环境准备与版本说明2.1 技术选型在开始写代码前先明确技术栈。下面这套方案以 Python 为主兼顾开发效率和生态成熟度模块选型说明编程语言Python 3.10RAG 生态最丰富示例代码基于 Python 3.10Web 框架FastAPI提供问答 API 接口支持异步和流式输出向量化模型sentence-transformers本地 embedding避免外部 API 调用延迟向量数据库Chroma轻量级适合原型和中小规模知识库大模型推理OpenAI API 或 Ollama 本地模型视成本、隐私和使用场景决定检索编排LangChain 或 LlamaIndex简化 RAG 流程组装版本需要根据你的项目实际情况调整。本文示例以常见环境为例重点演示配置思路。如果你的知识库规模较大或者对并发要求高可以考虑把 Chroma 换成 Milvus、Qdrant 等专业向量数据库如果使用 OpenAI API需要提前确认账号权限和网络连通性如果数据敏感、必须内网部署推荐用 Ollama 部署私有化大模型。2.2 虚拟环境与依赖安装先创建一个干净的虚拟环境python3.10 -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate然后安装依赖。这里为了便于理解不用 requirements.txt 一把梭而是按模块安装pip install fastapi uvicorn chromadb sentence-transformers langchain langchain-community pypdf如果使用 OpenAI API还需要安装 openai 库pip install openai如果使用 Ollama 本地模型则只需启动 Ollama 服务并拉取模型ollama pull llama3.2:3b ollama serve2.3 项目结构规划一个清晰的目录结构对后期维护很重要。示例项目目录如下rag_qa/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 入口 │ ├── config.py # 配置项 │ ├── database.py # 向量数据库操作 │ ├── retriever.py # 检索逻辑 │ ├── generator.py # 大模型生成逻辑 │ └── schemas.py # 请求/响应模型 ├── data/ │ └── knowledge/ # 知识库原始文档 ├── scripts/ │ └── build_index.py # 离线索引构建脚本 └── requirements.txt这个结构把不同职责拆开后续扩展、测试、替换组件都更方便。3. 核心概念与原理拆解3.1 文本切分Chunking文本切分是 RAG 中最容易被低估、却对效果影响最大的环节。切得太碎每个 chunk 语义不完整检索时容易漏掉关键信息切得太长向量化后语义可能被稀释而且超出模型上下文窗口。常见的切分策略有两种。按字符数切分from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, separators[\n\n, \n, 。, , , , , ] )按语义段落切分from langchain.text_splitter import MarkdownHeaderTextSplitter # 或使用针对特定文档结构的 splitter对于宗教经典这类结构复杂、有章节编号的长文建议先按章节级别切出大段落再在每个大段落内部做二次切分。这样既保留语义完整性又控制单块大小。还要注意设置 chunk_overlap重叠避免一句话被拦腰截断导致检索遗漏。3.2 Embedding 向量化Embedding 的作用是把文本映射成固定维度的向量语义相近的文本向量距离越近。常用的本地模型有sentence-transformers/all-MiniLM-L6-v2轻量适合英文768 维。BAAI/bge-small-zh-v1.5适合中文场景。text-embedding-3-smallOpenAI 提供的在线 embedding。代码示例from sentence_transformers import SentenceTransformer model SentenceTransformer(sentence-transformers/all-MiniLM-L6-v2) vector model.encode(What is the meaning of life?) print(vector.shape) # 输出 (384,)实际项目中可以把 embedding 模型封装成一个单例避免每次调用都重复加载模型。3.3 向量数据库向量数据库的核心能力是存储向量并且支持相似度检索。Chroma 是轻量级选择支持本地持久化适合原型验证import chromadb client chromadb.PersistentClient(path./chroma_db) collection client.get_or_create_collection( namegurbani_qa, metadata{hnsw:space: cosine} )相似度计算常用余弦相似度和欧氏距离。Chroma 中可通过 metadata 配置cosine 更适合文本语义检索。3.4 大模型生成检索只是拿到候选材料最终答案由大模型生成。使用 OpenAI API 时from openai import OpenAI client OpenAI( api_keyyour-api-key, base_urlhttps://api.openai.com/v1 ) response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是一个严谨的知识库问答助手。}, {role: user, content: 基于以下资料回答问题\n\n context} ] )使用 Ollama 本地模型时接口兼容 OpenAI 风格from openai import OpenAI client OpenAI( base_urlhttp://localhost:11434/v1, api_keyollama ) response client.chat.completions.create( modelllama3.2:3b, messages[ {role: system, content: 你是一个严谨的知识库问答助手。}, {role: user, content: 基于以下资料回答问题\n\n context} ] )本地模型和在线模型在答案质量上有差距尤其是对经典文本中需要“上下文理解”的问题。如果业务对准确度要求高建议优先考虑能力更强的在线模型如果数据必须内网使用则要选参数量大一些的本地模型并做好效果评估。3.5 Prompt 设计RAG 的 Prompt 设计直接影响回答质量。一个常见的误区是只把检索片段拼接在问题后面不加任何引导导致模型自由发挥。更好的方式是在 Prompt 中明确约束角色设定你是谁你的任务是什么。资料范围只允许根据提供的资料回答。无法回答时的处理明确说“资料中未找到相关内容”不要编造。引用要求输出答案时标明信息来自哪个文档片段。例如你是知识库问答助手。请根据以下文档片段回答用户问题。 规则 1. 只使用文档片段中的信息不要编造。 2. 如果文档片段没有相关内容请回答“知识库中未找到相关信息”。 3. 答案末尾列出参考片段编号。 文档片段 [1] {chunk_1} [2] {chunk_2} 用户问题{question}这样设计后模型的输出会更加可控。4. 完整实战案例基于经典文本的问答系统这一节逐步实现一个可运行的 RAG 问答系统。为了让示例不过度依赖英文资料我用一个模拟的“古籍章节文本”作为知识库数据。4.1 准备知识库文档在 data/knowledge 目录下放一个 example_knowledge.txt 文件内容格式如下实际项目中可换成原文 PDF、Word 或数据库导出的文本第一章 和平与宽恕 宽恕是一种力量它让心灵从仇恨的束缚中解脱。智者以和平回应敌意以慈悲化解纷争。 第二章 真理与言行 真理不仅是言语的真实更是行为的一致。一个追寻真理的人应当让言语、思想和行动保持统一。 第三章 服务与奉献 服务他人是最高的修行。通过无私的奉献人能够超越自我的局限体会到与万物的连接。 第四章 谦卑与学习 真正的智慧始于谦卑。只有承认自己的无知才能打开学习之门。骄傲是知识的敌人。这个文本模拟了经典文献的结构足够演示索引和检索流程。4.2 编写配置先写 app/config.pyimport os class Config: # 向量数据库目录 CHROMA_PERSIST_DIR ./chroma_db # 知识库原始文件路径 KNOWLEDGE_FILE ./data/knowledge/example_knowledge.txt # 集合名称 COLLECTION_NAME document_qa # 文本切分参数 CHUNK_SIZE 200 CHUNK_OVERLAP 20 # 检索返回的文档块数量 TOP_K 3 # 大模型配置openai 或 ollama LLM_PROVIDER os.getenv(LLM_PROVIDER, ollama) OPENAI_API_KEY os.getenv(OPENAI_API_KEY, ) OPENAI_BASE_URL os.getenv(OPENAI_BASE_URL, https://api.openai.com/v1) OPENAI_MODEL os.getenv(OPENAI_MODEL, gpt-4o-mini) OLLAMA_BASE_URL os.getenv(OLLAMA_BASE_URL, http://localhost:11434/v1) OLLAMA_MODEL os.getenv(OLLAMA_MODEL, llama3.2:3b) config Config()这样做的好处是环境切换时只需要修改环境变量不用改业务代码。4.3 编写向量数据库操作文件 app/database.pyimport chromadb from app.config import config class VectorStore: def __init__(self): self.client chromadb.PersistentClient(pathconfig.CHROMA_PERSIST_DIR) self.collection self.client.get_or_create_collection( nameconfig.COLLECTION_NAME, metadata{hnsw:space: cosine} ) def add_documents(self, chunks, embeddings, metadatasNone): ids [fchunk_{i} for i in range(len(chunks))] self.collection.add( idsids, documentschunks, embeddingsembeddings, metadatasmetadatas ) def search(self, query_embedding, top_k3): results self.collection.query( query_embeddings[query_embedding], n_resultstop_k, include[documents, metadatas, distances] ) return results store VectorStore()这里把 VectorStore 封装成独立类后续如果换成 Milvus只需要修改这一个类。4.4 编写离线索引脚本文件 scripts/build_index.pyimport sys sys.path.append(.) from langchain.text_splitter import RecursiveCharacterTextSplitter from sentence_transformers import SentenceTransformer from app.config import config from app.database import store def load_text(file_path): with open(file_path, r, encodingutf-8) as f: return f.read() def build_index(): # 1. 加载原始文本 raw_text load_text(config.KNOWLEDGE_FILE) # 2. 文本切分 splitter RecursiveCharacterTextSplitter( chunk_sizeconfig.CHUNK_SIZE, chunk_overlapconfig.CHUNK_OVERLAP, separators[\n\n, \n, 。, , , , ] ) chunks splitter.split_text(raw_text) # 3. 向量化 embedding_model SentenceTransformer(sentence-transformers/all-MiniLM-L6-v2) embeddings embedding_model.encode(chunks).tolist() # 4. 写入向量数据库 store.add_documents(chunks, embeddings) print(f索引完成共 {len(chunks)} 个文本块。) if __name__ __main__: build_index()运行索引脚本python scripts/build_index.py预期输出索引完成共 4 个文本块。4.5 编写检索与大模型生成文件 app/retriever.pyfrom sentence_transformers import SentenceTransformer from app.config import config from app.database import store class Retriever: def __init__(self): self.embedding_model SentenceTransformer(sentence-transformers/all-MiniLM-L6-v2) def retrieve(self, query, top_kNone): top_k top_k or config.TOP_K query_embedding self.embedding_model.encode(query).tolist() results store.search(query_embedding, top_ktop_k) return results[documents][0] retriever Retriever()文件 app/generator.pyfrom openai import OpenAI from app.config import config class Generator: def __init__(self): if config.LLM_PROVIDER openai: self.client OpenAI( api_keyconfig.OPENAI_API_KEY, base_urlconfig.OPENAI_BASE_URL ) self.model config.OPENAI_MODEL else: self.client OpenAI( api_keyollama, base_urlconfig.OLLAMA_BASE_URL ) self.model config.OLLAMA_MODEL def generate(self, question, contexts): context_block \n\n.join( [f[{i1}] {text} for i, text in enumerate(contexts)] ) prompt f你是一个严谨的知识库问答助手。请根据以下文档片段回答用户问题。 规则 1. 只使用文档片段中的信息不要编造。 2. 如果文档片段没有相关内容请回答“知识库中未找到相关信息”。 3. 回答末尾列出参考片段编号格式为“参考[n]”。 文档片段 {context_block} 用户问题{question} response self.client.chat.completions.create( modelself.model, messages[ {role: system, content: 你是严谨的知识库问答助手。}, {role: user, content: prompt} ], temperature0.2 ) return response.choices[0].message.content generator Generator()4.6 编写 FastAPI 接口文件 app/schemas.pyfrom pydantic import BaseModel class QueryRequest(BaseModel): question: str class QueryResponse(BaseModel): answer: str contexts: list[str]文件 app/main.pyfrom fastapi import FastAPI from app.schemas import QueryRequest, QueryResponse from app.retriever import retriever from app.generator import generator app FastAPI(titleRAG 知识库问答系统) app.get(/) def root(): return {message: RAG QA System is running.} app.post(/ask, response_modelQueryResponse) def ask_question(req: QueryRequest): # 1. 检索相关文本块 contexts retriever.retrieve(req.question) # 2. 生成回答 answer generator.generate(req.question, contexts) return QueryResponse(answeranswer, contextscontexts)4.7 启动服务并测试启动 FastAPI 服务uvicorn app.main:app --host 0.0.0.0 --port 8000用 curl 测试接口curl -X POST http://localhost:8000/ask \ -H Content-Type: application/json \ -d {question: 智者如何面对仇恨}预期输出类似{ answer: 根据文档片段智者以和平回应敌意以慈悲化解纷争。宽恕是一种力量它让心灵从仇恨的束缚中解脱。\n参考[1], contexts: [ 第一章 和平与宽恕\n宽恕是一种力量它让心灵从仇恨的束缚中解脱。智者以和平回应敌意以慈悲化解纷争。 ] }到这里一个完整可运行的 RAG 问答系统就完成了。你只需要替换知识库文件、调整切分参数和模型选择就能适配不同领域的文档问答场景。5. 常见问题与排查思路RAG 系统在真实项目中的问题远不止“代码跑不通过”更多是效果和稳定性的问题。下面整理高频问题。问题现象常见原因解决思路检索结果与问题不相关Embedding 模型与文档语言不匹配换用与语料同语言的 embedding 模型回答混乱、张冠李戴分块太大或太小语义被割裂调整 chunk_size设置重叠按章节切分模型仍编造答案Prompt 约束不强在 Prompt 中明确“只允许使用文档片段”提高约束上下文超长top_k 过大或 chunk_size 过大减小 top_k压缩分块长度本地模型效果差模型参数量太小换更大的本地模型或改用在线 API中文乱码文件编码不是 UTF-8统一使用 UTF-8 编码读取文件向量库重复添加数据重复运行索引脚本在写入前做去重或清空集合启动时依赖冲突chromadb、sentence-transformers 版本冲突使用虚拟环境固定版本号5.1 检索效果差如何排查检索是 RAG 的地基。如果检索结果本身就不相关大模型再怎么生成也白搭。建议按以下顺序排查打开检索代码直接打印出 top_k 返回的文本片段看看片段和问题是否语义相关。如果不相关先检查文本切分是否把关键信息切碎。再检查 embedding 模型是否适合当前语言。中文语料用英文模型效果通常很差。最后检查向量检索的相似度算法。文本检索优先用余弦相似度。5.2 回答幻觉如何降低即使 RAG 已经给模型提供了上下文模型仍可能“自由发挥”。降低幻觉有几个实用手段Prompt 增加硬性约束“如果没有找到相关信息只输出‘未找到’”。设置更低的 temperature 参数减少随机性。对回答做后处理如果检索到的片段本身不包含问题关键词可以拒绝回答。引入置信度提示要求模型在回答中标注来源片段方便人工判断。6. 最佳实践与工程建议6.1 文本清洗与预处理不要直接把原始 PDF 转出的文本扔进切分器。真实文档往往包含页眉页脚、断行、目录、引用编号等噪声。建议在切分前做一步清洗移除页眉页脚。合并段内的人工换行。删除多余的空白字符。统一标点符号。这些看起来不起眼的操作对检索效果影响非常大。经典文本尤其如此原版排版往往带有中古语言特征清洗不干净会直接污染向量表示。6.2 元数据与引用溯源在向量数据库中不要把文档原文作为唯一存储内容。建议为每个 chunk 额外保存元数据例如来源文件名。章节编号。原书页码。chunk 在原文中的起止位置。这样在返回答案时可以同时返回引用链接或页码帮助用户定位原始内容。对 Sikhbani.ai 这类使用宗教经典文本的场景溯源能力几乎是产品刚需——用户需要看到原文才能确认 AI 回答是否准确。元数据示例store.add_documents( chunks, embeddings, metadatas[ {source: chapter_1, page: 1, section: Peace and Forgiveness} ] )6.3 Embedding 模型选型建议Embedding 模型直接决定检索质量选型时考虑三点语言匹配度。中文语料选中文模型多语料选多语模型。向量维度。维度越高通常精度越好但存储和计算成本也会上升。是否需要本地化。涉及版权、隐私、敏感数据时必须本地部署。另外不要频繁更换 embedding 模型。如果换模型一定要重建整个索引否则旧向量和新模型向量不在同一语义空间检索会失效。6.4 深入优化重排Rerank向量检索拿到的 top_k 不完全可靠有时最相关的片段排不到前几名。生产级系统通常会加一个重排环节先用向量检索召回 20 到 50 个候选片段再用一个更强的排序模型Cross-Encoder精排取前 3 到 5 个最终用于生成。下面是用 sentence-transformers 中 CrossEncoder 做重排的示例思路from sentence_transformers import CrossEncoder reranker CrossEncoder(cross-encoder/ms-marco-MiniLM-L-6-v2) def rerank(query, candidates, top_n3): pairs [(query, doc) for doc in candidates] scores reranker.predict(pairs) ranked sorted(zip(candidates, scores), keylambda x: x[1], reverseTrue) return [doc for doc, _ in ranked[:top_n]]重排会显著提升最终回答质量尤其适合经典文献这种语义密集的文本。6.5 安全与合规注意RAG 工程落地时安全和合规问题必须前置API 密钥不要写死在代码里用环境变量或密钥管理服务。对敏感知识库优先使用本地模型和本地向量库避免数据出域。对经典文本、宗教文本这类内容要确保使用版本授权清晰并尊重原文版权和语境避免断章取义。对话接口要做输入输出过滤防止提示注入攻击比如用户通过“忽略之前指令”等话术试图让模型输出非知识库内容。6.6 效果评估机制很多团队把 RAG 系统上线后就再也不管这是大忌。建议建立一套评估集定期回归测试。评估集包含典型问题、预期答案片段、可接受回答范围。每次调整分块参数、embedding 模型或提示词后跑一遍评估集对比检索命中率和答案质量。简单的评估维度检索命中率预期片段是否出现在 top_k 中。回答完整性关键信息是否覆盖。事实一致性回答是否与原文冲突。引用正确性标注的参考片段是否真实相关。7. 总结与学习路线这篇文章从一个实际项目 Sikhbani.ai 切入完整介绍了基于 RAG 的知识库问答系统构建方法。我们实践了文本加载、分块、向量化、向量数据库存储、检索、Prompt 组装、大模型生成、FastAPI 接口封装的全流程也聊了重排、元数据、效果评估、安全合规这类生产环境必须考虑的问题。如果你是从零开始建议按下面顺序继续深入先跑通本文的基础代码替换你自己的知识库文件体验完整链路。尝试用不同文本切分策略、不同 embedding 模型做对比实验观察检索质量差异。引入重排模型对比加与不加的效果。将向量数据库替换为 Milvus 或 Qdrant学习分布式向量检索。深入学习 LangChain 或 LlamaIndex 的 RAG 抽象了解它们解决了哪些重复问题。深入学习提示工程和模型微调进一步优化生成质量。RAG 本身不是魔法它的质量上限由“文档处理质量”和“检索质量”共同决定。花时间把文本清洗、切分、检索调优做好效果会比盲目换大模型更明显。如果你在构建自己的 AI 问答系统时遇到具体报错或效果问题欢迎在评论区留言后续可以针对某个细节再做专题拆解。
返回列表