Cómo Nammu lee un expediente de 300 páginas: segmentación inteligente de documentos
La exportación de un expediente costarricense es un muro de páginas — demanda, resoluciones, notificaciones, sentencia, todo concatenado. Antes de que pueda ejecutarse cualquier análisis, Nammu debe dividirlo en unidades tipificadas y navegables. Así es el pipeline de dos fases que lo logra sin un LLM en cada página.
Inventum P5
Ingeniería · Galictis-Legal
10 de julio de 2026
10 min de lectura
La exportación de un expediente costarricense rara vez es un solo documento. Es una compilación cronológica: una demanda, la resolución de admisión del tribunal, una docena de actos de notificación, una contestación, piezas de evidencia, más resoluciones — todo concatenado en un solo PDF sin marcadores y sin límites en los metadatos. La Segmentación Inteligente de Documentos es la pasada que convierte ese muro de páginas en un índice tipificado y navegable en el momento en que el OCR termina, sin esperar a que un humano primero lo revise.
Ese índice tipificado — document_index.json — es también la base de la que se alimenta todo lo demás. La detección de jurisdicción, las líneas de tiempo de Trayectorias y el análisis de IA a nivel de caso consumen todos las etiquetas doc_kind que produce este módulo. Segmentar mal propaga errores a través de cada funcionalidad posterior.
Dos fases: reglas económicas primero, LLM solo cuando realmente se necesita
El módulo refleja la misma filosofía de diseño que el clasificador de jurisdicción: una pasada rápida, gratuita y basada en reglas corre primero; una llamada de LLM de pago solo refina lo que las reglas realmente no pudieron resolver. La mayoría de las páginas no son ambiguas — una página que abre con “SENTENCIA N° 015-2025” no necesita un LLM para decirte que es una sentencia. El LLM se reserva para el difícil 5–10%: escaneos descritos por visión y respaldos genéricos donde el razonamiento adicional realmente vale su costo.
- Siempre corre — sin LLM, costo casi nulo
- Compara frases conocidas de apertura de documento solo contra el encabezado de la página
- Ordenada de más específica a más genérica para evitar coincidencias en la sombra
- Relleno de huecos: toda página sin coincidencia hereda el segmento abierto
- Colapsa secuencias consecutivas del mismo tipo en un solo bloque navegable
- Solo para segmentos que la Fase 1 marcó como de baja confianza o genéricos
- Una sola llamada por lote — no una llamada de LLM por página
- Recibe fragmento de página, suposición de la regla, registro de autoría, taxonomía completa
- Cierre seguro: recurre a etiquetas genéricas explícitas, nunca adivina
- El resultado se rechaza si viola la salvaguarda de consistencia de autoría
El algoritmo central
segment_document() es el motor de la Fase 1. Para cada página, corren cinco pasos en orden:
Eliminar ruido y normalizar
Los prefijos de número de página (page_N:) y los sellos de folio (N. N) se eliminan. El texto se convierte a minúsculas, se le quitan los acentos y se colapsan los espacios en blanco. De forma crucial, los glifos de signo de grado °/º se convierten en un espacio — la eliminación de acentos NFKD por sí sola no los toca, lo cual causó un falso negativo real antes de agregar este paso: “SENTENCIA N° 015” y “SENTENCIA N 015” ahora se normalizan de forma idéntica.
Detectar el registro de autoría
Antes de comparar contra cualquier tipo de documento, la voz gramatical de la página se clasifica como tribunal, parte, mp, o unknown. Esta es una salvaguarda estructural, no una pista de tipo — se usa para bloquear familias completas de anclas de dispararse en el registro incorrecto (ver §Salvaguarda de autoría).
Comparar un ancla contra el encabezado de la página
El encabezado normalizado de la página — nunca la página completa, nunca el texto libre del cuerpo — se compara contra la lista de anclas. Esto es intencional: un documento que simplemente menciona “la demanda” en su narrativa nunca abre falsamente un nuevo segmento de demanda.
Relleno de huecos: heredar el segmento abierto
Una página sin coincidencia de ancla simplemente hereda el segmento que ya está abierto. El resultado es siempre una cobertura total de cada página — sin páginas huérfanas, sin tramos sin clasificar, aunque solo una minoría de páginas lleve un ancla explícita.
Colapsar y fusionar secuencias
Los límites adyacentes del mismo tipo se colapsan en un solo documento. Luego, si collapse_runs=True (el valor por defecto), documentos consecutivos del mismo tipo se fusionan en un solo bloque navegable con un count — 24 actos de notificación individuales se convierten en “notificación pp. 9–51, count=24” en lugar de 24 filas separadas.
caratula_datos_generales; de lo contrario, abre como un escrito_generico de baja confianza para que la pasada del LLM lo refine. De cualquier forma, nunca queda algo completamente sin clasificar.La salvaguarda de registro de autoría
Antes de que se dispare cualquier ancla, _detect_authorship() lee pistas de voz gramatical e institucional — no nombres de tipo de documento — para clasificar al autor de una página como tribunal, parte, mp, o unknown:
- Registro de tribunal: abre con el nombre de un tribunal, o contiene lenguaje resolutivo ("se resuelve", "por tanto"), o coincide con la fórmula canónica costarricense de autofechado judicial.
- Registro de parte: abre con un saludo al juzgado ("señores del juzgado") o un verbo de presentación en primera persona ("comparezco", "vengo a…").
- Registro de MP: menciona "Ministerio Público" o "Fiscalía" en los primeros ~70 caracteres.
cumplimiento_prevencion, subsanación) se disparaban en documentos tribunal, porque el lenguaje resolutivo de un tribunal a veces hace eco del mismo vocabulario que usaría una parte para cumplirlo. Corregir esto frase por frase sería una batalla perdida. La solución estructural: cualquier ancla cuya familia de taxonomía sea actos_de_parte se omite automáticamente cuando el registro detectado es tribunal — toda una clase de falso positivo, cerrada a nivel de mecanismo. La misma salvaguarda aplica a la propia salida del LLM: el LLM tampoco puede asignar un slug de acto de parte a un encabezado registrado como tribunal.Anclas de frase única vs. coocurrencia
La mayoría de las anclas son simples: un slug se mapea a frases, comparadas contra el encabezado de la página, ordenadas de más específica a más genérica. Pero varios tipos de documento no pueden identificarse de forma confiable a partir de una sola frase, porque el procedimiento costarricense reutiliza el mismo vocabulario genérico — “recurso de apelación”, “sentencia” — en asuntos civiles, penales, de familia y notariales. Este módulo no tiene parámetro de jurisdicción: la segmentación corre antes, e independientemente, de la detección de jurisdicción.
Para estos casos, _COOCCURRENCE_ANCHORS exige que dos o más grupos de frases independientes coincidan en la misma página antes de fijar un tipo. Un escrito notarial de inadmisión de apelación debe mostrar tanto una frase de apelación como una palabra de contexto notarial (“notarial”/“notario”/“disciplinario”) antes de ser etiquetado apelacion_inadmision_notarial.
recurso_familia / sentencia_familia / sentencia — únicamente porque las anclas genéricas de familia se disparan con el mismo vocabulario de “recurso”/“sentencia” que también usa un escrito notarial. La coocurrencia es la solución, aplicada en ambas direcciones.Un mecanismo _COOCCURRENCE_NEGATION_BLOCKERS maneja el único caso donde ni siquiera la coocurrencia de dos grupos es suficiente: una sentencia del tribunal puede narrar, en el fondo de su razonamiento, que tal incidente no existe en un expediente distinto — satisfaciendo ambos grupos de frases mientras es lo opuesto a un escrito genuino. La solución es una exclusión explícita de “a menos que la página también diga 'no consta ningún incidente'”, aplicada estrechamente solo al ancla que ha mostrado este modo de falla.
Salvaguardas de un solo documento: estatutos, cartas de honorarios y páginas de continuación
Tres situaciones reciben un cortocircuito a nivel de documento antes de que corra el bucle por página, porque la comparación de anclas por página fallaría repetidamente en todo un documento adjunto:
Anexos de ley / estatuto de referencia
Un texto completo de Código o Ley adjunto como prueba narra sus propios artículos de “recurso”/“sentencia”/“audiencia” a lo largo de todo el texto — eso es el contenido del propio estatuto, no un escrito del caso. Si la página 0 coincide con la fórmula de promulgación constitucional o un título de Ley sin citar (excluyendo explícitamente citas de escritos como “de conformidad con el Código Civil”), todo el PDF se convierte en un solo segmento ley_referencia. La comparación por página se omite por completo.
Cartas de propuesta de honorarios / contratación
Una “Propuesta de Servicios Legales” de un abogado lista, como sus propias viñetas de servicio, exactamente las frases que abren escritos reales — “interponer los recursos de apelación que correspondan”, “al emitirse la sentencia”. Una vez confirmada la página 0 como tal carta, las páginas posteriores de ese PDF no pueden abrir espuriamente un segmento de demanda/recurso_familia a partir del lenguaje de la lista de servicios — mientras que los anexos genuinos incluidos después de la carta se siguen detectando normalmente.
Páginas de continuación notarial
Un escrito notarial de varias páginas narra su argumento en el cuerpo (“…mediante sentencia número 396-02…”), lo cual dispara las anclas genéricas de sentencia de familia/civil en páginas de continuación que no llevan su propia señal de coocurrencia notarial (el membrete solo aparece en la página 0). Una vez confirmada la página 0 como notarial, las anclas genéricas propensas a fugas se suprimen en las páginas posteriores de ese mismo escrito — pero un segundo escrito notarial genuinamente nuevo más adelante en el mismo PDF sigue detectándose.
Las tres salvaguardas comparten la misma forma: establecer la identidad una vez a partir de la página inicial del documento, luego suprimir las anclas específicas que se sabe fallan sobre el propio texto del cuerpo de ese documento el resto de él.
Supresión de referencias de vía
Algunos doc_kinds tratan explícitamente sobre otra vía sin ser ellos mismos una nueva instancia de esa vía. Un defectos_querella (el tribunal señala defectos en una querella) o subsanación_querella (la parte corrige esos defectos) casi siempre volverá a citar lenguaje de la vía de querella en la misma página — pero eso es una referencia a la querella existente, no una segunda querella.
_TRACK_ROLE codifica esto como una tabla (references: [...] → a qué vía raíz apunta un tipo dado), y _suppress_track_root_cosegments() descarta cualquier co-segmento que sea la raíz de una vía ya referenciada por el segmento principal. Extender esto a una nueva vía es una fila de tabla, no una regla nueva a medida.
Esta pasada de supresión corre dos veces dentro de build_document_index() — una justo después de la pasada de reglas y otra después de la pasada del LLM — porque un tipo principal modificado puede calificar o descalificar de nuevo un co-segmento que antes no era relevante.
Nombre de archivo como desempate de último recurso
refine_segment_types() aplica reglas de patrón de nombre de archivo — pero solo para mejorar un tipo de respaldo ya genérico (escrito_generico, indeterminado, requerimiento_generico). Un segmento cuyo ancla de texto ya se resolvió a querella se deja intacto sin importar lo que diga el nombre de archivo. Esta es la misma disciplina de “Rango 5, solo corrobora” documentada para la detección de tipo de proceso a nivel de caso — los dos módulos llegaron independientemente a la misma regla: un nombre de archivo puede romper un empate entre evidencia débil, pero nunca puede superar el texto realmente leído de la página.
La taxonomía — agnóstica de jurisdicción por diseño
Cada slug doc_kind reconocido se agrupa en una de siete familias moldeadas por autoría. Los slugs civiles, penales, laborales, de familia, notariales y de tránsito coexisten todos dentro de las mismas familias — la segmentación nunca pregunta “¿qué jurisdicción es esta?” Esa pregunta pertenece por completo a case_classifier.py, en una etapa posterior.
| Familia | Significado | Autoría |
|---|---|---|
| actos_de_parte | Escritos presentados por una parte litigante | parte |
| actos_del_tribunal | Actos judiciales — autos, resoluciones, sentencias | tribunal |
| actos_del_ministerio_publico | Actos de la fiscalía — acusación, sobreseimiento | mp |
| prueba | Piezas de evidencia — documental, pericial, testimonial, médica… | — |
| actos_de_comunicacion | Notificaciones, edictos, citaciones | — |
| actos_instrumentales | Instrumentos — poderes, certificaciones, escrituras | — |
| control | Portadas, tipos genéricos y de respaldo, propuestas de honorarios | — |
Esta es una separación deliberada por capas: la segmentación identifica qué tipo de acto es una página; la detección de jurisdicción luego decide a qué rama del derecho pertenece el caso, usando la bolsa resultante de doc_kind como una de sus señales clasificadas (Rango 2 y 3 en la cascada descrita en la publicación de detección de jurisdicción). Mantener las dos preguntas separadas es lo que permitió que el bug de colisión notarial/familia del §5 pudiera diagnosticarse limpiamente.
El mapa de autoría (_SLUG_AUTHORSHIP) se deriva automáticamente de a qué familia pertenece un slug — sin lista mantenida a mano por slug — lo cual es lo que alimenta la salvaguarda tribunal/parte sin necesidad de mantener sincronizados dos vocabularios.
Fase 2: la pasada de confirmación por LLM
llm_confirm_segments() solo corre sobre segmentos que la Fase 1 marcó como confidence == "low" (límites obtenidos por OCR de visión) o genéricos (escrito_generico / resolucion_generica / indeterminado). Para cada candidato envía al LLM un fragmento de ~700 caracteres de la página de apertura, la propia suposición de la capa de reglas, el registro de autoría detectado independientemente con una bandera explícita ⚠️CONFLICTO_AUTORIA cuando el registro y el slug no coinciden, y la taxonomía completa agrupada por familia.
El prompt es explícitamente de cierre seguro: los escritos de parte no identificables recurren a escrito_generico, los actos del tribunal a resolucion_generica, los actos de la fiscalía a requerimiento_generico. Un pequeño conjunto de slugs de alto riesgo (demanda, sentencia, auto_apertura_juicio, declaratoria_herederos) requiere un encabezado inequívoco — al LLM se le instruye no asignarlos nunca sin uno.
tribunal, esa reclasificación se rechaza y se mantiene la suposición de la capa de reglas — un respaldo estructural, no solo una instrucción de prompt. Después de que corre el LLM, tanto la supresión de co-segmentos como el colapso de secuencias se vuelven a ejecutar, ya que un tipo principal modificado puede cambiar qué co-segmentos ahora son ruido.Cómo se ve la salida
build_document_index() escribe un sidecar junto a cada PDF procesado:
{
"schema_version": 1,
"file_name": "Expediente-0031-PRINCIPAL.pdf",
"num_pages": 307,
"proceso_type": "ordinario",
"method": "rules+llm",
"document_segments": [
{
"type": "caratula_datos_generales",
"page_start": 0, "page_end": 0,
"confidence": "high",
"source": "anchor",
"count": 1
},
{
"type": "demanda",
"page_start": 1, "page_end": 8,
"confidence": "high",
"source": "anchor"
},
{
"type": "notificacion",
"page_start": 9, "page_end": 51,
"confidence": "high",
"source": "anchor",
"count": 24
},
...
]
}method solo se marca como “rules+llm” si el LLM realmente reclasificó al menos un segmento. Un caso que no necesitó ninguna corrección de LLM se reporta honestamente como “rules” — los consumidores y auditores posteriores siempre pueden ver si la pasada más costosa contribuyó.
Tres consumidores posteriores leen este sidecar directamente: case_classifier.py (consolidado de jurisdicción / tipo de proceso), classify_document_segments() (banderas de relevancia de caso superpuestas), y el constructor de línea de tiempo de Trayectorias (anclando hitos a tipos de acto específicos).
Principios rectores
La misma disciplina documentada en la detección de jurisdicción y las restricciones de análisis — los detalles cambian, la filosofía no.
Comparar solo el encabezado, nunca el cuerpo
El tipo de un documento se anuncia en sus líneas de apertura. Una mención en el cuerpo de otro tipo de documento es narrativa, no un límite.
Específico antes que genérico, siempre
Cada lista de anclas está ordenada de más específica a más genérica para que una frase estrecha e inequívoca nunca sea eclipsada por una más amplia que aparece antes.
Corroborar vocabulario ambiguo con coocurrencia
Cuando la misma frase se usa legítimamente en varias ramas, la solución es exigir una segunda señal independiente en la misma página — no una ventana de coincidencia más amplia.
Los nombres de archivo y la salida del LLM corroboran, nunca anulan
Un nombre de archivo puede romper un empate entre evidencia débil o genérica. Nunca puede superar el texto realmente leído de la página.
Cobertura total, sin páginas huérfanas
Toda página termina en algún segmento. Omitir silenciosamente una página ilegible o sin coincidencia nunca es aceptable para un escrito legal.
Cierre seguro, no adivinanza segura
Todo clasificador — de regla o de LLM — recurre a una etiqueta genérica explícita en lugar de una suposición específica pero infundada cuando la evidencia no la respalda.
Toda ancla está fundamentada en un expediente real
No encabezados idealizados — archivos reales de workspace, citados en el código por nombre de workspace y desplazamiento de página. Cada corrección se remonta a una clasificación errónea concreta que cerró.
¿Quieres ver la segmentación corriendo sobre tus propios expedientes?
Podemos hacer una sesión en vivo con archivos de caso reales y recorrer lo que produce el segmentador — índice tipificado, puntajes de confianza, y todo lo demás.
Solicita una demo →