简介:本文详细介绍如何从零开始构建基于RAG(Retrieval-Augmented Generation)的文档检索系统,涵盖文档解析、索引构建、检索优化等核心环节。通过分步骤的代码实现与架构解析,帮助开发者掌握RAG技术原理并完成可落地的检索系统开发。
本教程旨在帮助开发者掌握RAG技术核心原理,通过构建基于层次化索引的文档检索系统,实现以下能力:
适用场景:
技术基础:
开发环境:
pip install dataclasses-json typing-extensions数据准备:
系统采用模块化设计,包含五大核心组件:
数据流说明:
mkdir pageindex-ragcd pageindex-ragpython -m venv venvsource venv/bin/activate # Linux/Mac# 或 venv\Scripts\activate (Windows)mkdir pageindextouch pageindex/{__init__.py,node.py,parser.py,indexer.py,retriever.py,storage.py,main.py}touch document.md
from dataclasses import dataclass, fieldfrom typing import Optional, Listfrom dataclasses_json import dataclass_json@dataclass_json@dataclassclass PageNode:title: str # 节点标题content: str # 原始文本内容(仅叶子节点有效)summary: str = "" # LLM生成的摘要depth: int = 0 # 节点深度(0=根节点)children: List['PageNode'] = field(default_factory=list)parent: Optional['PageNode'] = Nonedef is_leaf(self) -> bool:"""判断是否为叶子节点"""return len(self.children) == 0def add_child(self, child_node: 'PageNode'):"""添加子节点"""child_node.parent = selfself.children.append(child_node)
from typing import Listfrom .node import PageNodedef parse_document(raw_text: str, max_depth: int = 3) -> PageNode:"""递归解析文档为层次树结构Args:raw_text: 原始文档文本max_depth: 最大解析深度Returns:PageNode: 构建好的层次树根节点"""# 示例:使用正则表达式提取标题(实际项目中建议使用markdown解析库)import resections = re.split(r'(#+\s+.+?)\n', raw_text)root = PageNode(title="ROOT", depth=0)current_node = rootstack = []for i, section in enumerate(sections):if not section.strip():continue# 检测标题级别(示例简化逻辑)level = len(re.match(r'#*', section.split('\n')[0].strip()).group())if level == 0:continue# 创建新节点title = section.split('\n')[0].strip('# ').strip()new_node = PageNode(title=title, depth=level)# 处理层级关系while stack and stack[-1][1] >= level:stack.pop()if stack:parent_node = stack[-1][0]parent_node.add_child(new_node)else:root.add_child(new_node)stack.append((new_node, level))# 填充内容(简化处理)if i+1 < len(sections) and not sections[i+1].startswith('#'):content_lines = []j = i+1while j < len(sections) and not sections[j].startswith('#'):content_lines.append(sections[j])j += 1new_node.content = '\n'.join(content_lines)i = j-1return root
from .node import PageNodefrom typing import Dict, Anyimport jsondef generate_summaries(node: PageNode, llm_api_call: callable) -> None:"""递归生成节点摘要Args:node: 当前处理节点llm_api_call: LLM调用接口(示例中省略具体实现)"""if node.is_leaf():# 叶子节点:直接生成摘要node.summary = llm_api_call(f"总结以下文本:\n{node.content}")else:# 内部节点:汇总子节点摘要child_summaries = [child.summary for child in node.children if child.summary]if child_summaries:node.summary = llm_api_call(f"综合以下章节摘要生成父章节总结:\n" + "\n---\n".join(child_summaries))def serialize_index(root: PageNode) -> Dict[str, Any]:"""序列化索引为JSON结构"""def node_to_dict(node: PageNode) -> Dict:return {"title": node.title,"summary": node.summary,"depth": node.depth,"children": [node_to_dict(child) for child in node.children]}return {"root": node_to_dict(root),"metadata": {"node_count": count_nodes(root),"max_depth": get_max_depth(root)}}def count_nodes(node: PageNode) -> int:"""计算节点总数"""return 1 + sum(count_nodes(child) for child in node.children)def get_max_depth(node: PageNode) -> int:"""获取树的最大深度"""if not node.children:return node.depthreturn max(get_max_depth(child) for child in node.children)
from .node import PageNodefrom typing import Optional, Listdef retrieve_context(root: PageNode, query: str, llm_api_call: callable) -> Optional[str]:"""基于语义的路径导航检索Args:root: 索引树根节点query: 用户查询llm_api_call: LLM调用接口Returns:匹配的上下文文本"""current_node = rootpath = [current_node]while True:if current_node.is_leaf():break# 获取子节点摘要用于路径选择child_summaries = [(child.title, child.summary) for child in current_node.children]prompt = f"根据以下查询和章节摘要,选择最相关的子章节:\n\n" \f"查询:{query}\n\n" \f"选项:\n" + "\n".join(f"{i+1}. {title}\n 摘要:{summary[:100]}..."for i, (title, summary) in enumerate(child_summaries))# 模拟LLM选择(实际应调用LLM接口)selected_index = 0 # 这里简化处理,实际应解析LLM返回的选择if selected_index >= len(current_node.children):return Nonecurrent_node = current_node.children[selected_index]path.append(current_node)return current_node.content
def mock_llm_api(prompt: str) -> str:
“””模拟LLM调用(实际应替换为真实API调用)”””
return f”LLM生成的摘要(基于提示:{prompt[:50]}…)”
def main():
# 1. 加载文档with open("document.md", "r", encoding="utf-8") as f:doc_text = f.read()# 2. 解析文档doc_tree = parse_document(doc_text)# 3. 生成摘要generate_summaries(doc_tree, mock_llm_api)# 4. 序列化索引index_data = serialize_index(doc_tree)with open("document_index.json", "w", encoding="utf-8") as f:json.dump(index_data, f, ensure_ascii=False, indent=2)# 5. 执行检索测试query = "请解释RAG技术的核心优势"context = retrieve_context(doc_tree, query, mock_llm_api)print(f"检索到的上下文:\n{context[:200]}...")
if name == “main“:
main()
2. **运行验证**:```bashpython main.py
成功运行后应生成:
document_index.json:包含完整索引结构常见问题:
解析错误:
markdown-it-py)摘要质量差:
检索路径错误:
优化建议:
性能优化:
准确性提升:
可扩展性设计:
本教程完整实现了从文档解析到语义检索的全流程,关键收获包括:
后续可探索方向:
通过持续优化索引结构和检索策略,该系统可支持千万级文档的高效检索,为企业知识管理提供强大技术支撑。