Старые базы знаний неудобны для ИИ — это первая причина, по которой я взялся за перенос. Новые базы мы и так пишем с помощью ИИ: даём контекст, агент дописывает. «База знаний 2.0» Битрикс24 (модуль note) работает с Markdown — такой текст агент нормально читает и дописывает, а первое поколение баз для этого не приспособлено.
Вторая причина — Битрикс24 развивает ИИ-инструменты помощи для сотрудников и внешних заказчиков. Уже можно подключить бота, который по базе знаний подсказывает сотрудникам и отвечает клиентам. Но работает он только с базой знаний 2.0: первое поколение так не умеет.
Получается, перенос — не разовая уборка. Дальше эти базы будут актуализироваться с помощью ИИ, а не вручную, как пришлось бы в первой версии.
С нуля перенос занял у меня пять часов. Не потому что столько работы: на другом портале ту же операцию я делал с помощью Claude примерно за 15 минут на базу и начинал с ожиданием «две базы — полчаса-час». Пять часов съела не база знаний, а инструмент:
- агент долго искал, где в портале лежат базы знаний, хотя я подсказывал;
- в Коворке задача не пошла — пришлось перейти в режим Код;
- в Коде агент свернул не туда, путь мы правили вместе;
- часть ошибок была уже в самом инструменте, их тоже пришлось обходить.
Инструкция и промт ниже — чтобы вам не пришлось повторять мой путь: по правильному маршруту две базы переносятся за полчаса.
В этом кейсе перенос сделал ИИ-агент «Битрикс24 Коворк/Код»: прочитал исходный сайт, выгрузил статьи и структуру, создал новую базу и перенёс всё с сохранением вложенности. Две базы знаний, 43 статьи, 75 изображений и 39 видео — без ручного копирования и без потерь. Дальше — что предстояло сделать, на чём такие переносы обычно ломаются, как выстроить работу, готовый промт для агента и схема кода.
Что было и что хотели получить
Что было. База знаний компании жила в старом формате: страницы-лендинги, где меню и содержимое собраны из визуальных блоков. Формально это «сайт со статьями», фактически — вики, которую легко читать и почти невозможно поддерживать.
Что хотели получить. Единую базу в модуле «База знаний» Битрикс24: статьи с нормальным деревом разделов, встроенными картинками и видео, поиском и правами доступа — место, куда сотрудник идёт за инструкцией, а не блуждает по страницам сайта.
Что нужно было сохранить. Тексты — без потерь и без «пересочинения». Структуру: раздел → подраздел → статья. Вложения: изображения остаются внутри статей, видео — доступными. Исходный сайт — не трогать вообще: он остаётся рабочей копией на время проверки.
Масштаб: две базы, 43 статьи
| Показатель | «Системная работа» | «Техподдержка» | Итого |
|---|---|---|---|
| Статей | 34 | 9 | 43 |
| Разделов верхнего уровня | 16 | 4 | 20 |
| Изображений | 68 | 7 | 75 |
| Видео | 39 | 0 | 39 |
| Объём текста | ~121 000 знаков | ~11 000 знаков | ~132 000 знаков |
Страниц в исходном сайте первой базы было 40, а в дерево новой базы вошло 34: шесть страниц оказались служебными и в перенос не поехали. Это не потеря, а часть методики — почему, разберём ниже.
Почему это задача для агента
Работа большая, но однообразная: 43 страницы, у каждой — набор блоков, часть из них текст, часть — картинки и видео. Человек на такой рутине ошибается предсказуемо: пропускает вложенный раздел, теряет картинку, копирует служебную страницу. Агент делает это одинаково на первой и на сорок третьей статье — и, что важнее, повторяемо: если что-то пошло не так, прогон можно повторить, а не восстанавливать вручную.
На чём такие переносы ломаются
Перенос базы знаний выглядит как «скопируй текст в новое место». На практике ломается в пяти местах — и каждое стоит либо потерянного контента, либо вечера ручной сверки.
Дерево не переносится плоским списком
Главная ловушка. В исходном сайте вложенность живёт в меню: раздел «Продажи» содержит «Воронки», «Договора», «Счета», «Акты» и ещё шесть подразделов; «Интеграции заявок» — пять инструкций по подключению. Если перенести статьи списком, все они станут равноправными, и база превратится в свалку: раздел и его содержимое визуально не отличаются.
Хуже того, порядок создания важен: у статьи нельзя указать родителя, которого ещё не существует. Значит, нельзя «сначала все статьи, потом разложить» — создавать надо строго сверху вниз, иначе подраздел уедет в корень и вложенность придётся восстанавливать вручную.
Служебные страницы шаблона уезжают в новую базу
В исходном сайте есть страницы, которые не являются знаниями: главная страница шаблона, страница результатов поиска, сайдбар с меню, «Article example». Формально это те же страницы, и агент по умолчанию потащит их в новую базу. В результате в чистой базе знаний появляются пустые и технические статьи — и первое впечатление у сотрудника испорчено.
В этом кейсе из 40 страниц первой базы в перенос не пошли 6: «Стартовая», «Результаты поиска», «Сайдбар», «Страница категории», «Страница статьи» и устаревшая версия «BI Отчётов». Их надо распознавать по признакам шаблона (служебная страница, код шаблона вида wiki-light/main) и исключать осознанно — с записью причины, а не «на глаз».
Не-текстовые блоки: пустая статья или потерянный контент
Страница собрана из блоков: заголовок, текст, картинка, видео, разделитель, карта, соцкнопки. Если читать «весь HTML страницы», в статью приедет мусор из вёрстки. Если читать только текстовые блоки — потеряются картинки и видео, а статья с инструкцией «нажмите кнопку на скриншоте» станет бесполезной.
Правильный путь: обходить блоки по порядку, текст конвертировать в разметку, а на месте картинки оставлять маркер и заменять его на реальное изображение после загрузки файла. Тогда картинка остаётся там, где была в тексте, а не «где-то в конце статьи».
Ссылки меню могут вести в никуда
Меню — это ссылки. В нём встречаются пункты, которые указывают на отсутствующие или удалённые страницы, а одна и та же страница может быть указана дважды (например, «BI Отчёты» отдельным пунктом и внутри другого раздела). Если идти по меню вслепую, агент либо упадёт на 404, либо создаст дубль статьи.
Поэтому дерево строится не «по ссылкам», а по факту существования страницы, с журналом решений: какие пункты пропущены и почему, какие страницы в меню не упомянуты и остались неперенесёнными.
Портал отвечает медленно — это норма
В этом кейсе запросы к порталу шли через прокси приложения: на портальных путях они занимают десятки секунд, и ответ в 70–100 секунд — штатное поведение, а не авария. Отсюда два следствия.
Первое: таймаут на такие вызовы надо ставить в минутах. Обрыв на 30–60 секундах выбрасывает ответ, который уже был в пути, и перенос «падает» на здоровом портале.
Второе: если перенос идёт на сервере приложения, нельзя ходить в портал внутри запроса посетителя — дашборд, который делает десять последовательных чтений в обработчике, не ответит вообще. Данные собираются фоном и отдаются из памяти, с честной подписью «обновлено N минут назад».
Чего ещё стоит ждать
- Санитайзер портала. Встречается текст, похожий на HTML (
<scriptв примере кода), — портал разрывает такое слово, чтобы не пропустить скрипт. Лечится заранее экранированной сущностью. - Лимит на объём статьи. Слишком большой текст (больше мегабайта разметки) не сохранится — такие статьи надо дробить.
- Повторный прогон. Без журнала «что уже перенесено» второй запуск создаст дубли. Журнал (идентификатор источника → идентификатор статьи в новой базе) — обязательная часть, а не улучшение.
Как выстроить работу
Вся методика сводится к одной идее: сначала выгрузить всё локально, потом строить в новой базе. Пока данные не лежат у вас файлами, вы работаете вслепую — любой сбой на середине означает, что половину уже не восстановить. Локальная копия превращает перенос в повторяемую операцию: упало — прогон повторяется с того же места.
- Прочитать структуру и тексты. Источник не менять. Собрать список страниц, найти блок с меню и разобрать его в дерево «раздел → подраздел → статья». Отдельно выгрузить блоки каждой страницы и их содержимое. Источник на этом этапе — только для чтения: в любой момент можно сравнить «до» и «после» или откатиться.
- Сохранить всё локально файлами. Дерево — в один JSON (структура и признак исключения), статьи — в папки по идентификатору: текст в Markdown плюс журнал блоков. Это источник истины для следующих шагов. Побочный эффект: у вас сразу появляется архив базы знаний, независимый от портала.
- Отфильтровать служебные страницы. Убрать шаблонные и технические страницы, записав причину исключения для каждой. Результат — чистое дерево, в котором остались только статьи, несущие знания.
- Создать целевую базу. Одна коллекция в модуле «База знаний» с понятным названием. Перед созданием проверить, нет ли уже базы с таким именем: перенос должен быть идемпотентным.
- Перенести дерево сверху вниз. Обход в порядке «родитель раньше детей»: создаём раздел, получаем идентификатор, создаём в нём подразделы, передавая идентификатор родителя. Только так вложенность сохраняется с первого раза. Текст сохраняется сразу при создании, картинки — отдельным шагом, с заменой маркеров на реальные изображения.
- Сверить и отчитаться. Формальная сверка, а не «на глаз»: дерево в новой базе сравнивается с локальным (число статей, совпадение родителей), проверяется, что тексты непустые, маркеры заменены, число картинок совпадает с источником, а размеры и типы файлов — те же. Итог — отчёт: сколько статей, картинок и видео перенесено, что исключено и почему.
Что это дало на практике. Две базы, 43 статьи, 75 изображений, 39 видео — сверка не нашла расхождений. Один прогон скрипта создал дубли из-за неверного разбора ответа, но именно потому, что перенос шёл скриптом с журналом, дубли удалили подтверждённой операцией и прогон повторили — без ручного разбора 34 статей. В ручном режиме такой сбой означал бы вечер работы.
Промт для агента
Ниже — готовый промт. Замените три плейсхолдера в угловых скобках и отправьте агенту Коворк/Код в новом чате. Промт сформулирован так, чтобы агент сам прошёл все шесть шагов методики и не оставил вам ручной сверки.
Перенеси базу знаний из старого формата в модуль «База знаний» Битрикс24.
Источник: сайт «<название сайта-источника>» (id сайта <id сайта>).
Цель: новая коллекция «<имя новой базы>» в модуле «База знаний».
Правила работы:
1. Источник только читай. Ничего в нём не меняй, не удаляй и
не переименовывай.
2. Сначала выгрузи всё локально в файлы: дерево разделов (JSON) и
текст каждой статьи (Markdown по папкам). Локальные файлы —
источник истины для переноса.
3. Служебные и шаблонные страницы (главная, поиск, сайдбар, примеры
статей) в перенос не включай. Для каждой исключённой страницы
запиши причину.
4. Создай коллекцию с указанным именем. Если она уже существует —
используй её, дубль не создавай.
5. Переноси строго в порядке «родитель раньше детей»: сначала раздел,
затем его подразделы с указанием родителя. Вложенность должна
совпасть с исходной.
6. Изображения переноси внутрь статьи, сохраняя место в тексте.
Видео — ссылкой на источник. Текстовые блоки конвертируй в
Markdown, не-текстовые (разделители, карты, соцкнопки)
пропускай.
7. Перенос сделай идемпотентным: веди журнал «страница источника →
статья в новой базе», чтобы повторный запуск не создавал дубли.
8. В конце сверь результат с локальным деревом: число статей,
совпадение родителей, непустые тексты, число картинок.
Отчитайся таблицей: сколько статей, разделов, картинок и видео
перенесено, что исключено и почему, какие расхождения нашла
сверка.
Если что-то в источнике неоднозначно (пункт меню ведёт на отсутствующую
страницу, одна страница указана дважды, статья практически пустая) —
не угадывай: прими решение, запиши его в отчёт отдельным пунктом
и продолжай.
Что уточнить под себя. Название сайта и его id — из адреса или карточки сайта. Имя новой базы — так, как её будут искать сотрудники. Если в базе есть разделы, которые переносить не нужно (архив, черновики), допишите их списком исключений в правиле 3.
Чего в промте нет и почему. В нём нет ни ключей, ни адресов прокси, ни упоминания конкретного инструмента: доступ к порталу уже настроен в окружении, а способ вызова агент выбирает сам. Промт описывает результат и ограничения, а не пошаговые команды — так он остаётся рабочим, даже если внутренняя механика изменится.
Пример кода
Если хотите не «попросить агента», а сделать перенос сами (или дать агенту каркас), вот схема вызовов. Она намеренно короткая: чтение источника, создание коллекции, обход дерева, сверка. Ключ и базовый адрес берутся из окружения — в коде их быть не должно.
// Ключ и адрес — из окружения приложения, никогда не в коде и не в браузере.
const BASE = process.env.BITRIX_API_BASE_URL;
const KEY = process.env.BITRIX_API_KEY;
async function api(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: { "X-Api-Key": KEY, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
// Портал отвечает десятками секунд — таймаут в минутах, а не в секундах.
signal: AbortSignal.timeout(180_000),
});
const json = await res.json();
if (!res.ok || json.error) {
throw new Error(
`${json.error?.code ?? res.status}: ${json.error?.message ?? "request failed"}`
);
}
// Ответ прокси может быть обёрнут: разворачиваем data.data либо data.
return json.data?.data ?? json.data ?? json;
}
// 1. Чтение источника: страницы и блоки. Только чтение.
const pages = await api("GET", "/pages?scope=KNOWLEDGE&limit=500");
const blocks = await api("GET", `/pages/${pageId}/blocks?scope=KNOWLEDGE`);
const content = await api(
"GET",
`/pages/${pageId}/blocks/${blockId}/content?scope=KNOWLEDGE`
); // -> { html }
// 2. Дерево: разбираем блок меню в «раздел → подраздел → статья».
// Служебные страницы помечаем excluded с причиной, ссылки на несуществующие
// страницы пропускаем. Результат сохраняем локально в tree.json.
// 3. Целевая коллекция. Существует — используем, нет — создаём.
const collection = await api("POST", "/note/collections", {
name: "База знаний компании",
});
// 4. Перенос: строго сверху вниз, родитель до детей.
async function transfer(node, parentId) {
const markdown = renderMarkdown(node); // блоки -> Markdown, картинки -> маркеры
const doc = await api("POST", "/note/documents", {
collectionId: collection.id,
title: node.title,
markdown,
...(parentId ? { parentId } : {}), // родитель уже создан — вложенность сохранится
});
ledger.set(node.pageId, doc.id); // журнал: повторный прогон не создаст дубль
for (const file of node.images) {
// картинки — после текста, с заменой маркеров
await api("POST", `/note/documents/${doc.id}/files`, {
fileName: file.name,
fileContent: file.base64, // base64 читает скрипт с диска, не модель
});
}
for (const child of node.children ?? []) await transfer(child, doc.id);
}
for (const root of tree.root) await transfer(root, null);
// 5. Сверка: дерево в базе против локального дерева.
const remote = await api("GET", `/note/collections/${collection.id}/documents`);
// Проверяем: число статей совпадает, у каждого документа тот же родитель,
// тексты непустые, маркеров не осталось, число картинок равно источнику.
Три вещи, на которые стоит посмотреть в этом коде.
AbortSignal.timeout(180_000) — не перестраховка. Портал на портальных путях отвечает 70–100 секунд; таймаут в 30 секунд «уронит» перенос на полностью здоровом портале.
json.data?.data ?? json.data ?? json — разворачивание ответа. Ровно на этом месте в реальном переносе появились 34 пустых дубля: скрипт читал идентификатор не из того уровня вложенности, получал undefined и создавал статью заново вместо того, чтобы заполнить существующую. Дубли удалили, разбор ответа исправили.
ledger.set(node.pageId, doc.id) — журнал переноса. Он защищает от дублей при повторном прогоне и потом используется для сверки вложений: списка файлов у статьи в API нет, поэтому сверять приходится по сохранённым идентификаторам.
Что в итоге
Перенос, который вручную занял бы дни, агент выполнил за два сеанса — по одному на базу. Почти всё время ушло на ошибки инструмента, а не на статьи: 43 статьи, 75 изображений, 39 видео перенесены, вложенность сохранена и проверена скриптом. Исходный сайт остался нетронутым — его можно сравнить или держать как архив.
Главный вывод не про инструменты, а про порядок: сначала выгрузить всё в файлы, потом строить. Дерево — сверху вниз. Служебные страницы — исключать осознанно. Результат — сверять, а не «вроде выглядит нормально». Эти четыре правила делают перенос базы знаний повторяемой операцией, а не разовым подвигом.
Если перенос упал на середине
- Откройте журнал: в нём видно, какие страницы уже получили идентификатор в новой базе. Повторный прогон продолжит с этого места, а не создаст всё заново.
- Проверьте статьи из журнала: убедитесь, что текст непустой, маркеров вроде
<image-1>не осталось, а картинки на месте. Пустой текст обычно означает, что страницу прочитали только по текстовым блокам, а маркер — что не отработал шаг с файлами. - Сверьте число статей, разделов и картинок с локальным деревом и только потом отключайте старый сайт. Пока он на месте, любое расхождение можно проверить по исходнику.
Если хотите повторить — возьмите промт из раздела «Промт для агента», подставьте название и id своего сайта и попросите агента отчитаться таблицей: по числам сразу видно, всё ли доехало.