Механизм разбиения на блоки (Chunking)
Разбиение на блоки делит документ на единицы поиска. Меньшие блоки помогают сфокусироваться на конкретной теме, тогда как более крупные блоки сохраняют больше контекста. Размер блоков и диапазон перекрытия настраиваются с учетом структуры документа и результатов поиска.
Можно начать с настроек по умолчанию в интерфейсе (размер блока 512 символов, перекрытие 80 символов, адаптивная стратегия), а затем вносить корректировки на основе следующих наблюдений:
| Ситуация | Рекомендация |
|---|---|
| В ответе не хватает контекста, информация неполная | Увеличьте chunk_size или включите родительские и дочерние блоки (поиск по дочерним, ответ по родительским) |
| Найденные блоки слабо связаны с вопросом | Уменьшите chunk_size, чтобы темы блоков были более сфокусированными |
| Материалы представлены отдельными записями (FAQ, словари, таблицы параметров) | Установите перекрытие на 0, чтобы исключить взаимное загрязнение соседних записей |
| Документ представляет собой связное повествование (отчеты, статьи) | Увеличьте перекрытие до 150–200 для сохранения смысловых связей между блоками |
| Предварительный просмотр результатов разбиения | Используйте POST /api/v1/chunker/preview для предварительного просмотра без записи в базу данных |
После изменения параметров разбиения для существующих документов потребуется повторный анализ.
Таблица параметров и рекомендации по оптимизации
| Сценарий | strategy | chunk_size | chunk_overlap | Прочее |
|---|---|---|---|---|
| Стандартные документы (рекомендуемый старт) | auto | 512 | 80 | — |
| Структурированная техдокументация / руководства | auto (сработает heading) | 512–1024 | 80 | Хлебные крошки включаются автоматически |
| Сканированные PDF (OCR) / книги сплошным текстом | auto (сработает heuristic) | 512–1024 | 80–150 | Указание языков в languages уменьшает число ложных срабатываний |
| Объемные аналитические / аргументативные тексты | auto | 1000–2000 | 150–200 | Возможно сочетание с родительскими и дочерними блоками |
| Точный поиск + широкий контекст | Любая | — | — | enable_parent_child=true, parent 4096 / child 384 |
| FAQ / атомарные записи | Не применяется (в FAQ KB каждая запись — отдельный блок) | — | 0 | FAQIndexMode определяет включение ответов в индекс |
| Модели эмбеддингов со строгим лимитом токенов | Любая | — | — | Установите 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_size | int | 512 (символов) | Целевой размер блока. Примерно 100–130 токенов латиницы / 300 символов CJK. Для атомарного контента вроде FAQ рекомендуется 200–400, для связных текстов — 1000–2000 |
chunk_overlap | int | 80 (около 15%) | Перекрытие соседних блоков в символах. Для атомарных данных можно выставить 0, для длинных текстов — 150–200. При значении больше chunk_size/2 принудительно ограничивается половиной размера |
separators | []string | ["\n\n", "\n", "。"] | Приоритетный список разделителей для рекурсивного разбиения |
strategy | string | "" (= legacy) | Стратегия разбиения: auto / heading / heuristic / recursive / legacy, см. Адаптивная стратегия: три уровня и цепочка отката |
token_limit | int | 0 (отключено) | Ограничение размера блока по приближенному лимиту токенов; при >0 пересчитывает лимит символов по языку и выбирает меньшее значение (коэффициент безопасности 0.9) |
languages | []string | Пусто (автоопределение) | Подсказка языков для эвристического режима, например ["zh"], ["en","de"] |
enable_parent_child | bool | false | Включение двухуровневого разбиения на родительские и дочерние блоки, см. Родительские и дочерние блоки (Parent-Child / мультигранулярность) |
parent_chunk_size | int | 4096 | Размер родительского блока (только в режиме parent-child) |
child_chunk_size | int | 384 | Размер дочернего блока (только в режиме parent-child), перекрытие фиксировано на уровне child_size/5 (~20%) |
parser_engine_rules | []ParserEngineRule | Пусто | Маршрутизация типов файлов по движкам анализа, включая параметры вроде xlsx_first_row_as_header (относится к анализу, но размещено в этой структуре) |
table_metadata_instructions | string | Пусто | Бизнес-инструкции для генерации метаданных таблиц CSV/Excel |
Единым источником значений по умолчанию являются константы пакета chunker (splitter.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 определяют, в какие конвейеры направляются блоки:
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) формирует цепочку попыток:
// Кандидат 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 с оглавлением и закладками).
Алгоритм:
- Используя
DominantHeadingLevelв качестве базового уровня, функцияfindHeadingBoundariesнаходит все строки заголовков сlevel <= primaryLevelкак границы разделов (пропуская псевдозаголовки внутри блоков кода); если границ ≤1, происходит немедленный откат кSplitText. HeadingHierarchy(heading_hierarchy.go) ведет стек заголовков глубиной до 6 уровней: добавление заголовка уровня N извлекает все уровни ≥N, а методBreadcrumbWithHashes()формирует навигационную цепочку вида"# Глава 1\n## Раздел 1.2".- Для каждого раздела:
- Если
длина цепочки + 2 + длина раздела <= ChunkSize: весь раздел сохраняется как один Chunk, цепочка заголовков помещается вContextHeader(не входит в Content); - Если раздел слишком велик: внутри раздела вызывается вторичное разделение
SplitText, где каждый подблок получает черезsectionBreadcrumbs+breadcrumbAtOffsetнаиболее глубокий актуальный путь заголовков в качестве ContextHeader (подзаголовки###/####внутри раздела не уплощаются в заголовок раздела).
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) | Приоритет |
|---|---|---|
Разрыв страницы \f | FormFeedPattern | 100 |
Нумерованный подраздел (1.2.3 Заголовок, IV. Results) | NumberedSectionPattern | 90 |
Обозначение главы (Chapter 3 / Kapitel 2 / Глава 1, Раздел 3) | EnglishChapterPattern / GermanChapterPattern / ChineseChapterPattern (фильтруются по Languages, при пустом списке активны все) | 85 |
| Короткая строка в ВЕРХНЕМ РЕГИСТРЕ | AllCapsHeadingPattern | 70 |
Визуальный разделитель (---, ===, ***) | VisualSeparatorPattern | 60 |
Номер страницы / колонтитул (Page 3 of 10 / Страница 3) | PageFooterPattern | 50 |
| Подряд ≥3 символов перевода строки | ExcessiveBlanksPattern | 40 |
Затем:
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), которые никогда не разрезаются посередине:
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
защищены от разрыва; chunker.ExtractImageRefs(text)(splitter.go) извлекает ссылки на изображения с поддержкой одного уровня вложенности скобок для привязки картинок к блокам вprocessChunks;- Для каждого изображения на мультимодальном этапе генерируются два дочерних блока
image_captionиimage_ocr(ParentChunkIDуказывает на родительский текстовый блок), которые индексируются отдельно: изображение находится по смыслу, а ссылка ведет на фрагмент исходного документа.
Контекстный заголовок (ContextHeader)
Поле Chunk.ContextHeader представляет собой навигационную цепочку заголовков, которая хранится отдельно от основного содержимого Content:
// 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):
- Конфигурация
parentCfg(по умолчанию 4096 символов, наследует overlap и Strategy) формирует родительские блоки; - Каждый родительский блок нарезается через
childCfg(по умолчанию 384 символа, overlap = 20% от размера, наследует Strategy) на дочерние блоки; - Нумерация дочерних блоков
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 в зависимости от режима:
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)
Эндпоинт предварительного просмотра в режиме только для чтения, используемый панелью отладки разбиения в редакторе базы знаний для проверки параметров на образце текста — без записи в БД, без создания эмбеддингов и без логирования текста.
Тело запроса:
{
"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):
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) |
| Компоненты Python | docreader/splitter/splitter.py, docreader/splitter/header_hook.py |
