{"schemaVersion":1,"id":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration","url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration","title":"AI Orchestration Frameworks: Semantic Kernel vs LangChain vs LlamaIndex","description":"A real comparison of AI orchestration frameworks, plugin architectures, memory management, and building a RAG pipeline with each to see where they actually differ when the rubber meets the road.","language":"en","datePublished":"2026-01-18","dateModified":null,"contentFormat":"text/markdown","content":"AI/ML\n\n# AI Orchestration Frameworks: Semantic Kernel vs LangChain vs LlamaIndex\n\n2026-01-188 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/semantic-kernel-langchain-ai-orchestration>)\n\n## The Landscape in 2026\n\nThe AI orchestration space has matured significantly. [LangChain](<https://www.langchain.com/>) is no longer the disorganized framework it was in 2023, the v0.3 rewrite with LCEL brought real composability. [Semantic Kernel](<https://learn.microsoft.com/en-us/semantic-kernel/>) hit 1.0 and became Microsoft's official SDK for AI orchestration. [LlamaIndex](<https://www.llamaindex.ai/>) carved out a strong niche as the go-to framework for retrieval-augmented generation.\n\nBut they overlap significantly. All three can build a RAG pipeline. All three support tool calling. All three connect to vector stores. The differences are in philosophy, developer experience, and where they're strongest.\n\n## Architecture Comparison\n\n### LangChain: The Swiss Army Knife\n\nLangChain's core abstraction is the chain, a sequence of operations piped together. With LCEL, this becomes genuinely elegant:\n\n```python\n# langchain_rag.py - Modern LangChain RAG pipeline\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_openai import OpenAIEmbeddings\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.runnables import RunnablePassthrough, RunnableParallel\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import DirectoryLoader, PyPDFLoader\n\n# 1. Load and split documents\nloader = DirectoryLoader(\n './legal_docs/',\n glob=\"**/*.pdf\",\n loader_cls=PyPDFLoader,\n show_progress=True\n)\ndocs = loader.load()\n\nsplitter = RecursiveCharacterTextSplitter(\n chunk_size=512,\n chunk_overlap=50,\n separators=[\"\\n\\n\", \"\\n\", \". \", \" \", \"\"]\n)\nchunks = splitter.split_documents(docs)\n\n# 2. Create vector store\nembeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\nvectorstore = Chroma.from_documents(\n chunks,\n embeddings,\n collection_name=\"legal_docs\",\n persist_directory=\"./chroma_db\"\n)\nretriever = vectorstore.as_retriever(\n search_type=\"mmr\",\n search_kwargs={\"k\": 6, \"fetch_k\": 20, \"lambda_mult\": 0.7}\n)\n\n# 3. Define the RAG chain with LCEL\nllm = ChatAnthropic(model=\"claude-sonnet-4-20250514\", temperature=0)\n\nprompt = ChatPromptTemplate.from_messages([\n (\"system\", \"\"\"You are a legal research assistant. Answer questions based\n strictly on the provided context. If the context doesn't contain enough\n information, say so explicitly. Cite specific document sections.\n\n Context:\n {context}\"\"\"),\n (\"human\", \"{question}\")\n])\n\ndef format_docs(docs):\n return \"\\n\\n---\\n\\n\".join(\n f\"[{doc.metadata.get('source', 'unknown')}] {doc.page_content}\"\n for doc in docs\n )\n\n# The LCEL pipeline - clean, composable, debuggable\nrag_chain = (\n RunnableParallel(\n context=retriever | format_docs,\n question=RunnablePassthrough()\n )\n | prompt\n | llm\n | StrOutputParser()\n)\n\n# Usage\nresponse = rag_chain.invoke(\"What are the liability limitations in section 4.2?\")\n\n# Streaming - works natively with LCEL\nfor chunk in rag_chain.stream(\"Summarize the indemnification clauses\"):\n print(chunk, end=\"\", flush=True)\n```\n\nThe strength of LCEL is the pipe operator. ` retriever | format_docs ` reads exactly like what it does. Debugging is straightforward because you can invoke any step independently. The ` RunnableParallel ` runs the retrieval and question passthrough concurrently.\n\nThe downside: the ecosystem is enormous and confusing. There are ` langchain `, ` langchain-core `, ` langchain-community `, ` langchain-openai `, ` langchain-anthropic `, figuring out which package has which class is still a scavenger hunt.\n\n### Semantic Kernel: The Enterprise Player\n\nSemantic Kernel thinks in terms of plugins and planners. A plugin is a collection of functions that the AI can call. A planner decides which plugins to invoke and in what order.\n\n```python\n# semantic_kernel_rag.py\nimport semantic_kernel as sk\nfrom semantic_kernel.connectors.ai.open_ai import (\n AzureChatCompletion,\n AzureTextEmbedding\n)\nfrom semantic_kernel.connectors.memory.chroma import ChromaMemoryStore\nfrom semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin\nfrom semantic_kernel.functions import kernel_function\nfrom semantic_kernel.memory import SemanticTextMemory\n\n# 1. Initialize the kernel\nkernel = sk.Kernel()\n\n# Add AI services\nkernel.add_service(AzureChatCompletion(\n service_id=\"chat\",\n deployment_name=\"gpt-4o\",\n endpoint=\"https://myinstance.openai.azure.com/\",\n api_key=\"your-key\"\n))\n\nembedding_service = AzureTextEmbedding(\n service_id=\"embedding\",\n deployment_name=\"text-embedding-3-small\",\n endpoint=\"https://myinstance.openai.azure.com/\",\n api_key=\"your-key\"\n)\n\n# 2. Set up memory\nmemory_store = ChromaMemoryStore(persist_directory=\"./chroma_sk\")\nmemory = SemanticTextMemory(\n storage=memory_store,\n embeddings_generator=embedding_service\n)\nkernel.add_plugin(TextMemoryPlugin(memory), \"memory\")\n\n# 3. Create a custom RAG plugin\nclass LegalResearchPlugin:\n def __init__(self, memory: SemanticTextMemory):\n self._memory = memory\n\n @kernel_function(\n name=\"search_legal_docs\",\n description=\"Search legal documents for relevant clauses and sections. \"\n \"Use this when the user asks about contracts, clauses, \"\n \"liability, indemnification, or any legal topic.\"\n )\n async def search_legal_docs(\n self,\n query: str,\n collection: str = \"legal_docs\"\n ) -> str:\n \"\"\"Search the legal document corpus and return relevant passages.\"\"\"\n results = await self._memory.search(\n collection=collection,\n query=query,\n limit=6,\n min_relevance_score=0.7\n )\n\n if not results:\n return \"No relevant documents found for this query.\"\n\n passages = []\n for i, result in enumerate(results):\n source = result.metadata.get(\"source\", \"unknown\")\n passages.append(\n f\"[Source: {source}, Relevance: {result.relevance:.2f}]\\n\"\n f\"{result.text}\"\n )\n\n return \"\\n\\n---\\n\\n\".join(passages)\n\n @kernel_function(\n name=\"analyze_clause\",\n description=\"Analyze a specific legal clause for risks and obligations. \"\n \"Use after retrieving relevant documents.\"\n )\n async def analyze_clause(self, clause_text: str) -> str:\n return f\"Clause for analysis:\\n{clause_text}\"\n\n# Register plugin\nlegal_plugin = LegalResearchPlugin(memory)\nkernel.add_plugin(legal_plugin, \"legal_research\")\n\n# 4. Create the chat function with auto function calling\nexecution_settings = sk.PromptExecutionSettings(\n service_id=\"chat\",\n function_choice_behavior=\"auto\", # Let the LLM decide which functions to call\n temperature=0\n)\n\n# The kernel handles the function calling loop automatically\nchat_function = kernel.add_function(\n plugin_name=\"chat\",\n function_name=\"legal_assistant\",\n prompt=\"\"\"You are a legal research assistant with access to a document corpus.\n Use the legal_research plugin to find relevant information before answering.\n\n User question: {{$input}}\n\n Provide a thorough answer with citations.\"\"\",\n prompt_execution_settings=execution_settings\n)\n\n# Usage\nasync def main():\n response = await kernel.invoke(\n chat_function,\n input=\"What are the liability limitations in section 4.2?\"\n )\n print(response)\n```\n\nThe ` function_choice_behavior=\"auto\" ` setting is key. The LLM sees all registered plugins with their descriptions and decides which to call. The ` @kernel_function ` decorator with a good description is essential, the LLM reads it to decide when to use the function.\n\nSemantic Kernel's strength is in structured enterprise scenarios. The plugin model maps well to organizational boundaries, the legal team owns the legal plugin, the finance team owns the finance plugin. It's also the best option if you're deploying on Azure with managed identities and Azure [OpenAI](<https://openai.com/>).\n\n### LlamaIndex: The RAG Specialist\n\nLlamaIndex was purpose-built for retrieval-augmented generation, and it shows:\n\n```python\n# llamaindex_rag.py\nfrom llama_index.core import (\n VectorStoreIndex,\n SimpleDirectoryReader,\n Settings,\n StorageContext,\n load_index_from_storage,\n)\nfrom llama_index.core.node_parser import SentenceSplitter\nfrom llama_index.core.postprocessor import (\n SimilarityPostprocessor,\n LongContextReorder\n)\nfrom llama_index.core.query_engine import RetrieverQueryEngine\nfrom llama_index.core.retrievers import VectorIndexRetriever\nfrom llama_index.core.response_synthesizers import get_response_synthesizer\nfrom llama_index.llms.anthropic import Anthropic\nfrom llama_index.embeddings.openai import OpenAIEmbedding\nfrom llama_index.vector_stores.chroma import ChromaVectorStore\nimport chromadb\n\n# 1. Configure global settings\nSettings.llm = Anthropic(model=\"claude-sonnet-4-20250514\", temperature=0)\nSettings.embed_model = OpenAIEmbedding(model=\"text-embedding-3-small\")\nSettings.node_parser = SentenceSplitter(\n chunk_size=512,\n chunk_overlap=50\n)\n\n# 2. Load documents with metadata extraction\ndocuments = SimpleDirectoryReader(\n input_dir=\"./legal_docs/\",\n required_exts=[\".pdf\", \".docx\"],\n filename_as_id=True,\n).load_data()\n\n# Add custom metadata\nfor doc in documents:\n doc.metadata[\"doc_type\"] = \"legal_contract\"\n doc.metadata[\"jurisdiction\"] = \"US\"\n\n# 3. Create vector store index\nchroma_client = chromadb.PersistentClient(path=\"./chroma_li\")\nchroma_collection = chroma_client.get_or_create_collection(\"legal_docs\")\nvector_store = ChromaVectorStore(chroma_collection=chroma_collection)\nstorage_context = StorageContext.from_defaults(vector_store=vector_store)\n\nindex = VectorStoreIndex.from_documents(\n documents,\n storage_context=storage_context,\n show_progress=True\n)\n\n# 4. Build a sophisticated query engine\nretriever = VectorIndexRetriever(\n index=index,\n similarity_top_k=10, # Retrieve more, then rerank\n)\n\n# Post-processing pipeline\nnode_postprocessors = [\n SimilarityPostprocessor(similarity_cutoff=0.7),\n LongContextReorder(), # Put most relevant at start and end\n]\n\nresponse_synthesizer = get_response_synthesizer(\n response_mode=\"tree_summarize\", # Hierarchical summarization\n use_async=True,\n)\n\nquery_engine = RetrieverQueryEngine(\n retriever=retriever,\n node_postprocessors=node_postprocessors,\n response_synthesizer=response_synthesizer,\n)\n\n# 5. Query with source tracking\nresponse = query_engine.query(\n \"What are the liability limitations in section 4.2?\"\n)\n\nprint(response.response)\nprint(\"\\n--- Sources ---\")\nfor node in response.source_nodes:\n print(f\"Score: {node.score:.3f} | Source: {node.metadata.get('file_name')}\")\n print(f\"Text: {node.text[:200]}...\")\n print()\n```\n\nLlamaIndex's pipeline is more opinionated about retrieval. The ` LongContextReorder ` postprocessor is a perfect example, it reorders retrieved nodes so the most relevant are at the beginning and end of the context, based on research showing LLMs pay more attention to those positions. You'd have to build this yourself in LangChain.\n\nThe ` tree_summarize ` response mode is another strength. Instead of stuffing all retrieved chunks into one prompt, it builds a tree of summaries, summarizing groups of chunks, then summarizing the summaries. This handles cases where you retrieve 20 relevant chunks that wouldn't fit in a single context window.\n\n## Memory Management Compared\n\nMemory is where the frameworks diverge most:\n\n```python\n# LangChain - Explicit memory objects\nfrom langchain.memory import ConversationSummaryBufferMemory\n\nmemory = ConversationSummaryBufferMemory(\n llm=llm,\n max_token_limit=2000,\n return_messages=True\n)\n# Keeps recent messages verbatim, summarizes older ones\n\n# Semantic Kernel - ChatHistory with manual control\nfrom semantic_kernel.contents import ChatHistory\n\nhistory = ChatHistory()\nhistory.add_system_message(\"You are a legal assistant.\")\nhistory.add_user_message(\"What's in section 4.2?\")\nhistory.add_assistant_message(\"Section 4.2 covers...\")\n# You control exactly what goes in and what gets pruned\n\n# LlamaIndex - Chat engine with built-in memory\nfrom llama_index.core.memory import ChatMemoryBuffer\n\nmemory = ChatMemoryBuffer.from_defaults(token_limit=3000)\nchat_engine = index.as_chat_engine(\n chat_mode=\"condense_plus_context\",\n memory=memory,\n system_prompt=\"You are a legal research assistant.\"\n)\n# Automatically condenses conversation history into a query\nresponse = chat_engine.chat(\"What about section 4.2?\")\nresponse = chat_engine.chat(\"And what about indemnification?\")\n# ^ This automatically uses conversation context\n```\n\nLangChain gives you the most memory strategies but requires manual wiring. Semantic Kernel gives you the most control but the least magic. LlamaIndex's ` condense_plus_context ` mode is the most turnkey, it automatically reformulates follow-up questions using conversation history, which is exactly what you need for a chat-based RAG system.\n\n## Verdict After Building Three Systems\n\n**LlamaIndex** is the strongest choice for legal document search platforms. The document processing pipeline, metadata extraction, and retrieval postprocessors save weeks of custom development. If your primary job is \"search a corpus and answer questions,\" start here.\n\n**LangChain** is the strongest choice for customer support automation. Complex agent behaviors are often needed, routing between human agents and AI, calling external APIs for order status, switching between different LLM providers based on query complexity. LangChain's LCEL made these compositions clean and debuggable.\n\n**Semantic Kernel** is the strongest choice for internal knowledge bases in Microsoft-heavy enterprises. Azure AD integration, managed identity authentication to Azure OpenAI, and the plugin model that mapped to their organizational structure. The C# SDK is also a factor when the majority of the backend is .NET.\n\nNone of these frameworks is bad. They've all matured enormously. But they have different strengths, and pretending they're interchangeable does everyone a disservice. Pick the one that matches your primary use case, not the one with the most GitHub stars.\n\n## Frequently asked questions\n\n### Which framework should I start with for a RAG application?\n\nLlamaIndex if RAG is your primary use case, it has the most mature document processing and retrieval pipeline. LangChain if you need RAG as part of a larger agent system with tools and chains. Semantic Kernel if you're in the Microsoft ecosystem or need enterprise-grade .NET support.\n\n### Can I use multiple LLM providers with these frameworks?\n\nYes, all three support multiple providers. LangChain has the widest provider support. Semantic Kernel has first-class Azure OpenAI and OpenAI support with growing community connectors. LlamaIndex supports all major providers through its LLM abstraction.\n\n### How do these frameworks handle memory and conversation state?\n\nLangChain has explicit memory classes (ConversationBufferMemory, ConversationSummaryMemory). Semantic Kernel uses a ChatHistory object with plugin-based memory. LlamaIndex focuses on index-based memory through its chat engine abstractions.\n\n## Tips\n\n1. **Tip:** LangChain's ` LCEL ` (LangChain Expression Language) is the modern way to chain components. If you're still using ` LLMChain `, you're on deprecated APIs, migrate now.\n2. **Tip:** Semantic Kernel's ` KernelFunction ` decorator turns any Python/C# method into a plugin. Use ` @kernel_function(description=...) ` with detailed descriptions, the LLM uses them for function selection.\n3. **Tip:** LlamaIndex's ` VectorStoreIndex ` default chunk size of 1024 tokens is too large for most use cases. Start with ` chunk_size=512, chunk_overlap=50 ` and tune from there.\n\n- semantic-kernel\n- langchain\n- llamaindex\n- rag\n- ai-orchestration\n- llm\n- plugins\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"AI Orchestration Frameworks: Semantic Kernel vs LangChain vs LlamaIndex","url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#title"},{"level":2,"text":"The Landscape in 2026","url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#the-landscape-in-2026"},{"level":2,"text":"Architecture Comparison","url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#architecture-comparison"},{"level":3,"text":"LangChain: The Swiss Army Knife","url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#langchain-the-swiss-army-knife"},{"level":3,"text":"Semantic Kernel: The Enterprise Player","url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#semantic-kernel-the-enterprise-player"},{"level":3,"text":"LlamaIndex: The RAG Specialist","url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#llamaindex-the-rag-specialist"},{"level":2,"text":"Memory Management Compared","url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#memory-management-compared"},{"level":2,"text":"Verdict After Building Three Systems","url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#verdict-after-building-three-systems"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#questions"},{"level":3,"text":"Which framework should I start with for a RAG application?","url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#question-1"},{"level":3,"text":"Can I use multiple LLM providers with these frameworks?","url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#question-2"},{"level":3,"text":"How do these frameworks handle memory and conversation state?","url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#article-tips"}],"sections":[{"heading":"AI Orchestration Frameworks: Semantic Kernel vs LangChain vs LlamaIndex","level":1,"url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration","content":"AI/ML"},{"heading":"AI Orchestration Frameworks: Semantic Kernel vs LangChain vs LlamaIndex","level":1,"url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#title","content":"2026-01-188 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/semantic-kernel-langchain-ai-orchestration>)"},{"heading":"The Landscape in 2026","level":2,"url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#the-landscape-in-2026","content":"The AI orchestration space has matured significantly. [LangChain](<https://www.langchain.com/>) is no longer the disorganized framework it was in 2023, the v0.3 rewrite with LCEL brought real composability. [Semantic Kernel](<https://learn.microsoft.com/en-us/semantic-kernel/>) hit 1.0 and became Microsoft's official SDK for AI orchestration. [LlamaIndex](<https://www.llamaindex.ai/>) carved out a strong niche as the go-to framework for retrieval-augmented generation.\n\nBut they overlap significantly. All three can build a RAG pipeline. All three support tool calling. All three connect to vector stores. The differences are in philosophy, developer experience, and where they're strongest."},{"heading":"Architecture Comparison","level":2,"url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#architecture-comparison","content":""},{"heading":"LangChain: The Swiss Army Knife","level":3,"url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#langchain-the-swiss-army-knife","content":"LangChain's core abstraction is the chain, a sequence of operations piped together. With LCEL, this becomes genuinely elegant:\n\n```python\n# langchain_rag.py - Modern LangChain RAG pipeline\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_openai import OpenAIEmbeddings\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.runnables import RunnablePassthrough, RunnableParallel\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import DirectoryLoader, PyPDFLoader\n\n# 1. Load and split documents\nloader = DirectoryLoader(\n './legal_docs/',\n glob=\"**/*.pdf\",\n loader_cls=PyPDFLoader,\n show_progress=True\n)\ndocs = loader.load()\n\nsplitter = RecursiveCharacterTextSplitter(\n chunk_size=512,\n chunk_overlap=50,\n separators=[\"\\n\\n\", \"\\n\", \". \", \" \", \"\"]\n)\nchunks = splitter.split_documents(docs)\n\n# 2. Create vector store\nembeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\nvectorstore = Chroma.from_documents(\n chunks,\n embeddings,\n collection_name=\"legal_docs\",\n persist_directory=\"./chroma_db\"\n)\nretriever = vectorstore.as_retriever(\n search_type=\"mmr\",\n search_kwargs={\"k\": 6, \"fetch_k\": 20, \"lambda_mult\": 0.7}\n)\n\n# 3. Define the RAG chain with LCEL\nllm = ChatAnthropic(model=\"claude-sonnet-4-20250514\", temperature=0)\n\nprompt = ChatPromptTemplate.from_messages([\n (\"system\", \"\"\"You are a legal research assistant. Answer questions based\n strictly on the provided context. If the context doesn't contain enough\n information, say so explicitly. Cite specific document sections.\n\n Context:\n {context}\"\"\"),\n (\"human\", \"{question}\")\n])\n\ndef format_docs(docs):\n return \"\\n\\n---\\n\\n\".join(\n f\"[{doc.metadata.get('source', 'unknown')}] {doc.page_content}\"\n for doc in docs\n )\n\n# The LCEL pipeline - clean, composable, debuggable\nrag_chain = (\n RunnableParallel(\n context=retriever | format_docs,\n question=RunnablePassthrough()\n )\n | prompt\n | llm\n | StrOutputParser()\n)\n\n# Usage\nresponse = rag_chain.invoke(\"What are the liability limitations in section 4.2?\")\n\n# Streaming - works natively with LCEL\nfor chunk in rag_chain.stream(\"Summarize the indemnification clauses\"):\n print(chunk, end=\"\", flush=True)\n```\n\nThe strength of LCEL is the pipe operator. ` retriever | format_docs ` reads exactly like what it does. Debugging is straightforward because you can invoke any step independently. The ` RunnableParallel ` runs the retrieval and question passthrough concurrently.\n\nThe downside: the ecosystem is enormous and confusing. There are ` langchain `, ` langchain-core `, ` langchain-community `, ` langchain-openai `, ` langchain-anthropic `, figuring out which package has which class is still a scavenger hunt."},{"heading":"Semantic Kernel: The Enterprise Player","level":3,"url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#semantic-kernel-the-enterprise-player","content":"Semantic Kernel thinks in terms of plugins and planners. A plugin is a collection of functions that the AI can call. A planner decides which plugins to invoke and in what order.\n\n```python\n# semantic_kernel_rag.py\nimport semantic_kernel as sk\nfrom semantic_kernel.connectors.ai.open_ai import (\n AzureChatCompletion,\n AzureTextEmbedding\n)\nfrom semantic_kernel.connectors.memory.chroma import ChromaMemoryStore\nfrom semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin\nfrom semantic_kernel.functions import kernel_function\nfrom semantic_kernel.memory import SemanticTextMemory\n\n# 1. Initialize the kernel\nkernel = sk.Kernel()\n\n# Add AI services\nkernel.add_service(AzureChatCompletion(\n service_id=\"chat\",\n deployment_name=\"gpt-4o\",\n endpoint=\"https://myinstance.openai.azure.com/\",\n api_key=\"your-key\"\n))\n\nembedding_service = AzureTextEmbedding(\n service_id=\"embedding\",\n deployment_name=\"text-embedding-3-small\",\n endpoint=\"https://myinstance.openai.azure.com/\",\n api_key=\"your-key\"\n)\n\n# 2. Set up memory\nmemory_store = ChromaMemoryStore(persist_directory=\"./chroma_sk\")\nmemory = SemanticTextMemory(\n storage=memory_store,\n embeddings_generator=embedding_service\n)\nkernel.add_plugin(TextMemoryPlugin(memory), \"memory\")\n\n# 3. Create a custom RAG plugin\nclass LegalResearchPlugin:\n def __init__(self, memory: SemanticTextMemory):\n self._memory = memory\n\n @kernel_function(\n name=\"search_legal_docs\",\n description=\"Search legal documents for relevant clauses and sections. \"\n \"Use this when the user asks about contracts, clauses, \"\n \"liability, indemnification, or any legal topic.\"\n )\n async def search_legal_docs(\n self,\n query: str,\n collection: str = \"legal_docs\"\n ) -> str:\n \"\"\"Search the legal document corpus and return relevant passages.\"\"\"\n results = await self._memory.search(\n collection=collection,\n query=query,\n limit=6,\n min_relevance_score=0.7\n )\n\n if not results:\n return \"No relevant documents found for this query.\"\n\n passages = []\n for i, result in enumerate(results):\n source = result.metadata.get(\"source\", \"unknown\")\n passages.append(\n f\"[Source: {source}, Relevance: {result.relevance:.2f}]\\n\"\n f\"{result.text}\"\n )\n\n return \"\\n\\n---\\n\\n\".join(passages)\n\n @kernel_function(\n name=\"analyze_clause\",\n description=\"Analyze a specific legal clause for risks and obligations. \"\n \"Use after retrieving relevant documents.\"\n )\n async def analyze_clause(self, clause_text: str) -> str:\n return f\"Clause for analysis:\\n{clause_text}\"\n\n# Register plugin\nlegal_plugin = LegalResearchPlugin(memory)\nkernel.add_plugin(legal_plugin, \"legal_research\")\n\n# 4. Create the chat function with auto function calling\nexecution_settings = sk.PromptExecutionSettings(\n service_id=\"chat\",\n function_choice_behavior=\"auto\", # Let the LLM decide which functions to call\n temperature=0\n)\n\n# The kernel handles the function calling loop automatically\nchat_function = kernel.add_function(\n plugin_name=\"chat\",\n function_name=\"legal_assistant\",\n prompt=\"\"\"You are a legal research assistant with access to a document corpus.\n Use the legal_research plugin to find relevant information before answering.\n\n User question: {{$input}}\n\n Provide a thorough answer with citations.\"\"\",\n prompt_execution_settings=execution_settings\n)\n\n# Usage\nasync def main():\n response = await kernel.invoke(\n chat_function,\n input=\"What are the liability limitations in section 4.2?\"\n )\n print(response)\n```\n\nThe ` function_choice_behavior=\"auto\" ` setting is key. The LLM sees all registered plugins with their descriptions and decides which to call. The ` @kernel_function ` decorator with a good description is essential, the LLM reads it to decide when to use the function.\n\nSemantic Kernel's strength is in structured enterprise scenarios. The plugin model maps well to organizational boundaries, the legal team owns the legal plugin, the finance team owns the finance plugin. It's also the best option if you're deploying on Azure with managed identities and Azure [OpenAI](<https://openai.com/>)."},{"heading":"LlamaIndex: The RAG Specialist","level":3,"url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#llamaindex-the-rag-specialist","content":"LlamaIndex was purpose-built for retrieval-augmented generation, and it shows:\n\n```python\n# llamaindex_rag.py\nfrom llama_index.core import (\n VectorStoreIndex,\n SimpleDirectoryReader,\n Settings,\n StorageContext,\n load_index_from_storage,\n)\nfrom llama_index.core.node_parser import SentenceSplitter\nfrom llama_index.core.postprocessor import (\n SimilarityPostprocessor,\n LongContextReorder\n)\nfrom llama_index.core.query_engine import RetrieverQueryEngine\nfrom llama_index.core.retrievers import VectorIndexRetriever\nfrom llama_index.core.response_synthesizers import get_response_synthesizer\nfrom llama_index.llms.anthropic import Anthropic\nfrom llama_index.embeddings.openai import OpenAIEmbedding\nfrom llama_index.vector_stores.chroma import ChromaVectorStore\nimport chromadb\n\n# 1. Configure global settings\nSettings.llm = Anthropic(model=\"claude-sonnet-4-20250514\", temperature=0)\nSettings.embed_model = OpenAIEmbedding(model=\"text-embedding-3-small\")\nSettings.node_parser = SentenceSplitter(\n chunk_size=512,\n chunk_overlap=50\n)\n\n# 2. Load documents with metadata extraction\ndocuments = SimpleDirectoryReader(\n input_dir=\"./legal_docs/\",\n required_exts=[\".pdf\", \".docx\"],\n filename_as_id=True,\n).load_data()\n\n# Add custom metadata\nfor doc in documents:\n doc.metadata[\"doc_type\"] = \"legal_contract\"\n doc.metadata[\"jurisdiction\"] = \"US\"\n\n# 3. Create vector store index\nchroma_client = chromadb.PersistentClient(path=\"./chroma_li\")\nchroma_collection = chroma_client.get_or_create_collection(\"legal_docs\")\nvector_store = ChromaVectorStore(chroma_collection=chroma_collection)\nstorage_context = StorageContext.from_defaults(vector_store=vector_store)\n\nindex = VectorStoreIndex.from_documents(\n documents,\n storage_context=storage_context,\n show_progress=True\n)\n\n# 4. Build a sophisticated query engine\nretriever = VectorIndexRetriever(\n index=index,\n similarity_top_k=10, # Retrieve more, then rerank\n)\n\n# Post-processing pipeline\nnode_postprocessors = [\n SimilarityPostprocessor(similarity_cutoff=0.7),\n LongContextReorder(), # Put most relevant at start and end\n]\n\nresponse_synthesizer = get_response_synthesizer(\n response_mode=\"tree_summarize\", # Hierarchical summarization\n use_async=True,\n)\n\nquery_engine = RetrieverQueryEngine(\n retriever=retriever,\n node_postprocessors=node_postprocessors,\n response_synthesizer=response_synthesizer,\n)\n\n# 5. Query with source tracking\nresponse = query_engine.query(\n \"What are the liability limitations in section 4.2?\"\n)\n\nprint(response.response)\nprint(\"\\n--- Sources ---\")\nfor node in response.source_nodes:\n print(f\"Score: {node.score:.3f} | Source: {node.metadata.get('file_name')}\")\n print(f\"Text: {node.text[:200]}...\")\n print()\n```\n\nLlamaIndex's pipeline is more opinionated about retrieval. The ` LongContextReorder ` postprocessor is a perfect example, it reorders retrieved nodes so the most relevant are at the beginning and end of the context, based on research showing LLMs pay more attention to those positions. You'd have to build this yourself in LangChain.\n\nThe ` tree_summarize ` response mode is another strength. Instead of stuffing all retrieved chunks into one prompt, it builds a tree of summaries, summarizing groups of chunks, then summarizing the summaries. This handles cases where you retrieve 20 relevant chunks that wouldn't fit in a single context window."},{"heading":"Memory Management Compared","level":2,"url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#memory-management-compared","content":"Memory is where the frameworks diverge most:\n\n```python\n# LangChain - Explicit memory objects\nfrom langchain.memory import ConversationSummaryBufferMemory\n\nmemory = ConversationSummaryBufferMemory(\n llm=llm,\n max_token_limit=2000,\n return_messages=True\n)\n# Keeps recent messages verbatim, summarizes older ones\n\n# Semantic Kernel - ChatHistory with manual control\nfrom semantic_kernel.contents import ChatHistory\n\nhistory = ChatHistory()\nhistory.add_system_message(\"You are a legal assistant.\")\nhistory.add_user_message(\"What's in section 4.2?\")\nhistory.add_assistant_message(\"Section 4.2 covers...\")\n# You control exactly what goes in and what gets pruned\n\n# LlamaIndex - Chat engine with built-in memory\nfrom llama_index.core.memory import ChatMemoryBuffer\n\nmemory = ChatMemoryBuffer.from_defaults(token_limit=3000)\nchat_engine = index.as_chat_engine(\n chat_mode=\"condense_plus_context\",\n memory=memory,\n system_prompt=\"You are a legal research assistant.\"\n)\n# Automatically condenses conversation history into a query\nresponse = chat_engine.chat(\"What about section 4.2?\")\nresponse = chat_engine.chat(\"And what about indemnification?\")\n# ^ This automatically uses conversation context\n```\n\nLangChain gives you the most memory strategies but requires manual wiring. Semantic Kernel gives you the most control but the least magic. LlamaIndex's ` condense_plus_context ` mode is the most turnkey, it automatically reformulates follow-up questions using conversation history, which is exactly what you need for a chat-based RAG system."},{"heading":"Verdict After Building Three Systems","level":2,"url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#verdict-after-building-three-systems","content":"**LlamaIndex** is the strongest choice for legal document search platforms. The document processing pipeline, metadata extraction, and retrieval postprocessors save weeks of custom development. If your primary job is \"search a corpus and answer questions,\" start here.\n\n**LangChain** is the strongest choice for customer support automation. Complex agent behaviors are often needed, routing between human agents and AI, calling external APIs for order status, switching between different LLM providers based on query complexity. LangChain's LCEL made these compositions clean and debuggable.\n\n**Semantic Kernel** is the strongest choice for internal knowledge bases in Microsoft-heavy enterprises. Azure AD integration, managed identity authentication to Azure OpenAI, and the plugin model that mapped to their organizational structure. The C# SDK is also a factor when the majority of the backend is .NET.\n\nNone of these frameworks is bad. They've all matured enormously. But they have different strengths, and pretending they're interchangeable does everyone a disservice. Pick the one that matches your primary use case, not the one with the most GitHub stars."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#questions","content":""},{"heading":"Which framework should I start with for a RAG application?","level":3,"url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#question-1","content":"LlamaIndex if RAG is your primary use case, it has the most mature document processing and retrieval pipeline. LangChain if you need RAG as part of a larger agent system with tools and chains. Semantic Kernel if you're in the Microsoft ecosystem or need enterprise-grade .NET support."},{"heading":"Can I use multiple LLM providers with these frameworks?","level":3,"url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#question-2","content":"Yes, all three support multiple providers. LangChain has the widest provider support. Semantic Kernel has first-class Azure OpenAI and OpenAI support with growing community connectors. LlamaIndex supports all major providers through its LLM abstraction."},{"heading":"How do these frameworks handle memory and conversation state?","level":3,"url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#question-3","content":"LangChain has explicit memory classes (ConversationBufferMemory, ConversationSummaryMemory). Semantic Kernel uses a ChatHistory object with plugin-based memory. LlamaIndex focuses on index-based memory through its chat engine abstractions."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#article-tips","content":"1. **Tip:** LangChain's ` LCEL ` (LangChain Expression Language) is the modern way to chain components. If you're still using ` LLMChain `, you're on deprecated APIs, migrate now.\n2. **Tip:** Semantic Kernel's ` KernelFunction ` decorator turns any Python/C# method into a plugin. Use ` @kernel_function(description=...) ` with detailed descriptions, the LLM uses them for function selection.\n3. **Tip:** LlamaIndex's ` VectorStoreIndex ` default chunk size of 1024 tokens is too large for most use cases. Start with ` chunk_size=512, chunk_overlap=50 ` and tune from there.\n\n- semantic-kernel\n- langchain\n- llamaindex\n- rag\n- ai-orchestration\n- llm\n- plugins\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/semantic-kernel-langchain-ai-orchestration"},{"text":"LangChain","url":"https://www.langchain.com/"},{"text":"Semantic Kernel","url":"https://learn.microsoft.com/en-us/semantic-kernel/"},{"text":"LlamaIndex","url":"https://www.llamaindex.ai/"},{"text":"OpenAI","url":"https://openai.com/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration"},"headline":"AI Orchestration Frameworks: Semantic Kernel vs LangChain vs LlamaIndex","description":"A real comparison of AI orchestration frameworks, plugin architectures, memory management, and building a RAG pipeline with each to see where they actually differ when the rubber meets the road.","datePublished":"2026-01-18","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"AI/ML","keywords":"semantic-kernel, langchain, llamaindex, rag, ai-orchestration, llm, plugins","wordCount":1486,"timeRequired":"PT8M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/semantic-kernel-langchain-ai-orchestration"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"AI Orchestration Frameworks: Semantic Kernel vs LangChain vs LlamaIndex","item":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Which framework should I start with for a RAG application?","acceptedAnswer":{"@type":"Answer","text":"LlamaIndex if RAG is your primary use case, it has the most mature document processing and retrieval pipeline. LangChain if you need RAG as part of a larger agent system with tools and chains. Semantic Kernel if you're in the Microsoft ecosystem or need enterprise-grade .NET support."}},{"@type":"Question","name":"Can I use multiple LLM providers with these frameworks?","acceptedAnswer":{"@type":"Answer","text":"Yes, all three support multiple providers. LangChain has the widest provider support. Semantic Kernel has first-class Azure OpenAI and OpenAI support with growing community connectors. LlamaIndex supports all major providers through its LLM abstraction."}},{"@type":"Question","name":"How do these frameworks handle memory and conversation state?","acceptedAnswer":{"@type":"Answer","text":"LangChain has explicit memory classes (ConversationBufferMemory, ConversationSummaryMemory). Semantic Kernel uses a ChatHistory object with plugin-based memory. LlamaIndex focuses on index-based memory through its chat engine abstractions."}}]}]}],"contentHash":"7df4e81461a7a9a62688ace16dda95e07c7e8f9f0cb48532e789922c0e39ce99","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/semantic-kernel-langchain-ai-orchestration","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":67267,"contentBytes":15777}}
