Skip to content

Механизм разбиения на блоки (Chunking) ​

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

Можно начать с настроек по умолчанию в интерфейсе (размер блока 512 символов, перекрытие 80 символов, адаптивная стратегия), а затем вносить корректировки на основе следующих наблюдений:

СитуацияРекомендация
В ответе не хватает контекста, информация неполнаяУвеличьте chunk_size или включите родительские и дочерние блоки (поиск по дочерним, ответ по родительским)
Найденные блоки слабо связаны с вопросомУменьшите chunk_size, чтобы темы блоков были более сфокусированными
Материалы представлены отдельными записями (FAQ, словари, таблицы параметров)Установите перекрытие на 0, чтобы исключить взаимное загрязнение соседних записей
Документ представляет собой связное повествование (отчеты, статьи)Увеличьте перекрытие до 150–200 для сохранения смысловых связей между блоками
Предварительный просмотр результатов разбиенияИспользуйте POST /api/v1/chunker/preview для предварительного просмотра без записи в базу данных

После изменения параметров разбиения для существующих документов потребуется повторный анализ.

Таблица параметров и рекомендации по оптимизации ​

Сценарийstrategychunk_sizechunk_overlapПрочее
Стандартные документы (рекомендуемый старт)auto51280—
Структурированная техдокументация / руководстваauto (сработает heading)512–102480Хлебные крошки включаются автоматически
Сканированные PDF (OCR) / книги сплошным текстомauto (сработает heuristic)512–102480–150Указание языков в languages уменьшает число ложных срабатываний
Объемные аналитические / аргументативные текстыauto1000–2000150–200Возможно сочетание с родительскими и дочерними блоками
Точный поиск + широкий контекстЛюбая——enable_parent_child=true, parent 4096 / child 384
FAQ / атомарные записиНе применяется (в FAQ KB каждая запись — отдельный блок)—0FAQIndexMode определяет включение ответов в индекс
Модели эмбеддингов со строгим лимитом токеновЛюбая——Установите token_limit для автоматического пересчета лимита символов
Воспроизведение поведения старых версийlegacyИсходноеЯвно задать 64См. примечания по миграции в ChunkingConfig (уровень KB, переопределяемый при загрузке)

Справочник по механизму разбиения на блоки ​

Разбиение на блоки выполняется на стороне Go (пакет internal/infrastructure/chunker) по адаптивной архитектуре: «профилирование документа → многоуровневая стратегия → валидация результата → пошаговый откат». В Python-компоненте docreader/splitter/ сохранен аналогичный рекурсивный сплиттер для использования в сайдкаре docreader (основным рабочим путем является реализация на Go, в комментариях к docreader/splitter/splitter.py указано, что их параметры по умолчанию согласованы).

Модель конфигурации ​

ChunkingConfig (уровень KB, переопределяемый при загрузке) ​

internal/types/knowledgebase.go:

ПолеТипПо умолчаниюОписание
chunk_sizeint512 (символов)Целевой размер блока. Примерно 100–130 токенов латиницы / 300 символов CJK. Для атомарного контента вроде FAQ рекомендуется 200–400, для связных текстов — 1000–2000
chunk_overlapint80 (около 15%)Перекрытие соседних блоков в символах. Для атомарных данных можно выставить 0, для длинных текстов — 150–200. При значении больше chunk_size/2 принудительно ограничивается половиной размера
separators[]string["\n\n", "\n", "。"]Приоритетный список разделителей для рекурсивного разбиения
strategystring"" (= legacy)Стратегия разбиения: auto / heading / heuristic / recursive / legacy, см. Адаптивная стратегия: три уровня и цепочка отката
token_limitint0 (отключено)Ограничение размера блока по приближенному лимиту токенов; при >0 пересчитывает лимит символов по языку и выбирает меньшее значение (коэффициент безопасности 0.9)
languages[]stringПусто (автоопределение)Подсказка языков для эвристического режима, например ["zh"], ["en","de"]
enable_parent_childboolfalseВключение двухуровневого разбиения на родительские и дочерние блоки, см. Родительские и дочерние блоки (Parent-Child / мультигранулярность)
parent_chunk_sizeint4096Размер родительского блока (только в режиме parent-child)
child_chunk_sizeint384Размер дочернего блока (только в режиме parent-child), перекрытие фиксировано на уровне child_size/5 (~20%)
parser_engine_rules[]ParserEngineRuleПустоМаршрутизация типов файлов по движкам анализа, включая параметры вроде xlsx_first_row_as_header (относится к анализу, но размещено в этой структуре)
table_metadata_instructionsstringПустоБизнес-инструкции для генерации метаданных таблиц CSV/Excel

Единым источником значений по умолчанию являются константы пакета chunker (splitter.go):

go
const (
    DefaultChunkSize    = 512
    DefaultChunkOverlap = 80
)

Примечание по миграции (из комментариев в исходном коде): ранее в Go DefaultConfig использовалось значение 64, в knowledge.go — 50, а в Python docreader — 100. Теперь они стандартизированы на 80. Если в существующей базе знаний в БД сохранено ChunkOverlap=0, при переиндексации будет применено значение 80, поэтому векторы могут незначительно отличаться от прежних.

SplitterConfig (конфигурация времени выполнения) ​

Сервисный слой преобразует ChunkingConfig через функцию buildSplitterConfigFromChunking (knowledge_process.go) в chunker.SplitterConfig{ChunkSize, ChunkOverlap, Separators, Strategy, TokenLimit, Languages}; внутри пакета chunker функция ensureDefaults выполняет финальную валидацию:

  • При TokenLimit > 0: вычисляется charBudget = CharsForTokenLimit(TokenLimit, lang), и если оно меньше ChunkSize, то заменяет его (tokens.go, соотношение символов к токенам: en 4.0, de 4.5, zh 1.7, mixed 3.0 с коэффициентом безопасности 0.9 для гарантии непревышения контекста embedding-модели);
  • При ChunkOverlap > ChunkSize/2 перекрытие принудительно ограничивается значением ChunkSize/2.

Взаимосвязь IndexingStrategy и разбиения на блоки ​

Четыре переключателя в internal/types/indexing_strategy.go определяют, в какие конвейеры направляются блоки:

go
type IndexingStrategy struct {
    VectorEnabled  bool // Векторный индекс
    KeywordEnabled bool // Индекс ключевых слов BM25
    WikiEnabled    bool // Генерация Wiki
    GraphEnabled   bool // Извлечение графа
}
  • Если NeedsChunks() (хотя бы один включен) возвращает false, разбиение на блоки не требуется;
  • Если NeedsEmbedding() (vector || keyword) возвращает false, блоки сохраняются только в БД, пропуская этап BatchIndex (в processChunks вызывается skipStage(StageEmbedding));
  • Генераторы Wiki и графов задействуют текстовые блоки на этапе постобработки.

Адаптивная стратегия: три уровня и цепочка отката ​

Публичными точками входа являются chunker.Split(text, cfg) и chunker.SplitWithDiagnostics (strategy.go). Обработка значений cfg.Strategy (resolveChainWithProfile):

Значение StrategyЦепочка попыток (Tier Chain)Описание
autoОпределяется профайлером, обычно подмножество [heading, heuristic, legacy]Рекомендуемое значение, автоматический выбор под структуру документа
heading[heading, legacy]Принудительное разбиение по заголовкам с откатом к legacy
heuristic[heuristic, legacy]Принудительное эвристическое разбиение
recursive[legacy]recursive является публичным синонимом legacy
legacy / "" (пусто)[legacy]Классический рекурсивный сплиттер, вариант по умолчанию для совместимости

Результат каждого уровня должен пройти валидацию Validator (validator.go), иначе цепочка переходит к следующему уровню; уровень legacy является гарантированным фундаментом — даже если он не проходит валидацию, его результат все равно принимается (пустой результат исключен):

Правила отклонения Validator:

ПравилоПричина отклонения
Нет выходных данныхno chunks produced
Документ больше 2*chunkSize, но получен всего 1 блокsingle chunk for large document
Мелких блоков (<50 символов не в конце) больше 1/4 от общего числа и больше 2 штукtoo many tiny chunks
Максимальный блок меньше chunkSize/4 (чрезмерная фрагментация)all chunks far below target size
Максимальный блок превышает 2*chunkSize (игнорирование лимита)chunk exceeds 2x target size

Профилирование документов (profiler.go) ​

Функция ProfileDocument(text) за один проход формирует структуру DocProfile: общее число символов/строк, среднее и дисперсию длины строк, количество заголовков Markdown всех уровней, число нумерованных разделов, коротких строк в ВЕРХНЕМ РЕГИСТРЕ, последовательных пустых строк, символов разрыва страницы \f, горизонтальных разделителей, маркеров глав на немецком/английском/китайском, строк колонтитулов, наличие таблиц/кода, долю кода и определенный язык (по первым 4096 байтам через DetectLanguage возвращает zh/de/en/mixed на основе доли CJK/латиницы).

Функция SelectStrategy(profile) формирует цепочку попыток:

go
// Кандидат Tier 1: структура заголовков Markdown
if p.MdHeadingTotal >= 3 && p.HeadingDensity() > 0.005 && p.DominantHeadingLevel() > 0 {
    chain = append(chain, TierHeading)
}
// Кандидат Tier 2: эвристические границы
if p.HeuristicMarkerTotal() >= 5 || p.FormFeedCount > 0 ||
    p.GermanChapterCount+p.EnglishChapterCount+p.ChineseChapterCount > 0 {
    chain = append(chain, TierHeuristic)
}
chain = append(chain, TierLegacy) // Всегда в качестве страховки

DominantHeadingLevel выбирает основной уровень разделения: предпочтение отдается «наиболее верхнему уровню, встречающемуся ≥3 раз» (структурному скелету документа), в противном случае берется наиболее глубокий из встретившихся уровней.

Блок-схема принятия решений при разбиении на блоки ​

Детальный разбор трех алгоритмов разбиения ​

Tier 1: Разбиение с учетом заголовков (heading_splitter.go) ​

Применяется для: документов с четкой структурой заголовков Markdown (техническая документация, экспортированные файлы Word, PDF с оглавлением и закладками).

Алгоритм:

  1. Используя DominantHeadingLevel в качестве базового уровня, функция findHeadingBoundaries находит все строки заголовков с level <= primaryLevel как границы разделов (пропуская псевдозаголовки внутри блоков кода); если границ ≤1, происходит немедленный откат к SplitText.
  2. HeadingHierarchy (heading_hierarchy.go) ведет стек заголовков глубиной до 6 уровней: добавление заголовка уровня N извлекает все уровни ≥N, а метод BreadcrumbWithHashes() формирует навигационную цепочку вида "# Глава 1\n## Раздел 1.2".
  3. Для каждого раздела:
  • Если длина цепочки + 2 + длина раздела <= ChunkSize: весь раздел сохраняется как один Chunk, цепочка заголовков помещается в ContextHeader (не входит в Content);
  • Если раздел слишком велик: внутри раздела вызывается вторичное разделение SplitText, где каждый подблок получает через sectionBreadcrumbs + breadcrumbAtOffset наиболее глубокий актуальный путь заголовков в качестве ContextHeader (подзаголовки ###/#### внутри раздела не уплощаются в заголовок раздела).
  1. coalesceTinyChunks: соседние блоки меньше ChunkSize/2 (с нижним порогом 200), имеющие общий префикс заголовков и непрерывные позиции (cur.End == next.Start), объединяются — благодаря этому короткие разделы в стиле FAQ не сваливаются целиком в legacy из-за ошибки «too many tiny chunks».

Инвариант позиционирования: равенство End - Start == utf8.RuneCountInString(Content) сохраняется всегда (навигационные цепочки не входят в подсчет символов Content), на что опираются восстановление документов и подсветка цитат в интерфейсе.

Tier 2: Эвристическое разбиение по границам (heuristic_splitter.go + patterns.go) ​

Применяется для: документов без явных заголовков Markdown, но с различимыми структурными признаками (PDF после OCR, текстовые инструкции, сканированные книги).

Сначала выполняется поиск всех потенциальных границ (при совпадении смещения выбирается наивысший приоритет):

Тип границыРегулярное выражение (patterns.go)Приоритет
Разрыв страницы \fFormFeedPattern100
Нумерованный подраздел (1.2.3 Заголовок, IV. Results)NumberedSectionPattern90
Обозначение главы (Chapter 3 / Kapitel 2 / Глава 1, Раздел 3)EnglishChapterPattern / GermanChapterPattern / ChineseChapterPattern (фильтруются по Languages, при пустом списке активны все)85
Короткая строка в ВЕРХНЕМ РЕГИСТРЕAllCapsHeadingPattern70
Визуальный разделитель (---, ===, ***)VisualSeparatorPattern60
Номер страницы / колонтитул (Page 3 of 10 / Страница 3)PageFooterPattern50
Подряд ≥3 символов перевода строкиExcessiveBlanksPattern40

Затем:

  • dropBoundsInsideSpans: границы, попавшие внутрь защищенных диапазонов (таблицы, блоки кода, формулы, см. Tier 3: Рекурсивное разбиение legacy (splitter.go, портировано с Python)), отбрасываются, сохраняются только границы по краям;
  • Жадная упаковка: блоки накапливаются по границам, фиксируя Chunk при превышении ChunkSize и объеме не менее max(ChunkSize/4, 50);
  • Слишком большие блоки между границами рекурсивно передаются в SplitText;
  • Выравнивание перекрытия: applyOverlapAligned в окне [curEnd-2*overlap, curEnd) стремится привязаться к ближайшей смысловой границе или переносу строки, исключая обрыв следующего блока на середине слова.

Tier 3: Рекурсивное разбиение legacy (splitter.go, портировано с Python) ​

Базовая реализация, перенесенная из docreader/splitter/splitter.py, служит запасным механизмом для всех уровней и движком вторичного разделения внутри длинных разделов. Выполняется в три шага:

Шаг 1 — Определение защищенных диапазонов (protectedSpans), которые никогда не разрезаются посередине:

go
var protectedPatterns = []*regexp.Regexp{
    regexp.MustCompile(`(?s)\$\$.*?\$\$`),                        // Блочные формулы LaTeX
    regexp.MustCompile(`!\[[^\]\n]{0,200}\]\([^)\n]{1,500}\)`),   // Изображения Markdown (однострочные, с лимитом длины)
    regexp.MustCompile(`\[[^\]\n]{1,200}\]\([^)\n]{1,500}\)`),    // Ссылки Markdown (однострочные, с лимитом длины)
    /* Заголовок + строка разделителя */ /* Строки данных таблицы */ // Таблицы Markdown
    regexp.MustCompile("(?s)```(?:\\w+)?[\\r\\n].*?```"),          // Огражденные блоки кода (fenced)
    regexp.MustCompile("`[^`\\r\\n]+`"),                            // Встроенный код в строке
}

Сопоставление картинок и ссылок ограничено одной строкой: текст ссылки не более 200 символов, адрес не более 500 символов (в соответствии с правилом CommonMark о запрете пустых строк внутри ссылок). Это предотвращает объединение изолированной открывающей скобки [ от OCR с далекой ]( в один неделимый гигантский диапазон.

Если защищенный диапазон превышает maxProtectedSize = 7500 рун (огромные таблицы или длинные блоки кода), он принудительно разбивается по переносам строк или пробелам во избежание превышения лимитов API эмбеддингов.

Шаг 2 — Рекурсивное разделение (splitBySeparators): текст нарезается по списку разделителей Separators (по умолчанию \n\n → \n → 。), и если фрагменты все еще больше ChunkSize, к ним рекурсивно применяется следующий разделитель.

Шаг 3 — Слияние и перекрытие (mergeUnits): мелкие элементы компонуются в блоки; когда curLen + uLen + headersLen > chunkSize, блок фиксируется, а computeOverlap берет фрагмент из хвоста текущего блока в качестве начала следующего; абсолютный предел составляет absoluteMaxSize = 7500.

Функция computeOverlap извлекает семантический суффикс, а не просто фиксированный срез символов:

  • ChunkOverlap задает жесткий верхний предел, а не обязательный размер. Окно выбирает min(ChunkOverlap, ChunkSize - длина следующего элемента) символов с конца блока, захватывая дополнительно 4 символа вперед (semanticOverlapLookbehind, длина максимального разделителя \r\n\r\n), чтобы разделитель не оказался срезан границей окна;
  • Приоритет границ: разделитель абзацев (\n\n) > перевод строки (\n) > знаки конца предложения (。, ?, !, а также англоязычные . / ? / ! с обязательным последующим пробелом, чтобы не разрывать конструкции вида 3.14 или v1.2). При равном приоритете выбирается самая ранняя граница в окне для максимизации полезного перекрытия;
  • Окно может разрезать отдельный splitUnit изнутри (в старых реализациях перенос шел только целыми юнитами, что часто обнуляло перекрытие), но не пересекает синтетические элементы нулевой ширины start == end вроде заголовков таблиц, сохраняя инвариант соответствия смещений Start/End телу блока;
  • Разделители внутри защищенных областей (блоки кода, инлайн-код, формулы, таблицы, ссылки) границами не признаются;
  • Если внутри окна нет корректных семантических границ, перекрытие не сохраняется, исключая обрыв на середине слова.

Обработка таблиц: отслеживание заголовков (header_tracker.go) ​

Когда большая таблица Markdown делится на несколько блоков, последующие блоки теряют заголовки столбцов. Компонент headerTracker (портирован из docreader/splitter/header_hook.py) решает эту проблему:

  • Фиксирует структуру «строка заголовков + строка разделителей» (| A | B | + | --- | --- |) как активный заголовок, удерживая его до завершения таблицы (пустая строка или строка, не начинающаяся с |);
  • При формировании нового блока в mergeUnits, если активный заголовок отсутствует в зоне перекрытия или следующем элементе и число колонок совпадает (headerAlreadyPresent / headerColumnMismatch), заголовок вставляется как элемент нулевой ширины start==end в начало нового блока — каждый фрагмент таблицы содержит названия колонок;
  • Пустые заголовки (частые в MarkItDown || + |---|---|) восполняются именами из первой строки данных (pendingExtend);
  • Контроль границ таблиц: при появлении строк новой таблицы после \n\n в конце блока или изменении числа колонок действие старого заголовка прекращается с принудительной фиксацией блока (headerEndedThisUnit), исключая перенос заголовка на другую таблицу.

Кроме того, встроенные HTML-таблицы от всех движков анализа (MinerU, PaddleOCR-VL, VLM OCR и др.) и рукописного Markdown перед нарезкой на блоки преобразуются в таблицы GFM Markdown с помощью NormalizeHTMLTables (docparser/html_table_normalizer.go), попадая под общую логику защиты и трекинга заголовков. Таблицы с реальными объединениями ячеек (rowspan/colspan > 1), которые невозможно безопасно преобразовать, сохраняются в HTML, но каждый тег <tr> обособляется в отдельную строку с отступами, что позволяет нарезать блоки по границам строк, не упираясь в жесткий лимит 7500 символов.

Обработка изображений ​

  • Ссылки на изображения Markdown ![alt](url) защищены от разрыва;
  • chunker.ExtractImageRefs(text) (splitter.go) извлекает ссылки на изображения с поддержкой одного уровня вложенности скобок для привязки картинок к блокам в processChunks;
  • Для каждого изображения на мультимодальном этапе генерируются два дочерних блока image_caption и image_ocr (ParentChunkID указывает на родительский текстовый блок), которые индексируются отдельно: изображение находится по смыслу, а ссылка ведет на фрагмент исходного документа.

Контекстный заголовок (ContextHeader) ​

Поле Chunk.ContextHeader представляет собой навигационную цепочку заголовков, которая хранится отдельно от основного содержимого Content:

go
// internal/types/chunk.go
// ContextHeader is a Markdown heading breadcrumb prepended when indexing.
// It is persisted so a later content edit can rebuild the same index input.
ContextHeader string `json:"-" gorm:"type:text"`

func (c *Chunk) EmbeddingContent() string {
    body := strings.TrimSpace(c.Content)
    if c.ContextHeader == "" { return body }
    return c.ContextHeader + "\n\n" + body
}

Особенности архитектуры:

  • Влияет только на эмбеддинг, не искажая исходный текст: processChunks формирует индексируемый текст в виде Заголовок знания + "\n" + chunk.EmbeddingContent(), благодаря чему вектор вбирает контекст раздела, тогда как Content остается чистым исходным фрагментом со стабильными смещениями StartAt/EndAt;
  • Сохраняется в столбце БД chunks.context_header (миграция 000078). В ранних версиях поле существовало только в оперативной памяти (gorm:"-") и удалялось после индексации; с появлением ручного редактирования блоков для точного перестроения индекса потребовалось постоянное хранение. Тег json:"-" сохранен, поле не передается в ответах API;
  • В режиме родительских и дочерних блоков функция mergeBreadcrumbs (strategy.go) объединяет цепочки уровней без дублирования верхних строк, обеспечивая дочерним блокам максимально детальный путь.

Родительские и дочерние блоки (Parent-Child / мультигранулярность) ​

При включении EnableParentChild = true активируется двухуровневое разделение (chunker.SplitParentChild с поддержкой стратегий; legacy-вариант — SplitTextParentChild):

  1. Конфигурация parentCfg (по умолчанию 4096 символов, наследует overlap и Strategy) формирует родительские блоки;
  2. Каждый родительский блок нарезается через childCfg (по умолчанию 384 символа, overlap = 20% от размера, наследует Strategy) на дочерние блоки;
  3. Нумерация дочерних блоков Seq сквозная по всему документу, смещения Start/End рассчитываются от начала документа, а ParentIndex ссылается на родительский блок. Если родительский блок порождает ровно один идентичный дочерний блок, родительский блок опускается (ParentIndex = -1) для экономии места.

Правила сохранения на уровне сервиса (knowledge_process.go processChunks):

  • Родительские блоки получают тип ChunkTypeParentText, сохраняются только в БД и не попадают в векторный индекс, связываясь цепочкой PreChunkID/NextChunkID;
  • Дочерние блоки получают тип ChunkTypeText с заполненным ParentChunkID и являются единственными объектами векторизации и индексации;
  • При поиске совпадение происходит по дочернему блоку, но в выдачу возвращается содержимое родительского блока: точный поиск на малом окне сочетается с широким контекстом при ответе.

Функция buildParentChildConfigs требует явной передачи Strategy: иначе пустая стратегия трактуется как legacy-уровень, приводя к тихой потере выравнивания заголовков и навигационных цепочек ContextHeader.

Специфика блоков FAQ ​

Базы знаний FAQ не используют алгоритмы нарезки: каждый вопрос-ответ изначально представляет собой отдельный блок с типом ChunkTypeFAQ (knowledge_faq.go), тело Content формируется функцией buildFAQChunkContent в зависимости от режима:

go
builder.WriteString(fmt.Sprintf("Q: %s\n", meta.StandardQuestion))
// Similar Questions: перечисляются по порядку
// Негативные примеры (NegativeQuestions) не включаются в Content — они не должны индексироваться
if mode == types.FAQIndexModeQuestionAnswer && len(meta.Answers) > 0 {
    // Answers: перечисляются по порядку
}
  • Структурированные данные хранятся в Chunk.Metadata (FAQChunkMetadata), а ContentHash (нормализованный SHA256) используется для дедупликации при импорте и дифференциальной синхронизации клонов;
  • Режимы индексации: question_only / question_answer (на уровне базы знаний FAQIndexMode); режимы индексации вопросов: combined (стандартный и похожие вопросы объединяются в один вектор) / separate (каждый похожий вопрос получает отдельный вектор с поддержкой инкрементных обновлений);
  • Рекомендация устанавливать chunk_overlap = 0 для FAQ выполняется естественным образом — перекрытия между записями нет.

Интеграция в конвейер добавления знаний ​

Цепочка вызовов в knowledge_process.go:

processDocument
  └─ convert()                          // docreader → Markdown
  └─ imageResolver.ResolveAndStore()    // сохранение картинок, обновление URL
  └─ buildSplitterConfigFromChunking()  // ChunkingConfig → SplitterConfig
  └─ chunker.Split / SplitParentChild   // алгоритмы разбиения
  └─ processChunks()                    // формирование строк Chunk, EmbeddingContent → BatchIndex

Этап разбиения выделен в отдельный Span (StageChunking с метриками chunks_planned/chunks_written/total_text_chars), при ошибках возвращается код ErrCodeChunkingFailed.

Возможности отладки: POST /api/v1/chunker/preview (chunker_debug.go) ​

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

Тело запроса:

json
{
  "text": "Образец текста…",
  "chunking_config": {
    "chunk_size": 512, "chunk_overlap": 80,
    "separators": ["\n\n", "\n", "。"],
    "strategy": "auto", "token_limit": 0, "languages": ["zh"],
    "enable_parent_child": false,
    "parent_chunk_size": 4096, "child_chunk_size": 384
  }
}

Если передано enable_parent_child: true, предварительный просмотр возвращает дочерние блоки (единицы реального поиска), а диагностические данные формируются на этапе нарезки родительских блоков. Превью и рабочий конвейер используют единые функции chunker.NormalizeSplitterConfig() и chunker.DeriveParentChildConfigs(), что гарантирует полное совпадение результатов теста и реальной обработки.

Структура ответа (PreviewChunkingResponse):

ПолеОписание
selected_tierВыбранный уровень стратегии (heading/heuristic/legacy)
tier_chainЦепочка попыток
rejectedСписок отклоненных уровней с причинами от Validator (TierRejection{tier, reason})
profileПолный профиль DocProfile (при auto берется из процесса выбора, при явном выборе рассчитывается по требованию)
chunks[]Массив блоков с полями seq/start/end/size_chars/size_tokens_approx/context_header/content
statsСтатистика count/avg_chars/min_chars/max_chars/stddev_chars, рассчитанная по полному набору блоков; при усечении содержит truncated_to

Защитные ограничения: входной лимит previewMaxChars = 64k рун (ошибка 413), максимальное число возвращаемых блоков previewMaxChunks = 500 (статистика рассчитывается по полному объему), тайм-аут previewTimeout = 5s (при превышении возвращается код 504). Диагностика формируется методом chunker.SplitWithDiagnostics, формат JSON является частью публичного API.

Регистрация маршрута (internal/router/routes_knowledge.go):

go
g.apiKeyRoute(r, http.MethodPost, "/chunker/preview",
    apiKeyRetrieve(apiKeyIngest(apiKeyFullAccess())), g.Viewer(), handler.PreviewChunking)

Разделитель на стороне Python (docreader/splitter/) ​

Класс TextSplitter в docreader/splitter/splitter.py представляет собой прототип реализации legacy на Go и сохранен в сайдкаре docreader:

  • Значения по умолчанию согласованы с Go: DEFAULT_CHUNK_SIZE = 512, DEFAULT_CHUNK_OVERLAP = 80; стандартные разделители ["\n", "。", " "] с посимвольной страховкой;
  • Тот же набор защищенных регулярных выражений (формулы, картинки, ссылки, таблицы, блоки кода), конвейер _split (рекурсивное деление) → _split_protected + _join (изоляция зон) → _merge (слияние перекрытий + добавление заголовков HeaderTracker);
  • Возвращает тройки (start, end, text) с проверкой возможности полного восстановления "".join(splits) == text; функция restore_text демонстрирует алгоритм сборки без дублирования перекрытий;
  • Класс HeaderTracker в docreader/splitter/header_hook.py полностью соответствует поведению header_tracker.go в Go (распознавание заголовков, восполнение пустых колонок, сброс при несовпадении колонок).

Реализация ​

Исходный код компонентов:

МодульФайл
Точка входа стратегий и откатinternal/infrastructure/chunker/strategy.go
Профилирование документовinternal/infrastructure/chunker/profiler.go
Tier 1: Разбиение по заголовкамinternal/infrastructure/chunker/heading_splitter.go, heading_hierarchy.go
Tier 2: Эвристическое разбиениеinternal/infrastructure/chunker/heuristic_splitter.go, patterns.go
Tier 3: Рекурсивное разбиение (legacy)internal/infrastructure/chunker/splitter.go
Отслеживание заголовков таблицinternal/infrastructure/chunker/header_tracker.go
Валидация результатовinternal/infrastructure/chunker/validator.go
Оценка токеновinternal/infrastructure/chunker/tokens.go
Структуры конфигурацииinternal/types/knowledgebase.go (ChunkingConfig), internal/types/indexing_strategy.go
Подключение к конвейеруinternal/application/service/knowledge_process.go (buildSplitterConfigFromChunking / buildParentChildConfigs / processChunks)
Эндпоинт отладкиinternal/handler/chunker_debug.go (POST /api/v1/chunker/preview)
Компоненты Pythondocreader/splitter/splitter.py, docreader/splitter/header_hook.py

Версия v0.8.3 · MIT License