Проект RAG — это не один огромный файл на Python: структура папок, способная масштабироваться
Проект в формате RAG не должен представлять собой один огромный файл на Python. Необходима структура папок для загрузки данных, разбиения на части, генерации эмбеддингов, хранения векторов, поиска, формирования запросов, API и тестов.
Источники: **Your RAG Project Should Not Be One Giant Python File**
Демонстрация работы RAG занимает один день: загружается PDF, он разделяется на части, эти части встраиваются, помещаются в векторную базу данных, задаётся вопрос и считывается ответ. Этого более чем достаточно для эксперимента на выходных. В качестве основы для реальной работы такой подход быстро терпит неудачу, и причина редко кроется в самой модели или даже в векторном хранилище — причина в структуре.
Проекты для начинающих обычно начинаются с одного файла app.py, в котором объединены загрузка файлов, их разбиение на части, генерация векторных представлений, хранение, поиск, шаблоны запросов, вызовы модели, маршруты API, логирование и настройки. Работа с одним файлом кажется удобной, пока проект не начинает расти: появляется новый источник документов, требуется протестировать другую стратегию разбиения на части, ChromaDB заменяется на Pinecone, меняются шаблоны запросов, появляется конечная точка FastAPI, качество поиска оказывается низким, и кто-то требует тестирования. К тому моменту работать с одним файлом становится крайне сложно.
Система, удобная в обслуживании, предоставляет каждой функции своё место. Механизм ввода загружает данные, разбиение на части позволяет разделить документы, эмбеддинги преобразуют текст в векторы, база данных векторов хранит их, механизм поиска находит полезный контекст, промпты формируют запросы, слой LLM выполняет вызовы модели, а 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 является входной дверью в проект. В нем должно быть описано, что делает система, как её настроить и запустить, а также для чего предназначены основные папки. Новички часто откладывают его написание, но хороший документ помогает понять собственный проект уже через месяц. К минимуму следует включить цель проекта, шаги настройки, необходимые переменные среды, способы загрузки документов, инструкции по запуску API, примеры вопросов и известные ограничения. Для проекта в портфолио это ещё более важно, поскольку рецензент может бегло просмотреть его перед тем, как решить, стоит ли что-либо запускать.
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, папок 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). Если понадобится заменить базу данных, достаточно изменить именно этот слой, не затрагивая остальное.
retrieval/
Здесь поступает вопрос, и выбираются наиболее релевантные фрагменты. Базовый способ поиска — это простой поиск по сходству, тогда как более сложные версии включают фильтры метаданных, гибридный поиск, переранжирование, переписывание запроса или поиск с использованием нескольких запросов. Это имеет огромное значение, поскольку модель может давать качественные ответы только при наличии правильного контекста, а слабый поиск приводит к слабым ответам.
Файл 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/
Здесь размещаются небольшие вспомогательные функции, используемые во всем проекте: загрузка конфигураций, форматирование логов, очистка текста, проверка путей, измерение времени выполнения и обработка распространённых ошибок. Соблюдайте дисциплину, ведь эта папка легко превращается в свалку. Функции, относящиеся к процессу ввода данных, должны находиться в папке ingestion, а те, что связаны с их извлечением — в папке retrieval. Используйте utils только для того, что действительно предназначено для общего использования.
tests/
Тесты отделяют серьёзные проекты от хрупких демо-версий, а системы RAG состоят из множества небольших компонентов, которые можно тестировать по отдельности. Вы можете проверить, правильно ли загружаются PDF-файлы, соответствуют ли размеры блоков заданным параметрам, сохраняются ли метаданные, создаются ли векторные представления, возвращаются ли при извлечении релевантные документы, содержат ли промпты как контекст, так и вопрос, и соответствует ли API ожидаемой структуре.
Не обязательно обеспечивать идеальное покрытие с первого дня, но необходимо достаточно данных, чтобы понимать, когда что-то сломалось. Тесты повторного поиска особенно ценны: храните небольшой набор образцов документов и вопросов, определите, какой источник должен быть возвращён для каждого из них, и перезапускайте их после любых изменений в настройках разбиения на части или встраивания. Это позволяет получить базовую информацию о качестве до того, как пользователи обнаружат проблемы.
logs/
Логи из продакшена могут поступать в инструменты мониторинга, но простой файл логов полезен во время разработки. Логи должны отвечать на вопросы: что спрашивал пользователь, какие части были возвращены, какие источники использовались, сколько времени занял поиск и какая модель была задействована. Когда пользователь говорит, что ответ был неверным, необходимо увидеть, что на самом деле было получено и какой запрос был отправлен.
Сохранение компактности файла main.py
Точка входа должна оставаться простой. Она запускает сервер API, загружает конфигурацию или вызывает основную функцию обработки данных, и больше ничего. Простая точка входа — это признак того, что реальная работа находится в соответствующих модулях.
Что это дает вам
- Отладка: выявление причины сбоя. Если результат неверен, проверьте процесс получения данных, затем их разбиение на части и генерацию векторных представлений. Если API не работает, проверьте слой маршрутизации.
- Тестирование: возможность проверки каждого компонента отдельно, что позволяет убедиться, что для тестирования загрузчика PDF не требуется запуск всего приложения.
- Масштабирование: улучшение одного слоя без переписывания остальных, будь то добавление пакетной обработки данных, фоновых задач, кэширования, переранжирования или использование новой векторной базы данных.
- Сотрудничество: один человек может улучшать процесс ввода данных, в то время как другой занимается формулировкой запросов или маршрутами.
Прежде всего это делает проект понятным, а проект, который легко понять, вызывает больше доверия.
Правило для первых проектов
Используйте по одной папке на каждую задачу и не стремитесь к идеальной архитектуре. Просто избегайте одного огромного файла. Начните с загрузки данных, их разбиения на части, генерации эмбеддингов, использования векторной базы данных, поиска информации, формирования запросов, вызовов больших языковых моделей, API, тестирования и ведения логов. Этого достаточно для большинства проектов начинающих и для портфолио. Дополнительные компоненты, такие как переранжирование результатов, оценка качества, кэширование, аутентификация, фоновые задачи, версионирование документов и панели управления, можно добавлять по мере появления необходимости, при этом основная идея остается неизменной: сохраняйте разделение процессов обработки данных и самого приложения. Полезной привычкой является запись в файл README информации о том, какую папку следует изменять для каждого типа изменений, поскольку если ответ на этот вопрос становится неясным, это, скорее всего, означает, что структура начала размываться.
Демонстрация показывает, что идея может сработать. Проект, поддающийся обслуживанию, демонстрирует возможность роста системы. Папки сами по себе не являются целью; важно найти четкое место для каждой задачи.
Связанные статьи
- Рабочий процесс локального агента LLM, построенный на папках и файлах Markdown — Создание многоэтапной локальной системы агентов с контролем доступа, где каждый этап представлен папкой, запросы формируются в формате Markdown и человек проверяет результат перед выполнением дорогостоящих действий.
- Агентный подход в RAG: позвольте LLM решать, когда и где получать информацию — Почему статический подход RAG не справляется с вопросами, требующими нескольких шагов или оперативных ответов, и как агент LangGraph осуществляет переход между системами Qdrant и веб-поиском с использованием ограничений и предостережений.