El Panorama en 2026
El espacio de orquestación AI maduró significativamente. LangChain ya no es el problema complejo desparramado que era en 2023, la reescritura v0.3 con LCEL trajo composabilidad real. Semantic Kernel llegó a 1.0 y se convirtió en el SDK oficial de Microsoft para orquestación AI. LlamaIndex se talló un nicho fuerte como el framework de cabecera para retrieval-augmented generation.
Pero se solapan bastante. Los tres pueden construir un pipeline RAG. Los tres soportan tool calling. Los tres se conectan a vector stores. Las diferencias están en la filosofía, la experiencia de desarrollo y dónde son más fuertes.
Comparación de Arquitectura
LangChain: La Navaja Suiza
La abstracción core de LangChain es la cadena, una secuencia de operaciones pipeadas juntas. Con LCEL, esto se vuelve genuinamente elegante:
# langchain_rag.py - Pipeline RAG moderno con LangChain
from langchain_anthropic import ChatAnthropic
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader
# 1. Cargar y splitear documentos
loader = DirectoryLoader(
'./legal_docs/',
glob="**/*.pdf",
loader_cls=PyPDFLoader,
show_progress=True
)
docs = loader.load()
splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=50,
separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = splitter.split_documents(docs)
# 2. Crear vector store
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
chunks,
embeddings,
collection_name="legal_docs",
persist_directory="./chroma_db"
)
retriever = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={"k": 6, "fetch_k": 20, "lambda_mult": 0.7}
)
# 3. Definir la cadena RAG con LCEL
llm = ChatAnthropic(model="claude-sonnet-4-20250514", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", """Usted es un asistente de investigación legal. Respondé preguntas
basándote estrictamente en el contexto proporcionado. Si el contexto no
tiene información suficiente, decilo explícitamente. Citá secciones específicas.
Contexto:
{context}"""),
("human", "{question}")
])
def format_docs(docs):
return "\n\n---\n\n".join(
f"[{doc.metadata.get('source', 'unknown')}] {doc.page_content}"
for doc in docs
)
# El pipeline LCEL - limpio, componible, debuggeable
rag_chain = (
RunnableParallel(
context=retriever | format_docs,
question=RunnablePassthrough()
)
| prompt
| llm
| StrOutputParser()
)
# Uso
response = rag_chain.invoke("¿Cuáles son las limitaciones de responsabilidad en la sección 4.2?")
# Streaming - funciona nativamente con LCEL
for chunk in rag_chain.stream("Resumí las cláusulas de indemnización"):
print(chunk, end="", flush=True)
La fortaleza de LCEL es el operador pipe. retriever | format_docs se lee exactamente como lo que hace. Debuggear es directo porque es posible invocar cualquier paso independientemente. El RunnableParallel corre el retrieval y el passthrough de la pregunta concurrentemente.
La desventaja: el ecosistema es enorme y confuso. Hay langchain, langchain-core, langchain-community, langchain-openai, langchain-anthropic, descifrar qué paquete tiene qué clase sigue siendo una búsqueda del tesoro.
Semantic Kernel: El Jugador Enterprise
Semantic Kernel piensa en términos de plugins y planners. Un plugin es una colección de funciones que la AI puede llamar. Un planner decide qué plugins invocar y en qué orden.
# semantic_kernel_rag.py
import semantic_kernel as sk
from semantic_kernel.connectors.ai.open_ai import (
AzureChatCompletion,
AzureTextEmbedding
)
from semantic_kernel.connectors.memory.chroma import ChromaMemoryStore
from semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin
from semantic_kernel.functions import kernel_function
from semantic_kernel.memory import SemanticTextMemory
# 1. Inicializar el kernel
kernel = sk.Kernel()
kernel.add_service(AzureChatCompletion(
service_id="chat",
deployment_name="gpt-4o",
endpoint="https://myinstance.openai.azure.com/",
api_key="your-key"
))
embedding_service = AzureTextEmbedding(
service_id="embedding",
deployment_name="text-embedding-3-small",
endpoint="https://myinstance.openai.azure.com/",
api_key="your-key"
)
# 2. Configurar memoria
memory_store = ChromaMemoryStore(persist_directory="./chroma_sk")
memory = SemanticTextMemory(
storage=memory_store,
embeddings_generator=embedding_service
)
kernel.add_plugin(TextMemoryPlugin(memory), "memory")
# 3. Crear un plugin RAG custom
class LegalResearchPlugin:
def __init__(self, memory: SemanticTextMemory):
self._memory = memory
@kernel_function(
name="search_legal_docs",
description="Busca documentos legales por cláusulas y secciones relevantes. "
"Usar cuando el usuario pregunta sobre contratos, cláusulas, "
"responsabilidad, indemnización, o cualquier tema legal."
)
async def search_legal_docs(
self,
query: str,
collection: str = "legal_docs"
) -> str:
results = await self._memory.search(
collection=collection,
query=query,
limit=6,
min_relevance_score=0.7
)
if not results:
return "No se encontraron documentos relevantes para esta consulta."
passages = []
for result in results:
source = result.metadata.get("source", "desconocido")
passages.append(
f"[Fuente: {source}, Relevancia: {result.relevance:.2f}]\n"
f"{result.text}"
)
return "\n\n---\n\n".join(passages)
# Registrar plugin
legal_plugin = LegalResearchPlugin(memory)
kernel.add_plugin(legal_plugin, "legal_research")
# 4. Crear la función de chat con function calling automático
execution_settings = sk.PromptExecutionSettings(
service_id="chat",
function_choice_behavior="auto",
temperature=0
)
chat_function = kernel.add_function(
plugin_name="chat",
function_name="legal_assistant",
prompt="""Usted es un asistente de investigación legal con acceso a un corpus de documentos.
Utilizar el plugin legal_research para encontrar información relevante antes de responder.
Pregunta del usuario: {{$input}}
Proporcioná una respuesta exhaustiva con citas.""",
prompt_execution_settings=execution_settings
)
El setting function_choice_behavior="auto" es la clave. El LLM ve todos los plugins registrados con sus descripciones y decide cuáles llamar. El decorator @kernel_function con una buena descripción es esencial, el LLM la lee para decidir cuándo usar la función.
La fortaleza de Semantic Kernel está en escenarios enterprise estructurados. El modelo de plugins mapea bien a límites organizacionales, el equipo legal es dueño del plugin legal, el equipo de finanzas del plugin de finanzas.
LlamaIndex: El Especialista en RAG
LlamaIndex fue construido a propósito para retrieval-augmented generation, y se nota:
# llamaindex_rag.py
from llama_index.core import (
VectorStoreIndex, SimpleDirectoryReader, Settings, StorageContext
)
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.postprocessor import (
SimilarityPostprocessor, LongContextReorder
)
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.retrievers import VectorIndexRetriever
from llama_index.core.response_synthesizers import get_response_synthesizer
from llama_index.llms.anthropic import Anthropic
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.vector_stores.chroma import ChromaVectorStore
import chromadb
Settings.llm = Anthropic(model="claude-sonnet-4-20250514", temperature=0)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.node_parser = SentenceSplitter(chunk_size=512, chunk_overlap=50)
documents = SimpleDirectoryReader(
input_dir="./legal_docs/",
required_exts=[".pdf", ".docx"],
filename_as_id=True,
).load_data()
for doc in documents:
doc.metadata["doc_type"] = "legal_contract"
doc.metadata["jurisdiction"] = "AR"
chroma_client = chromadb.PersistentClient(path="./chroma_li")
chroma_collection = chroma_client.get_or_create_collection("legal_docs")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
index = VectorStoreIndex.from_documents(
documents, storage_context=storage_context, show_progress=True
)
retriever = VectorIndexRetriever(index=index, similarity_top_k=10)
node_postprocessors = [
SimilarityPostprocessor(similarity_cutoff=0.7),
LongContextReorder(), # Pone los más relevantes al principio y final
]
response_synthesizer = get_response_synthesizer(
response_mode="tree_summarize",
use_async=True,
)
query_engine = RetrieverQueryEngine(
retriever=retriever,
node_postprocessors=node_postprocessors,
response_synthesizer=response_synthesizer,
)
response = query_engine.query(
"¿Cuáles son las limitaciones de responsabilidad en la sección 4.2?"
)
El pipeline de LlamaIndex es más opinado sobre retrieval. El postprocessor LongContextReorder es un ejemplo perfecto, reordena los nodos retrieveados para que los más relevantes estén al principio y al final del contexto, basado en investigación que muestra que los LLMs prestan más atención a esas posiciones. Eso lo tendrías que construir usted mismo en LangChain.
El modo de respuesta tree_summarize es otra fortaleza. En vez de meter todos los chunks retrieveados en un solo prompt, construye un árbol de resúmenes, resumiendo grupos de chunks, después resumiendo los resúmenes.
Manejo de Memoria Comparado
La memoria es donde más divergen los frameworks:
# LangChain - Objetos de memoria explícitos
from langchain.memory import ConversationSummaryBufferMemory
memory = ConversationSummaryBufferMemory(
llm=llm, max_token_limit=2000, return_messages=True
)
# Mantiene mensajes recientes verbatim, resume los más viejos
# Semantic Kernel - ChatHistory con control manual
from semantic_kernel.contents import ChatHistory
history = ChatHistory()
history.add_system_message("Usted es un asistente legal.")
history.add_user_message("¿Qué hay en la sección 4.2?")
# Se controla exactamente qué entra y qué se poda
# LlamaIndex - Chat engine con memoria built-in
from llama_index.core.memory import ChatMemoryBuffer
memory = ChatMemoryBuffer.from_defaults(token_limit=3000)
chat_engine = index.as_chat_engine(
chat_mode="condense_plus_context",
memory=memory,
system_prompt="Usted es un asistente de investigación legal."
)
# Automáticamente condensa el historial en una query
response = chat_engine.chat("¿Qué hay en la sección 4.2?")
response = chat_engine.chat("¿Y qué dice sobre indemnización?")
LangChain te da más estrategias de memoria pero requiere cableado manual. Semantic Kernel te da más control pero menos magia. El modo condense_plus_context de LlamaIndex es el más turnkey.
Veredicto Después de Construir Tres Sistemas
LlamaIndex es la opción más fuerte para plataformas de búsqueda de documentos legales. El pipeline de procesamiento de documentos, extracción de metadata y postprocessors de retrieval ahorran semanas.
LangChain es la opción más fuerte para automatización de soporte al cliente. A menudo se necesitan comportamientos de agentes complejos, routing entre agentes humanos e AI, llamando APIs externas para estado de pedidos.
Semantic Kernel es la opción más fuerte para bases de conocimiento internas en empresas heavy en Microsoft. Integración con Azure AD, autenticación con managed identity a Azure OpenAI, y el modelo de plugins que mapeaba a su estructura organizacional.
Ninguno de estos frameworks es malo. Todos maduraron enormemente. Pero tienen fortalezas diferentes, y pretender que son intercambiables le hace un flaco favor a todos. Elegir el que matchee tu caso de uso principal, no el que tenga más estrellas en GitHub.