Ir al contenido

LangChain.js — el que tiene de todo

LangChain es el framework “todo incluido” del ecosistema LLM. Su tesis: en lugar de que cada equipo reescriba retrievers, document loaders, vector store wrappers, output parsers, etc., debería existir una librería con todo eso pre-armado. Y esa librería es LangChain.

Resultado: 200+ integraciones en @langchain/community (vector stores, LLMs, embedders, document loaders, retrievers). Si tu RAG necesita conectar con Pinecone, Weaviate, Postgres + pgvector, Astra DB, Notion, Confluence, Slack — casi todo eso ya tiene un módulo.

El precio: complexity tax. Más conceptos para entender (Runnables, Chains, LCEL, output parsers), más capas para debuggear, más sorpresas cuando algo se rompe en el internals.

packages/02-langchain/src/ingest.ts — usa la abstracción QdrantVectorStore:

02-langchain/ingest.ts: stages clave
const docs = await loadMarkdownDir(DATA_DIR);
const chunks = chunkDocs(docs);
const embeddings = new OllamaEmbeddings({ model: OLLAMA_EMBED, baseUrl: OLLAMA_URL });
const documents = chunks.map(
(c) =>
new Document({
pageContent: c.content,
metadata: { docId: c.docId, filename: c.filename, index: c.index },
}),
);
await QdrantVectorStore.fromDocuments(documents, embeddings, {
client: qdrantClient,
collectionName: COLLECTION_NAME,
});

QdrantVectorStore.fromDocuments(...) hace embeber, crear collection si no existe, y upsertar en una sola call. Internamente bate los docs en lotes, maneja la serialización del payload, etc. Es el value-add de LangChain: una line.

Trade-off oculto: si querés ver qué embeddings generó, qué point IDs uso, o cómo serializó el payload, tenés que leer el código fuente del wrapper. La abstracción es opaca.

packages/02-langchain/src/query.ts — 64 líneas. La parte clave:

02-langchain/query.ts: retrieve + generate
const vectorStore = await QdrantVectorStore.fromExistingCollection(embeddings, {
client: qdrantClient,
collectionName: COLLECTION_NAME,
});
const results = await vectorStore.similaritySearchWithScore(question, TOP_K);
const context = results
.map(([doc, score], i) => {
const filename = String(doc.metadata['filename'] ?? 'unknown');
return `[${i + 1}] (file: ${filename}, score: ${score.toFixed(4)})\n${doc.pageContent}`;
})
.join('\n\n---\n\n');
const llm = new ChatOllama({ model: OLLAMA_LLM, baseUrl: OLLAMA_URL });
const response = await llm.invoke([
new SystemMessage('You are a helpful assistant. ...'),
new HumanMessage(`Context:\n\n${context}\n\nQuestion: ${question}`),
]);

Tres conceptos LangChain en estas pocas líneas:

  • Document y pageContent: el wrapper representa cada chunk como un objeto Document con pageContent + metadata. Útil para integraciones; verbose para casos simples.
  • similaritySearchWithScore: nombre largo pero claro. Hay también similaritySearch (sin scores) y maxMarginalRelevanceSearch (MMR, anti-redundancia). Cubre los casos comunes.
  • SystemMessage / HumanMessage: roles tipados para el chat. Funcionan con cualquier ChatModel. Bonus: cuando cambiás de Ollama a Claude, el código no cambia.

C — el más laxo de los 4. Tres lugares donde el casting es obligatorio:

casts típicos en LangChain.js
const filename = String(doc.metadata['filename'] ?? 'unknown');
// ^^^^^^^^^ es de tipo Record<string, any>
const { content } = response;
// ^^^^^^^ tipado como MessageContent = string | MessageContentComplex[]
// necesita un narrow para usarlo como string

Document.metadata es Record<string, any>. MessageContent es una union para soportar multi-modal (text + images). Para un RAG tipado tenés que castear o type-narrow.

Para evals, esta laxitud cuesta: el harness usa query.fn.ts que extrae los chunks crudos del result, y tenemos que castear metadata campo por campo.

  • Cuando necesitás integraciones específicas: vector store oscuro, LLM nuevo, document loader para PDFs con tablas, retriever con MMR. Si existe el módulo, te ahorra horas.
  • Para chat con memoria persistente y history-aware retrieval: viene pre-empaquetado. La conversación con history reformulation es 5 líneas con LangChain, 30 con Vercel.
  • Equipos que vienen de LangChain Python: la versión JS sigue conceptos parecidos. Curva de aprendizaje 10× menor que reentrenar el equipo.
  • Multimodal y agents: hay capas extras (LangGraph, LangSmith) que cubren casos avanzados que en otros frameworks habría que armar a mano.
  • TS-strict: tipos laxos forzando casts. Si tu CI rechaza as any en code review, vas a tener que pelearte con Document.metadata.
  • Curva de aprendizaje: Runnables, LCEL, OutputParsers, AgentExecutor. Cada uno hace algo distinto y los nombres no son intuitivos al principio.
  • Versionado: la lib evoluciona rápido. Breaking changes son frecuentes (especialmente en @langchain/community). Pin versions y leé changelogs antes de bumps.
  • Debuggear errores de runtime: cuando una chain falla, el stack trace típicamente cruza 4-5 capas de Runnables. Sin LangSmith (su tracer dedicado), encontrar la causa real puede tardar.

LOC mínimo del query: 15 líneas. Latencia base: ~1900 ms. Compacto en LOC pero requiere familiaridad con LangChain para ser productivo. El gap real con Vercel no es velocidad — es cuánto sabés del framework.

LlamaIndex.TS — el especializado en RAG. Único de los 4 cuya razón de existir es retrieval-augmented generation. Resultado: APIs RAG-first y opinionated.