CreaRack-SL

trimChunksBySentence: Recorte a frases relevantes

Firma

export async function trimChunksBySentence(
  ai: Ai,
  queryVec: number[],
  chunks: ChunkMatch[],
  opts: { threshold: number; minSentences: number; window: number },
): Promise<ChunkMatch[]>

Propósito

Recorta cada ChunkMatch a las frases relevantes para la query. Mantiene contexto ±N frases, garantiza nunca vaciar un chunk (minSentences), y marca elisiones con [...]. Reduce tokens de input sin perder señal.

Algoritmo

1. Troceado y aplanamiento

  • Llama a splitSentences en el .content de cada chunk (tras separar el prefijo [Doc: ...] que no se trocea).
  • Aplana todas las frases en un array global con rastreador de propiedad (qué chunk es cada frase).

2. Embedding batch

Embebe todas las frases en una sola llamada a embedTexts(ai, flat) → más eficiente que una por una.

3. Scoring

Calcula similaridad coseno entre queryVec y cada vector de frase. Agrupa scores por chunk.

4. Selección de frases

Para cada chunk:

  • Threshold: Índices donde score >= threshold.
  • Garantía mínima: Si menos de minSentences, añade las mejores (por score) hasta alcanzar el mínimo.
  • Ventana de contexto: Si window > 0, por cada frase conservada, incluye ±window frases adjacentes.

5. Reconstrucción

  • Si conservamos casi todas (≥100% de lo que quedaría), emitir chunk sin cambios (evitar marcar [...] inútil).
  • Si recortamos, reconstruir ordenado por índice original, separando tramos no contiguos con " [...] ".
  • Reprepender el prefijo [Doc: ...] si existía.

6. Output

ChunkMatch[] modificados, mismos campos excepto .content que puede ser más corto.

Parámetros de opciones

ParámetroTipoDefaultDescripción
thresholdnumber0.38Coseno mínimo de frase para conservarla sin garantía mínima
minSentencesnumber1Frases garantizadas por chunk (las mejores si no se alcanzan por threshold)
windownumber0Frases de contexto ±N alrededor de las conservadas

Ejemplos

Ejemplo 1: Threshold puro

const chunk = {
  id: 1,
  content: "[Doc: RFC-123] Sentence A. Sentence B (relevante). Sentence C.",
  score: 0.85
};

// queryVec es un embedding de "relevante"
const opts = { threshold: 0.5, minSentences: 1, window: 0 };

// Split:
// - "[Doc: RFC-123]"
// - ["Sentence A.", "Sentence B (relevante).", "Sentence C."]

// Scores (aprox):
// - "Sentence A." → 0.3 ❌
// - "Sentence B (relevante)." → 0.8 ✅
// - "Sentence C." → 0.2 ❌

// Output:
// content = "[Doc: RFC-123]\nSentence B (relevante)."

Ejemplo 2: Ventana de contexto

const opts = { threshold: 0.5, minSentences: 1, window: 1 };

// Conserva B, pero window=1 añade A (anterior) y C (siguiente)
// Output:
// content = "[Doc: RFC-123]\nSentence A. Sentence B (relevante). Sentence C."
// (sin cambio, porque incluye todo)

Ejemplo 3: Garantía mínima

const sentences = ["A", "B", "C", "D"];
const scores = [0.1, 0.2, 0.3, 0.05];
const opts = { threshold: 0.5, minSentences: 2, window: 0 };

// Ninguna supera threshold=0.5
// Tomar 2 mejores: C (0.3), B (0.2)
// Ordenar: [B, C]
// Output: "B C"

Detalles de implementación

Estructura interna

interface ChunkProcessing {
  c: ChunkMatch;
  prefix: string;
  sentences: string[];
}

Cálculo de similaridad coseno

function cosineSimilarity(vecA: number[], vecB: number[]): number {
  let dot = 0, normA = 0, normB = 0;
  for (let i = 0; i < vecA.length; i++) {
    dot += vecA[i] * vecB[i];
    normA += vecA[i] * vecA[i];
    normB += vecB[i] * vecB[i];
  }
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}

Marcado de elisiones

Entre índices prev e i no contiguos:

if (prev >= 0 && i > prev + 1) pieces.push('[...]');

Fuentes de datos

  • Entrada: queryVec (embedding de la pregunta limpia), chunks del retrieval base.
  • Salida: Chunks recortados.
  • API externa: Workers AI (embedTexts).
  • Parámetros: Threshold, minSentences, window (ajustables sin redeploy si se pasan a retrieve()).

Performance

  • Batch embedding: Una sola llamada a embedTexts con todas las frases (mejor que N llamadas).
  • Scoring: O(n·d) donde n = frases, d = dimensión del vector (típicamente 384 para BGE).
  • Selección + ventana: O(n log n) para sort (solo si se necesita garantía mínima).
  • Overhead: ~10-30ms para chunks típicos (5-10 chunks, 10-50 frases totales).

Casos edge

  1. Chunk sin frases: Split devuelve array vacío → emite el chunk original sin cambios.
  2. Ninguna frase supera threshold: Garantía mínima toma las mejores (por score), evita vaciar.
  3. Recortamos casi nada: Si keep.size ≥ sentences.length, emite original (sin marcar [...] inútil).
  4. Ventana grande: Puede hacer que keep incluya casi todas las frases → emite casi original.

Notas

  • Determinismo: Para mismo queryVec y threshold, mismo output (embedding + coseno deterministas).
  • Internacionalización: Hereda idiomas de splitSentences (ES/EN).
  • Costo: Cada llamada embebe frases nuevas. Recomendado batch (harness) sobre muchas preguntas para amortizar overhead.

Véase también

  • [[feature—biblioteca—arcrift-sentence-trimming]]
  • [[entity—functions—service—split-sentences]]
  • [[entity—functions—service—retrieve]]