LlamaIndex.TS — el especializado en RAG
Filosofía
Sección titulada «Filosofía»LlamaIndex es el único de los 4 cuya razón de existir es RAG. No es “un framework de LLMs que también hace RAG” — es “el framework de RAG, que también hace otras cosas”.
Resultado: la API tiene primitivas específicas — Index, Retriever, QueryEngine, ResponseSynthesizer, NodeParser. Cada una corresponde a un concepto del mundo RAG. Si conocés el dominio, navegás la API rápido. Si no lo conocés, los nombres confunden.
Filosofía declarada: “RAG-first abstractions”. Trade-off: opinionated pero potente — para casos típicos, el camino es directo; para casos atípicos, la abstracción cuesta romper.
Cómo se hace ingest
Sección titulada «Cómo se hace ingest»packages/03-llamaindex/src/ingest.ts:
Settings.embedModel = new OllamaEmbedding({ model: OLLAMA_EMBED, config: { host: OLLAMA_URL },});
const qdrantClient = new QdrantClient({ url: QDRANT_URL });const vectorStore = new QdrantVectorStore({ client: qdrantClient, collectionName: COLLECTION_NAME,});
const documents = chunks.map( (c) => new Document({ text: c.content, metadata: { docId: c.docId, filename: c.filename, index: c.index }, }),);
await VectorStoreIndex.fromDocuments(documents, { vectorStore });Tres conceptos LlamaIndex en juego:
Settings: estado global.Settings.embedModel = ...setea el embedder para todo lo que pase después. Conveniente para scripts; molesto para tests (hay que resetear el global).Document: objeto canónico. Como en LangChain, un wrapper de texto + metadata.VectorStoreIndex.fromDocuments(...): la única call que hace todo — embebe, crea collection, upsertea. Una línea.
Cómo se hace query
Sección titulada «Cómo se hace query»packages/03-llamaindex/src/query.ts — 47 líneas. La parte clave:
Settings.llm = new Ollama({ model: OLLAMA_LLM, config: { host: OLLAMA_URL } });Settings.embedModel = new OllamaEmbedding({ model: OLLAMA_EMBED, config: { host: OLLAMA_URL } });
const vectorStore = new QdrantVectorStore({ client: qdrantClient, collectionName: COLLECTION_NAME });const index = await VectorStoreIndex.fromVectorStore(vectorStore);
const queryEngine = index.asQueryEngine({ similarityTopK: TOP_K });
const response = await queryEngine.query({ query: question });
console.log(response.toString());index.asQueryEngine().query(...) es el path idiomático y compacto al extremo: una sola call hace embedear la pregunta, retrievear top-K, armar el prompt con un response synthesizer, y generar la respuesta. El más conciso de los 4 frameworks para el caso default.
Trade-off: la magia esconde el prompt. ¿Querés cambiar la instrucción anti-alucinación? Tenés que pasar un ResponseSynthesizer custom. ¿Querés ver los chunks retrieveados antes de generar? response.sourceNodes los expone, pero tuviste que generar primero.
La asimetría del harness
Sección titulada «La asimetría del harness»El path idiomático con asQueryEngine() no expone los contextos antes de generar la respuesta. Para el harness — que necesita medir retrieval@k y faithfulness con los chunks crudos — usamos un path manual en query.fn.ts:
const retriever = index.asRetriever({ similarityTopK: topK });const nodes = await retriever.retrieve({ query: question });
const contexts = nodes.map((n) => ({ filename: String(n.node.metadata['filename'] ?? 'unknown'), content: n.node.getContent(MetadataMode.NONE), score: n.score ?? 0,}));
const prompt = buildPrompt(question, contexts);const llmResponse = await Settings.llm.complete({ prompt });Esto se ve idéntico al patrón de los otros 3 frameworks — una decisión deliberada para que la comparativa de evals sea honesta. La CLI de query.ts mantiene asQueryEngine(); solo el query.fn.ts usa el camino manual.
C — comparable a LangChain. Hay any y unknown repartidos:
const filename = String(node.node.metadata['filename'] ?? 'unknown');// ^^^^^^^^^ es Record<string, any>
const score = node.score != null ? node.score.toFixed(4) : 'n/a';// ^^^^^ tipado number | undefinedmetadata es Record<string, any> por compatibilidad con loaders heterogéneos. Tenés que castear o type-narrow para usar campos específicos.
Cuándo brilla
Sección titulada «Cuándo brilla»- Si querés moverte rápido y el caso es RAG estándar: corpus → embed → retrieve → answer. Una línea de query y listo.
- Para experimentar con índices avanzados: knowledge graph indexes, summary indexes, document agents, sub-question query engines. Cada uno es una abstracción de alto nivel ya implementada.
- Cuando el equipo conoce LlamaIndex Python: la versión TS sigue conceptos casi idénticos. Migrar un POC de Python a producción TS es relativamente directo.
- Para parsing de documentos complejos: el ecosistema LlamaIndex tiene mejor soporte para PDFs con tablas, código, etc. que cualquiera de los otros 3.
Cuándo sufre
Sección titulada «Cuándo sufre»- TS-strict: la laxitud de tipos es comparable a LangChain. Para evals tuvimos que romper la abstracción de
asQueryEngine(). - Documentación TS vs Python: gaps notorios. Muchas features documentadas en Python no están en TS o están en distintas versiones. Si necesitás algo no-trivial, mirá el código fuente.
- Ecosistema TS más chico: integraciones (vector stores raros, observability hooks, etc.) son menos que LangChain.
- Estado global con
Settings: para scripts es OK. Para servidores con concurrencia, tenés que pensar cómo aislar config por request — LlamaIndex no te lo facilita.
El número honesto
Sección titulada «El número honesto»LOC mínimo del query: 15 líneas. Latencia base: ~2200 ms (~10% más alta que Vercel/LangChain por las capas de abstracción de QueryEngine). El más compacto para el caso simple, el más opaco cuando algo hay que customizar.
Lo que viene
Sección titulada «Lo que viene»Mastra — el agéntico. Único de los 4 cuyo modo idiomático no es un pipeline lineal, sino un agent con tools. RAG es un componente, no el centro.