Что если бы вы могли увидеть, почему RAG не работает? Отладка RAG в 3D с помощью Project_Golem и Milvus
Когда поиск по RAG идет не так, вы обычно знаете, что он сломан - релевантные документы не отображаются или отображаются нерелевантные. Но выяснить причину - совсем другая история. Все, с чем вам приходится работать, - это оценки сходства и плоский список результатов. Нет возможности увидеть, как документы расположены в векторном пространстве, как фрагменты соотносятся друг с другом или где ваш запрос оказался относительно контента, который он должен был найти. На практике это означает, что отладка RAG сводится в основном к методу проб и ошибок: меняем стратегию чанкинга, меняем модель встраивания, корректируем top-k и надеемся, что результаты улучшатся.
Project_Golem - это инструмент с открытым исходным кодом, который делает векторное пространство видимым. Он использует UMAP для проецирования высокоразмерных вкраплений в 3D и Three.js для их интерактивного отображения в браузере. Вместо того чтобы гадать, почему поиск не удался, вы можете увидеть, как семантически кластеризуются фрагменты, куда попадает ваш запрос и какие документы были получены - и все это в едином визуальном интерфейсе.
Это потрясающе. Однако оригинальный Project_Golem был разработан для небольших демо-версий, а не для реальных систем. Он опирается на плоские файлы, грубый поиск и перестройку всего набора данных, а это значит, что он быстро выходит из строя, когда объем ваших данных превышает несколько тысяч документов.
Чтобы устранить этот пробел, мы интегрировали Project_Golem с Milvus (в частности, с версией 2.6.8) в качестве векторной основы. Milvus - это высокопроизводительная векторная база данных с открытым исходным кодом, которая обрабатывает данные в реальном времени, масштабируемое индексирование и поиск на миллисекундном уровне, а Project_Golem остается сосредоточенным на том, что он делает лучше всего: делает поведение векторного поиска видимым. Вместе они превращают 3D-визуализацию из игрушечной демонстрации в практический инструмент отладки для производственных систем RAG.
В этом посте мы расскажем о Project_Golem и покажем, как мы интегрировали его с Milvus, чтобы сделать поведение векторного поиска наблюдаемым, масштабируемым и готовым к производству.
Что такое Project_Golem?
Отладка RAG затруднена по простой причине: векторные пространства являются высокоразмерными, и человек не может их увидеть.
Project_Golem - это браузерный инструмент, позволяющий увидеть векторное пространство, в котором работает ваша RAG-система. Он берет высокоразмерные вкрапления, которые используются для поиска - обычно это 768 или 1536 измерений - и проецирует их в интерактивную 3D-сцену, которую вы можете исследовать напрямую.
Вот как это работает под капотом:
- Снижение размерности с помощью UMAP. Project_Golem использует UMAP для сжатия высокоразмерных векторов до трех измерений с сохранением их относительных расстояний. Куски, семантически схожие в исходном пространстве, остаются рядом в 3D-проекции; несвязанные куски оказываются далеко друг от друга.
- 3D-рендеринг с помощью Three.js. Каждый фрагмент документа отображается в виде узла в 3D-сцене, отображаемой в браузере. Вы можете вращать, масштабировать и исследовать пространство, чтобы увидеть, как группируются ваши документы - какие темы тесно сгруппированы, какие пересекаются и где находятся границы.
- Выделение при запросе. Когда вы выполняете запрос, поиск по-прежнему происходит в исходном высокоразмерном пространстве с использованием косинусного сходства. Но после получения результатов извлеченные фрагменты подсвечиваются в 3D-виде. Вы можете сразу же увидеть, куда попал ваш запрос относительно результатов - и, что не менее важно, относительно документов, которые он не извлек.
Именно это делает Project_Golem полезным для отладки. Вместо того чтобы смотреть на ранжированный список результатов и гадать, почему был пропущен релевантный документ, вы можете увидеть, находится ли он в отдаленном кластере (проблема встраивания), перекрывается нерелевантным контентом (проблема объединения) или просто едва выходит за порог поиска (проблема конфигурации). 3D-видение превращает абстрактные показатели сходства в пространственные отношения, о которых можно рассуждать.
Почему Project_Golem не готов к производству
Project_Golem создавался как прототип визуализации, и он отлично справляется с этой задачей. Но в его архитектуре заложены предположения, которые быстро разрушаются при масштабировании - в тех случаях, когда вы хотите использовать его для реальной отладки RAG.
Каждое обновление требует полной пересборки
Это самое фундаментальное ограничение. В первоначальном варианте добавление новых документов приводит к полной перестройке конвейера: встраивания регенерируются и записываются в файлы .npy, UMAP повторно запускается для всего набора данных, а 3D-координаты повторно экспортируются в формате JSON.
Даже при 100 000 документов запуск UMAP на одном ядре занимает 5-10 минут. При масштабе в миллион документов это становится совершенно нецелесообразным. Вы не сможете использовать этот метод для любых наборов данных, которые постоянно меняются - новостные ленты, документация, разговоры пользователей, - поскольку каждое обновление означает ожидание полного цикла обработки.
Грубый поиск не масштабируется
У стороны поиска есть свой потолок. Оригинальная реализация использует NumPy для грубого поиска по косинусному сходству - линейная сложность по времени, без индексации. На наборе данных в миллион документов один запрос может занять более секунды. Это неприемлемо для любой интерактивной или онлайновой системы.
Проблема усугубляется нехваткой памяти. Каждый 768-мерный вектор float32 занимает примерно 3 КБ, поэтому набор данных из миллиона векторов требует более 3 ГБ памяти - и все это загружено в плоский массив NumPy без индексной структуры, чтобы сделать поиск эффективным.
Нет фильтрации метаданных, нет многопользовательского доступа
В реальной системе RAG сходство векторов редко является единственным критерием поиска. Почти всегда требуется фильтрация по метаданным, таким как тип документа, временные метки, разрешения пользователя или границы на уровне приложения. Например, RAG-система поддержки клиентов должна ограничивать поиск документами конкретного арендатора, а не искать по всем данным.
Project_Golem не поддерживает ничего из этого. Здесь нет ни ANN-индексов (таких как HNSW или IVF), ни скалярной фильтрации, ни изоляции арендаторов, ни гибридного поиска. Это слой визуализации, под которым нет механизма поиска.
Как Milvus обеспечивает работу поискового слоя Project_Golem
В предыдущем разделе были выявлены три недостатка: полная перестройка при каждом обновлении, поиск с применением грубой силы и отсутствие поиска с учетом метаданных. Все три недостатка связаны с одной и той же первопричиной - у Project_Golem нет слоя базы данных. Извлечение, хранение и визуализация объединены в единый конвейер, поэтому изменение любой части приводит к перестройке всего.
Исправление заключается не в оптимизации этого конвейера. Его нужно разделить на части.
Интегрировав Milvus 2.6.8 в качестве векторной основы, поиск становится выделенным, производственным уровнем, который работает независимо от визуализации. Milvus занимается хранением векторов, индексацией и поиском. Project_Golem фокусируется исключительно на визуализации - потреблении идентификаторов документов из Milvus и их выделении в 3D-виде.
Такое разделение позволяет создать два чистых, независимых потока:
Поток извлечения (онлайн, миллисекундный уровень)
- Ваш запрос преобразуется в вектор с помощью вкраплений OpenAI.
- Вектор запроса отправляется в коллекцию Milvus.
- Milvus AUTOINDEX выбирает и оптимизирует подходящий индекс.
- Поиск по косинусному сходству в реальном времени возвращает идентификаторы соответствующих документов.
Поток визуализации (автономный, демонстрационный)
- UMAP генерирует 3D-координаты во время приема данных (n_neighbors=30, min_dist=0.1).
- Координаты хранятся в файле golem_cortex.json.
- Фронтенд выделяет соответствующие 3D-узлы, используя идентификаторы документов, возвращаемые Milvus.
Важный момент: поиск больше не ждет визуализации. Вы можете загружать новые документы и сразу же искать их - 3D-видение будет работать по собственному расписанию.
Что меняют потоковые узлы
Захват документов в режиме реального времени обеспечивается новой возможностью в Milvus 2.6.8: потоковыми узлами. В более ранних версиях для ингестирования в реальном времени требовалась внешняя очередь сообщений, например Kafka или Pulsar. Streaming Nodes переносит эту координацию в сам Milvus - новые векторы поступают непрерывно, индексы обновляются постепенно, а вновь добавленные документы становятся доступными для поиска немедленно, без полной перестройки и внешних зависимостей.
Для Project_Golem это то, что делает архитектуру практичной. Вы можете постоянно добавлять документы в систему RAG - новые статьи, обновленные документы, пользовательский контент - и поиск будет оставаться актуальным без дорогостоящего цикла UMAP → JSON → перезагрузка.
Расширение визуализации до миллионных масштабов (будущий путь)
В настоящее время Project_Golem, поддерживающий Milvus, поддерживает интерактивные демонстрации на уровне около 10 000 документов. Извлечение документов масштабируется гораздо шире - Milvus справляется с миллионами, - но конвейер визуализации все еще полагается на пакетные запуски UMAP. Чтобы устранить этот пробел, архитектура может быть расширена инкрементным конвейером визуализации:
Триггеры обновлений: Система прослушивает события вставки в коллекцию Milvus. Как только количество добавленных документов достигает определенного порога (например, 1 000 единиц), запускается инкрементное обновление.
Инкрементная проекция: Вместо повторного запуска UMAP по всему набору данных новые векторы проецируются в существующее 3D-пространство с помощью метода UMAP transform(). Это позволяет сохранить глобальную структуру и значительно сократить вычислительные затраты.
Синхронизация с фронтендом: Обновленные фрагменты координат передаются на фронтенд через WebSocket, что позволяет динамически появляться новым узлам без перезагрузки всей сцены.
Помимо масштабируемости, Milvus 2.6.8 обеспечивает гибридный поиск, сочетая векторное сходство с полнотекстовым поиском и скалярной фильтрацией. Это открывает возможности для более богатых 3D-взаимодействий, таких как выделение ключевых слов, фильтрация по категориям и нарезка по времени, предоставляя разработчикам более мощные способы изучения, отладки и рассуждений о поведении RAG.
Как развернуть и изучить Project_Golem с помощью Milvus
Обновленный Project_Golem теперь имеет открытый исходный код на GitHub. Используя официальную документацию Milvus в качестве набора данных, мы рассмотрим полный процесс визуализации поиска RAG в 3D. В настройке используются Docker и Python, и ее легко выполнить, даже если вы начинаете с нуля.
Необходимые условия
- Docker ≥ 20.10
- Docker Compose ≥ 2.0
- Python ≥ 3.11
- Ключ API OpenAI
- Набор данных (документация Milvus в формате Markdown)
1. Разверните Milvus
Download docker-compose.yml
wget https://github.com/milvus-io/milvus/releases/download/v2.6.8/milvus-standalone-docker-compose.yml -O docker-compose.yml
Start Milvus(verify port mapping:19530:19530)
docker-compose up -d
Verify that the services are running
docker ps | grep milvus
You should see three containers:milvus-standalone, milvus-etcd, milvus-minio
2. Реализация ядра
Интеграция Milvus (ingest.py)
Примечание: Реализация поддерживает до восьми категорий документов. Если количество категорий превышает этот лимит, цвета используются повторно по кругу.
from pymilvus import MilvusClient
from pymilvus.milvus_client.index import IndexParams
from openai import OpenAI
from langchain_text_splitters import RecursiveCharacterTextSplitter
import umap
from sklearn.neighbors import NearestNeighbors
import json
import numpy as np
import os
import glob
--- CONFIG ---
MILVUS_URI = "http://localhost:19530"
COLLECTION_NAME = "golem_memories"
JSON_OUTPUT_PATH = "./golem_cortex.json"
Data directory (users place .md files in this folder)
DATA_DIR = "./data"
OpenAI Embedding Config
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_BASE_URL = "https://api.openai.com/v1" #
OPENAI_EMBEDDING_MODEL = "text-embedding-3-small"
1536 dimensions
EMBEDDING_DIM = 1536
Color mapping: colors are assigned automatically and reused in a round-robin manner
COLORS = [
[0.29, 0.87, 0.50],
Green
[0.22, 0.74, 0.97],
Blue
[0.60, 0.20, 0.80],
Purple
[0.94, 0.94, 0.20],
Gold
[0.98, 0.55, 0.00],
Orange
[0.90, 0.30, 0.40],
Red
[0.40, 0.90, 0.90],
Cyan
[0.95, 0.50, 0.90],
Magenta
]
def get_embeddings(texts):
"""Batch embedding using OpenAI API"""
client = OpenAI(api_key=OPENAI_API_KEY, base_url=OPENAI_BASE_URL)
embeddings = []
batch_size = 100
OpenAI allows multiple texts per request
for i in range(0, len(texts), batch_size):
batch = texts[i:i + batch_size]
response = client.embeddings.create(
model=OPENAI_EMBEDDING_MODEL,
input=batch
)
embeddings.extend([item.embedding for item in response.data])
print(f" ↳ Embedded {min(i + batch_size, len(texts))}/{len(texts)}...")
return np.array(embeddings)
def load_markdown_files(data_dir):
"""Load all markdown files from the data directory"""
md_files = glob.glob(os.path.join(data_dir, "**/*.md"), recursive=True)
if not md_files:
print(f" ❌ ERROR: No .md files found in '{data_dir}'")
print(f" 👉 Create a '{data_dir}' folder and put your markdown files there.")
print(f" 👉 Example: {data_dir}/doc1.md, {data_dir}/docs/doc2.md")
return None
docs = []
print(f"\n📚 FOUND {len(md_files)} MARKDOWN FILES:")
for i, file_path in enumerate(md_files):
filename = os.path.basename(file_path)
Categories are derived from the file’s path relative to data_dir
rel_path = os.path.relpath(file_path, data_dir)
category = os.path.dirname(rel_path) if os.path.dirname(rel_path) else "default"
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
docs.append({
"title": filename,
"text": content,
"cat": category,
"path": file_path
})
print(f" {i+1}. [{category}] {filename}")
return docs
def ingest_dense():
print(f"🧠 PROJECT GOLEM - NEURAL MEMORY BUILDER")
print(f"=" * 50)
if not OPENAI_API_KEY:
print(" ❌ ERROR: OPENAI_API_KEY environment variable not set!")
print(" 👉 Run: export OPENAI_API_KEY='your-key-here'")
return
print(f" ↳ Using OpenAI Embedding: {OPENAI_EMBEDDING_MODEL}")
print(f" ↳ Embedding Dimension: {EMBEDDING_DIM}")
print(f" ↳ Data Directory: {DATA_DIR}")
1. Load local markdown files
docs = load_markdown_files(DATA_DIR)
if docs is None:
return
2. Split documents into chunks
print(f"\n📦 PROCESSING DOCUMENTS...")
splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=50)
chunks = []
raw_texts = []
colors = []
chunk_titles = []
categories = []
for doc in docs:
doc_chunks = splitter.create_documents([doc['text']])
cat_index = hash(doc['cat']) % len(COLORS)
for i, chunk in enumerate(doc_chunks):
chunks.append({
"text": chunk.page_content,
"title": doc['title'],
"cat": doc['cat']
})
raw_texts.append(chunk.page_content)
colors.append(COLORS[cat_index])
chunk_titles.append(f"{doc['title']} (chunk {i+1})")
categories.append(doc['cat'])
print(f" ↳ Created {len(chunks)} text chunks from {len(docs)} documents")
3. Generate embeddings
print(f"\n🔮 GENERATING EMBEDDINGS...")
vectors = get_embeddings(raw_texts)
4. 3D Projection (UMAP)
print("\n🎨 CALCULATING 3D MANIFOLD...")
reducer = umap.UMAP(n_components=3, n_neighbors=30, min_dist=0.1, metric='cosine')
embeddings_3d = reducer.fit_transform(vectors)
5. Wiring (KNN)
print(" ↳ Wiring Synapses (finding connections)...")
nbrs = NearestNeighbors(n_neighbors=8, metric='cosine').fit(vectors)
distances, indices = nbrs.kneighbors(vectors)
6. Prepare output data
cortex_data = []
milvus_data = []
for i in range(len(chunks)):
cortex_data.append({
"id": i,
"title": chunk_titles[i],
"cat": categories[i],
"pos": embeddings_3d[i].tolist(),
"col": colors[i],
"nbs": indices[i][1:].tolist()
})
milvus_data.append({
"id": i,
"text": chunks[i]['text'],
"title": chunk_titles[i],
"category": categories[i],
"vector": vectors[i].tolist()
})
with open(JSON_OUTPUT_PATH, 'w') as f:
json.dump(cortex_data, f)
7. Store vectors in Milvus
print("\n💾 STORING IN MILVUS...")
client = MilvusClient(uri=MILVUS_URI)
Drop existing collection if it exists
if client.has_collection(COLLECTION_NAME):
print(f" ↳ Dropping existing collection '{COLLECTION_NAME}'...")
client.drop_collection(COLLECTION_NAME)
Create new collection
print(f" ↳ Creating collection '{COLLECTION_NAME}' (dim={EMBEDDING_DIM})...")
client.create_collection(
collection_name=COLLECTION_NAME,
dimension=EMBEDDING_DIM
)
Insert data
print(f" ↳ Inserting {len(milvus_data)} vectors...")
client.insert(
collection_name=COLLECTION_NAME,
data=milvus_data
)
Create index for faster search
print(" ↳ Creating index...")
index_params = IndexParams()
index_params.add_index(
field_name="vector",
index_type="AUTOINDEX",
metric_type="COSINE"
)
client.create_index(
collection_name=COLLECTION_NAME,
index_params=index_params
)
print(f"\n✅ CORTEX GENERATED SUCCESSFULLY!")
print(f" 📊 {len(chunks)} memory nodes stored in Milvus")
print(f" 📁 Cortex data saved to: {JSON_OUTPUT_PATH}")
print(f" 🚀 Run 'python GolemServer.py' to start the server")
if __name__ == "__main__":
ingest_dense()
Визуализация фронтенда (GolemServer.py)
from flask import Flask, request, jsonify, send_from_directory
from openai import OpenAI
from pymilvus import MilvusClient
import json
import os
import sys
--- CONFIG ---
Explicitly set the folder to where this script is located
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
OpenAI Embedding Config
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_BASE_URL = "https://api.openai.com/v1"
OPENAI_EMBEDDING_MODEL = "text-embedding-3-small"
Milvus Config
MILVUS_URI = "http://localhost:19530"
COLLECTION_NAME = "golem_memories"
These match the files generated by ingest.py
JSON_FILE = "golem_cortex.json"
UPDATED: Matches your new repo filename
HTML_FILE = "index.html"
app = Flask(__name__, static_folder=BASE_DIR)
print(f"\n🧠 PROJECT GOLEM SERVER")
print(f" 📂 Serving from: {BASE_DIR}")
--- DIAGNOSTICS ---
Check if files exist before starting
missing_files = []
if not os.path.exists(os.path.join(BASE_DIR, JSON_FILE)):
missing_files.append(JSON_FILE)
if not os.path.exists(os.path.join(BASE_DIR, HTML_FILE)):
missing_files.append(HTML_FILE)
if missing_files:
print(f" ❌ CRITICAL ERROR: Missing files in this folder:")
for f in missing_files:
print(f" - {f}")
print(" 👉 Did you run 'python ingest.py' successfully?")
sys.exit(1)
else:
print(f" ✅ Files Verified: Cortex Map found.")
Check API Key
if not OPENAI_API_KEY:
print(f" ❌ CRITICAL ERROR: OPENAI_API_KEY environment variable not set!")
print(" 👉 Run: export OPENAI_API_KEY='your-key-here'")
sys.exit(1)
print(f" ↳ Using OpenAI Embedding: {OPENAI_EMBEDDING_MODEL}")
print(" ↳ Connecting to Milvus...")
milvus_client = MilvusClient(uri=MILVUS_URI)
Verify collection exists
if not milvus_client.has_collection(COLLECTION_NAME):
print(f" ❌ CRITICAL ERROR: Collection '{COLLECTION_NAME}' not found in Milvus.")
print(" 👉 Did you run 'python ingest.py' successfully?")
sys.exit(1)
Initialize OpenAI client
openai_client = OpenAI(api_key=OPENAI_API_KEY, base_url=OPENAI_BASE_URL)
--- ROUTES ---
@app.route('/')
def root():
Force serve the specific HTML file
return send_from_directory(BASE_DIR, HTML_FILE)
@app.route('/')
def serve_static(filename):
return send_from_directory(BASE_DIR, filename)
@app.route('/query', methods=['POST'])
def query_brain():
data = request.json
text = data.get('query', '')
if not text: return jsonify({"indices": []})
print(f"🔎 Query: {text}")
Get query embedding from OpenAI
response = openai_client.embeddings.create(
model=OPENAI_EMBEDDING_MODEL,
input=text
)
query_vec = response.data[0].embedding
Search in Milvus
results = milvus_client.search(
collection_name=COLLECTION_NAME,
data=[query_vec],
limit=50,
output_fields=["id"]
)
Extract indices and scores
indices = [r['id'] for r in results[0]]
scores = [r['distance'] for r in results[0]]
return jsonify({
"indices": indices,
"scores": scores
})
if __name__ == '__main__':
print(" ✅ SYSTEM ONLINE: http://localhost:8000")
app.run(port=8000)
Скачайте набор данных и поместите его в указанный каталог
https://github.com/milvus-io/milvus-docs/tree/v2.6.x/site/en
3. Запустите проект
Преобразование текстовых вкраплений в 3D-пространство
python ingest.py
[image]
Запустите службу фронтенда
python GolemServer.py
4. Визуализация и взаимодействие
После того как фронтенд получает результаты поиска, яркость узлов масштабируется на основе косинусоидального сходства, при этом оригинальные цвета узлов сохраняются, чтобы сохранить четкие кластеры категорий. Полупрозрачные линии проводятся от точки запроса до каждого совпадающего узла, а камера плавно поворачивается и увеличивается, чтобы сфокусироваться на активированном кластере.
Пример 1: Внутридоменное сопоставление
Запрос: "Какие типы индексов поддерживает Milvus?".
Поведение визуализации:
В 3D-пространстве примерно 15 узлов в красном кластере с надписью INDEXES заметно увеличивают яркость (примерно на 2-3×).
Соответствующие узлы включают фрагменты из таких документов, как index_types.md, hnsw_index.md и ivf_index.md.
От вектора запроса к каждому совпадающему узлу отрисовываются полупрозрачные линии, и камера плавно фокусируется на красном кластере.
Пример 2: Отклонение запросов вне домена
Запрос: "Сколько стоит столовая KFC?".
Поведение визуализации:
Все узлы сохраняют свои оригинальные цвета, с незначительными изменениями размера (менее 1,1×).
Совпавшие узлы разбросаны по нескольким кластерам с разными цветами, не показывая четкой смысловой концентрации.
Камера не вызывает действия фокусировки, поскольку порог сходства (0,5) не достигнут.
Заключение
Project_Golem в паре с Milvus не заменит существующий конвейер оценки RAG, но он добавляет то, чего не хватает большинству конвейеров: возможность видеть, что происходит внутри векторного пространства.
С помощью этой системы вы можете отличить неудачный поиск, вызванный плохим вложением, неудачный поиск, вызванный плохим разбиением на части, от неудачного поиска, вызванного слишком жестким порогом. Раньше для такой диагностики требовались догадки и итерации. Теперь вы можете это увидеть.
Текущая интеграция поддерживает интерактивную отладку в демонстрационном масштабе (~10 000 документов), при этом векторная база данных Milvus выполняет поиск производственного уровня за кулисами. Путь к визуализации в миллионном масштабе намечен, но еще не построен, поэтому сейчас самое время принять участие.
Проверьте Project_Golem на GitHub, попробуйте его с вашим собственным набором данных и посмотрите, как на самом деле выглядит ваше векторное пространство.
Если у вас есть вопросы или вы хотите поделиться своими находками, присоединяйтесь к нашему каналу Slack или запишитесь на сеанс Milvus Office Hours, чтобы получить практическое руководство по настройке.
Try Managed Milvus for Free
Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.
Get StartedLike the article? Spread the word


