从零构建RAG文档问答系统:技术栈与实现方案详解

引言

在人工智能快速发展的今天,如何让AI模型基于特定文档内容进行准确回答,成为了一个重要的技术挑战。传统的问答系统往往存在”幻觉”问题,即模型会生成看似合理但实际不准确的信息。为了解决这个问题,我们构建了一个基于RAG(Retrieval-Augmented Generation)技术的文档问答系统。

本文将详细介绍这个项目的技术栈选择、架构设计、实现方案以及开发过程中的关键决策。

项目概述

项目源代码: https://github.com/xhuaustc/rag-qa-system

rag 流程图

我们的RAG文档问答系统具有以下核心特性:

  • 🔍 多格式文档支持: PDF、DOCX、Markdown、TXT等
  • 🤖 多LLM后端: Ollama、OpenAI、Azure OpenAI
  • 📝 智能文档分块: 支持中英文混合文本的智能分块
  • 🔗 向量检索: 基于ChromaDB的高效向量检索
  • 💬 智能问答: 基于文档内容的智能问答
  • ⚙️ 灵活配置: 支持环境变量和代码配置
  • 🛠️ 模块化设计: 清晰的模块分离和扩展性

技术栈选择

核心框架

LangChain: 作为我们的核心框架,LangChain提供了丰富的LLM集成、文档处理和向量存储功能。它支持多种LLM提供商,并且有良好的扩展性。

python
1
2
3
4
5
# 核心依赖
langchain>=0.1.0
langchain-community>=0.0.10
langchain-openai>=0.0.5
langchain-ollama>=0.1.0

ChromaDB: 选择ChromaDB作为向量数据库,主要考虑其以下优势:

  • 轻量级,易于部署
  • 支持本地存储,无需额外服务
  • 良好的Python集成
  • 支持元数据过滤

LLM提供商

我们支持三种主要的LLM提供商:

  1. Ollama: 本地部署,隐私保护,成本可控
  2. OpenAI: 云端服务,性能稳定,功能丰富
  3. Azure OpenAI: 企业级服务,合规性好

文档处理

python
1
2
3
4
5
6
# 文档处理依赖
markdown>=3.5.0
pypdf>=3.17.0
python-docx>=1.1.0
docx2txt>=0.8
unstructured>=0.11.0

系统架构设计

整体架构

我们的系统采用模块化设计,主要包含以下几个核心模块:

plaintext
1
2
3
4
5
6
7
ollama/
├── config/ # 配置管理
├── doc_proc/ # 文档处理
├── embed/ # 嵌入处理
├── llm/ # LLM客户端
├── utils/ # 工具模块
└── main.py # 主程序

核心模块详解

1. 配置管理模块 (config/)

使用Python的dataclasses模块构建了层次化的配置系统:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@dataclass
class LLMConfig:
"""LLM客户端配置"""
provider: str = field(default_factory=lambda: os.getenv("LLM_PROVIDER", "ollama"))
model: str = field(default_factory=lambda: os.getenv("LLM_MODEL", "qwen3:8b"))
embedding_model: str = field(default_factory=lambda: os.getenv("LLM_EMBEDDING_MODEL", "shaw/dmeta-embedding-zh"))
api_key: Optional[str] = field(default_factory=lambda: os.getenv("LLM_API_KEY"))
base_url: str = field(default_factory=lambda: os.getenv("LLM_BASE_URL", "http://localhost:11434"))

@dataclass
class AppSettings:
"""应用程序设置"""
llm: LLMConfig = field(default_factory=LLMConfig)
storage: StorageConfig = field(default_factory=StorageConfig)
document_processing: DocumentProcessingConfig = field(default_factory=DocumentProcessingConfig)

这种设计的优势:

  • 类型安全: 使用dataclass提供类型检查
  • 环境变量集成: 自动从环境变量读取配置
  • 默认值: 提供合理的默认配置
  • 验证: 支持配置验证逻辑

2. LLM客户端模块 (llm/)

采用工厂模式和策略模式设计LLM客户端:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class BaseLLMClient(ABC):
"""LLM客户端基类"""

@abstractmethod
def generate_text(self, prompt: str, **kwargs) -> Dict[str, Any]:
"""生成文本"""
pass

@abstractmethod
def generate_embeddings(self, texts: List[str]) -> List[List[float]]:
"""生成嵌入向量"""
pass

class OllamaLLMClient(BaseLLMClient):
"""Ollama LLM客户端"""

def __init__(self, config: LLMConfig):
self.config = config
self._initialize_components()

def _initialize_components(self) -> None:
"""初始化Ollama组件"""
self.llm = OllamaLLM(model=self.config.model, base_url=self.config.base_url)
self.embeddings = OllamaEmbeddings(model=self.config.embedding_model, base_url=self.config.base_url)

class LLMClient:
"""LLM客户端工厂"""

def __init__(self, provider: str = "ollama", **kwargs):
self.provider = provider
self.config = LLMConfig(provider=provider, **kwargs)
self._client = self._create_client()

def _create_client(self) -> BaseLLMClient:
"""创建具体的LLM客户端"""
if self.provider == "ollama":
return OllamaLLMClient(self.config)
elif self.provider == "openai":
return OpenAILLMClient(self.config)
else:
raise ValueError(f"不支持的LLM提供商: {self.provider}")

3. 文档处理模块 (doc_proc/)

文档处理模块负责文档的加载、分块和预处理:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class MultilingualChunker:
"""多语言文档分块器"""

def __init__(self, chunk_size: int = 1000, chunk_overlap: int = 200):
self.chunk_size = chunk_size
self.chunk_overlap = chunk_overlap
self.text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
separators=["\n\n", "\n", "。", "!", "?", ".", "!", "?", " ", ""]
)

def clean_base64_data(self, text: str) -> str:
"""清理文本中的Base64数据"""
# 移除Base64编码的图片数据
base64_patterns = [
r'data:image/[^;]+;base64,[A-Za-z0-9+/=]+',
r'[A-Za-z0-9+/]{50,}={0,2}' # 长Base64字符串
]

cleaned_text = text
for pattern in base64_patterns:
cleaned_text = re.sub(pattern, '[图片数据]', cleaned_text, flags=re.IGNORECASE)

return cleaned_text.strip()

def split_documents(self, documents: List[Document]) -> List[Document]:
"""分割文档"""
processed_docs = []

for doc in documents:
# 清理Base64数据
cleaned_content = self.clean_base64_data(doc.page_content)
doc.page_content = cleaned_content

# 分割文档
chunks = self.text_splitter.split_text(cleaned_content)

for i, chunk in enumerate(chunks):
chunk_doc = Document(
page_content=chunk,
metadata={
**doc.metadata,
"chunk_id": i,
"total_chunks": len(chunks)
}
)
processed_docs.append(chunk_doc)

return processed_docs

4. 嵌入处理模块 (embed/)

嵌入处理模块负责文档的向量化和存储:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class EmbeddingProcessor:
"""嵌入处理器"""

def __init__(self, collection_name: str, llm_provider: str = "ollama", **kwargs):
self.collection_name = collection_name
self.llm_client = LLMClient(provider=llm_provider, **kwargs)
self.chroma_client = chromadb.PersistentClient(path=settings.storage.chroma_db_path)
self._setup_collection()

def _setup_collection(self) -> None:
"""设置ChromaDB集合"""
try:
# 尝试获取现有集合
try:
self.collection = self.chroma_client.get_collection(name=self.collection_name)
logger.info(f"已获取现有集合: {self.collection_name}")
except:
# 如果集合不存在,创建新集合
self.collection = self.chroma_client.create_collection(name=self.collection_name)
logger.info(f"已创建新集合: {self.collection_name}")
except Exception as e:
logger.error(f"集合设置失败: {e}")
raise

def process_documents(self, documents: List[Document]) -> None:
"""处理文档并存储到向量数据库"""
# 生成嵌入向量
texts = [doc.page_content for doc in documents]
embeddings = self.llm_client.generate_embeddings(texts)

# 准备元数据
metadatas = []
ids = []

for i, doc in enumerate(documents):
metadata = {
**doc.metadata,
"embedding_model": self.llm_client.config.embedding_model,
"processed_at": datetime.now().isoformat()
}
metadatas.append(metadata)
ids.append(f"doc_{i}_{hash(doc.page_content) % 1000000}")

# 存储到ChromaDB
self.collection.add(
embeddings=embeddings,
documents=texts,
metadatas=metadatas,
ids=ids
)

logger.info(f"成功处理并存储 {len(documents)} 个文档块")

def query_similar(self, query: str, n_results: int = 5) -> List[Dict]:
"""查询相似文档"""
# 生成查询嵌入
query_embedding = self.llm_client.generate_embeddings([query])[0]

# 向量检索
results = self.collection.query(
query_embeddings=[query_embedding],
n_results=n_results
)

# 格式化结果
formatted_results = []
for i in range(len(results['documents'][0])):
formatted_results.append({
'content': results['documents'][0][i],
'metadata': results['metadatas'][0][i],
'distance': results['distances'][0][i] if 'distances' in results else None
})

return formatted_results

关键技术实现

1. 智能文档分块策略

文档分块是RAG系统的关键环节。我们采用了以下策略:

  • 递归字符分割: 使用RecursiveCharacterTextSplitter,支持多种分隔符
  • 多语言支持: 针对中英文混合文本优化分隔符
  • 元数据保持: 在分块过程中保持原始文档的元数据
  • Base64清理: 自动清理文档中的Base64编码图片数据

2. 向量检索优化

  • 相似度计算: 使用余弦相似度进行向量匹配
  • 元数据过滤: 支持基于文档类型、来源等元数据进行过滤
  • 结果排序: 按相似度降序排列检索结果

3. 错误处理机制

我们实现了分层的错误处理系统:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class BaseError(Exception):
"""基础异常类"""
pass

class LLMClientError(BaseError):
"""LLM客户端错误"""
pass

class DocumentProcessingError(BaseError):
"""文档处理错误"""
pass

class EmbeddingError(BaseError):
"""嵌入处理错误"""
pass

4. 配置管理

支持多种配置方式:

  • 环境变量配置
  • 代码配置
  • 配置文件

开发过程中的关键决策

1. 模块化设计

决策: 将系统拆分为多个独立模块
理由:

  • 提高代码可维护性
  • 便于单元测试
  • 支持功能扩展
  • 降低模块间耦合

2. 抽象基类设计

决策: 为LLM客户端和文档加载器设计抽象基类
理由:

  • 统一接口规范
  • 便于添加新的提供商
  • 提高代码复用性

3. 配置验证

决策: 在配置类中添加验证逻辑
理由:

  • 及早发现配置错误
  • 提供清晰的错误信息
  • 避免运行时错误

4. 日志系统

决策: 实现统一的日志系统
理由:

  • 便于调试和监控
  • 记录关键操作
  • 支持不同日志级别

性能优化

1. 批量处理

文档处理和嵌入生成采用批量处理方式,减少API调用次数:

python
1
2
3
4
def process_documents(self, documents: List[Document]) -> None:
# 批量生成嵌入向量
texts = [doc.page_content for doc in documents]
embeddings = self.llm_client.generate_embeddings(texts)

2. 向量数据库优化

  • 使用持久化存储,避免重复计算
  • 支持增量更新
  • 元数据索引优化

3. 内存管理

  • 流式处理大文档
  • 及时释放不需要的对象
  • 控制批处理大小

部署和使用

环境要求

bash
1
2
3
# Python 3.8+
# 内存: 至少4GB
# 存储: 根据文档数量确定

快速开始

bash
1
2
3
4
5
6
7
8
9
10
# 1. 安装依赖
pip install -r requirements.txt

# 2. 配置环境变量
export LLM_PROVIDER=ollama
export LLM_MODEL=qwen3:8b
export LLM_BASE_URL=http://localhost:11434

# 3. 运行系统
python main.py

使用示例

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from embed.embedding import EmbeddingProcessor

# 初始化处理器
processor = EmbeddingProcessor(
collection_name="my_docs",
llm_provider="ollama",
llm_model="qwen3:8b"
)

# 处理文档
processor.process_directory("docs/")

# 问答
query = "如何配置系统?"
results = processor.query_similar(query, n_results=3)

未来扩展方向

1. 功能扩展

  • 多模态支持: 支持图片、音频等多媒体文档
  • 实时更新: 支持文档的实时更新和增量处理
  • 用户界面: 开发Web界面,提升用户体验
  • API服务: 提供RESTful API接口

2. 性能优化

  • 分布式处理: 支持大规模文档的分布式处理
  • 缓存机制: 实现智能缓存,提高查询速度
  • 索引优化: 优化向量索引结构

3. 智能化提升

  • 自动分块优化: 基于内容语义的智能分块
  • 查询理解: 改进查询理解和重写
  • 答案生成: 优化答案生成的质量和准确性

总结

通过这个RAG文档问答系统的开发,我们深入理解了RAG技术的核心原理和实现细节。项目的成功关键在于:

  1. 合理的架构设计: 模块化、可扩展的架构
  2. 技术栈选择: 成熟稳定的技术栈
  3. 错误处理: 完善的错误处理和日志系统
  4. 性能优化: 针对性的性能优化措施
  5. 用户体验: 简单易用的接口设计

这个项目不仅解决了实际的文档问答需求,也为后续的AI应用开发提供了良好的基础架构。通过持续优化和扩展,我们可以构建更加智能和强大的AI应用系统。

参考资料


本文由 AI 辅助生成,如有错误或建议,欢迎指出。