Команда Python for Devs подготовила практическое руководство по сборке полноценной RAG-системы из пяти open source-инструментов. MarkItDown, LangChain, ChromaDB, Ollama и Gradio превращают разрозненные документы в умную базу знаний с потоковой генерацией ответов. Всё локально, без облаков и с открытым кодом — попробуйте собрать свой ChatGPT прямо у себя.
Бывало, вы тратили по полчаса, просматривая ветки Slack, вложения к письмам и общие диски, лишь чтобы найти ту самую техническую спецификацию, о которой коллега упоминал на прошлой неделе?
Это типичный сценарий, который ежедневно повторяется в компаниях по всему миру. Специалисты тратят ценное время на поиск информации, которая должна быть доступна мгновенно, и в итоге теряют продуктивность.
Системы Retrieval-Augmented Generation (RAG) решают эту проблему, превращая ваши документы в интеллектуальную, доступную по запросам базу знаний. Задавайте вопросы на естественном языке и получайте мгновенные ответы со ссылками на источники, избавляясь от долгих ручных поисков.
В этой статье мы построим полноценный RAG-пайплайн, который превратит коллекции документов в систему ответов на вопросы на базе ИИ.
Ключевые выводы
Вот чему вы научитесь:
Конвертировать документы с MarkItDown за 3 строки
Умно разбивать текст с помощью LangChain RecursiveCharacterTextSplitter
Генерировать эмбеддинги локально с моделью SentenceTransformers
Хранить векторы в персистентной базе ChromaDB
Генерировать ответы с локальными LLM в Ollama
Развернуть веб-интерфейс с потоковой передачей (streaming) в Gradio
Получить код: полный исходник и Jupyter-ноутбук для этого руководства доступны на GitHub. Клонируйте репозиторий, чтобы идти по шагам!
Введение в RAG-системы
RAG (Retrieval-Augmented Generation) объединяет поиск по документам и генерацию текста, создавая интеллектуальные системы вопросов-ответов. Вместо того чтобы полагаться только на обучающие данные, RAG-системы просматривают ваши документы, находят релевантную информацию и используют этот контекст для генерации точных ответов с указанием источников.
Настройка окружения
Установите необходимые библиотеки для сборки вашего RAG-пайплайна:
pip install markitdown[pdf] sentence-transformers langchain-text-splitters chromadb gradio langchain-ollama ollama
Эти библиотеки дают следующее:
markitdown: инструмент Microsoft для конвертации документов, который преобразует PDF, Word и другие форматы в чистый markdown
sentence-transformers: локальная генерация эмбеддингов для преобразования текста в поисковые векторы
langchain-text-splitters: интеллектуальное разбиение текста на фрагменты с сохранением смысла
chromadb: самохостируемая векторная база данных для хранения и запросов эмбеддингов документов
gradio: конструктор веб-интерфейсов для создания удобных Q&A-приложений
langchain-ollama: интеграция LangChain для локального инференса LLM
Установите Ollama и скачайте модель:
curl -fsSL https://ollama.com/install.sh | sh ollama pull llama3.2
Далее создайте структуру проекта для организации файлов:
mkdir processed_docs documents
Эти каталоги нужны для порядка:
processed_docs: хранит конвертированные markdown-файлы
documents: содержит исходные файлы (PDF, Word и т. п.)
Создайте эти каталоги в текущей рабочей директории с корректными правами на чтение/запись.
Подготовка датасета: техническая документация по Python
Для демонстрации RAG-конвейера мы используем книгу «Think Python» Аллена Дауни — полноценное руководство по программированию, свободно доступное по лицензии Creative Commons.
Скачаем это руководство по Python и сохраним его в каталоге documents.
import requests from pathlib import Path # Get the file path output_folder = «documents» filename = «think_python_guide.pdf» url = «https://greenteapress.com/thinkpython/thinkpython.pdf» file_path = Path(output_folder) / filename def download_file(url: str, file_path: Path): response = requests.get(url, stream=True, timeout=30) response.raise_for_status() file_path.write_bytes(response.content) # Download the file if it doesn’t exist if not file_path.exists(): download_file( url=url, file_path=file_path, )
Далее преобразуем этот PDF в формат, который наша RAG-система сможет обрабатывать и искать по нему.
Загрузка документов с помощью MarkItDown
RAG-системам нужны документы в структурированном формате, который модели ИИ могут корректно понимать и эффективно обрабатывать.
MarkItDown решает эту задачу, конвертируя любые форматы документов в чистый markdown с сохранением исходной структуры и смысла.
Преобразование вашего Python-руководства
Начните с конвертации руководства по Python, чтобы понять, как работает MarkItDown:
from markitdown import MarkItDown # Initialize the converter md = MarkItDown() # Convert the Python guide to markdown result = md.convert(file_path) python_guide_content = result.text_content # Display the conversion results print(«First 300 characters:») print(python_guide_content[:300] + «…»)
В этом коде:
MarkItDown() создаёт конвертер документов, который автоматически обрабатывает несколько форматов.
convert() обрабатывает PDF и возвращает объект результата с извлечённым текстом.
text_content предоставляет «чистый» markdown-текст, готовый к дальнейшей обработке.
Вывод:
First 300 characters: Think Python How to Think Like a Computer Scientist Version 2.0.17 Think Python How to Think Like a Computer Scientist Version 2.0.17 Allen Downey Green Tea Press Needham, Massachusetts Copyright © 2012 Allen Downey. Green Tea Press 9 Washburn Ave Needham MA 02492 Permission is granted…
MarkItDown автоматически распознаёт формат PDF и извлекает «чистый» текст, сохраняя структуру книги — главы, разделы и примеры кода.
Подготовка документа к обработке
Теперь, когда вы поняли базовую конвертацию, подготовим содержимое документа к обработке. Мы сохраним текст руководства с информацией об источнике, чтобы позже использовать её при разбиении на фрагменты и извлечении данных.
# Organize the converted document processed_document = { ‘source’: file_path, ‘content’: python_guide_content } # Create a list containing our single document for consistency with downstream processing documents = [processed_document] # Document is now ready for chunking and embedding print(f»Document ready: {len(processed_document[‘content’]):,} characters»)
Вывод:
Document ready: 460,251 characters
Когда документ успешно преобразован в markdown, следующий шаг — разбить его на небольшие, удобные для поиска части.
Интеллектуальное разбиение с LangChain
Модели ИИ не могут обрабатывать целые документы из-за ограниченных окон контекста. Разбиение (chunking) делит документы на более мелкие, поисковые фрагменты при сохранении смысловой связности.
Понимание разбиения текста на простом примере
Давайте посмотрим, как работает разбиение текста на примере простого документа.
from langchain_text_splitters import RecursiveCharacterTextSplitter # Create a simple example that will be split sample_text = «»» Machine learning transforms data processing. It enables pattern recognition without explicit programming. Deep learning uses neural networks with multiple layers. These networks discover complex patterns automatically. Natural language processing combines ML with linguistics. It helps computers understand human language effectively. «»» # Apply chunking with smaller size to demonstrate splitting demo_splitter = RecursiveCharacterTextSplitter( chunk_size=150, # Small size to force splitting chunk_overlap=30, separators=[«nn», «n», «. «, » «, «»], # Split hierarchy ) sample_chunks = demo_splitter.split_text(sample_text.strip()) print(f»Original: {len(sample_text.strip())} chars → {len(sample_chunks)} chunks») # Show chunks for i, chunk in enumerate(sample_chunks): print(f»Chunk {i+1}: {chunk}»)
Вывод:
Original: 336 chars → 3 chunks Chunk 1: Machine learning transforms data processing. It enables pattern recognition without explicit programming. Chunk 2: Deep learning uses neural networks with multiple layers. These networks discover complex patterns automatically. Chunk 3: Natural language processing combines ML with linguistics. It helps computers understand human language effectively.
Обратите внимание, как работает разделитель текста:
Разбил текст длиной 336 символов на 3 фрагмента, каждый меньше лимита в 150 символов.
Применил перекрытие в 30 символов между соседними фрагментами.
Сепараторы учитывают смысловые границы в порядке приоритета: абзацы (nn) → предложения (.) → слова () → отдельные символы.
Обработка множества документов на масштабе
Теперь давайте настроим разделитель на более крупные фрагменты и применим его ко всем преобразованным документам.
# Configure the text splitter with Q&A-optimized settings text_splitter = RecursiveCharacterTextSplitter( chunk_size=600, # Optimal chunk size for Q&A scenarios chunk_overlap=120, # 20% overlap to preserve context separators=[«nn», «n», «. «, » «, «»] # Split hierarchy )
Затем используйте разделитель текста для обработки всех наших документов.
def process_document(doc, text_splitter): «»»Process a single document into chunks.»»» doc_chunks = text_splitter.split_text(doc[«content»]) return [{«content»: chunk, «source»: doc[«source»]} for chunk in doc_chunks] # Process all documents and create chunks all_chunks = [] for doc in documents: doc_chunks = process_document(doc, text_splitter) all_chunks.extend(doc_chunks)
Проанализируйте, как процесс разбиения распределил содержимое по документам.
from collections import Counter source_counts = Counter(chunk[«source»] for chunk in all_chunks) chunk_lengths = [len(chunk[«content»]) for chunk in all_chunks] print(f»Total chunks created: {len(all_chunks)}») print(f»Chunk length: {min(chunk_lengths)}-{max(chunk_lengths)} characters») print(f»Source document: {Path(documents[0][‘source’]).name}»)
Вывод:
Total chunks created: 1007 Chunk length: 68-598 characters Source document: think_python_guide.pdf
Наши текстовые фрагменты готовы. Далее мы преобразуем их в вид, который позволит выполнять «умный» поиск по сходству.
Создание поисковых эмбеддингов с SentenceTransformers
RAG-системам важно «понимать» смысл текста, а не просто совпадения по ключевым словам. SentenceTransformers преобразует текст в числовые векторы, отражающие семантические отношения, так что система находит действительно релевантную информацию, даже если точных совпадений слов нет.
Генерация эмбеддингов
Давайте сгенерируем эмбеддинги для наших текстовых фрагментов.
from sentence_transformers import SentenceTransformer # Load Q&A-optimized embedding model (downloads automatically on first use) model = SentenceTransformer(‘multi-qa-mpnet-base-dot-v1’) # Extract documents and create embeddings documents = [chunk[«content»] for chunk in all_chunks] embeddings = model.encode(documents) print(f»Embedding generation results:») print(f» — Embeddings shape: {embeddings.shape}») print(f» — Vector dimensions: {embeddings.shape[1]}»)
В этом коде:
SentenceTransformer() загружает модель, оптимизированную под Q&A, которая преобразует текст в 768-мерные векторы.
multi-qa-mpnet-base-dot-v1 специально обучена на 215 млн пар «вопрос–ответ» для лучшей точности в Q&A-сценариях.
model.encode() преобразует все текстовые фрагменты в числовые эмбеддинги за один пакетный проход.
На выходе видно, что 1007 фрагментов преобразованы в 768-мерные векторы.
Embedding generation results: — Embeddings shape: (1007, 768) — Vector dimensions: 768
Тест семантического сходства
Давайте проверим семантическое сходство, задав запросы по концепциям программирования на Python:
# Test how one query finds relevant Python programming content from sentence_transformers import util query = «How do you define functions in Python?» document_chunks = [ «Variables store data values that can be used later in your program.», «A function is a block of code that performs a specific task when called.», «Loops allow you to repeat code multiple times efficiently.», «Functions can accept parameters and return values to the calling code.» ] # Encode query and documents query_embedding = model.encode(query) doc_embeddings = model.encode(document_chunks)
Теперь рассчитаем оценки сходства и отсортируем результаты. Функция util.cos_sim() вычисляет косинусное сходство между векторами и возвращает значения от 0 (нет сходства) до 1 (идентичное значение):
# Calculate similarities using SentenceTransformers util similarities = util.cos_sim(query_embedding, doc_embeddings)[0] # Create ranked results ranked_results = sorted( zip(document_chunks, similarities), key=lambda x: x[1], reverse=True ) print(f»Query: ‘{query}'») print(«Document chunks ranked by relevance:») for i, (chunk, score) in enumerate(ranked_results, 1): print(f»{i}. ({score:.3f}): ‘{chunk}'»)
Вывод:
Query: ‘How do you define functions in Python?’ Document chunks ranked by relevance: 1. (0.674): ‘A function is a block of code that performs a specific task when called.’ 2. (0.607): ‘Functions can accept parameters and return values to the calling code.’ 3. (0.461): ‘Loops allow you to repeat code multiple times efficiently.’ 4. (0.448): ‘Variables store data values that can be used later in your program.’
Оценки сходства демонстрируют понимание смысла: фрагменты, связанные с функциями, получают высокие баллы (0.7+), тогда как нерелевантные программные концепции — значительно ниже (0.2-).
Построение базы знаний с ChromaDB
Эти эмбеддинги показывают возможности семантического поиска, но хранение в памяти ограничено масштабируемостью. Большие наборы векторов быстро исчерпывают системные ресурсы.
Векторные базы данных дают ключевые возможности для продакшена:
Постоянное хранение: данные переживают перезапуски и сбои системы
Оптимизированная индексация: быстрый поиск по сходству с использованием алгоритмов HNSW
Эффективность по памяти: миллионы векторов без исчерпания RAM
Параллельный доступ: одновременные запросы от множества пользователей
Фильтрация по метаданным: поиск по свойствам и атрибутам документов
ChromaDB предоставляет эти возможности через Python-ориентированный API и безболезненно встраивается в ваш существующий конвейер данных.
Инициализация векторной базы
Сначала настроим клиент ChromaDB и создадим коллекцию для хранения векторов документов.
import chromadb # Create persistent client for data storage client = chromadb.PersistentClient(path=»./chroma_db») # Create collection for business documents (or get existing) collection = client.get_or_create_collection( name=»python_guide», metadata={«description»: «Python programming guide»} ) print(f»Created collection: {collection.name}») print(f»Collection ID: {collection.id}»)Created collection: python_guide Collection ID: 42d23900-6c2a-47b0-8253-0a9b6dad4f41
В этом коде:
PersistentClient(path=»./chroma_db») создает локальную векторную базу, которая сохраняет данные на диск
get_or_create_collection() создает новую коллекцию или возвращает существующую с тем же именем
Сохранение документов с метаданными
Теперь сохраним фрагменты документов с базовыми метаданными в ChromaDB с помощью метода add().
# Prepare metadata and add documents to collection metadatas = [{«document»: Path(chunk[«source»]).name} for chunk in all_chunks] collection.add( documents=documents, embeddings=embeddings.tolist(), # Convert numpy array to list metadatas=metadatas, # Metadata for each document ids=[f»doc_{i}» for i in range(len(documents))], # Unique identifiers for each document ) print(f»Collection count: {collection.count()}»)
Вывод:
Collection count: 1007
В базе теперь хранится 1007 искомых фрагментов документов с их векторными представлениями. ChromaDB сохраняет эти данные на диск, что позволяет сразу выполнять запросы без повторной обработки документов после перезапуска.
Запрос к базе знаний
Давайте выполним поиск по векторной базе с помощью вопросов на естественном языке и извлечем релевантные фрагменты документов.
def format_query_results(question, query_embedding, documents, metadatas): «»»Format and print the search results with similarity scores»»» from sentence_transformers import util print(f»Question: {question}n») for i, doc in enumerate(documents): # Calculate accurate similarity using sentence-transformers util doc_embedding = model.encode([doc]) similarity = util.cos_sim(query_embedding, doc_embedding)[0][0].item() source = metadatas[i].get(«document», «Unknown») print(f»Result {i+1} (similarity: {similarity:.3f}):») print(f»Document: {source}») print(f»Content: {doc[:300]}…») print() def query_knowledge_base(question, n_results=2): «»»Query the knowledge base with natural language»»» # Encode the query using our SentenceTransformer model query_embedding = model.encode([question]) results = collection.query( query_embeddings=query_embedding.tolist(), n_results=n_results, include=[«documents», «metadatas», «distances»], ) # Extract results and format them documents = results[«documents»][0] metadatas = results[«metadatas»][0] format_query_results(question, query_embedding, documents, metadatas)
В этом коде:
collection.query() выполняет поиск по сходству векторов, используя текст вопроса в качестве входных данных.
query_texts принимает список вопросов на естественном языке для пакетной обработки.
n_results ограничивает число возвращаемых наиболее похожих документов.
include задаёт, какие данные вернуть: текст документов, метаданные и значения дистанций (сходства).
Давайте проверим функцию запроса на примере вопроса:
query_knowledge_base(«How do if-else statements work in Python?»)
Вывод:
Question: How do if-else statements work in Python? Result 1 (similarity: 0.636): Document: think_python_guide.pdf Content: 5.6 Chained conditionals Sometimes there are more than two possibilities and we need more than two branches. One way to express a computation like that is a chained conditional: if x < y: print ’ elif x > y: ’ print x is less than y ’ x is greater than y ’ else: print ’ x and y are equa… Result 2 (similarity: 0.605): Document: think_python_guide.pdf Content: 5. An unclosed opening operator—(, {, or [—makes Python continue with the next line as part of the current statement. Generally, an error occurs almost immediately in the next line. 6. Check for the classic = instead of == inside a conditional. 7. Check the indentation to make sure it lines up the…
Поиск находит релевантное содержимое с высокими показателями сходства (0.636 и 0.605).
Расширенная генерация ответов с Open Source LLMs
Поиск по сходству векторов извлекает связанные фрагменты, но результаты могут быть разбросаны по нескольким кускам и не образовывать цельного ответа.
LLM справляются с этим, «сшивая» извлечённый контекст в единый ответ, который непосредственно отвечает на вопрос пользователя.
В этом разделе мы интегрируем локальные LLM из Ollama с нашим векторным поиском, чтобы генерировать связные ответы на основе полученных фрагментов.
Реализация генерации ответов
Сначала настроим компоненты для генерации ответов с помощью LLM.
from langchain_ollama import OllamaLLM from langchain.prompts import PromptTemplate # Initialize the local LLM llm = OllamaLLM(model=»llama3.2:latest», temperature=0.1)
Затем создадим сфокусированный шаблон промпта для запросов по технической документации.
prompt_template = PromptTemplate( input_variables=[«context», «question»], template=»»»You are a Python programming expert. Based on the provided documentation, answer the question clearly and accurately. Documentation: {context} Question: {question} Answer (be specific about syntax, keywords, and provide examples when helpful):»»» ) # Create the processing chain chain = prompt_template | llm
Создадим функцию, которая извлекает релевантный контекст по заданному вопросу.
def retrieve_context(question, n_results=5): «»»Retrieve relevant context using embeddings»»» query_embedding = model.encode([question]) results = collection.query( query_embeddings=query_embedding.tolist(), n_results=n_results, include=[«documents», «metadatas», «distances»], ) documents = results[«documents»][0] context = «nn—SECTION—nn».join(documents) return context, documents def get_llm_answer(question, context): «»»Generate answer using retrieved context»»» answer = chain.invoke( { «context»: context[:2000], «question»: question, } ) return answer def format_response(question, answer, source_chunks): «»»Format the final response with sources»»» response = f»**Question:** {question}nn» response += f»**Answer:** {answer}nn» response += «**Sources:**n» for i, chunk in enumerate(source_chunks[:3], 1): preview = chunk[:100].replace(«n», » «) + «…» response += f»{i}. {preview}n» return response def enhanced_query_with_llm(question, n_results=5): «»»Query function combining retrieval with LLM generation»»» context, documents = retrieve_context(question, n_results) answer = get_llm_answer(question, context) return format_response(question, answer, documents)
Тестирование расширенной генерации ответов
Давайте протестируем улучшенную систему на нашем сложном вопросе:
# Test the enhanced query system enhanced_response = enhanced_query_with_llm(«How do if-else statements work in Python?») print(enhanced_response)
Вывод:
**Question:** How do if-else statements work in Python? **Answer:** If-else statements in Python are used for conditional execution of code. Here’s a breakdown of how they work: **Syntax** The basic syntax of an if-else statement is as follows: «`text if condition: # code to execute if condition is true elif condition2: # code to execute if condition1 is false and condition2 is true else: # code to execute if both conditions are false «`text **Keywords** The keywords used in an if-else statement are: * `if`: used to check a condition * `elif` (short for «else if»): used to check another condition if the first one is false * `else`: used to specify code to execute if all conditions are false **How it works** Here’s how an if-else statement works: 1. The interpreter evaluates the condition inside the `if` block. 2. If the condition is true, the code inside the `if` block is executed. 3. If the condition is false, the interpreter moves on to the next line and checks the condition in the `elif` block. 4. If the condition in the `elif` block is true, the code inside that block is executed. 5. If both conditions are false, the interpreter executes the code inside the `else` block. **Sources:** 1. 5.6 Chained conditionals Sometimes there are more than two possibilities and we need more than two … 2. 5. An unclosed opening operator—(, {, or [—makes Python continue with the next line as part of the c… 3. if x == y: print else: ’ x and y are equal ’ if x < y: 44 Chapter 5. Conditionals and recur…
Обратите внимание, как LLM организует несколько фрагментов в логические разделы с примерами синтаксиса и пошаговыми объяснениями. Такое преобразование превращает «сырое» извлечение в практические рекомендации по программированию.
Реализация потокового интерфейса
Пользователи теперь ожидают «живого» стриминга ответов, как в ChatGPT и Claude. Статические ответы, появляющиеся целиком, выглядят устаревшими и создают впечатление низкой производительности.
Побуквенная/потоковая генерация сглаживает этот разрыв, создавая привычный эффект печати, сигнализирующий об активной обработке.
Чтобы реализовать потоковый интерфейс, мы воспользуемся методом chain.stream(), который генерирует токены по одному.
def stream_llm_answer(question, context): «»»Stream LLM answer generation token by token»»» for chunk in chain.stream({ «context»: context[:2000], «question»: question, }): yield getattr(chunk, «content», str(chunk))
Давайте посмотрим, как работает стриминг, объединив наши модульные функции:
import time # Test the streaming functionality question = «What are Python loops?» context, documents = retrieve_context(question, n_results=3) print(«Question:», question) print(«Answer: «, end=»», flush=True) # Stream the answer token by token for token in stream_llm_answer(question, context): print(token, end=»», flush=True) time.sleep(0.05) # Simulate real-time typing effect
Вывод:
Question: What are Python loops? Answer: Python → loops → are → structures → that → repeat → code… [Each token appears with typing animation] Final: «Python loops are structures that repeat code blocks.»
Это создаёт знакомую анимацию «печати по буквам» в стиле ChatGPT, когда токены появляются постепенно.
Создание простого приложения на Gradio
Теперь, когда у нас есть полноценная RAG-система с расширенной генерацией ответов, сделаем её доступной через веб-интерфейс.
Вашей RAG-системе нужен интуитивный интерфейс, к которому легко смогут обращаться не технические пользователи. Gradio решает эту задачу благодаря:
Нулевой веб-разработке: интерфейсы создаются прямо из Python-функций
Автоматической генерации UI: поля ввода и кнопки появляются сами
Мгновенному деплою: веб-приложения запускаются одной строкой кода
Функция интерфейса
Создадим полноценный интерфейс Gradio, который объединит написанные функции в потоковую RAG-систему.
import gradio as gr def rag_interface(question): «»»Gradio interface reusing existing format_response function»»» if not question.strip(): yield «Please enter a question.» return # Use modular retrieval and streaming context, documents = retrieve_context(question, n_results=5) response_start = f»**Question:** {question}nn**Answer:** » answer = «» # Stream the answer progressively for token in stream_llm_answer(question, context): answer += token yield response_start + answer # Use existing formatting function for final response yield format_response(question, answer, documents)
Настройка и запуск приложения
Теперь настроим веб-интерфейс Gradio с примерами вопросов и запустим приложение для доступа пользователей.
# Create Gradio interface with streaming support demo = gr.Interface( fn=rag_interface, inputs=gr.Textbox( label=»Ask a question about Python programming», placeholder=»How do if-else statements work in Python?», lines=2, ), outputs=gr.Markdown(label=»Answer»), title=»Intelligent Document Q&A System», description=»Ask questions about Python programming concepts and get instant answers with source citations.», examples=[ «How do if-else statements work in Python?», «What are the different types of loops in Python?», «How do you handle errors in Python?», ], allow_flagging=»never», ) # Launch the interface with queue enabled for streaming if __name__ == «__main__»: demo.queue().launch(share=True)
В этом коде:
gr.Interface() создаёт аккуратное веб-приложение с автоматической генерацией UI
fn указывает функцию, которая вызывается при отправке вопроса пользователем (включая потоковый вывод)
inputs/outputs задают компоненты интерфейса (текстовое поле для вопросов, markdown для форматированных ответов)
examples предоставляет кликабельные примеры вопросов, демонстрирующие возможности системы
demo.queue().launch(share=True) включает потоковый вывод и создаёт локальный и публичный URL
Запуск приложения даёт следующий вывод:
* Running on local URL: http://127.0.0.1:7861 * Running on public URL: https://bb9a9fc06531d49927.gradio.live
Протестируйте интерфейс локально или поделитесь публичной ссылкой, чтобы показать возможности вашей RAG-системы.

Публичный URL истекает через 72 часа. Для постоянного доступа задеплойте на Hugging Face Spaces.
gradio deploy
Теперь у вас есть полноценная RAG-система с поддержкой стриминга, готовая к продакшену: с генерацией токенов в реальном времени и указанием источников.
Русскоязычное сообщество про Python

Друзья! Эту статью подготовила команда Python for Devs — канала, где каждый день выходят самые свежие и полезные материалы о Python и его экосистеме. Подписывайтесь, чтобы ничего не пропустить!
Заключение
В этой статье мы построили полный RAG-пайплайн, который превращает ваши документы в систему ответов на вопросы на базе ИИ.
Мы использовали следующие инструменты:
MarkItDown для конвертации документов
LangChain для разбиения текста и генерации эмбеддингов
ChromaDB для хранения векторов
Ollama для локального инференса LLM
Gradio для веб-интерфейса
Поскольку все эти инструменты — Open source, вы можете без труда развернуть систему в своей инфраструктуре.
Источник: habr.com



























