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
splitSentencesen el.contentde 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ámetro | Tipo | Default | Descripción |
|---|---|---|---|
threshold | number | 0.38 | Coseno mínimo de frase para conservarla sin garantía mínima |
minSentences | number | 1 | Frases garantizadas por chunk (las mejores si no se alcanzan por threshold) |
window | number | 0 | Frases 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
embedTextscon 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
- Chunk sin frases: Split devuelve array vacío → emite el chunk original sin cambios.
- Ninguna frase supera threshold: Garantía mínima toma las mejores (por score), evita vaciar.
- Recortamos casi nada: Si keep.size ≥ sentences.length, emite original (sin marcar
[...]inútil). - Ventana grande: Puede hacer que
keepincluya casi todas las frases → emite casi original.
Notas
- Determinismo: Para mismo
queryVecy 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]]