Files
Enterprise-Knowledge-Base/internal/infrastructure/chunker/validator.go
T
Claude ae6fde2310 feat(chunker): foundation for adaptive 3-tier chunking
Adds the building blocks for tiered chunking strategies — multilingual
patterns, token approximation, document profiling, validation, and a
strategy resolver that today still falls through to the legacy
SplitText. Default Strategy is empty/legacy so existing knowledge bases
continue to chunk identically.

- tokens.go: char/token approximation per language (en/de/zh/mixed)
  and a coarse DetectLanguage heuristic
- patterns.go: source-of-truth regex bibliothek for headings, numbered
  sections, multilingual chapter markers, visual separators, etc.
- profiler.go: single-pass document scan + SelectStrategy chain
- validator.go: rejects obviously broken chunk-sets to drive fallbacks
- strategy.go: public Split / SplitParentChild entry points; resolves
  StrategyAuto via the profiler, anything else maps to a fixed tier
- splitter.go: SplitterConfig gains Strategy / TokenLimit / Languages
  fields and DefaultChunkSize / DefaultChunkOverlap constants (overlap
  raised to 80 ≈ 15%)
- knowledge.go: switches both call sites to chunker.Split /
  SplitParentChild — behavior unchanged for empty Strategy

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00

76 lines
2.3 KiB
Go

// Package chunker - validator.go inspects a tier's output and decides whether
// it is good enough to ship or whether the strategy chain should fall through
// to the next tier. The validator is intentionally permissive: a single
// "obviously broken" output is rejected, but plausible-looking variation is
// accepted so we don't oscillate between tiers.
package chunker
import "math"
// ValidationResult captures the verdict and reason for a chunk-set.
type ValidationResult struct {
OK bool
Reason string
}
// ValidateChunks checks whether the given chunks form a usable result for a
// document of totalChars characters with a target chunkSize. Returns OK=true
// when no broken-output indicator triggers.
func ValidateChunks(chunks []Chunk, totalChars, chunkSize int) ValidationResult {
if len(chunks) == 0 {
return ValidationResult{Reason: "no chunks produced"}
}
// A single chunk for a document much larger than chunkSize means the
// strategy did not actually split — fail so the next tier runs.
if len(chunks) == 1 && totalChars > 2*chunkSize {
return ValidationResult{Reason: "single chunk for large document"}
}
// Compute size statistics.
var sum, sumSq float64
maxLen, minLen := 0, math.MaxInt32
for _, c := range chunks {
l := len([]rune(c.Content))
sum += float64(l)
sumSq += float64(l * l)
if l > maxLen {
maxLen = l
}
if l < minLen {
minLen = l
}
}
avg := sum / float64(len(chunks))
// All but the last chunk should carry meaningful content. We allow the
// last chunk to be tiny because tail residue is normal.
tinyCount := 0
for i, c := range chunks {
if i == len(chunks)-1 {
continue
}
if len([]rune(c.Content)) < 50 {
tinyCount++
}
}
if tinyCount > len(chunks)/4 && tinyCount > 2 {
return ValidationResult{Reason: "too many tiny chunks"}
}
// Reject when no chunk reached at least 25% of the target — the splitter
// is fragmenting too aggressively to be useful.
if maxLen < chunkSize/4 && totalChars > chunkSize {
return ValidationResult{Reason: "all chunks far below target size"}
}
// Sanity check on absolute upper bound. Anything past 2x chunkSize is a
// red flag — the splitter ignored its size budget.
if maxLen > 2*chunkSize && chunkSize > 0 {
return ValidationResult{Reason: "chunk exceeds 2x target size"}
}
_ = avg
return ValidationResult{OK: true}
}