RAG项目并非一个庞大的Python文件:可扩展的文件夹结构
RAG项目不应是一个庞大的Python文件,而应采用合理的文件夹结构,分别用于数据导入、分块处理、嵌入生成、向量存储、检索、提示词设计、API接口以及测试。
来源: **Your RAG Project Should Not Be One Giant Python File**
一个RAG演示只需一个下午的时间:加载PDF文件、将其分割、嵌入嵌入向量、将这些向量存入向量数据库、提出问题、读取答案。这样的流程足以用于周末的小实验。但若要用于实际工作,它很快就会出现问题,而问题往往不出在模型或向量存储上,而在于结构。
初学者的项目通常从一个包含文件加载、分块处理、嵌入生成、数据存储、检索功能、提示词模板、模型调用、API接口、日志记录及配置设置的app.py文件开始。在项目规模尚小时,单个文件确实方便管理。但随着项目发展,新的文档来源出现,需要尝试不同的分块策略,ChromaDB被替换为Pinecone,提示词也需要调整,还会出现新的FastAPI接口,检索质量不佳,还有人要求添加测试功能。到那时,再想修改那个单一的文件就变得十分困难了。
一个易于维护的系统会为每个功能模块分配独立的处理空间。数据摄取模块负责加载数据,分块模块用于拆分文档,嵌入模块将文本转换为向量,向量数据库则存储这些向量,检索模块用于查找有用的上下文,提示词模块负责构建请求,大语言模型层负责调用模型,而API则将所有功能呈现给用户。正是这种分离机制让演示版本转变为真正的工程系统。
一种思维模型:流水线与应用程序
可以这样理解:流水线负责准备知识,应用程序则利用这些知识。两者应当保持分离。
流水线部分负责加载文档、清洗文本、拆分内容、生成嵌入向量并存储它们;而应用程序部分则负责接收问题、检索相关数据片段、构建提示词、调用模型、通过API返回答案,以及记录生产环境中的运行行为。
将各种元素混在一起时,任何改动都存在风险。尝试新的数据块大小不应需要修改 API 路由;从一种模型提供者切换到另一种也不必重写数据检索逻辑;添加网站内容导入功能更不应影响提示词模板。
初始布局
这是一个可作为初稿的布局。它并非唯一的可行方案,但每个文件夹的存在都有明确的理由。
rag-project/
|-- README.md
|-- requirements.txt
|-- .env
|-- .gitignore
|-- config.yaml
|-- main.py
|-- src/
| |-- ingestion/
| | |-- __init__.py
| | `-- loader.py
| |-- chunking/
| | |-- __init__.py
| | `-- chunker.py
| |-- embeddings/
| | |-- __init__.py
| | `-- embedder.py
| |-- vectordb/
| | |-- __init__.py
| | `-- vector_store.py
| |-- retrieval/
| | |-- __init__.py
| | `-- retriever.py
| |-- prompts/
| | |-- __init__.py
| | `-- prompt_templates.py
| |-- llm/
| | |-- __init__.py
| | `-- llm_client.py
| |-- api/
| | |-- __init__.py
| | `-- routes.py
| `-- utils/
| |-- __init__.py
| `-- helpers.py
|-- tests/
| `-- test_app.py
`-- logs/
`-- app.log
README.md
README就像项目的入口。其中应说明系统的作用、如何安装与运行,以及各个主要文件夹的功能。初学者常常忽视编写README,但实际上一份优质的README能在一个月后帮助你更好地理解自己的项目。至少应包含项目的目的、安装步骤、所需的环境变量、文档导入方式、API运行方法、示例问题以及已知的限制。对于作品集而言,README更为重要,因为评审者在决定是否进一步测试之前可能会先浏览它。
requirements.txt
该文件列出了项目所需的Python包。一个简单的RAG服务可能包含类似以下的内容。
fastapi
uvicorn
python-dotenv
pydantic
langchain
chromadb
sentence-transformers
openai
pypdf
你的依赖列表取决于你所使用的技术栈。原则是不要在自己的机器上隐藏任何依赖项。任何克隆该项目的人都应该能够通过一条命令安装所有依赖。
pip install -r requirements.txt
这样可以让项目具有可复现性,同时也更便于部署和审查。
.env 和 .gitignore
.env 文件用于存储 API 密钥等敏感信息。一个典型的文件内容如下所示。
OPENAI_API_KEY=your_key_here
VECTOR_DB_URL=your_vector_db_url
它绝不能被上传到 GitHub,因此你的 .gitignore 文件应将其与日志和字节码一起排除在外。
.env
logs/
__pycache__/
*.pyc
在 Python 文件中硬编码密钥可能在本地可行,但并不安全。应将敏感信息置于代码之外。
config.yaml
非敏感设置应放在 config.yaml 中,例如:
chunking:
chunk_size: 800
chunk_overlap: 120
retrieval:
top_k: 5
models:
embedding_model: text-embedding-3-small
llm_model: gpt-4.1-mini
vector_db:
provider: chromadb
collection_name: company_docs
RAG系统需要调整参数,你可能需要更改分块大小、重叠比例、需检索的结果数量、模型名称、温度参数以及向量数据库的相关设置。当这些参数分散在不同的随机文件中时,调整工作会变得十分繁琐。相关设置应当便于查找和修改。
代码文件夹
ingestion/
该文件夹可从PDF文件、网站、CSV文件、API接口、数据库、Markdown格式内容、Notion导出的数据、Google Drive文件夹以及任何其他来源读取数据。它的功能并非回答问题,而是以整洁且可预测的格式将原始内容导入系统。loader.py文件可能会提供诸如load_pdf(path)、load_website(url)、load_csv(path)和load_database_table(connection, table_name)之类的函数,每个函数都会以统一的标准格式返回文档,通常包括文本和元数据。
元数据应得到比目前更多的重视。一段文本固然有用,但若包含其来源名称、页码、URL、文档类型、作者及时间戳,其价值便会大幅提升,因为这些数据有助于实现过滤、引用、调试以及建立信任。
分块/
分块看似简单,却对质量有着重大影响。过大的分块会返回充满无关内容的冗长段落,而过小的分块则会丢失模型所需的上下文。分块的目标是保留含义,因此不应盲目每500个字符就分割一次,而应考虑按照标题、段落、章节、页面、代码块、表格或语义边界来划分。chunker.py可将完整文档拆分成适合嵌入的片段。
将此逻辑单独处理是非常值得的,因为这是提升RAG系统性能的最快捷方法之一。由于分块功能是独立存在的,因此可以在不影响其他部分的情况下测试新的策略。
embeddings/
嵌入向量是文本的数值表示形式,能让系统比较文本含义:问题会被转换为嵌入向量,然后与对应的嵌入分块进行匹配。embedder.py应负责统一处理模型加载、单个输入及批量输入的嵌入生成、速率限制控制、失败重试以及记录生成向量的模型版本等信息。
这种分离非常重要,因为嵌入模型会不断变化。你可能最初使用本地的sentence-transformer模型,之后转为使用托管的API,又可能因为成本或质量原因更换服务提供商。只要将相关逻辑集中在一处,每次变更都便于管理。
vectordb/
该文件夹用于存储嵌入向量及元数据。常见的选择包括:用于本地实验的 FAISS、适用于简单本地环境或小型生产环境的 ChromaDB、用于托管式云搜索的 Pinecone,以及针对更大规模或更复杂需求而设计的 Weaviate、Qdrant 或 Milvus。
vector_store.py 应当将数据库的具体实现细节与项目其他部分隔离开来。检索代码无需了解 Pinecone 或 ChromaDB 的详细信息,而应调用项目层面的通用方法,如 add_documents(chunks)、similarity_search(query, top_k)、delete_collection(name) 和 get_document_by_id(id)。一旦需要更换数据库,只需修改这一层代码即可,无需改动其他部分。
检索/
此时会有问题输入,系统会筛选出最相关的内容。基本的检索方式就是简单的相似度搜索,而更高级的版本则会加入元数据过滤、混合搜索、重排序、查询重写或多查询检索等功能。这一点至关重要,因为只有当模型获得正确的上下文时才能给出优质答案,而低效的检索机制只会导致答案质量低下。
retriever.py 可以负责对查询进行嵌入处理、在存储库中搜索、应用过滤条件、进行重排序、去除重复内容,最终为提示词提供干净的上下文。将这两者分开处理能极大简化调试过程,因为这样就可以判断问题是出在模型本身,还是检索机制提供了错误的上下文。如果两者混在一起,就很难给出明确的答案。
prompts/
提示词不应分散在代码库的各个地方。prompt_templates.py 可以用来存储类似这样的模板。
You are a helpful assistant answering questions using the provided context.
Use only the context below. If the answer is not in the context, say you do not know.
Context:
{context}
Question:
{question}
Answer:
将提示词保存到单独的文件中是值得尽早养成的习惯。随着你对指令、输出格式、引用风格、安全规则或拒绝机制的不断尝试,这些提示词会频繁变化。如果将它们嵌在路由或模型调用中,就很难进行版本控制和测试。应将其视为设计的一部分,并为其分配专门的文件。
llm/
无论使用的是 OpenAI、Claude、Gemini、本地的 Ollama 模型还是其他类型,所有语言模型调用都应集中存放在一处。应用程序的其他部分无需关心这些细节。llm_client.py 可以处理初始化、API 调用、重试、超时设置、温度参数调节、响应解析、令牌使用情况记录以及针对不同提供商的错误处理。模型更新速度很快:今天可能是 GPT,明天可能是 Claude,下个月则可能有适合简单查询的较小模型和适合复杂任务的更强模型。集中化的调用方式能让模型切换变得十分方便。
api/
这一层允许其他人和应用程序使用该系统,通常通过 FastAPI 或 Flask 实现。它应当接收并验证请求,调用检索与生成流程,返回结构化的响应,并妥善处理错误。它本身不应包含业务逻辑。每个路由应保持简洁:接收请求,调用相应的服务,返回结果。如果一个接口端点同时承担数据导入、分块处理、嵌入操作、检索、提示词构建以及模型调用等功能,那么该项目将变得难以维护。API 应该只是入口,而非整个系统。
utils/
项目中常用的小型辅助工具都放在这里,比如配置加载、日志格式化、文本清洗、路径验证、计时功能以及常见的错误处理。务必保持规范,否则这个文件夹很容易变成垃圾堆。属于数据摄取阶段的工具应放入摄取相关目录,属于检索阶段的工具则放入检索相关目录。只有真正需要共享的代码才可使用 utils 目录。
tests/
测试能够将成熟的项目与不稳定的演示版本区分开来,而 RAG 系统由许多可以逐一测试的小组件构成。你可以检查 PDF 是否能成功加载、数据块大小是否合理、元数据是否保留、嵌入向量是否生成、检索结果是否包含相关文档、提示词是否同时包含上下文和问题,以及 API 返回的数据结构是否符合预期。
第一天并不需要做到完美覆盖,但至少要能够及时发现问题。检索测试尤为重要:准备少量样本文档和问题,明确每个问题应返回哪些来源,同时在分块或嵌入设置发生任何变化后重新运行这些测试。这样在用户发现问题之前就能获得基本的质量反馈。
日志/
生产环境中的日志可以接入监控工具,但在开发阶段简单的日志文件就足够用了。日志应记录用户提出了什么问题、返回了哪些分块、使用了哪些来源、检索耗时多久以及调用了哪个模型。当用户称答案有误时,就需要查看实际检索到了什么内容以及发送了什么提示语。
保持main.py简洁
入口点应保持简单。它仅负责启动 API 服务器、加载配置或调用主处理函数,仅此而已。简洁的入口点意味着实际工作集中在正确的模块中。
这样做的好处
- 调试:能够快速定位故障点。如果结果错误,可依次检查数据检索、分块处理以及嵌入生成环节;若 API 失败,则需检查路由层。
- 测试:可以单独测试每个组件,因此验证 PDF 加载器时无需启动整个应用。
- 扩展性:无需重写其他部分即可优化某一层,比如添加批量处理、后台任务、缓存机制、重新排序功能或新的向量数据库。
- 协作:一人负责数据导入优化,另一人则可专注处理提示词或路由设计。
最重要的是,这能让项目更易于理解,而易于理解的项目也更容易获得信任。
首个项目的准则
为每项职责设置一个文件夹,无需追求完美的架构设计,只要避免出现巨大的单个文件即可。从数据导入、分块处理、嵌入向量、向量数据库、检索功能、提示词生成、LLM调用、API接口、测试以及日志记录开始构建。这些内容已足以满足大多数初学者和作品集项目的需求。随着需求出现,可以逐步添加重排、评估、缓存、身份认证、后台任务、文档版本控制以及控制面板等额外功能,但核心原则始终不变:将处理流程与应用程序分开。一个有助于推进工作的良好习惯是在README中注明每种变更需要修改的文件夹,因为如果答案变得模糊不清,往往意味着架构结构已经开始混乱。
演示可以证明某个想法是可行的,而一个易于维护的项目则能体现系统具备发展潜力。创建文件夹本身并非目的,为每项职责指定明确的归属地才是关键。
相关阅读
- 基于文件夹和 Markdown 文件构建的本地 LLM 智能体工作流 — 构建一个分阶段、可控制的本地智能体流程,每个阶段对应一个文件夹,提示词为可编辑的 Markdown 格式,并在执行昂贵操作前由人工审核输出结果。
- 智能体驱动的 RAG:让 LLM 决定何时何地检索信息 — 静态 RAG 为何无法处理多跳查询和时效性强的问题,以及 LangGraph 智能体如何在不同数据源之间进行路由,同时附有相关限制与注意事项。