El límite superior de calidad de RAG está determinado por la calidad del corpus. No importa cuán sofisticado sea el algoritmo de incrustación y búsqueda, si los datos originales no se refinan, los resultados serán malos. En este proyecto, la razón por la que la canalización de fragmentación puede respetar los separadores --- y los límites de los párrafos es porque los datos ya se han refinado en esas estructuras en la etapa de recopilación.
books/
├── 0. 자체 정리/ ← 가장 고품질: 1차 수집 → AI 재분류 → 주제별 MD
│ ├── 01_음양론/
│ ├── 02_오행론/
│ ├── ... (총 20개 주제 폴더 + 부록)
│ └── 20_실전사례/
├── 1. 나의 사주명리-심화편/ ← 고품질: 전자책 추출, 장/절 구조 유지
├── 2. 사주명리 실전 100구문/ ← 고품질: 전자책 추출
└── 99. 온라인 자료 (정리 X)/ ← 2차 수집 원본: PDF/TXT, 구조 불균일
Observación clave: El nivel 0 es el resultado de una reclasificación precisa por categoría utilizando IA de datos recopilados principalmente en línea. El texto original se dividió en temas (Teoría del Yin-Yang, Teoría de los Cinco Elementos, Teoría del Gyeok-Guk, etc.) y se le dio una estructura Markdown (--- línea divisoria, encabezado ##) para maximizar la calidad de la fragmentación. Tier 99 es una segunda recogida online que se realizó posteriormente y que aún no está organizada. La calidad de la fragmentación es alta (de 0 a 2) y relativamente baja (99). En otras palabras, mejor es el rendimiento de RAG para los datos que han pasado por los siguientes pasos: recopilación → reclasificación basada en IA → estructuración.
Reglas aplicadas al recopilar materiales en línea para este proyecto:
1. Fuente requerida:
모든 파일에 `출처: URL` 또는 `https://` 포함 필수.
없으면 자동 삭제:
grep -cE '(https?://|출처:)' "$f" → 0이면 rm
Los documentos sin fuentes no son fiables y, por tanto, no se incluyen en el corpus.
2. --- Separe los temas con divisores:
Cuando haya varios temas en un archivo TXT, distribúyalos claramente con ---. Este es un dispositivo clave que evita "fragmentos que crucen los límites del tema" durante la etapa de fragmentación.
# 좋은 예: 주제가 명확히 분리됨
갑목(甲木)은 양의 목기운으로, 큰 나무를 상징한다...
---
을목(乙木)은 음의 목기운으로, 풀이나 덩굴을 상징한다...
3. No AI-generated content:
Cuando el contenido creado por LLM se agrega al corpus, se reproduce la alucinación. Todos los documentos deben ser textos originales escritos por humanos (trabajos académicos, libros de texto, artículos de expertos).
4. Límites de tamaño de archivo para la recopilación automática:
Cuando el agente de recopilación automática (saju-research-collector) crea un nuevo archivo, está limitado a 200 líneas o menos por archivo. Esto se debe a un problema de estabilidad del agente (fallo al generar más de 400 líneas), y no es una limitación de todo el corpus. El corpus real normalmente incluye archivos de 400 a 1700 líneas. Sin embargo, si se mezclan demasiados temas en un archivo, es posible que se produzca contaminación de temas durante la fragmentación, por lo que es recomendable separar los archivos por tema si es posible.
El mismo principio se aplica cuando una empresa de TI crea un plan de servicios o una base de conocimientos como un corpus RAG:
| principio | Proyecto Saju | aplicación de empresa de TI |
|---|---|---|
| marcado estructurado | --- divisor, ## encabezado |
Separación de encabezados de Confluence, bloques de Notion y documentos API por punto final |
| Seguimiento de fuente | URL, número de página | ID del documento, versión, autor, fecha de última modificación |
| Indique si se crea IA | prohibición | Etiquetado (source: human vs source: ai-draft) |
| Gestión de redundancia | similitud del coseno dedup.py | Versionado de documentos + archivo de versiones anteriores |
Ejemplo de proceso de recopilación de datos (Confluence → RAG):
Confluence API → 페이지 추출
→ HTML → Markdown 변환 (pandoc / html2text)
→ 메타데이터 추출 (제목, 스페이스, 라벨, 최종수정일)
→ 빈 페이지 / 템플릿 필터링
→ 구조화: 헤딩 기준 섹션 분리
→ 이전 버전 대비 diff → 변경분만 재처리
→ chunks.jsonl에 추가
# chunk.py 핵심 파라미터
--chunk-size 512 # 최대 512자 (글자 단위, 토큰이 아니라 char)
--overlap 64 # 64자 오버랩으로 문맥 단절 방지
Superposición significa: Incluir los últimos 64 caracteres del fragmento anterior al comienzo del siguiente fragmento. Por ejemplo, si hay un fragmento que termina con "...Gapmok tiene una personalidad fuerte", el siguiente fragmento continúa con "Gapmok tiene una personalidad fuerte. Por lo tanto, liderazgo..." Esto garantiza que las oraciones divididas en los límites de los fragmentos se puedan recuperar con el contexto completo en cualquiera de los fragmentos.
Razón para elegir 512 caracteres:
Relación con el modelo de incrustación: errores descubiertos y lecciones aprendidas:
En el proceso de análisis de este proyecto, se descubrió un error de pérdida de rendimiento en la incrustación densa. La arquitectura del modelo de paraphrase-multilingual-MiniLM-L12-v2 admite 512 tokens (max_position_embeddings: 512), pero la biblioteca de transformadores de oraciones tiene por defecto 128 tokens, por lo que solo se incrusta la primera parte del fragmento y el resto se trunca.
| artículo | valor |
|---|---|
| Soporte de arquitectura de modelo | 512 fichas |
| Valores predeterminados de la biblioteca (error, corregido) | 128 fichas |
| 512 caracteres coreanos → número real de tokens | ~337 fichas |
| El límite de 128 tokens refleja la incorporación real | Primeros ~200 caracteres (~39% del total) |
| 128 vs 512 incrustando similitud de coseno | 0,55 (la información es significativamente diferente) |
Cómo solucionarlo: model.max_seq_length = 512 Agregue una línea y reconstruya el índice. Sin embargo, dado que este modelo puede haber sido entrenado en base a 128 tokens, el grado de mejora de la calidad cuando se expande a 512 debe confirmarse con un punto de referencia.
Por qué el sistema funcionó de todos modos:
Lección: No confíes ciegamente en los valores predeterminados de la biblioteca. El rango admitido por la arquitectura del modelo y la configuración de la biblioteca puede diferir. Al crear una canalización de inserción, debe verificar la cantidad de tokens en el fragmento real con max_seq_length. Este tipo de discrepancia en la configuración es difícil de detectar porque reduce silenciosamente el rendimiento sin causar errores.
Razón para elegir la superposición de 64 caracteres:
Simplemente no se trunca cada 512 caracteres. Partición jerárquica que respeta las unidades semánticas:
Level 1: --- 구분선으로 섹션 분리 (atomic boundary, 절대 넘지 않음)
Level 2: 빈 줄(\n\n)로 단락 분리
Level 3: 문장 단위 분할 (단락이 512자 초과 시)
Código principal (chunk.py:116-137):
def _split_into_paragraphs(text: str) -> list[str]:
# --- 구분선이 있으면 섹션별로 분리, 각 섹션을 atomic 단락으로
if _SECTION_SEPARATOR.search(text):
sections = _SECTION_SEPARATOR.split(text)
paragraphs = []
for section in sections:
section = section.strip()
if not section:
continue
paragraphs.append(section)
return paragraphs
# 없으면 빈 줄로 분리
parts = re.split(r"\n{2,}", text)
return [p.strip() for p in parts if p.strip()]
Por qué --- las líneas separadoras son importantes:
------ como límite atómico bloquea la contaminación del tema en la fuente.Misma estructura en servicios TI:
El documento API (Swagger/OpenAPI) tiene límites naturales para cada punto final y la página de Confluence está dividida en secciones ## 헤딩. Para dichos documentos, utilice el encabezado (##) como límite atómico en lugar de ---. Por ejemplo, si GET /api/users y POST /api/users se mezclan en el mismo fragmento, causa confusión durante la búsqueda, pero si fragmenta por punto final, obtendrá resultados precisos.
Requisitos previos para que esto funcione:
Como se explica en el Capítulo 4, los datos ya deben contener el delimitador --- durante la fase de recopilación. Esta estrategia no funciona para documentos no estructurados sin divisores (PDF escaneados, archivos de Word heredados, etc.) y requiere un procesamiento previo adicional (detección de secciones basada en LLM, extracción de encabezados basada en reglas).
Cuando un párrafo supera los 512 caracteres, se divide en oraciones, teniendo en cuenta las terminaciones finales en coreano:
sentences = re.split(
r"(?<=[.!?。!?\n])\s*|(?<=[다요음함임됨것]\.)\s+",
para,
)
.!?。!? Dividido por detrás (universal)다., 요., 음., 함., etc. División después del final de la final coreana + puntodef _detect_lang(text: str) -> str:
"""텍스트 첫 2000자의 문자 유형 비율로 언어 판별"""
# 한글 음절(AC00-D7AF) > 20% → ko
# CJK 통합(4E00-9FFF) > 20% → zh
# ASCII 알파벳이 대다수 → en
Las etiquetas de idioma se almacenan en metadatos fragmentados, lo que determina la selección del tokenizador durante la búsqueda y la dirección de la expansión de la consulta BM25.