- BrainTools - https://www.braintools.ru -

Облачные ИИ не справляются, MiniLM-L6 ломается на философии: строим локальный RAG для сложных семантических текстов

Этот проект долго вынашивался и, в конце концов, начался как очередная попытка разобраться в философских текстах, написанных Джейн Робертс во второй половине двадцатого века.

Естественно, с появлением мощных облачных ИИ не оставлялись попытки найти в них “качественных собеседников” по предложенной теме. Но, как показала практика, они справились с этой задачей далеко не блестяще! То галлюцинировали факты из общих знаний, то упираясь в границы своих датасетов.

Ну что, остаётся последнее: пилить RAG на строго структурированной базе. А т.к. готовых корпусов в открытом доступе не нашлось, пришлось брать сырые сканы, вычищать артефакты распознавания и переводить их в XML, пригодный для эмбеддинга.

На сегодняшний день проделана часть работы. Создан репозиторий в статусе исследовательского лога, куда выкладываются сырые скрипты обработки, предсобранный индекс ChromaDB и пайплайн на базе Qwen2.5 + LM Studio.

Зачем я это пишу? Мне нужен технический аудит от тех, кто глубже работает с векторными базами и архитектурой чат-ботов. В процессе сборки возникли несколько вопросов, по которым у меня пока нет однозначного ответа:

Архитектура индекса: как грамотнее организовать хранение в Chroma?

Сохранение контекста диалога: как правильно реализовать память [1] о предыдущих вопросах пользователя без раздувания токен-бюджета?

Ранжирование выдачи: может ли reranker (например, cross-encoder) улучшить точность на философских текстах? И с какого объёма стоит начинать эту оптимизацию?


А теперь немного более развёрнуто обо всех этапах по порядку.

Подготовка данных

Первая из разряда самых трудоёмких и затратных по времени задач – подготовка текста для трансформации txt -> xml.

Проблема в том, что имеющийся в наличии вариант сканов иной раз больше похож на кашу из артефактов. Облачные модели неплохо подсвечивают опечатки, но финальная правка всегда делается вручную, ибо эти товарищи (опять же, как показала практика) могут либо что-нибудь “подзабыть”, либо “напридумать” если попросить их сделать всю работу.

Разметка

Собственно, вторая из самых трудоёмких и затратных по времени задач.

Идея была такова: досконально разметить имеющиеся тексты на спикеров чтобы не дать будущей модели и шанса на ошибку [2] при обучении [3]. Тут очень важно чтобы модель чётко понимала чей текст, т.к. важна философия первоисточника, а не описание окружающего пространства, собственные ощущения или догадки окружающих людей. Только первоисточник.

Аннотация, при помощи вымученного промпта (если кому будет интересно могу запостить), была поручена локальному qwen2.5-coder-14b. Но (как показала практика) с расстановкой тегов опираясь на смысл текста, он ошибается больше чем хотелось бы. Поэтому “результат его работы” проверялся облачной моделью и затем все правки делались вручную с “диагональной читкой” всего получившегося xml.

Пример итоговой структуры:

Скрытый текст
<?xml version="1.0" encoding="UTF-8"?>
<session book="Seth Speaks" session_number="SESSION 511" date="JANUARY 21, 1970">
  <dialogue>
    <utterance speaker="Seth" type="statement">
      <text>Ruburt may write an introduction if he likes.</text>
    </utterance>
    <utterance speaker="Seth" type="delivery">
    <text>(Pause.)</text>
</utterance>
    <utterance speaker="Seth" type="chapter_start" chapter="1">
      <text>Chapter One</text>
    </utterance>
  </dialogue>
</session>

Примечание: выделение stage directions (Pause) в отдельный тип delivery позволяет исключать их из поиска, чтобы они не размывали семантику утверждений.

Чанкинг, индексация и векторное пространство

Из-за того что вся смысловая нагрузка конкретно этой книги лежит исключительно на монологах автора, чанкинг простейший – отдельными параграфами по спикеру.

Для эмбеддинга выбран multilingual-e5-large.

Классический all-MiniLM-L6-v2 не справился с абстрактной терминологией Сета, демонстрируя высокий семантический дрейф (OOD), тогда как E5 продемонстрировал отличное удержание контекста предложенных понятий.

Пайплайн индексации:

Скрытый текст
import json
from chromadb import Client
from chromadb.config import Settings
from sentence_transformers import SentenceTransformer

input_file = "seth_chunks.jsonl"
db_dir = "./chroma_db_v3"

model = SentenceTransformer("C:/SP/Workbook/models/multilingual-e5-large")
client = Client(Settings(persist_directory=db_dir, is_persistent=True))
collection = client.get_or_create_collection("seth_speaks")

chunks = []
with open(input_file, "r", encoding="utf-8") as f:
    for line in f:
        chunks.append(json.loads(line))

texts = ["passage: " + c["text"] for c in chunks]
ids = [f"chunk_{i}" for i in range(len(chunks))]
metadatas = [{"session": c["session"], "chapter": c["chapter"], "type": c["type"]} for c in chunks]

embeddings = model.encode(texts).tolist()
collection.add(embeddings=embeddings, documents=texts, ids=ids, metadatas=metadatas)

print(f"Done. {len(chunks)} chunks indexed with multilingual-e5-large.")

Архитектурные детали этого блока:

Обогащение промпта: префикс “passage: ” критически важен для моделей семейства E5 — без него модель воспринимает текст как короткий поисковый запрос и теряет семантику длинных философских абзацев.

Метаданные: вынос номера сессии и главы в метаданные позволит фильтровать выдачу SQL-подобными запросами прямо в Chroma.

Retrieval и управление «голосом» модели

Найти фрагменты недостаточно — нужно заставить модель отвечать как Сет, строго придерживаться цитат и общаться на языке пользователя:

Скрытый текст
import json
import requests
from chromadb import Client
from chromadb.config import Settings
from sentence_transformers import SentenceTransformer

db_dir = "./chroma_db_v3"
model = SentenceTransformer("C:/SP/Workbook/models/multilingual-e5-large")
client = Client(Settings(persist_directory=db_dir, is_persistent=True))
collection = client.get_collection("seth_speaks")
LMSTUDIO_URL = "http://localhost:1234/v1/chat/completions"

SYSTEM_PROMPT = """You are Seth... You MUST answer based ONLY on the provided quotes...
Always speak in Seth's voice: wise, direct, with gentle humor.
Always respond in the same language as the user's question."""

while True:
    query = input("nYou: ")
    if query.lower() == "exit":
        break

    query_embedding = model.encode(["query: " + query]).tolist()
    results = collection.query(query_embeddings=query_embedding, n_results=7)

    context_parts = []
    for doc, meta in zip(results["documents"][0], results["metadatas"][0]):
        if meta.get("type") == "delivery":
            continue
        session = meta["session"]
        chapter = meta.get("chapter", "-")
        context_parts.append(f"[{session}, Ch.{chapter}]n{doc}")

    context = "nn---nn".join(context_parts)

    # Дебаг-вывод найденных фрагментов
    print("n--- Found chunks ---")
    for i, (doc, meta) in enumerate(zip(results["documents"][0], results["metadatas"][0])):
        if meta.get("type") == "delivery": continue
        print(f"n[{i+1}] {meta['session']}, Ch.{meta.get('chapter', '-')}:")
        print(doc[:200])
    print("--- End chunks ---n")

    prompt = f"""Quotes from my book (use ONLY these to answer):nn{context}nnQuestion: {query}nnAnswer as Seth..."""

    response = requests.post(LMSTUDIO_URL, json={
        "messages": [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": prompt}
        ],
        "temperature": 0.7,
        "max_tokens": 1024
    })

    data = response.json()
    answer = data["choices"][0]["message"]["content"]
    print(f"nSeth: {answer}")

Ключевые инженерные решения:

Двойное обогащение: инструкции даются и эмбеддеру (query:), и LLM (Quotes from my book…).

Фильтрация шума: исключение типа delivery защищает ответ от “шума” пояснений (Pause).

Мультиязычность: модель понимает язык вопроса и отвечает на том же языке, опираясь на английские тексты.

Прозрачность: блок вывода Found chunks оставлен намеренно — визуализация выдачи позволяет наблюдать баги.


Вопросы к сообществу

На данный момент пайплайн работает стабильно для одиночных запросов, но упирается в несколько архитектурных вопросов:

Архитектура индекса Chroma: сейчас всё лежит в одной коллекции. При росте базы до 10+ томов не станет ли поиск слишком шумным? Стоит ли дробить базу по книгам/сессиям, или один монолитный индекс при правильной сегментации всегда будет работать быстрее за счёт внутреннего кэширования?

Управление состоянием диалога: каждая итерация сейчас начинается «в вакууме». Простое добавление истории чата в конец переменной prompt быстро сожрёт контекстное окно Qwen2.5-32B. Есть ли смысл выносить историю во внешнее хранилище и делать Retrieval уже по предыдущим вопросам?

Ранжирование выдачи: сейчас мы просто берём top-7 из Chroma. Может ли reranker (например, cross-encoder) улучшить точность выдачи на философских текстах, или для 911 чанков это преждевременная оптимизация?

Если у вас есть опыт [4] масштабирования векторных баз или архитектурой памяти агентов — welcome!

Внутри репозитория прямо сейчас в корневом ридми есть мануал на квикстарт. Желающие могут прямо сейчас ознакомиться и запустить проект в текущем варианте. [https://github.com/Joseph-m-l/seth_material_rag [5]]


Проект остаётся исследовательским логом. Но если мы вместе решим вопросы архитектуры индекса и памяти, из этого набора скриптов получится самый глубокий собеседник по текстам Джейн Робертс, который можно запустить локально.

Следующий шаг — индексация всех 22 глав первой книги и эксперименты с памятью диалога.

Автор: joseph-m-l

Источник [6]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/33771

URLs in this post:

[1] память: http://www.braintools.ru/article/4140

[2] ошибку: http://www.braintools.ru/article/4192

[3] обучении: http://www.braintools.ru/article/5125

[4] опыт: http://www.braintools.ru/article/6952

[5] https://github.com/Joseph-m-l/seth_material_rag: https://github.com/Joseph-m-l/seth_material_rag

[6] Источник: https://habr.com/ru/articles/1064936/?utm_campaign=1064936&utm_source=habrahabr&utm_medium=rss

www.BrainTools.ru

Rambler's Top100