The Daily Newsstand · Free, Always
Thursday, October 29, 2026

Anthropic заблокировала меня и я потерял подписку за $100, но сохранил год работы и собрал личную RAG-базу

Translate

Кто нибудь уже получал такие письма счастья? Причина - страна, оно и понятно, я вообще почитал что они начали массово банить за подключение через VPN и даже не разбираются что стоит за этим туннелем. Китай много так же банят. Я активно пользовался (около 5 часов в сутки) больше года активно и тут еще сгорела подписка за 100$.

Деньги - это неприятно, конечно... Но для меня более важно: проекты, переписки, решения, объяснения, почему мы выбрали именно такой вариант, где уже обкакались. Конкретный объем контекста меня в широком смысле: и работы, и личных вопросов.

Пару дней ушло на то что бы потестить современные харнессы, и что бы они как по маслу работали с удобными мне роутерами. Ну и на второй день я остановился на Deepseek Harness.

Я задался целью - не потерять эту историю и снова использовать её в работе.

Просто загрузить бэкап, увы, не получилось

Благо, что эта контора позволяет выгрузить бэкап всех переписок даже с заблоченным аккаунтом. Экспорт Claude.ai, локальные сессии Claude Code, память, проекты и артефакты. Один conversations.json весил около 146 МБ. По грубой оценке, внутри было примерно 46 миллионов символов реального текста: часть кириллицы в JSON записана последовательностями вроде \u041f - очень понятно.

А еще архив сессий Claude Code: сотни файлов и сотни мегабайт. Было бы круто заиметь базу знаний из этого, которую можно прикрепить к новому чату и сказать "вот, теперь ты в курсе дел".

Да и даже если бы файл влезал в контекст, это не решало бы задачу. Мне нужно спросить про конкретный проект, решение или факт — и получить несколько подходящих фрагментов, так, что бы не тащить в контекст год переписок.

Сначала скрипты

Разбирать сотни мегабайт с помощью субагентов я не стал - я вообще им не доверяю! Управляющий агент, бывает, забывает о своих воркерах, они действуют немного в разных стилистиках, и по итогу еще сколько токенов надо потратить на сведение. поэтому нужны классические скрипты. Все четко и детерминированно. Сделал примерно такой набор: 

  1. переносим переписки Claude.ai и Claude Code

  2. память и артефакты

  3. третий строим INDEX.md по реальной структуре.Названия проектов сводились через таблицу алиасов: один и тот же проект в переписках мог называться по-разному. Не знаю с чем это связано - с тем что я менял названия чатов по мере их развития, или это личные интерпретации LLM.

Идея такая, что скрипт не должен был каждый раз пересобирать идеальную структуру. После первого переноса я правил её руками и поэтому дальше ничего не удалял и не затирал без необходимости. Эта часть работы вышла самой дешёвой по токенам. Скрипт прочитал исходник один раз и модель не тратила контекст на миллионы символов, которые всё равно пришлось бы потом индексировать. Я бы уже тут разорился!

Markdown стал единственным источником истины

Базу я положил в Obsidian. Умеет ли Obsidian в RAG — вопрос тридесятый, мне важнее другое: знания должны оставаться обычными файлами, которые можно открыть, прочитать и поправить руками, мне как кожаному мешку.

Структура получилась примерно такая:

knowledge/
├── INDEX.md
├── Memory/
│   ├── profile.md
│   ├── preferences.md
│   ├── people/
│   └── areas/
├── Projects/
│   └── <project>/
│       ├── CONTEXT.md
│       └── Notes/
├── Topics/
└── Artifacts/

Memory/ — профиль, предпочтения и устойчивый контекст. Projects/ — конкретные проекты. Topics/ — темы, которые проходят через несколько проектов. Переписки остаются отдельными Markdown-файлами, а не сливаются в один огромный документ.

Да по итогу все просто:

Markdown — оригинал. SQLite — индекс, который можно пересобрать.

Если завтра, например, удалю index.db, знания не пропадут. Можно запустить переиндексацию и получить его снова, ну а если я отредактирую файл в Obsidian, именно эта правка и будет правильной версией. Копия же в базе просто догонит.

Короче - это понятность для обоих миров: Obsidian нужен человеку, RAG — поиску.

Почему хватило SQLite

По слову "RAG" часто сразу представляют отдельную векторную базу, ещё один сервис, контейнер и мониторинг, но тут это был бы перебор. 

Сейчас в базе около 12 тысяч чанков. Для такого объёма хватает SQLite: в нём лежат файлы, фрагменты и полнотекстовый индекс, а эмбеддинги хранятся там же как бинарные массивы float32, и numpy считает косинусное сходство обычным перебором.

Схема простенькая для тупенького меня:

files       — путь, хеш, дата изменения, проект и число чанков
chunks      — текст, заголовок, номера строк, tier и проект
chunks_fts  — полнотекстовый индекс FTS5
embeddings  — id чанка, модель, размерность и вектор в BLOB
meta        — версия схемы и состояние индекса

Почему так? Потому что 12 тысяч векторов — это не тот масштаб, ради которого стоит поднимать и обслуживать отдельную систему. Если данных станет на несколько порядков больше или измерения покажут, что SQLite не справляется, архитектуру можно заменить. Ну а пока она закрывает задачу без лишней инфраструктуры.

Это ответ на один конкретный вопрос: «что достаточно для моей базы сейчас?». А для чужой базы ответ может быть другим.

У чанка есть адрес

Поиск работает по чанкам, но если нарезать текст каждые тысячу символов, можно разорвать мысль посередине, а повреждение мыслей я никому не желаю! Поэтому Markdown разбивается по заголовкам и абзацам.

Целевой размер — примерно 600 токенов, на крайний случай — 1000. Есть и маленькая защита от чрезмерного дробления: новый заголовок не обязательно закрывает текущий чанк, пока тот не набрал хотя бы 200 токенов. Иначе ж документ с короткими подзаголовками превращается в россыпь бесполезнейших обрывков.

У каждого чанка остаются заголовок и диапазон исходных строк, метаданные по итогу связывают чанки с .md: поиск говорит «вот этот файл, строки 42–77», и агент читает именно этот отрывок. Документ целиком ему уже не нужен.

поиск → файл.md, строки 42–77 → чтение диапазона → ответ

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

Переписка не должна заглушить профиль

Исторических чатов гораздо больше, чем коротких заметок о проектах и о любимом Мне. Если ранжировать всё одинаково, ответ из старого диалога может вытеснить профиль или CONTEXT.md.

Поэтому содержимое разделено на два уровня:

  • tier 1 — память, заметки и обзорные файлы;

  • tier 2 — история переписок и остальные материалы.

Уровень определяется по имени файла и расположению: например, Memory/ и корневой Notes/ идут в первый уровень, а проект — по пути Projects/<имя>/.... Правило видно в структуре, оно предсказуемое и настраивается.

В текущем индексе около 468 обзорных чанков против 11 тысяч с лишним чанков переписки. Для tier 1 при ранжировании есть небольшой множитель, чтобы он не тонул в истории.

Тут я, кстати, поймал неочевидную асимметрию: заметка в Notes/ и заметка того же типа в Projects/<проект>/Notes/ классифицируются по-разному, потому что классификатор смотрит на первый сегмент пути. Это же отличный пример: простые правила стоит проверять на реальной работе — один конфиг такой дисбаланс не покажет.

Поиск по словам и по смыслу

Точный поиск нужен, когда я помню конкретный термин, имя или же ,например технологию. Для него используется FTS5 и BM25. А поиск по смыслу нужен, когда я помню содержание, но не исходную формулировку. Текст запроса и каждый чанк превращаются в векторы через OpenAI-совместимый API эмбеддингов и затем векторы сравниваются по косинусному сходству.

Один поиск не заменяет другой. Поэтому результаты двух списков объединяются через Reciprocal Rank Fusion:

score[chunk_id] += 1 / (K + rank)

Если фрагмент высоко стоит и в полнотекстовой, и в векторной выдаче, он подымается выше. Для этой базы я остановился на K = 10: при большем значении разница между первыми и средними позициями становилась слишком плоской. Векторная часть использует text-embedding-3-small, размерность 1536, запросы отправляются пачками. Сам индекс не привязан к этому провайдеру: адрес API, модель, размерность, пакет и таймаут задаются в knowledge.toml, секрет берётся из переменной окружения или файла credentials.

Как модель получает доступ: MCP

Конечно жеж, индекс сам по себе ничего не меняет в поведении нового чата, ведь агентам нужен интерфейс, через который можно искать, читать и записывать заметки. Для этого я подключил MCP-сервер. Он работает по stdio и обменивается JSON-RPC 2.0. Отдельную библиотеку MCP для сервера я не подключал: достаточно небольшого обработчика протокола и существующего Python-пакета.

У модели есть инструменты для разных операций:

Инструмент

Что делает

knowledge_search

Ищет по словам и смыслу

knowledge_read

Читает файл или диапазон строк

project_context

Загружает стартовый контекст проекта

knowledge_write

Создаёт или обновляет Markdown

knowledge_append

Дописывает в существующий файл

knowledge_commit

Сохраняет заметку о решении или сессии

knowledge_status

Показывает состояние индекса

knowledge_projects

Возвращает список проектов

knowledge_reindex

Перестраивает индекс

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

Но ведь инструмент ещё нужно заставить вызвать!

В один из новых чатов я спросил: "как меня зовут?" Агент ответил, не обратившись к базе. Инструменты MCP были подключены, файлы существовали. но вот... модель увидела косвенные признаки в локальной системе и решила, что уже знает ответ. Списала короче, а не поработала мозгами. То есть индекс работал как надо, а проблема была в поведении модели.

Поэтому в глобальном AGENTS.md появилось правило старта сессии: один раз прочитать профиль пользователя и искать контекст, относящийся к первому запросу, и там же в AGENTS.md живёт инструкция: где брать биографические факты и что делать с базой.

Подробности вынесены в скилл knowledge-first. Когда вопрос касается пользователя, прошлой работы или решений, DSH подгружает эту справку по требованию. В ней описано, когда вызывать поиск, как читать найденный файл и как безопасно записывать результат.

Разделение такое:

AGENTS.md       — поведение на каждой сессии
knowledge-first — подробная инструкция по теме, загружается по триггеру
MCP             — реальные инструменты доступа к данным
Markdown        — единственное место, где живут факты

В новом чате сценарий сработал: инструкция привела агента к профилю, он прочитал файл и ответил по нему, не выводя личные данные из имени компьютера или .gitconfig.

Obsidian редактирует напрямую, а индекс должен догонять

Что мне нужно было - что бы человек смог поменять файл в Obsidian, ничего не сообщая MCP-серверу. Значит-с, индекс надо сверять с диском! Перед чтением MCP-инструменты делают инкрементальную проверку Markdown-файлов, но не чаще раза в 10 секунд. На текущих нескольких сотнях файлов обход занимает около 0,03 секунды. Для изменения файла старые чанки удаляются и строятся заново; удалённый файл убирается из индекса.

Новые эмбеддинги догоняются уже очень ограниченными порциями. Если там временно недоступен API, текст всё равно остаётся в Markdown и полнотекстовом индексе; вектор можно досчитать позже. Лайфхак: свежая заметка какое-то время лучше ищется по словам, пока не появился её эмбеддинг. Вроде ж очевидно, а вроде нет...

Такой вот казус был

Как то был прикол: несколько новых чанков не получили эмбеддинги, а вызовы базы начали зависать (такой себе прикол). Прямой запрос к CometAPI (он для эмбеддингов у меня) возвращал HTTP 200, но вот тело ответа обрывалось посередине. Наверное пора отказываться от этого CometAPI! Включил VPN и продолжил проверять — снова через curl. Он показывал всё ту же обрезанную загрузку. Я уже начал строить теории заговора против себя про прокси CometAPI, Azure и потерю пакетов.

Проблема оказалась в диагностике. На macOS curl в моей конфигурации не использовал системный прокси: переменных окружения не было. Он нагло шел напрямую по маршруту, который и правда обрывал ответ. Python, на котором работает RAG, системные настройки прокси читал. Когда я проверил запрос именно из Python-рантайма, шесть чанков успешно получили эмбеддинги за четыре секунды.

То есть провайдер не был сломан, а база не нуждалась в срочном переключении на другой API. Я просто двадцать минут проверял не тот сетевой путь, которым пользуется приложение. Бывает, ШТОШ.
Для меня это отдельный инженерный урок: проверять надо маршрут, HTTP-клиент и окружение того процесса, который реально ходит в API. "curl работает"- это конечно радует, но что бы "приложение работает" - надо немного извернуться.

Что получилось

Сейчас в базе 313 Markdown-файлов, около 11 752 чанков, 16 проектов; у всех чанков есть эмбеддинги. История не загружается в контекст целиком, на запрос система возвращает несколько фрагментов с путями и номерами строк, после чего агент может прочитать источник полностью или ограниченный диапазон. Нейронка получает по задаче несколько нужных отрывков вместо миллионов символов бэкапа.

Итоговая архитектура:

экспорт Claude.ai и Claude Code
            │
            ▼
одноразовые скрипты импорта
            │
            ▼
Markdown в Obsidian — оригиналы и ручная правка
            │
            ├── SQLite + FTS5 — поиск по словам
            ├── embeddings + numpy — поиск по смыслу
            ├── MCP — поиск, чтение и запись для агента
            └── AGENTS.md + skill — когда и как обращаться к базе

Заблокированный аккаунт не вернул мне подписку, но больше года работы не оказался заперт в одном чате или одном сервисе. Теперь это обычные Markdown-файлы, которые я могу открыть сам, и индекс, который помогает агенту найти нужное.

Сам репозиторий моего MCP

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

Поток сознания и мысли по таким ситуациям складываю в канал.

Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.