Команда 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=["\n\n", "\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 символов между соседними фрагментами.
Сепараторы учитывают смысловые границы в порядке приоритета: абзацы (
\n\n) → предложения (.) → слова () → отдельные символы.
Обработка множества документов на масштабе
Теперь давайте настроим разделитель на более крупные фрагменты и применим его ко всем преобразованным документам.
# 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=["\n\n", "\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 = "\n\n---SECTION---\n\n".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}\n\n"
response += f"**Answer:** {answer}\n\n"
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}\n\n**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()создаёт аккуратное веб-приложение с автоматической генерацией UIfnуказывает функцию, которая вызывается при отправке вопроса пользователем (включая потоковый вывод)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, вы можете без труда развернуть систему в своей инфраструктуре.
Комментарии (14)

aladkoi
16.10.2025 14:11Еще нужно создать граф связей между пересекающимися данными в чанках. И так, как описано в статье, создание dataset для rag в формате вопрос - ответ , Claude.ai делать не рекомендует.
webhamster
Когда происходит разбивка на фрагменты, естественным образом должен возникнуть вопрос: а откуда, собственно, этот фрагмент взят? То есть, надо иметь ссылку не просто на 750-страничный документ, а ссылку либо на раздел/главу/параграф в этом документе, или, на худой конец, на номер страницы в PDF/DOC/ODT файле.
Есть ли какой-либо способ получать именно такую ссылку, чтобы показывать пользователю в ответе используемые материалы не просто:
.
Найдены документы:
1 . Питон_для_чайников.pdf
2 . Этот_сумасшедший_змей.pdf
а
Найдены документы:
1 . Питон_для_чайников.pdf,
Часть II. Системное программирование,
Глава 3. Контекст выполнения сценариев,
Перенаправление потоков ввода-вывода
2 . Этот_сумасшедший_змей.pdf
стр. 255, 897
.
Возможно, должен быть какой-то стандарт записи такого адреса. И еще надо иметь возможность открыть документ именно на этом месте с помощью, конечно, OpenSource решения.
avshkol
Согласен, у каждого документа нужно собирать реквизиты документа, и оглавление, и помечать каждый кусок текста в базе ссылкой на этот документ, в идеале на главу/раздел.
Но, насколько я понимаю, чтобы такое делать, проще пульнуть документ в LLM с большим окном токенов, и получить оттуда данные в типовом формате. См. мою статью с таким разбором: https://habr.com/ru/articles/879876/
Далее, когда мы имеем оглавление, берем раздел за разделом, и какой объем раздела сохранять в один кусок информации? Разумеется, минимальный осмысленный объем. Иногда это абзац, но иногда, и два, и три, иначе потеряется смысл... Вот здесь без LLM и хорошего промпта уже не обойтись. Возможно, при этом еще и немного переписывать, отжимая из текста воду.
А еще стоит вопрос сохранения рисунков, таблиц и формул...