LangChain.js — el que tiene de todo
Filosofía
Sección titulada «Filosofía»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.
Cómo se hace ingest
Sección titulada «Cómo se hace ingest»packages/02-langchain/src/ingest.ts — usa la abstracción QdrantVectorStore:
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.
Cómo se hace query
Sección titulada «Cómo se hace query»packages/02-langchain/src/query.ts — 64 líneas. La parte clave:
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:
DocumentypageContent: el wrapper representa cada chunk como un objetoDocumentconpageContent+metadata. Útil para integraciones; verbose para casos simples.similaritySearchWithScore: nombre largo pero claro. Hay tambiénsimilaritySearch(sin scores) ymaxMarginalRelevanceSearch(MMR, anti-redundancia). Cubre los casos comunes.SystemMessage/HumanMessage: roles tipados para el chat. Funcionan con cualquierChatModel. 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:
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 stringDocument.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.
Cuándo brilla
Sección titulada «Cuándo brilla»- 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.
Cuándo sufre
Sección titulada «Cuándo sufre»- TS-strict: tipos laxos forzando casts. Si tu CI rechaza
as anyen code review, vas a tener que pelearte conDocument.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.
El número honesto
Sección titulada «El número honesto»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.
Lo que viene
Sección titulada «Lo que viene»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.