最近知识星球中新加入的一些新手盆友,又集中问到了如何快速入门 RAG 的问题。我早在今年三月初就在星球中做过相关答疑回复。简单来说,从个人 24 年年初开始一路的实践(踩坑)经验来看,通过原生开发的方式进行入门,无疑是种更加务实的学习路线。
这篇结合最近写的几篇书稿的章节内容,试图说清楚:
RAG 技术栈生态、框架与平台选型策略、企业规章制度问答场景拆解、原生 RAG 问答系统的完整技术实现,以及工程经验和架构演进梳理。
1、RAG 技术栈全景
RAG 的技术生态并非一个扁平的技术集合,而是一个可以自下而上理解的多层次结构。每一层提供了不同程度的抽象和封装,以满足开发者在不同场景下对开发效率、灵活性和系统性能的权衡。
需要说明的是,下述划分中的层级间并非严格的单向依赖关系,跨层组合使用很常见,层级更多是功能分工。此外,某些工具具有跨层特性,可能同时承担多个层级的功能。不同层次的工具和平台相辅相成,共同构成了从开发到应用的完整技术体系。理解这个分层架构,是做出明智技术选型的第一步。
1.1、基础层 ((Foundation Layer)
提供关键的编程环境和科学计算支持,可为所有上层应用的开发、训练和推理提供最基础的计算能力。代表技术有 Python(主流开发语言)、NumPy(数值计算库)、PyTorch(深度学习框架)等。
1.2、组件层 (Components Layer)
由实现单一具体功能的底层库组成,是构建 RAG 工作流程的基础组件。代表技术:PyPDF2(PDF 内容提取)、Transformers(模型库)、FAISS(向量检索)等。可直接调用这些库,以最大灵活度手动实现 RAG 流程的每个步骤。
1.3、工具层 (Tools Layer)
该层级提供针对 RAG 特定环节的专业化、生产级解决方案,产品化程度更高。代表技术:Pinecone(生产级向量数据库)、MinerU(复杂文档解析)、Ragas(RAG 应用评估)等。
1.4、框架层 (Framework Layer)
该层级是高度封装的集成框架,提供标准化接口来串联和管理底层组件与工具。代表技术:LangChain、LlamaIndex、Haystack 等。可以像组合积木一样快速搭建和切换不同的 RAG 配置,在开发效率和灵活性间取得平衡。
1.5、应用层 (Application Layer)
该层级抽象层次最高,以可视化界面和开箱即用平台的形式存在。代表技术:RAGFlow、Dify、Flowise 等。将技术实现细节完全封装,通过拖拽和配置即可快速构建完整的 RAG 应用,极大降低技术门槛。但同样不好之处在于对新手是个黑箱,实际调试过程会相对麻烦。
2、框架与平台选型策略
技术选型的本质在于在多重约束条件下寻找最适合的平衡点。从组件层理解原理,到框架层快速迭代,再到工具层优化关键环节的渐进式演进路径,已经是RAG过去大半年一线从业者逐步形成的共识。无论选择哪种技术栈,关键在于明确当前阶段的核心目标,并为未来的技术演进预留足够的灵活性。
3、制度问答场景拆解
企业规章制度类文档(以下简称“制度文档”)天然具有格式异构性,存储格式通常是由制度文档的性质和用途决定。例如,为了保证格式的统一性和内容不可篡改,正式发布的《员工手册》通常是以 PDF 格式归档;而对于需要内部流转与频繁修订的《差旅报销细则》,则一般是多以 DOCX 格式存在。一言以蔽之,这种也算是个典型的多源异构的文档问答场景。是 RAG 问答比较适合先去落地解决的问题。
在正式开始介绍技术实现前,先给各位大致看下后续问答测试所使用的两份文档。这两份文档也是今年 2 月底我最早一批做的项目中的原始材料(已脱敏)。员工手册.pdf 文档中包括了人力资源管理的各个方面,包括双通道职级体系(专业序列 P1-P6,管理序列 M1-M5)、工作时间考勤、假期管理、薪酬福利和绩效评估等制度。
差旅报销细则.docx 文档中包括基于职级和城市等级(一、二、三线)的差旅费用标准体系,包括交通工具选择权限、住宿费用上限和餐饮通讯补贴标准。
后续技术实现部分的两个问题测试,也试图从实际场景出发,分别检验系统在单文档信息定位和跨文档信息整合场景方面的表现。
4、技术实现
4.1、核心架构
以下按照四个核心模块展开:全局配置与参数管理、文档解析与结构化分块、向量化与索引存储系统、检索与生成核心引擎。
4.2、全局配置与参数管理
config.py 作为系统的配置中心,采用集中化管理策略,实现配置与业务逻辑的解耦,确保系统的可维护性和灵活性。系统支持本地 Ollama 和在线 SiliconFlow 两种部署模式,通过环境变量和模型标识符进行区分,其核心配置代码如下。
# API密钥配置 SILICONFLOW_API_KEY = os.getenv("SILICONFLOW_API_KEY") # 本地模式配置 LOCAL_EMBEDDING_MODEL = "BAAI/bge-small-zh-v1.5" LOCAL_LLM_MODEL = "qwen3:30b" # 根据配置进行调整 OLLAMA_BASE_URL = "http://localhost:11434" # 在线模式配置 ONLINE_EMBEDDING_MODEL = "Qwen/Qwen3-Embedding-8B" ONLINE_LLM_MODEL = "deepseek-ai/DeepSeek-R1" SILICONFLOW_BASE_URL = "https://api.siliconflow.cn/v1"注:Ollma 上的问答模型中,在中等尺寸模型中 Qwen3:30B 效果较为出色,有条件的可以切换成这个模型进行测试。配置有限也可换成 8b 尺寸。
直接影响检索效果和问答质量的关键参数,在本系统中的具体设定如下。
文本分块配置CHUNK_SIZE = 600 # 文本块目标大小 CHUNK_OVERLAP = 120 # 相邻块重叠长度检索调优配置DEFAULT_RETRIEVAL_K = 5 # 检索返回数量 DEFAULT_RETRIEVAL_THRESHOLD = 0.4 # 相似度过滤阈值这些参数的设定遵循以下原则:CHUNK_SIZE 设为 600 字符,既保证了语义的完整性,又控制了向量化的计算开销;CHUNK_OVERLAP 设为 120 字符(约 20%重叠率),有效防止重要信息在块边界处被切断;检索参数 K=5 和阈值 0.4 的组合,在保证召回质量的同时控制了上下文长度。通过这种集中化配置策略,后续各模块只需引入相应参数即可,大幅提升了系统的可配置性和维护效率。
4.3、文档解析与结构化分块
这个模块采用混合分块方案:在语义分块的基础上结合递归字符分块,形成“先语义预分块,后递归精细切分”的两阶段处理策略。具体实现由 document_parser.py 和 text_chunker.py 两个脚本协同完成。前者负责基于文档结构特征的语义预分块,后者则采用递归字符分块算法进行精细化处理。
语义预分块
PDF 文档处理采用 PyMuPDF 库进行文本提取,通过正则表达式识别章节标题模式,实现按章节的结构化分割。该策略在 _load_pdf_document 函数中的核心代码实现如下。
def _load_pdf_document(file_path: str) -> list[LangchainDocument]: doc = fitz.open(file_path) # 提取全文并按章节分割 chapter_pattern = r'第[一二三四五六七八九十\d]+章[::].' chapter_matches = list(re.finditer(chapter_pattern, full_text)) # ... 按章节位置精确分割逻辑DOCX 文档处理通过 pypandoc 转换为 Markdown 格式,保留结构信息并按一级标题进行语义分割。该处理逻辑在 _load_docx_document 函数中核心代码实现如下。
def _load_docx_document(file_path: str) -> list[LangchainDocument]: # 转换为Markdown保留结构 md_content = pypandoc.convert_file(file_path, 'gfm', format='docx') # 按一级标题分割 chunks = re.split(r'(^#\s[^#].*)', md_content, flags=re.MULTILINE)系统通过统一的入口函数根据文件类型自动选择解析策略。这个调度逻辑由 load_document 函数实现,其核心代码实现如下。
def load_document(file_path: str) -> list[LangchainDocument]: file_extension = os.path.splitext(file_path)[1].lower() if file_extension == ".docx": return _load_docx_document(file_path) elif file_extension == ".pdf":递归精细切分
递归精细切分阶段由 text_chunker.py 执行,使用 RecursiveCharacterTextSplitter 对预分块结果进行精细化处理。该分割器按照双换行符、单换行符、空格的优先级递归切分,有效保护句子完整性。该功能的核心实现封装在 split_documents 函数中,其核心代码实现如下。
def split_documents(documents: List[Document]) -> List[Document]: text_splitter = RecursiveCharacterTextSplitter( chunk_size=CHUNK_SIZE, # 400字符目标大小 chunk_overlap=CHUNK_OVERLAP, # 80字符重叠防止切断 add_start_index=True # 记录原文位置索引 ) return text_splitter.split_documents(documents)两阶段分块策略的核心优势在于结合了语义保持和检索优化。通过这种两阶段处理策略,系统将异构的制度文档高效转换为标准化的 Document 对象列表,为后续向量化流程提供高质量的结构化输入。
4.4、向量化与索引存储系统
核心实现位于 vector_indexer.py 脚本中,主要包含双模式文本向量化、FAISS 索引构建与优化、以及索引持久化机制三个功能模块。文本向量化部分不做赘述了,着重介绍后两部分。
FAISS 索引构建
系统选用 Meta AI(原 Facebook AI Research)开源的 FAISS 库构建向量索引,VectorIndexer 类的 build_index 方法实现索引构建的完整流程。该方法集成了批量向量化、索引创建和 L2 归一化等关键步骤,其核心代码实现如下。
# 批量向量化与索引构建 embeddings = self.embedding_model.encode(texts) embedding_dim = embeddings.shape[1] # 创建内积索引并执行L2归一化 self.index = faiss.IndexFlatIP(embedding_dim) faiss.normalize_L2(embeddings) # 归一化后内积等价于余弦相似度 self.index.add(embeddings.astype(np.float32))注:IndexFlatIP 是 FAISS 中的内积索引类型(IP 即 Inner Product,通过计算向量间的内积来衡量相似性)。L2 归一化是将向量长度标准化为 1 的数学操作,经过 L2 归一化后,两个向量的内积运算在数学上等价于余弦相似度计算。余弦相似度专门用于衡量向量方向的一致性,忽略向量长度差异,因此更适合度量文本语义相似性
索引持久化
为避免重复执行耗时的向量化和索引构建,系统实现了索引持久化机制。save_index 方法将构建完成的索引和元数据分别存储。该方法将 FAISS 索引与文档数据分离存储,以确保高效读写,其核心代码实现如下。
# 分离存储FAISS索引和文档数据 faiss.write_index(self.index, f"{self.index_path}.faiss") with open(f"{self.index_path}_docs.pkl", 'wb') as f: pickle.dump({ 'documents': [doc.page_content for doc in self.documents], 'metadata': self.document_metadata }, f)注:FAISS 索引以二进制格式(即计算机直接读取的 0 和 1 编码格式,相比文本格式具有存储紧凑、读取速度快的优势)保存为.faiss 文件。由于 FAISS 专门针对向量数据优化,仅存储数值向量而不包含原始文本,因此系统需要单独保存文本内容和元数据。pickle 是 Python 的对象序列化工具,能将复杂的数据结构(如包含文本和字典的列表)转换为字节流并保存为.pkl 文件,实现完整数据结构的持久化存储。对应的 load_index 方法在系统启动时从磁盘快速恢复索引器的完整状态。
4.5、检索与生成模块
检索与生成核心模块负责处理员工查询的完整流程:从向量检索到答案生成。由 retriever.py 和 generator.py 两个脚本构成,分别承担检索上下文和生成答案的职责,协同完成 RAG 系统的核心问答流程。
文档检索器
retriever.py 脚本中的 DocumentRetriever 类负责从 FAISS 索引中高效检索与员工问题语义最相关的文档块。核心的 retrieve 方法实现了完整的检索流程:
它首先调用与索引构建时相同的嵌入模型将查询文本向量化,并进行 L2 归一化以匹配索引格式;随后,在 FAISS 索引中执行相似度搜索,获取 Top-K 个最相似的文档块;最后,应用配置的相似度阈值对结果进行过滤,并将最终结果封装为 RetrievalResult 对象返回。该方法封装了这一完整的检索逻辑,其核心代码实现如下。
# retrieve方法核心逻辑 def retrieve(self, query: str, k: int, score_threshold: float): # 调用向量索引器执行语义搜索 raw_results = self.indexer.search(query, k=k) # 应用相似度阈值过滤低质量结果 if score_threshold is not None: raw_results = [r for r in raw_results if r['score'] >= score_threshold] # 封装检索结果并返回检索指标 return results, metrics答案生成器
generator.py 脚本中的 AnswerGenerator 类负责整合检索信息并调用大模型生成最终答案。PromptTemplate 类负责构建结构化的大模型输入。build_prompt 方法将检索到的文档块格式化为清晰的上下文,与员工问题一同填入以下预定义模板。
# 员工指令模板示例 DEFAULT_USER_TEMPLATE = """基于以下文档内容,请回答员工的问题。 **员工问题:** {query} **相关文档内容:** {context} **请提供准确、有条理的回答:**"""generate_answer 方法实现完整的生成过程。首先调用 PromptTemplate 构建完整提示词,然后传递给对应模式的大模型客户端获取生成结果。
# generate_answer方法核心逻辑 def generate_answer(self, query: str, retrieval_results: List[RetrievalResult]): # 构建结构化提示词 system, user_prompt = PromptTemplate.build_prompt(query, retrieval_results) # 调用大模型生成答案 answer, prompt_tokens, completion_tokens = self.llm_client.generate( system, user_prompt ) # 封装生成结果并返回完整指标 return GenerationResult(...)5、交互界面问题测试
我选择了 Gradio 库快速构建一个 Web 界面,整体界面采用双栏布局,主要目的是方便对分块效果,召回结果以及思维链和最终回答的对比展示。先初始化界面上传“差旅报销细节.docx”和“员工手册.pdf”两份制度文档,系统自动完成解析、分块和索引构建。以下通过两个测试问题,分别检验系统在不同层面的能力。
5.1、差旅餐费补贴查询
在问题输入框中,输入第一个测试问题:“部门主管可以选择什么舱位的机票?一天餐费是多少钱?”
这个测试问题需要系统同时获取部门主管的交通工具权限标准和餐饮补贴费用信息两个维度的数据。提交问题后,系统在右侧召回片段详情区域展示了三个高度相关的文档块:片段 1(相似度 0.613)精准召回了餐饮补贴标准表格,清晰显示各城市等级的费用标准;片段 2(相似度 0.604)准确定位到交通工具标准条款,明确了部门主管的舱位选择权限;片段 3(相似度 0.549)提供了任职标准的补充信息。
左侧答案显示区域呈现了系统的完整回答,展现了清晰的思维链推理过程:首先明确“部门主管”的职级定义,然后分别针对机票舱位和餐费标准给出精确答案。系统准确识别了部门主管可选择经济舱或在特定条件下预订公务舱,并根据出差城市等级提供了 150 元、120 元、100 元的差异化餐费标准,充分验证了单文档信息整合和精准回答的能力。
5.2、跨文档职级住宿查询
接下来测试更具挑战性的问题:“M3 级别员工去上海出差住宿标准”。这个问题看似更简单,但实际需要系统进行跨文档信息整合,从不同文档中获取职级定义和住宿标准信息。系统展现了出色的跨文档检索能力,召回了 5 个高度相关的文档片段,相似度分数分布在 0.543 至 0.620 区间。
系统的跨文档推理过程清晰可见:首先从片段 5“员工手册.pdf”(相似度 0.543)中准确识别出“M1-M2:项目经理/部门主管,M3:部门总监”的职级对应关系,确定 M3 级别对应部门总监;然后从片段 2“差旅报销细则.docx”(相似度 0.620)中获取城市分级信息,明确上海属于一线城市;最后从片段 1 的住宿标准表格中查找到部门总监在一线城市的住宿标准为 1000 元/晚。左侧答案显示区域完整展现了这三步推理过程,系统准确执行了“职级映射→城市分级→标准匹配”的逻辑链条,最终给出了精确的住宿费用标准。
6、工程经验与架构演进
6.1、工程经验
config.py 的集中化配置策略在实践中展现出显著优势,支持本地 Ollama 与云端 SiliconFlow 的无缝切换,使相同业务逻辑能够适配不同技术栈。
双模式架构设计体现了重要的工程价值。本地模式提供完全自主可控的解决方案,适合数据安全要求严格的环境;云端模式获得更强的模型能力和服务稳定性。这种设计为技术验证提供了渐进式路径,开发者可先用云端 API 验证业务逻辑,成熟后再切换到本地部署。
原生实现使每个技术环节完全可见、可控制,从 PyMuPDF 的文档解析到 FAISS 的向量检索,所有核心逻辑都以最直接方式呈现。当系统出现问题时,透明的实现方式使问题定位变得简单,每个模块的输入输出都是标准 Python 对象,便于添加调试信息和修改处理逻辑。这种可控性在原型开发和算法调优阶段具有不可替代的价值。
6.2、架构演进
当前原生实现在处理复杂业务逻辑时暴露出开发成本高、功能扩展困难等问题。每增加一个新功能都需要从底层开始实现,如添加多轮对话支持需要自建会话管理、实现混合检索需要整合多种算法。成熟的 RAG 框架通过标准化的组件和接口,能够显著降低这些高级功能的实现复杂度,让开发者将更多精力集中在业务逻辑优化上。
LangChain 作为当前最主流的 RAG 开发框架,提供了完整的组件生态系统。迁移路径可以采用渐进式策略:首先用 LangChain 的 Document Loaders 替代自建的文档解析器,利用其丰富的格式支持;然后采用 VectorStores 统一向量存储接口,获得更好的扩展性;最后通过 Chains 将检索和生成逻辑串联,实现更复杂的 RAG 工作流。LangChain 的最大优势在于其丰富的集成能力和活跃的社区生态,能够快速适配各种模型和向量数据库。
LlamaIndex 专门针对文档理解和知识问答场景进行了深度优化,在处理结构化文档和复杂查询方面表现突出。其提供的 Index 抽象层能够更好地处理文档间的层次关系,Query Engine 支持多种高级检索策略。对于需要深度文档理解的应用场景,LlamaIndex 往往能提供更精准的检索和生成效果。
实际迁移过程中建议采用混合策略,保留原生实现中验证有效的核心逻辑,逐步引入框架组件替代复杂度高的部分。这种方式既能享受框架带来的开发效率提升,又能保持对关键逻辑的控制力。
7、写在最后
在 Agent 和 Context Engineering 概念满天飞的时候,回过头来重温下原生 RAG 似乎有些逆潮流。但正本清源或许是更加务实的做法。anyway,下周进一步介绍利用 LangChain、LlamaIndex 等框架,结合简历筛选分析场景,构建更加一个复杂 RAG 应用,感兴趣的可以蹲一蹲。然后9月第一周更新鸽了很久的京东joyagent二开案例演示。
项目源码及文档已上传至知识星球,欢迎加入和200位一线从业者交流实践