Servicio de sentence-trimming en archivo-core.ts
Resumen
Módulo de procesamiento de texto (archivo-core.ts) que proporciona sentence-splitting resiliente y trimming by relevance para reducir ruido y tokens en el contexto del LLM. Usado por bib_ask (Workers) y ask.ts (Oraculo) cuando RETRIEVAL_TRIM=1.
Componentes
1. splitSentences(text: string): string[]
Proposito: Trocea markdown en “frases” (unidades mínimas de significado) respetando estructuras que NO deben partirse.
Estrategia conservadora:
- Code blocks (``` / ~~~) → unidades atómicas. No parte un comando
docker run --rm -it ...en mitad. - Líneas estructurales (tabla, bullets, listas numeradas, blockquotes) → una unidad por línea.
- Abreviaturas + números → protegidas contra split erróneo:
.en abreviaturas (Sr.,e.g.,vol.,approx.)..en versiones (v1.2.3), decimales (3.14), IPs (192.168.1.1).
- Prosa normal → parte por
. ! ?+ espacio + mayúscula/apertura (",().
Entrada/Salida:
input: "El sistema soporta multi-tenancy (v1.2.3). Véase RFC 3986..."
output: [
"El sistema soporta multi-tenancy (v1.2.3).",
"Véase RFC 3986..."
]
Implementación:
- Extraer code blocks → placeholders.
- Separar por párrafos (línea en blanco).
- Para cada párrafo:
- Si es estructural (tabla/lista/quote) → partir por línea.
- Si es prosa → partir por límite de frase con protección de abreviaturas.
- Restaurar code blocks.
Fuente: functions/api/mcp/handlers/archivo-core.ts:440–510.
Helpers privados:
protectDots(s: string)— sustituye.en abreviaturas/números por placeholderZ9DOTZ9.restoreCode(s: string, codeBlocks)— restaura code blocks desde placeholders.splitChunkPrefix(content)— separa encabezado[Doc: ... ]del cuerpo (no trocea el prefijo).
2. trimChunksBySentence(...): Promise<ChunkMatch[]>
Propósito: Recorta chunks al contenido relevante para la query.
Entrada:
ai: Ai // Workers AI binding
queryVec: number[] // embedding de la query
chunks: ChunkMatch[] // chunks enteros del retrieval base
opts: {
threshold: number // coseno mínimo (ej 0.38)
minSentences: number // garantía mínima por chunk (ej 1)
window: number // contexto ±N frases (ej 0)
}
Proceso:
- Troceado:
splitSentencesen cada chunk (respetando prefijo[Doc:...]). - Embedding batch: todas las frases de todos los chunks en un único batch (eficiente).
- Scoring: coseno
queryVecvs cada frase. - Selección:
- Frases con score ≥ threshold.
- Si insuficientes: añadir las mejores (top K) hasta
minSentences. - Si
window > 0: incluir ±N frases alrededor de las seleccionadas.
- Reconstrucción:
- Ordenar índices conservados.
- Insertar
[...]entre tramos no contiguos. - Recomponer con prefijo original.
Salida:
ChunkMatch[] con content recortado y metadatos intactos (id, score original, source)
Ejemplo:
Entrada (3 frases):
"La multi-tenancy aislaba datos. Cada tenant veía solo su esquema. Las queries explícitas mantenían la RLS."
Si solo se conserva frase 0 y 2 (relevantes para "aislamiento de datos"):
"La multi-tenancy aislaba datos. [...] Las queries explícitas mantenían la RLS."
Fuente: functions/api/mcp/handlers/archivo-core.ts:550–610.
3. retrieve(env, queryVec, topK, opts, filters): Promise<RetrievalResult>
Propósito: Wrapper unificado que encadena búsqueda vectorial → trimming opcional → estimación de tokens.
Interfaz:
interface RetrieveOpts {
trim?: boolean;
trimThreshold?: number; // default 0.38
trimMinSentences?: number; // default 1
trimWindow?: number; // default 0
}
interface RetrievalResult {
chunks: ChunkMatch[];
tokensInContext: number; // estimación post-trimming
tokensBefore: number; // estimación del retrieval base
}
Lógica:
- Búsqueda vectorial base:
searchChunks(env, queryVec, topK, filters)→ chunks enteros. - Estimar tokens antes:
estimateTokens(raw)(~1.3 tokens/palabra). - Si
opts.trim && env.AI && chunks.length > 0:- Aplicar
trimChunksBySentencecon parámetros deopts.
- Aplicar
- Estimar tokens después:
estimateTokens(trimmed). - Retornar tanto chunks como métricas.
Consumo:
const result = await retrieve(env, queryVec, 5, {
trim: true,
trimThreshold: 0.38,
trimWindow: 1
}, { app: 'core' });
console.log(`Tokens: ${result.tokensBefore} → ${result.tokensInContext}`);
// Chunks recortados en result.chunks
Fuente: functions/api/mcp/handlers/archivo-core.ts:620–650.
4. Estimación de tokens
estimateTokens(chunks): number:
- Cuenta palabras en chunks (split por whitespace).
- Multiplica por 1.3 (heurística tokens/palabra para BGE/Gemma).
- Redondea al entero.
Usado pre- y post-trimming para medir reducción exacta.
Integración en bib_ask
Ubicación: functions/api/mcp/handlers/archivo.ts:191–202.
const trimEnabled = args.trim === true || env.RETRIEVAL_TRIM === '1';
if (trimEnabled && env.AI && matches.length > 0) {
matches = await trimChunksBySentence(env.AI, queryVec, matches, {
threshold: Number(env.RETRIEVAL_TRIM_THRESHOLD) || 0.38,
minSentences: 1,
window: 1,
});
}
Activación:
- Argumento MCP
trim: true, O - Variable de entorno
RETRIEVAL_TRIM=1.
OFF por defecto → help widget (proxy Django sin arg trim) no ve cambio.
Integración en Oraculo
Ubicación: functions/api/oraculo/ask.ts:138–149.
if (env.RETRIEVAL_TRIM === '1' && env.AI && matches.length > 0) {
matches = await trimChunksBySentence(env.AI, queryVec, matches, {
threshold: Number(env.RETRIEVAL_TRIM_THRESHOLD) || 0.38,
minSentences: 1,
window: 1,
});
}
Solo activo si env RETRIEVAL_TRIM=1. Parámetros idénticos a Workers.
Optimización A/B y calibración
La tool bib_eval_retrieval permite validar el trimming sin redeploy:
bib_eval_retrieval(
questions: ["¿Multi-tenancy?", ...],
trim_threshold: 0.40,
trim_window: 2
)
Métricas producidas:
avg_tokens_baseline/avg_tokens_trim— ahorro medible.token_reduction_pct— % reducción (ej 43%).avg_overlap_vs_baseline— loss de chunks (ej 0.92 = 8% chunks perdidos).
Ajustar RETRIEVAL_TRIM_THRESHOLD hasta hallar el punto óptimo (máx reducción, mín overlap loss).
Véase también
- [[feature—biblioteca—arcrift-cherry-pick-1]]
- [[entity—functions—tool—bib-eval-retrieval]]
- [[entity—functions—handler—archivo-ask]]
- [[concept—biblioteca—retrieval-pipeline]]
- [[concept—nlp—sentence-boundaries]]