第4篇:RAG系统架构设计 —— 从文档上传到智能问答的全链路

第4篇:RAG系统架构设计 —— 从文档上传到智能问答的全链路
第4篇RAG系统架构设计 —— 从文档上传到智能问答的全链路RAG检索增强生成是Agent获取外部知识最核心的手段也是当前Agent系统中应用最广泛的技术之一。本文从零开始设计一个完整的RAG知识库问答系统涵盖文档解析、文本分块策略、向量化存储以及检索-生成的全链路架构通过清晰的模块划分展示可扩展的系统设计。一、RAG的本质用“检索”弥补“生成”的缺陷大语言模型虽然强大但存在三个固有缺陷知识截止训练数据截止后的事件模型不知道幻觉问题模型会“编造”看起来合理但错误的信息私有数据访问公司内部文档、个人笔记等数据模型从未见过RAGRetrieval-Augmented Generation的核心思路是在用户提问时先从外部知识库中检索相关的文档片段然后将这些片段作为“上下文”附加到用户问题中一起交给LLM生成答案。这样答案基于检索到的真实信息而非模型记忆中的“二手知识”。RAG的价值可以量化为对比维度纯LLM生成RAG增强生成知识时效性受限于训练数据截止日期可实时更新知识库幻觉率较高尤其是长尾知识显著降低有上下文约束可溯源能力无法追溯信息来源可标注引用来源私有数据支持不支持天然支持更新成本需重新训练/微调仅需更新向量库二、RAG系统的五层架构一个完整的RAG系统可以划分为五个核心模块每一层都有明确的职责边界用户请求 │ ▼ ┌─────────────────────────────────────────────────┐ │ Layer 5: 问答与生成层 │ │ - 构建增强Prompt │ │ - 调用LLM生成答案 │ │ - 格式化返回答案 引用来源 Token统计 │ └─────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ Layer 4: 检索层 │ │ - 将用户问题向量化 │ │ - 在向量数据库中检索相似文档块 │ │ - 可选重排序Rerank提升检索精度 │ └─────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ Layer 3: 向量存储层 │ │ - ChromaDB / Pinecone / FAISS │ │ - 存储文档的向量表示 元数据 │ │ - 支持相似度检索余弦距离/欧氏距离 │ └─────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ Layer 2: 文档处理层 │ │ - PDF / TXT / Markdown 解析 │ │ - 文本分块Chunking │ │ - 向量化Embedding │ └─────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ Layer 1: 数据接入层 │ │ - 文件上传Web界面 / API │ │ - 文件格式校验类型/大小 │ │ - 增量索引管理 │ └─────────────────────────────────────────────────┘关键设计原则每一层只负责自己的职责不越界。Layer 1不关心如何分块Layer 2不关心如何存储Layer 3不关心如何生成答案。这种清晰的边界划分使得系统易于扩展——当你需要替换向量数据库时只需要修改Layer 3的实现其他层不受影响。三、文档处理层解析与分块策略3.1 文档解析不同文件格式需要不同的解析器文件格式解析工具说明PDFPyPDFLoader/UnstructuredPDFLoader提取文本保留页面信息TXTTextLoader直接读取编码需指定UTF-8MarkdownTextLoader/UnstructuredMarkdownLoader可直接读取或解析结构# 根据文件扩展名选择解析器 def load_document(file_content: bytes, filename: str) - List[str]: ext os.path.splitext(filename)[1].lower() with tempfile.NamedTemporaryFile(deleteFalse, suffixext) as tmp: tmp.write(file_content) tmp_path tmp.name try: if ext .pdf: loader PyPDFLoader(tmp_path) else: loader TextLoader(tmp_path, encodingutf-8) documents loader.load() return documents finally: os.unlink(tmp_path)3.2 文本分块Chunking分块是RAG系统中最容易被忽视但影响深远的设计决策。分块质量直接影响检索精度和生成质量。分块参数的核心权衡参数小块200-300字符中块500-800字符大块1000字符检索精度高噪声少匹配精准中低噪声多匹配模糊上下文完整性低可能切断关键信息中高保留完整段落检索延迟快向量小中慢向量大推荐配置通用场景splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个块约500字符 chunk_overlap50, # 块间重叠50字符防止跨块信息丢失 separators[\n\n, \n, 。, , , , , , ], length_functionlen, )分块的最佳实践重叠Overlap避免关键信息恰好被分割在两个块的边界。重叠的字符在两个块中都出现保证任意一端都能获得完整信息。分隔符优先级优先在段落边界\n\n分割其次是句子边界。最后才是空格。这保证了语义完整性。按语义而非固定长度对于代码或技术文档可以按函数/类边界分割对于聊天记录可以按对话轮次分割。四、向量存储层从文本到向量的转换4.1 Embedding模型的选择Embedding模型将文本转换为固定维度的向量语义相似的文本在向量空间中距离更近。Embedding模型维度大小中文效果适用场景BAAI/bge-small-zh-v1.5512~400MB优秀个人项目、资源有限BAAI/bge-m31024~2GB最佳生产环境、多语言text-embedding-3-small1536API良好云服务、无本地资源paraphrase-multilingual-MiniLM-L12-v2384~120MB中等嵌入式设备建议个人项目优先使用bge-small-zh-v1.5在精度、速度和资源消耗之间取得最佳平衡。4.2 ChromaDB的持久化与操作# 初始化持久化客户端 import chromadb from sentence_transformers import SentenceTransformer client chromadb.PersistentClient(path./chroma_db) collection client.get_or_create_collection(knowledge_base) # 生成向量并存储 model SentenceTransformer(BAAI/bge-small-zh-v1.5) embeddings model.encode(documents, normalize_embeddingsTrue).tolist() collection.add( ids[str(uuid.uuid4()) for _ in documents], documentsdocuments, metadatas[{source: filename, chunk_index: i} for i in range(len(documents))], embeddingsembeddings )元数据过滤是ChromaDB的一个重要能力可以在检索时缩小搜索范围# 只检索特定文档的块 results collection.query( query_embeddings[query_embedding], n_results5, where{source: important_doc.pdf} )五、检索层从问题到相关文档块检索层的核心任务是将用户问题转换为向量在向量数据库中查找最相似的文档块。def search_similar(query: str, top_k: int 4) - List[Dict]: query_embedding model.encode(query, normalize_embeddingsTrue).tolist() results collection.query( query_embeddings[query_embedding], n_resultstop_k, include[documents, metadatas, distances] ) items [] for i, doc in enumerate(results[documents][0]): items.append({ content: doc, metadata: results[metadatas][0][i], score: 1 - results[distances][0][i] # 距离转相似度 }) return items重要细节ChromaDB返回的是距离distance值越小越相似。使用1 - distance可得到相似度分数0-1之间越大越相似。余弦距离和余弦相似度互为补数。六、问答与生成层构建增强Prompt检索到相关文档块后需要将其组装成结构化的PromptSYSTEM_PROMPT 你是一个专业的知识库问答助手。请根据以下上下文回答用户的问题。 规则 1. 只根据提供的上下文回答问题不要使用你自己的知识。 2. 如果上下文不包含相关信息请回答根据现有文档无法回答该问题。 3. 回答结尾必须列出引用的来源编号格式如(来源: [1][2])。 def build_prompt(question: str, contexts: List[Dict]) - str: context_str for i, ctx in enumerate(contexts, 1): source ctx[metadata].get(source, unknown) context_str f[{i}] (来自: {source})\n{ctx[content]}\n\n return f{SYSTEM_PROMPT}\n\n上下文\n{context_str}\n用户问题{question}\n回答Prompt设计的三个关键原则明确边界告诉模型只根据上下文回答防止模型使用自己的知识补充或编造处理空检索当检索结果为空时模型应当明确告知而不是强行回答强制引用来源要求模型在回答末尾标注引用编号实现答案可溯源七、API层的完整整合from fastapi import FastAPI, File, UploadFile, HTTPException from app.document_processor import process_document from app.vector_store import add_documents, search_similar from app.rag_chain import ask_question app FastAPI() app.post(/upload) async def upload_document(file: UploadFile File(...)): # 1. 文档解析与分块 texts, metadatas process_document(await file.read(), file.filename) # 2. 向量化与存储 chunk_count add_documents(texts, metadatas) return {success: True, chunk_count: chunk_count} app.post(/ask) async def ask(request: AskRequest): # 1. 检索 contexts search_similar(request.question) # 2. 构建Prompt并调用LLM result ask_question(request.question, contexts) return {answer: result[answer], sources: result[sources]}八、完整RAG系统的工作流总览[用户上传] PDF文件 │ ▼ [文档解析] PyPDFLoader提取文本 │ ▼ [文本分块] RecursiveCharacterTextSplitter → 多个文本块 │ ▼ [向量化] bge-small-zh-v1.5 → 每个块生成512维向量 │ ▼ [存储] ChromaDB持久化到磁盘 │ ▼ [用户提问] 什么是RAG │ ▼ [问题向量化] 同一个Embedding模型 │ ▼ [向量检索] ChromaDB查询 → 返回Top-4最相似块 │ ▼ [Prompt构建] 上下文 问题 系统提示 → 完整Prompt │ ▼ [LLM生成] DeepSeek API → 生成答案 │ ▼ [格式化返回] 答案 引用来源 Token统计思考与动手建议使用不同大小的chunk_size200、500、1000对同一文档建立索引然后提问同一问题观察答案质量的差异。在metadatas中添加更多字段如author、date尝试使用元数据过滤缩小检索范围。尝试用不同的Embedding模型如bge-m3替换bge-small-zh对比检索精度的变化。