The Problem With “Just Use LangChain” for Enterprise Work
Most AI tutorials steer you straight to LangChain. I did the same — spun up a Python service, chained together some tools, shipped it, and assumed we were done. Six months later, a LangChain minor version bump broke half our integrations. The code was nearly untestable, and onboarding a .NET developer from another team felt like handing them a completely foreign codebase.
That experience pushed me toward Microsoft Semantic Kernel. It’s Microsoft’s open-source SDK for building AI applications — and after running it in production for over a year, I’d say it’s essential to learn if enterprise reliability matters to you. The plugin architecture is clean, the planning system is explicit, and the same SDK runs in Python, .NET, and Java. That last point matters more than it sounds: onboarding a .NET developer took an afternoon instead of a week.
This guide walks through building a working AI agent from scratch — kernel setup, native plugins, semantic plugins, vector memory, and auto-invocation planning.
Core Concepts You Need First
The Kernel: Your DI Container for AI
Everything in Semantic Kernel revolves around a Kernel object. Think of it like a dependency injection container — you register your LLM connection, your plugins, and your memory store, then the kernel orchestrates everything. This design makes unit testing clean: swap out the real LLM for a mock, run your tests, done.
Plugins Replace Chains
Where LangChain uses “chains” and “tools,” Semantic Kernel uses plugins. Two types matter here:
- Native plugins: Regular Python functions decorated with
@kernel_function. These run real code — call an API, query a database, do calculations. - Semantic plugins: Prompt templates stored as text files. You write the prompt, SK handles the LLM call. Great for language tasks like summarization or translation, and they version-control cleanly in Git.
Planning: Auto-Invocation That Actually Works
SK’s planner takes a user goal and figures out which plugins to call, in what order. In SK 1.x, the recommended approach uses the LLM’s native tool-calling capability via FunctionChoiceBehavior. Compared to the custom chain-of-thought tricks earlier frameworks relied on, it’s noticeably more reliable in practice — fewer hallucinated tool calls, better handling of multi-step requests.
Hands-On: Building an AI Agent Step by Step
Installation and Setup
pip install semantic-kernel openai python-dotenv
Create a .env file:
OPENAI_API_KEY=sk-...
OPENAI_CHAT_MODEL_ID=gpt-4o
Your First Kernel
import asyncio
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from dotenv import load_dotenv
load_dotenv()
async def main():
kernel = Kernel()
kernel.add_service(OpenAIChatCompletion())
result = await kernel.invoke_prompt(
"What are the top 3 benefits of Kubernetes for microservices?"
)
print(result)
asyncio.run(main())
Not exciting yet — but this baseline architecture pays off the moment you add plugins.
Building a Native Plugin
Here’s a practical plugin that simulates fetching server metrics — the kind you’d wire up to Prometheus or Datadog in production:
import random
from semantic_kernel.functions import kernel_function
class ServerMetricsPlugin:
@kernel_function(
name="get_cpu_usage",
description="Get current CPU usage percentage for a given server hostname"
)
def get_cpu_usage(self, hostname: str) -> str:
# In production: query Prometheus, Datadog, etc.
usage = random.uniform(10, 95)
return f"Server {hostname} CPU usage: {usage:.1f}%"
@kernel_function(
name="get_memory_usage",
description="Get current memory usage percentage for a given server hostname"
)
def get_memory_usage(self, hostname: str) -> str:
usage = random.uniform(20, 90)
return f"Server {hostname} memory usage: {usage:.1f}%"
Wiring Up Auto-Invocation Planning
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import (
OpenAIChatCompletion,
OpenAIChatPromptExecutionSettings
)
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
async def main():
kernel = Kernel()
kernel.add_service(OpenAIChatCompletion())
kernel.add_plugin(ServerMetricsPlugin(), plugin_name="ServerMetrics")
settings = OpenAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto()
)
result = await kernel.invoke_prompt(
"Check CPU and memory for web-server-01. Flag anything concerning.",
settings=settings
)
print(result)
asyncio.run(main())
Behind the scenes, the kernel figures out it needs both get_cpu_usage and get_memory_usage, calls them both, then hands the results to the LLM for analysis. No manual chaining. No brittle sequence definitions.
Adding Memory with Vector Search
Semantic Kernel’s memory layer works with Qdrant, Pinecone, Azure AI Search, and others. For prototyping, the in-memory volatile store gets you started in minutes:
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAITextEmbedding
from semantic_kernel.memory import SemanticTextMemory, VolatileMemoryStore
async def setup_memory():
kernel = Kernel()
kernel.add_service(OpenAIChatCompletion(service_id="chat"))
kernel.add_service(OpenAITextEmbedding(service_id="embedding"))
memory = SemanticTextMemory(
storage=VolatileMemoryStore(),
embeddings_generator=kernel.get_service(service_id="embedding")
)
await memory.save_information(
collection="runbooks",
id="restart-nginx",
text="Restart nginx: 'sudo systemctl restart nginx'. "
"Check status: 'systemctl status nginx'."
)
await memory.save_information(
collection="runbooks",
id="disk-full",
text="Disk full: check with 'df -h', find large files with "
"'du -sh /* | sort -rh | head -20', clean /var/log."
)
results = await memory.search("runbooks", "server won't start after disk issue")
for r in results:
print(f"[{r.relevance:.2f}] {r.text[:80]}...")
asyncio.run(setup_memory())
Ready for production? Swap VolatileMemoryStore for Qdrant or Azure AI Search. The interface stays identical — that’s the abstraction doing its job.
Semantic Plugins: Prompts in Version Control
For language tasks, semantic plugins store prompts as plain files. Prompt changes show up in pull requests like any other diff — reviewable, commentable, and easy to roll back:
plugins/
SummaryPlugin/
SummarizeText/
skprompt.txt
config.json
Contents of skprompt.txt:
Summarize the following text in 2-3 sentences, focusing on actionable insights:
{{$input}}
Summary:
Contents of config.json:
{
"schema": 1,
"description": "Summarizes text into 2-3 actionable sentences",
"execution_settings": {
"default": {
"max_tokens": 256,
"temperature": 0.3
}
}
}
Load and invoke it:
plugin = kernel.add_plugin(
parent_directory="./plugins",
plugin_name="SummaryPlugin"
)
result = await kernel.invoke(
plugin["SummarizeText"],
input="Your long incident report or log text goes here..."
)
print(result)
Semantic Kernel vs LangChain: The Honest Comparison
This isn’t an argument that one framework always wins. Both have real strengths depending on your context:
- Choose Semantic Kernel when you’re on Azure, your team spans Python and .NET, you need a stable API surface across quarters, or you want explainable planning steps for enterprise audit requirements.
- Stick with LangChain when you need integrations with the latest experimental vector databases, you’re prototyping fast, or your team has a stable pinned version and deep expertise.
Azure OpenAI, Azure AI Search, enterprise security requirements — SK fits those scenarios more naturally than most alternatives. Unit testing is also a different story: you mock individual plugins instead of wrestling with the entire chain.
Where to Go From Here
What you’ve built above is a solid, testable foundation — not a demo, but an actual architecture pattern that scales. From here, three moves make sense. Connect a persistent vector store like Qdrant or Azure AI Search for real RAG workloads. Add authentication and rate limiting to your native plugins before anything hits production. And look into multi-agent patterns, where separate kernels hand off work to each other.
Once the plugin architecture clicks, building complex workflows becomes surprisingly manageable. Debugging is a different experience too — you’re tracing individual plugin calls, not untangling a chain of opaque objects where state mutated somewhere in the middle.

