
1. 项目缘起为什么从TXT文件开始构建RAG如果你正在关注AI应用开发尤其是检索增强生成RAG技术那么“从零开始构建知识库”这个任务大概率是你绕不开的第一道坎。市面上很多教程一上来就讲如何接入各种向量数据库、如何调用复杂的Embedding模型却常常忽略了一个最朴素、也最关键的起点你的知识原材料到底是什么形态在我过去几年参与和观察的数十个企业级RAG项目中超过70%的原始知识资产最初都是以最普通的TXT文本文件形式存在的。可能是从老旧的CMS系统导出的文章可能是爬虫抓取的网页内容清洗后的结果也可能是内部文档系统批量生成的纯文本报告。因此这个“15天学会AI应用开发”系列的第十一篇聚焦于“从TXT文件构建RAG知识库”我认为是切中了要害。它没有好高骛远地从复杂的PDF解析、图像OCR开始而是选择了最通用、最基础也最能让你理解RAG流水线本质的入口。通过处理TXT文件你可以剥离掉格式解析的干扰专注于RAG最核心的三个环节文本的拆分Chunking、文本的向量化Embedding、以及向量检索Retrieval。掌握好这个流程未来无论面对PDF、Word还是HTML你只需要在前面加上对应的解析器后面的核心流水线是完全复用的。2. 核心流程总览从文本文件到智能问答在深入每个技术细节之前我们先建立一个全局认知。一个基于TXT文件的RAG系统其构建与运行流程可以清晰地分为“离线构建”和“在线查询”两个阶段。理解这个二分法对于后续的架构设计和问题排查至关重要。离线构建阶段知识库准备 这个阶段的目标是将静态的TXT文件转化为可供高效检索的向量索引。它通常是“一次性的”或“周期性的”批处理任务。加载与读取从文件系统或对象存储中读取TXT文件内容。文本分割Chunking将长文本切割成大小适中、语义相对完整的片段Chunk。这是影响检索效果最关键的步骤之一。向量化Embedding使用Embedding模型将每个文本片段转换为一个高维向量一组数字。这个向量在数学空间中的“位置”代表了该文本的语义。存储与索引将文本片段原始文本和其对应的向量向量表示持久化存储到数据库中。通常向量会存入专门的向量数据库如Milvus, Pinecone, Weaviate或支持向量搜索的关系型数据库如PgVector并建立索引以加速检索原始文本则可以存入向量数据库如果支持或普通的键值数据库如Redis甚至本地文件用于后续的召回与展示。在线查询阶段问答服务 这个阶段响应用户的实时提问是RAG系统的服务核心。问题向量化将用户输入的自然语言问题使用与离线阶段相同的Embedding模型转换为查询向量。向量检索在向量数据库中使用近似最近邻ANN搜索算法快速找到与查询向量最相似的若干个文本片段向量。上下文组装将检索到的Top-K个相关文本片段原始文本组合起来形成一段增强的上下文Context。提示工程与生成将用户问题和增强后的上下文按照预设的提示词Prompt模板进行组装发送给大语言模型LLM如GPT-4, Claude, 或开源Llama系列。答案生成与返回LLM基于提供的上下文生成答案最终返回给用户。整个流程的核心思想是“大海捞针”。你的TXT知识库是“大海”Embedding模型和向量索引帮你快速定位到可能藏有“针”答案的“区域”相关文本片段最后LLM从这个区域里精准地找出或总结出“针”。接下来我们就拆解离线构建阶段中最关键的几个实操环节。3. 文本分割的艺术与科学不止是“按句切分”拿到一个TXT文件第一反应是按固定长度比如500个字符或者按句子边界切割在实际操作中这种简单粗暴的方式往往会导致灾难性的后果——检索出来的文本片段支离破碎无法为LLM提供完整的上下文。文本分割是RAG的“地基”地基不稳上层建筑再漂亮也没用。3.1 分割策略的深度考量分割的目标是生成“语义完整”的块。这意味着一个块应该尽可能讲述一个完整的小主题或逻辑单元。以下是几种主流策略及其适用场景固定大小重叠分割这是最基础的方法。设定一个固定长度如512个token和一个重叠长度如50个token。像滑动窗口一样移动切分文本。重叠部分是为了防止一个完整的句子或概念被生硬地切断在两个块之间。优点实现简单对于长度均匀、结构不明显的文本如小说、长篇文章有一定效果。缺点极易破坏段落、列表或代码块的结构。例如一个问题的描述和它的答案可能被切到两个不同的块里。实操参数chunk_size500,chunk_overlap50。这里的chunk_size需要根据你使用的Embedding模型的最大输入长度来调整通常为512或1024并预留一些空间。递归字符文本分割器这是目前社区公认更优的通用策略。它采用一个递归的“由大到小”的分隔符列表。例如优先按“\n\n”双换行通常代表段落分割如果分割后的块仍然太大再按“\n”单换行分割如果还大再按句号、空格等分割直到每个块的大小符合预设范围。优点最大程度地尊重了文档的天然结构段落、标题、列表生成的块语义完整性更高。缺点对于没有明显分隔符的“流水账”文本效果会下降。工具实现LangChain的RecursiveCharacterTextSplitter就是这一策略的经典实现。基于语义的分割这是更前沿的方法使用另一个轻量级的模型或算法来理解文本在语义发生自然转折的地方进行切割。例如使用句子Transformer计算相邻句子的相似度在相似度骤降的地方切分。优点理论上能产生语义最连贯的块。缺点计算开销大速度慢不适合海量文本的批处理算法本身也可能引入噪声。我的经验与建议对于绝大多数从TXT起步的场景递归字符文本分割器是首选。它平衡了效果、速度和实现复杂度。你需要调试的核心参数是chunk_size和chunk_overlap。一个常见的误区是认为块越大越好。实际上过大的块如2000token会导致检索精度下降因为块内包含的无关信息太多稀释了核心语义同时也会在提示词中占用过多宝贵的上下文窗口。我通常从chunk_size500-800开始测试。3.2 分割后的元数据关联切分之后一个容易被忽视但极其重要的步骤是为每个文本块附加元数据Metadata。为什么需要这个想象一下你检索到一个有用的片段但你知道它来自哪个文件在原文的哪个大致位置吗元数据就是这些信息的载体。每个文本块至少应该关联以下元数据source: 源文件名如company_policy_2023.txt。chunk_index: 该块在原文中的顺序索引如15。可选start_line,end_line: 在原文中的起始和结束行号便于溯源。在后续检索时这些元数据可以随文本块一起返回。当LLM生成的答案需要引用来源时你可以清晰地告诉用户“该信息来源于《XX文件》第X部分”这大大增强了系统的可信度和可维护性。在代码中这通常意味着你的数据单元是一个字典{“text”: “分割后的文本内容”, “metadata”: {“source”: “…”, “index”: …}}。4. Embedding模型选型与本地化部署文本被分割成块后下一步就是将它们转化为向量。Embedding模型的质量直接决定了你的知识库的“检索能力上限”。一个糟糕的Embedding模型即使文本分割得再好也无法将相似语义映射到向量空间的相近位置。4.1 开源 vs. 闭源一个关键抉择闭源API如OpenAI的text-embedding-ada-002优点开箱即用效果稳定无需关心部署和算力。对于快速原型验证、数据量不大或对效果要求极高的场景是很好的选择。缺点有持续的使用成本数据需要上传到服务提供商的云端可能涉及数据安全和合规问题存在API调用延迟和速率限制。开源模型如BGE、Sentence Transformers系列优点数据完全私有部署在内网安全可控一次部署无限次使用无持续调用成本可以根据特定领域语料进行微调Fine-tuning获得比通用模型更好的领域内效果。缺点需要一定的运维和GPU资源模型效果需要自行评估和选择。对于“从TXT文件构建”这个主题我强烈建议从开源模型开始。因为这本身就是一个强调可控性和私有化的过程。使用本地部署的Embedding模型能与整个离线构建流水线无缝集成形成完全内网化的数据闭环。4.2 主流开源Embedding模型实战选型截至当前有几个经过大规模实践检验的开源模型值得考虑模型名称发布方核心特点适用场景注意事项BGE (BAAI General Embedding)智源研究院中文嵌入效果公认领先英文也不错有不同尺寸版本如BGE-large-zh-v1.5。中文知识库首选或中英文混合知识库。需要从Hugging Face下载模型文件首次加载较慢。text2vec郎曦轻量级速度快中文效果优秀。对推理速度要求高、资源受限的场景。模型容量相对较小对非常复杂的语义区分可能稍弱。Sentence-BERT (all-MiniLM-L6-v2)UKPLab英文领域的经典标杆轻量80MB速度快。纯英文知识库或作为基线模型对比。对于中文支持不是其原生设计重点。Multilingual-E5Microsoft专为多语言设计在数十种语言上表现均衡。知识库涉及多种语言的场景。模型体积相对较大。我的选择与部署经验如果你的知识库以中文为主BGE系列是当前的不二之选。以BAAI/bge-large-zh-v1.5为例部署和使用非常简单。你可以使用Sentence Transformers库它封装了模型加载、编码和池化等操作。# 安装依赖 # pip install sentence-transformers torch from sentence_transformers import SentenceTransformer # 指定模型名称会自动从Hugging Face下载需网络 model SentenceTransformer(BAAI/bge-large-zh-v1.5) # 编码单个句子 sentence [这是一个测试句子。] embedding model.encode(sentence) print(embedding.shape) # 输出如 (1, 1024)表示1个句子向量维度1024 # 批量编码高效 sentences [段落一的内容。, 段落二的内容。, 段落三的内容。] embeddings model.encode(sentences, batch_size32, show_progress_barTrue)关键提示在调用encode之前根据BGE模型的推荐在输入文本前加上指令前缀会提升检索效果query_instruction “为这个句子生成表示以用于检索相关文章”。对于待存入知识库的文本则不需要加。但在使用SentenceTransformer时其内部可能已做处理具体需查阅模型卡片。4.3 性能优化与批量处理处理成千上万个文本块时效率很重要。启用GPU确保你的环境安装了CUDA版本的PyTorch模型会自动运行在GPU上速度有数量级提升。批量编码如上例所示务必使用model.encode()的批量处理功能而不是循环编码单个句子。batch_size可以根据你的GPU内存调整如16, 32, 64。归一化许多向量数据库如Milvus在进行相似度计算内积或余弦相似度时要求向量是归一化的模长为1。SentenceTransformer的encode方法默认返回的就是归一化后的向量normalize_embeddingsTrue。如果你自己处理务必确认这一点。5. 向量数据库的选型与数据持久化有了文本块和对应的向量我们需要一个专门的家来存放它们并能进行高速的相似度搜索。这就是向量数据库。5.1 轻量级起步ChromaDB对于初学者或个人项目我推荐从ChromaDB开始。它是一个嵌入优先的向量数据库设计理念就是简单易用可以完全在内存中运行也可以持久化到磁盘。它提供了Python原生API与LangChain等框架集成度极高。# 安装 # pip install chromadb import chromadb from chromadb.config import Settings # 创建一个持久化的客户端 client chromadb.PersistentClient(path./my_chroma_db) # 获取或创建一个集合Collection类似数据库的表 collection client.get_or_create_collection(namemy_knowledge_base) # 准备要存入的数据 # 假设 text_chunks 是文本块列表 embeddings 是对应的向量列表 ids [fchunk_{i} for i in range(len(text_chunks))] # 为每个块生成唯一ID metadatas [{source: doc1.txt, index: i} for i in range(len(text_chunks))] # 元数据 # 向集合中添加数据 collection.add( embeddingsembeddings, # 向量列表 documentstext_chunks, # 原始文本列表 metadatasmetadatas, # 元数据列表 idsids # ID列表 ) print(数据已存入ChromaDB。) # 进行相似度查询 query_embedding model.encode([用户提出的问题是什么]) results collection.query( query_embeddingsquery_embedding, n_results3 # 返回最相似的3个结果 ) print(检索到的文档, results[documents]) print(对应的来源, results[metadatas])ChromaDB在背后会自动为你的向量创建索引。对于中小规模的数据集比如十万级以下向量它的性能完全足够且管理成本极低。5.2 面向生产Milvus / Weaviate当你的知识库规模增长到百万甚至千万级向量或者需要分布式、高可用特性时就需要考虑更专业的向量数据库。Milvus国产开源功能强大性能卓越是当前最流行的开源向量数据库之一。它支持多种索引类型IVF_FLAT, HNSW, SCANN等可以针对精度和速度进行精细调优。部署相对复杂通常需要Docker或Kubernetes。Weaviate一个开源的向量搜索引擎除了向量存储还内置了GraphQL接口、模块化设计可以将向量化、分类等步骤作为管道集成并且有云托管服务。它的设计更“全栈”一些。选型建议在“15天”的学习路径上第一天先用ChromaDB快速跑通全流程建立感性认识。在后续深入优化时再考虑将数据迁移到Milvus等数据库。迁移的核心工作是1. 从Chroma中读出所有数据向量、文本、元数据2. 按照Milvus的SDK格式重新写入并建立索引。6. 完整流水线搭建与避坑指南现在让我们把前几个环节串联起来形成一个完整的、可运行的离线知识库构建脚本。这里会遇到很多“坑”我会结合经验一一说明。6.1 一个健壮的构建脚本import os from pathlib import Path from typing import List, Dict, Any from sentence_transformers import SentenceTransformer import chromadb from chromadb.config import Settings # 假设我们使用LangChain的文本分割器它非常成熟 from langchain.text_splitter import RecursiveCharacterTextSplitter class TxtRAGBuilder: def __init__(self, model_name: str BAAI/bge-large-zh-v1.5, persist_dir: str ./chroma_db): 初始化构建器。 Args: model_name: Embedding模型名称。 persist_dir: ChromaDB持久化目录。 # 1. 加载Embedding模型 print(f正在加载Embedding模型: {model_name}...) self.embed_model SentenceTransformer(model_name) # 2. 初始化文本分割器 self.text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个块大约500字符 chunk_overlap50, # 块间重叠50字符 length_functionlen, # 使用字符长度计算 separators[\n\n, \n, 。, , , , , 、, , ] # 递归分割符列表 ) # 3. 初始化ChromaDB客户端 self.chroma_client chromadb.PersistentClient(pathpersist_dir) self.collection self.chroma_client.get_or_create_collection(nameknowledge_base) def load_and_split_txt(self, file_path: str) - List[Dict[str, Any]]: 加载单个TXT文件并进行分割返回包含文本和元数据的字典列表。 try: with open(file_path, r, encodingutf-8) as f: text f.read() except UnicodeDecodeError: # 尝试其他编码 with open(file_path, r, encodinggbk) as f: text f.read() except Exception as e: print(f读取文件 {file_path} 失败: {e}) return [] # 使用分割器 splits self.text_splitter.split_text(text) print(f文件 {Path(file_path).name} 被分割成 {len(splits)} 个块。) # 组装数据 documents [] for idx, chunk in enumerate(splits): doc { text: chunk, metadata: { source: Path(file_path).name, chunk_index: idx, start_char: text.find(chunk) # 记录起始位置近似 } } documents.append(doc) return documents def process_directory(self, dir_path: str): 处理一个目录下的所有TXT文件。 all_docs [] txt_files list(Path(dir_path).glob(**/*.txt)) print(f找到 {len(txt_files)} 个TXT文件。) for file_path in txt_files: docs self.load_and_split_txt(str(file_path)) all_docs.extend(docs) # 批量生成向量效率关键 texts_to_embed [doc[text] for doc in all_docs] print(f开始为 {len(texts_to_embed)} 个文本块生成向量...) embeddings self.embed_model.encode(texts_to_embed, batch_size32, show_progress_barTrue, normalize_embeddingsTrue) # 准备存入ChromaDB的数据 ids [f{doc[metadata][source]}_{doc[metadata][chunk_index]} for doc in all_docs] metadatas [doc[metadata] for doc in all_docs] documents [doc[text] for doc in all_docs] # 存入数据库 print(正在将向量和文本存入向量数据库...) self.collection.add( embeddingsembeddings.tolist(), # ChromaDB接收list documentsdocuments, metadatasmetadatas, idsids ) print(f知识库构建完成共处理 {len(all_docs)} 个文本块。) # 使用示例 if __name__ __main__: builder TxtRAGBuilder() builder.process_directory(./my_txt_documents) # 你的TXT文件目录6.2 实操中必踩的“坑”与解决方案编码问题TXT文件可能使用UTF-8、GBK、GB2312等多种编码。如果统一用utf-8打开遇到中文Windows系统生成的文本常会报UnicodeDecodeError。解决方案像上面脚本中一样使用try-except进行编码回退或者使用chardet库自动检测编码。文本清洗原始TXT可能包含大量空格、换行符、特殊字符如\xa0不换行空格。这些噪声会影响分割和向量化的质量。解决方案在分割前增加一个清洗步骤使用正则表达式或简单的字符串替换清理文本。import re def clean_text(text: str) - str: text re.sub(r\s, , text) # 将多个空白字符含换行替换为单个空格 text text.strip() # 移除其他特殊字符... return text内存溢出当处理数万个文本块时一次性生成所有向量并加载到内存可能导致OOM内存不足。解决方案采用批处理流水线。例如每次处理1000个文件生成向量后立即存入数据库然后清空内存再处理下一批。向量维度不匹配不同的Embedding模型产出不同维度的向量如384维、768维、1024维。一旦知识库建立后续查询必须使用同一个模型否则向量无法进行有意义的相似度比较。解决方案将模型名称作为元数据或配置项持久化在加载查询器时进行校验。重复插入多次运行脚本会导致数据重复。解决方案在add之前先根据id或元数据如source查询是否已存在或者每次构建前清空集合collection.delete(where{...})或重建集合。7. 效果评估与迭代优化知识库建好了但效果如何不能只靠感觉。你需要一套简单的评估方法来指导优化。7.1 构建测试集准备10-20个你认为知识库里应该能回答的问题Q并标注每个问题对应的标准答案A以及答案所在源文本的精确位置如文件名和大致段落。这就是你的“黄金测试集”。7.2 核心评估指标检索召回率Retrieval Recall对于每个问题执行向量检索比如返回Top-5个块。检查标准答案所在的源文本块是否出现在这Top-5的结果中。出现的比例就是召回率。这是评估Embedding和分割质量的核心指标。如果召回率低说明相关文档没被找出来后续LLM再强也没用。检索精度Retrieval Precision在检索返回的Top-K个结果中有多少个是真正与问题相关的这个指标衡量检索结果是否“干净”。如果精度低说明返回了很多噪声会干扰LLM。端到端答案质量将检索到的上下文和问题交给LLM生成答案人工或使用GPT-4等模型对比生成的答案与标准答案的吻合度。这更综合但成本也高。7.3 基于评估的迭代优化如果召回率低按以下顺序排查和优化调整分割策略这是最常见的原因。尝试增大或减小chunk_size调整chunk_overlap。对于技术文档可以尝试按“## ”Markdown二级标题分割。更换Embedding模型如果分割策略调整后效果仍不佳考虑换一个更强大的Embedding模型比如从BGE-base升级到BGE-large。查询增强对用户的问题进行“重写”或“扩展”再用于检索。例如利用LLM将问题“它怎么工作”扩展为“请解释XX系统的工作原理和主要步骤”。这能提升问题与文档的语义匹配度。如果精度低噪声多优化分割可能是chunk_size太大一个块里包含了多个不相关的主题。尝试减小chunk_size。后处理过滤在检索后增加一个步骤使用一个轻量级分类器或规则如关键词匹配对检索结果进行二次过滤剔除明显不相关的结果。调整检索数量不要盲目返回Top-5对于某些问题Top-2可能更干净。可以设计自适应策略。构建RAG知识库不是一个一蹴而就的过程而是一个“构建-评估-优化”的循环。从简单的TXT文件开始让你能更聚焦于这个核心循环而不被复杂的文件解析分散精力。当你用TXT文件跑通了整个流程并调优到一个满意的基线效果后再去集成PDF、Word等解析器就会事半功倍因为后面的流水线都是现成的。