Getting Started with Semantic Kernel — Part 1: Your First Kernel & Chat History

Welcome to the first part of our hands-on series, "Getting Started with Microsoft Semantic Kernel"! If you're looking to integrate powerful AI capabilities into your .NET applications, you've come to the right place. Semantic Kernel is a lightweight SDK that lets you combine your existing C# skills with the latest in large language models (LLMs).
In this post, we're going to tackle two fundamental concepts:
- Building Your First Kernel: We'll get a
Kernelinstance up and running and see how the level of detail in your prompts directly impacts the AI's output. - Maintaining Context with Chat History: We'll learn how to give our AI a memory, allowing it to remember past interactions and build on previous responses, even letting us track token usage along the way.
Ready? Let's dive in!
Setting Up Our Environment
Before we write any code, make sure you have the .NET 9 SDK installed. Semantic Kernel is evolving rapidly, and we'll be leveraging some of the latest features. You'll also need an OpenAI API key. For our examples, we'll store this in an environment variable named OpenAiTestKey.
# Example for Windows Command Prompt
setx OpenAiTestKey "YOUR_OPENAI_API_KEY"
# Example for PowerShell
$env:OpenAiTestKey="YOUR_OPENAI_API_KEY"
# Example for Linux/macOS Bash
export OpenAiTestKey="YOUR_OPENAI_API_KEY"
Your First Semantic Kernel
At the heart of every Semantic Kernel application is, well, the Kernel. It's the orchestrator that connects your code to the AI models. Creating one is straightforward, thanks to the Kernel.CreateBuilder() pattern.
Here's how we set up our Kernel to use OpenAI's gpt-4o model:
using Microsoft.SemanticKernel;
namespace BuildingKernels
{
internal class Program
{
static async Task Main(string[] args)
{
string modelId = "gpt-4o";
string OpenAiKey = Environment.GetEnvironmentVariable("OpenAiTestKey");
var kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(modelId, OpenAiKey)
.Build();
// ... rest of our code
}
}
}
In this snippet, we're building a Kernel and adding the AddOpenAIChatCompletion service. This extension method knows how to connect to OpenAI using the modelId (I'm using gpt-4o here) and the OpenAiKey we configured. Once Build() is called, we have a fully functional kernel instance.
Now, let's see how we can use it to get some C# code. Semantic Kernel makes it incredibly easy to send a prompt and get a response using InvokePromptAsync.
Let's start with a very basic request:
// Basic prompt
var prompt1 = "Write a hello world program in C#";
Console.WriteLine("Basic Prompt Result:");
Console.WriteLine(await kernel.InvokePromptAsync(prompt1));
When you run this, you'll get a simple, functional C# "Hello, World!" program. It does the job, but it's pretty barebones.
What if we want something a bit more specific? This is where prompt engineering comes in. The more detail you provide, the better the AI can tailor its response. Let's try adding some requirements:
// More detailed prompt
var prompt2 = "Write a C# console application that displays 'Hello, World!' in green text and waits for a key press before closing";
Console.WriteLine("Detailed Prompt Result:");
Console.WriteLine(await kernel.InvokePromptAsync(prompt2));
See how adding just a little more detail changes the generated code? The AI now includes Console.ForegroundColor = ConsoleColor.Green; and Console.ReadKey();. This is a powerful concept: your prompts are your instructions. The more precise you are, the more aligned the output will be with your expectations. You can keep adding more and more detail, like asking for exception handling, centering text, or specific .NET features, and the AI will do its best to follow.
Maintaining Context with Chat History
The InvokePromptAsync method is fantastic for one-shot requests, but what if you're building a conversational AI? What if you want the AI to remember what you just said or asked it to do? InvokePromptAsync is stateless; each call is independent.
This is where IChatCompletionService and ChatHistory come into play. We can retrieve the chat completion service from our kernel and then manage a ChatHistory object to keep track of the conversation.
First, let's get our IChatCompletionService:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
namespace ChatHistoryExample
{
internal class Program
{
static async Task Main(string[] args)
{
string modelId = "gpt-4o";
string OpenAiKey = Environment.GetEnvironmentVariable("OpenAiTestKey");
var kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(modelId, OpenAiKey)
.Build();
var ChatCompletionService = kernel.Services.GetRequiredService<IChatCompletionService>();
// Create a new chat history
var chatHistory = new ChatHistory();
// ... rest of our code
}
}
}
Now we have our ChatCompletionService and an empty ChatHistory. Let's simulate a two-turn conversation. We'll start by asking for the same "Hello, World!" C# program:
// Add user message to chat history
var message1 = "Write a hello world program in C#";
Console.WriteLine(message1);
chatHistory.AddUserMessage(message1);
var Answer1 = await ChatCompletionService.GetChatMessageContentAsync(chatHistory);
Console.WriteLine("Press any key to continue");
Console.WriteLine(Answer1);
WriteTokenUsage(Answer1);
Console.ReadKey();
//Add the first answer to the history
chatHistory.AddAssistantMessage(Answer1.ToString());
Notice a couple of things here: we use chatHistory.AddUserMessage(message1) to add our prompt, and then critically, after we get the Answer1 from GetChatMessageContentAsync, we add the AI's response back to the history using chatHistory.AddAssistantMessage(Answer1.ToString()). This is how the AI "remembers" its own previous answer.
Now for the magic. We'll ask it to rewrite the previous answer in VB.NET. Because we've maintained the chatHistory, the model knows what "previous answer" refers to:
// Add another user message to chat history
var message2 = "Rewrite previous answer in VB.NET";
Console.WriteLine(message2);
chatHistory.AddUserMessage(message2);
var Answer2 = await ChatCompletionService.GetChatMessageContentAsync(chatHistory);
Console.WriteLine("Press any key to continue");
Console.WriteLine(Answer2);
WriteTokenUsage(Answer2);
Console.ReadKey();
If you run this, you'll see the C# "Hello, World!" program first, and then the AI will generate the equivalent VB.NET code. This demonstrates the power of ChatHistory – it allows for rich, multi-turn conversations where the AI builds on previous interactions.
Tracking Token Usage
Another neat feature available in the response is metadata, including token usage. This is incredibly useful for monitoring costs and understanding the complexity of your prompts. The ChatMessageContent object's Metadata property contains this information.
Here's a helper method to extract and display the token counts:
private static void WriteTokenUsage(Microsoft.SemanticKernel.ChatMessageContent response)
{
if (response?.Metadata == null) return;
if (response.Metadata.TryGetValue("Usage", out var usage) && usage != null)
{
var usageDict = usage as OpenAI.Chat.ChatTokenUsage;
if (usageDict != null)
{
Console.WriteLine(quot;Input Tokens: {usageDict.InputTokenCount}");
Console.WriteLine(quot;Output Tokens: {usageDict.OutputTokenCount}");
Console.WriteLine(quot;Total Tokens: {usageDict.TotalTokenCount}");
}
}
}
As you can see, we're checking the Metadata for a key named "Usage", which contains a ChatTokenUsage object with details on input, output, and total tokens used for that particular interaction.
One-Shot Prompts vs. Stateful Conversations
To recap, here's the key difference we've explored:
InvokePromptAsync: Ideal for single, independent requests where the AI doesn't need to remember past interactions. It's a quick way to get a response to a specific prompt.IChatCompletionServicewithChatHistory: Essential for building conversational experiences where context needs to be maintained across multiple turns. By explicitly adding both user and assistant messages to theChatHistory, you guide the AI through a coherent conversation.
What's Next?
This is just the beginning of what you can do with Microsoft Semantic Kernel! We've built our first kernel, understood the importance of detailed prompts, and learned how to manage conversational context.
In the next parts of this series, we'll dive into more advanced topics like creating custom functions, building plugins, and even integrating with local LLMs.
If you want to explore the full source code for these examples, you can find the repository on GitHub: egarim/IntroductionToSemanticKernel2025.
Stay tuned for Part 2!