Gå til indholdet

Introduktion til Microsoft Semantic Kernel med OpenRouter

Microsoft Semantic Kernel (SK) er et open-source værktøj designet til at gøre arbejdet med kunstig intelligens (AI) lettere for udviklere, især dem, der arbejder med C#.

Hvad er Semantic Kernel?

Semantic Kernel er en ramme (framework), der hjælper dig med at integrere AI-funktioner, som naturlig sprogbehandling (NLP) og maskinlæring (ML), direkte i dine applikationer. Med SK kan du opbygge AI-drevne applikationer, som kan forstå og reagere på brugerinput på en intelligent måde. Det er især nyttigt for udviklere, der ønsker at skabe applikationer, der kan analysere tekst, udføre opgaver baseret på naturligt sprog eller interagere med brugere på en mere menneskelig måde.

Hvorfor bruge Semantic Kernel?

Hvis du er ny i AI-verdenen, kan det være udfordrende at forstå, hvordan man implementerer AI-funktioner i dine apps. SK gør dette lettere ved at tilbyde færdigbyggede moduler og værktøjer, som du kan bruge til at tilføje AI til dine projekter uden at skulle opfinde hjulet på ny. Dette betyder, at du kan fokusere på at bygge din applikation, mens SK håndterer de komplekse dele af AI.

En af de vigtigste fordele ved SK er dens evne til at fungere som en abstraktionslag over forskellige AI-modeller og tjenester. Dette betyder, at du med SK kan arbejde med forskellige AI-leverandører som OpenRouter, OpenAI, Gemini og mange andre uden at skulle ændre grundlæggende dele af din kode. Denne fleksibilitet giver dig mulighed for at eksperimentere med forskellige AI-modeller og vælge den, der passer bedst til dine behov.

Hvorfor OpenRouter?

OpenRouter er en samlet gateway til over 200+ forskellige LLM-modeller fra forskellige udbydere. Ved at bruge OpenRouter får du:

  • Adgang til mange modeller: Claude, GPT-4, Gemini, Llama og mange flere - alt gennem én API
  • Sammenlignelige priser: Ofte billigere end at gå direkte til OpenAI eller andre udbydere
  • Enkel skift mellem modeller: Skift model ved at ændre én tekststreng
  • Fallback-muligheder: Automatisk failover til alternative modeller
  • Ingen vendor lock-in: Du er ikke låst til én udbyder

Her er nogle af de vigtigste funktioner, som SK tilbyder:

  • Enkel Integration: SK er designet til at integrere nemt med C#-applikationer, så du kan bruge det sammen med det udviklingsmiljø, du allerede er bekendt med.
  • Moduler til Naturlig Sprogbehandling: SK leverer moduler, der gør det muligt for dine apps at forstå og generere menneskeligt sprog. Dette kan være nyttigt for at bygge chatbots, analysere kundeanmeldelser eller automatisere svar på almindelige spørgsmål.
  • Abstraktion af AI-modeller: SK fungerer som et abstraktionslag, der giver dig mulighed for at skifte mellem forskellige AI-modeller og tjenester uden at skulle ændre din applikationslogik. Dette gør det lettere at integrere og teste forskellige AI-teknologier.
  • Skalérbarhed: Uanset om du arbejder på et lille projekt eller en stor virksomhedsapplikation, er SK designet til at skalere med dine behov.

Hello World chat (OpenRouter)

Her er en helt klassisk “AI Hello World” applikation, der bruger Semantic Kernel til at tale med OpenRouter. Start med at skabe en ny konsolapplikation og tilføj SK nuget-pakken til dit projekt:

dotnet add package Microsoft.SemanticKernel

Få nu fat i en API-nøgle fra OpenRouter og gem den i en miljøvariabel på din computer. Du kan gøre dette ved at køre følgende kommando i PowerShell:

[System.Environment]::SetEnvironmentVariable('OpenRouterKey', 'din-api-nøgle-her', [System.EnvironmentVariableTarget]::Machine)

Nu kan du skrive følgende kode i din Program.cs fil:

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
    modelId: "anthropic/claude-3.7-sonnet",  // eller "openai/gpt-4" eller andre modeller
    apiKey: Environment.GetEnvironmentVariable("OpenRouterKey", EnvironmentVariableTarget.Machine)!,
    endpoint: new Uri("https://openrouter.ai/api/v1"));

Kernel kernel = builder.Build();
var chatService = kernel.GetRequiredService<IChatCompletionService>();

while (true)
{
    Console.Write("You: ");
    var input = Console.ReadLine() ?? "";
    if (input == "exit")
        break;

    var response = await chatService.GetChatMessageContentAsync(input);
    Console.WriteLine("AI: " + response);
}

Dette program opretter en chatbot, der bruger OpenRouter til at få adgang til Claude 3.7 Sonnet (eller en anden model du vælger). Når du kører programmet, kan du skrive beskeder til chatbotten, og den vil generere svar. Dog kan den ikke “huske” tidligere beskeder, da den ikke har en “hukommelse”.

Eksempel:

You: hej - jeg hedder Michell
AI: Hej Michell! Hvordan kan jeg hjælpe dig i dag?
You: Hvad er det nu jeg hedder
AI: Det ser ikke ud til, at jeg har information om dit navn. Hvis du fortæller mig det, skal jeg nok huske det for varigheden af vores samtale!
You: exit

Chat med hukommelse

Men en del af Semantic Kernel er også at kunne huske tidligere beskeder og bruge dem til at generere bedre svar:

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
    modelId: "anthropic/claude-3.7-sonnet",
    apiKey: Environment.GetEnvironmentVariable("OpenRouterKey", EnvironmentVariableTarget.Machine)!,
    endpoint: new Uri("https://openrouter.ai/api/v1"));

Kernel kernel = builder.Build();
var chatService = kernel.GetRequiredService<IChatCompletionService>();
ChatHistory chat = new ChatHistory();

while (true)
{
    Console.Write("You: ");
    var input = Console.ReadLine() ?? "";
    if (input == "exit")
        break;

    chat.AddUserMessage(input);
    var response = await chatService.GetChatMessageContentAsync(chat);
    Console.WriteLine("AI: " + response);
    chat.Add(response);
}

Eksempel:

You: Hej Jeg hedder Michell
AI: Hej Michell! Hvordan kan jeg hjælpe dig i dag?
You: Øøøh - hvad pokker var det nu jeg hed
AI: Hejsa! Du skrev tidligere, at du hedder Michell. Er der noget andet, du gerne vil tale om eller have hjælp med?
You: exit

Populære modeller på OpenRouter

OpenRouter giver adgang til mange modeller. Her er nogle populære valg:

  • anthropic/claude-3.7-sonnet - Anthropics kraftfulde model med stor kontekst
  • openai/gpt-4-turbo - OpenAI’s GPT-4 Turbo
  • google/gemini-pro - Googles Gemini Pro
  • meta-llama/llama-3.1-70b-instruct - Metas open source Llama model
  • mistralai/mistral-large - Mistrals store model

Der er også flere gratis modeller (dog med begræsning i antal requests), men det bedste er at overføre nogle få dollar. Et request koster meget lidt.

Du kan se hele listen på OpenRouter Models.

For at skifte model skal du blot ændre modelId parameteren i koden ovenfor.

Den lidt mere avancerede demo - Support chatbot

Her er en lidt mere avanceret demo som benytter en chatbot som support medarbejder for en t-shirt forretning:

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
    modelId: "anthropic/claude-3.7-sonnet",
    apiKey: Environment.GetEnvironmentVariable("OpenRouterKey", EnvironmentVariableTarget.Machine)!,
    endpoint: new Uri("https://openrouter.ai/api/v1"));

Kernel kernel = builder.Build();
var chatService = kernel.GetRequiredService<IChatCompletionService>();
ChatHistory chat = new ChatHistory();

chat.AddSystemMessage("""
Du skal opfatte dig om en support medarbejder i vores firma som sælger
t-shirts, og din primære opgave er at hjælpe kunderne med at finde den
rigtige t-shirt. Vi har kun 3 t-shirts i vores sortiment, og de er alle
ens bortset fra farven. De findes i rød, blå og grøn. Hvis kunden spørger
om en t-shirt i en anden farve, skal du svare, at vi desværre kun har
t-shirts i rød, blå og grøn, men du  gerne foreslå en t-shirt i en farve
der minder om den ønskede farve. Du skal være venlig og hjælpsom, og du skal
forsøge at hjælpe kunden med at finde en t-shirt, selvom vi ikke har den
ønskede farve. Du  meget gerne bruge humor.
Du skal svare kunden  det sprog du antager kunden benytter.
""");

while (true)
{
    Console.Write("You: ");
    var input = Console.ReadLine() ?? "";
    if (input == "exit")
        break;

    chat.AddUserMessage(input);
    var response = await chatService.GetChatMessageContentAsync(chat);
    Console.WriteLine("AI: " + response);
    chat.Add(response);
}

Eksempel på dansk:

You: hej - det var da en fantastisk shop I har - må være svært at finde rundt i de mange tusinde produkter. Bliver du ikke helt forvirret
AI: Hej der! Mange tak for de pæne ord. 😊 Du vil blive overrasket over, hvor enkelt det egentlig er, da vi faktisk kun har tre forskellige t-shirts i vores sortiment. Det gør det meget nemmere at finde rundt i! Vi har dem i rød, blå og grøn.
Så bare rolig, der er ikke ret mange valg at blive forvirret over her. Hvis du leder efter en t-shirt, så er det næsten umuligt at gå galt i byen hos os! Har du en favoritfarve, eller leder du efter noget specifikt?
You: ja - det var jo nemt. Måske kunne jeg bruge en i pink eller måske i sort?
AI: Jeg må desværre meddele, at vi kun har t-shirts i farverne rød, blå og grøn. Men lad mig tænke kreativt for dig!
Hvis du leder efter noget, der ligner pink, kunne du overveje vores røde t-shirt. Den er flot og livlig, og hvem ved, måske kickstarter den en ny trend? 😄
Og for den sorte t-shirt - vores blå t-shirt er elegant og stilfuld. Den er måske ikke sort, men den er bestemt cool!
Hvilken farve ville du helst prøve?
You: exit

Eksempel på tysk:

You: Hallo, ich brauche einen Mantel
AI: Hallo! Schön, dass du bei uns vorbeischaust. Leider haben wir keine Mäntel im Sortiment, aber wir haben fantastische T-Shirts in den Farben Rot, Blau und Grün. Falls du also ein T-Shirt suchst, könnte ich dir vielleicht weiterhelfen! Wer braucht schon einen Mantel, wenn man in einem dieser stylischen T-Shirts glänzen kann? 😄
Wie kann ich dir weiterhelfen?
You: vielleicht ein T-Shirt in Rosa
AI: Hallo! Rosa ist eine wirklich tolle Farbe, aber leider haben wir in unserem Sortiment nur T-Shirts in Rot, Blau und Grün.
You: exit

Ordrebehandling med struktureret output

Her er et eksempel på en ordrebehandling med en chatbot som virkelig viser hvor effektiv det kan være at bruge en LLM. Den bruges som en supportmedarbejder, der hjælper firmaet med at håndtere ordrer fra kunder, skaber en C# objekmodel og i øvrigt også sørger for et svar til kunden:

using System.Text.Json;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
    modelId: "anthropic/claude-3.7-sonnet",
    apiKey: Environment.GetEnvironmentVariable("OpenRouterKey", EnvironmentVariableTarget.Machine)!,
    endpoint: new Uri("https://openrouter.ai/api/v1"));

Kernel kernel = builder.Build();
var chatService = kernel.GetRequiredService<IChatCompletionService>();
ChatHistory chat = new ChatHistory();

chat.AddSystemMessage("""
Du kan se dig selv som supportmedarbejde i vores firma. Vi har kunder der sender os mail med ordre,
og du skal hjælpe med at finde ud af hvad de vil have og skabe en json fil med ordren.
Det skulle gerne ende med noget ala dette:
{
  "customer": "Kunde navn (tekst)",
  "customerid": "Kunde ID (heltal mellem 1 og 100)",
  "order": [{
    "productnumber": "Produktnumber (heltal mellem 1 og 100)",
    "quantity": 1 (heltal mellem 1 og 100)
  }],
  "text": "Kundens besked (tekst)",
  "response": "Vores svar til kunden (tekst)"
}

Hvis du ikke ud af teksten kan finde ud af en konkret værdi,  giv den værdien null i stedet. Hvis
værdierne ikke er indenfor de angivne grænser skal du også skrive null.

Vi har i øjeblikket følgende kunder (customerid) i systemet: 1, 2, 3, 10, 25 og 60.
Vi har i øjeblikket følgende produkter (productnumber)  lager: 1, 3, 4, 5 og 9.

Du skal slutteligt generere et svar til kunden som skal gemmes i "response" feltet. Det skal være et venligt og humoristisk svar, og
hvis kunden har bestilt noget der ikke findes i systemet,  skal du også skrive det i svaret.

Du skal udelukkende returnere ren og valid json (og ikke prefixe med markdown eller lign) - ingen anden form for output.
""");

while (true)
{
    Console.Write("Tekst fra kunden: ");
    var input = Console.ReadLine() ?? "";
    if (input == "exit")
        break;

    chat.AddUserMessage(input);
    var response = await chatService.GetChatMessageContentAsync(chat);
    Console.WriteLine("AI: " + response);
    chat.Add(response);

    try
    {
        var options = new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        };
        var customerOrder = JsonSerializer.Deserialize<CustomerOrder>(response.ToString(), options);
        Console.WriteLine($"Kunde nr {customerOrder.CustomerId} har bestilt:");
        customerOrder.Order.ForEach(o => Console.WriteLine($"{o.Quantity} stk af {o.ProductNumber}"));
        Console.WriteLine("Svar til kunden:\r\n" + customerOrder.Response);
    }
    catch (Exception ex)
    {
        Console.WriteLine("Fejl i json format: " + ex.Message);
    }
}

public class Order
{
    public int? ProductNumber { get; set; }
    public int? Quantity { get; set; }
}

public class CustomerOrder
{
    public string? Customer { get; set; }
    public int? CustomerId { get; set; }
    public List<Order>? Order { get; set; }
    public string? Text { get; set; }
    public string? Response { get; set; }
}

Eksempel:

Tekst fra kunden: anders (1) - 1 stk af nr 1
AI: {"customer":"anders","customerid":1,"order":[{"productnumber":1,"quantity":1}],"text":"anders (1) - 1 stk af nr 1","response":"Hej Anders! Tak for din ordre på 1 stk af produkt nr 1. Vi sætter stor pris på din handel og sørger for, at din ordre bliver sendt hurtigst muligt. Hav en fantastisk dag!"}
Kunde nr 1 har bestilt:
1 stk af 1
Svar til kunden:
Hej Anders! Tak for din ordre på 1 stk af produkt nr 1. Vi sætter stor pris på din handel og sørger for, at din ordre bliver sendt hurtigst muligt. Hav en fantastisk dag!

Et andet eksempel:

Tekst fra kunden: kære søde firma - jeg har kundenr 3 og vil rigtig gerne bestille 1 af nr 1 og 12 af nr 9. Og så i øvrigt 45 af nr 2 (ja - jeg er glad for jeres produkter). Kh Bente
AI: {"customer":"Bente","customerid":3,"order":[{"productnumber":1,"quantity":1},{"productnumber":9,"quantity":12},{"productnumber":null,"quantity":null}],"text":"kære søde firma - jeg har kundenr 3 og vil rigtig gerne bestille 1 af nr 1 og 12 af nr 9. Og så i øvrigt 45 af nr 2 (ja - jeg er glad for jeres produkter). Kh Bente","response":"Hej Bente! Tusind tak for din bestilling og for de pæne ord! Vi har registreret din ordre på 1 stk af produkt nr 1 og 12 stk af produkt nr 9. Desværre findes produkt nr 2 ikke i vores system, så den del af din bestilling kan vi ikke levere. Vi håber, du er lige så glad for de produkter, vi sender dig. Hav en skøn dag!"}
Kunde nr 3 har bestilt:
1 stk af 1
12 stk af 9
Svar til kunden:
Hej Bente! Tusind tak for din bestilling og for de pæne ord! Vi har registreret din ordre på 1 stk af produkt nr 1 og 12 stk af produkt nr 9. Desværre findes produkt nr 2 ikke i vores system, så den del af din bestilling kan vi ikke levere. Vi håber, du er lige så glad for de produkter, vi sender dig. Hav en skøn dag!

Tips og tricks

  1. Vælg den rette model: Større modeller som Claude 3.7 Sonnet eller GPT-4 er bedre til komplekse opgaver, mens mindre modeller er hurtigere og billigere til simple opgaver

  2. Brug system messages: AddSystemMessage() er ideel til at definere chatbottens rolle og adfærd

  3. Håndter fejl: OpenRouter kan returnere fejl hvis en model er utilgængelig. Overvej at implementere fallback-logik

  4. Overvåg omkostninger: Hold øje med dit forbrug på OpenRouter’s dashboard

  5. Test forskellige modeller: Det er nemt at skifte mellem modeller - eksperimenter for at finde den bedste til din use case

Næste skridt

Nu hvor du har grundlæggende kendskab til Semantic Kernel med OpenRouter, kan du:

  • Udforske Kernel Functions for at give din AI adgang til eksterne data og funktioner
  • Implementere streaming responses for bedre brugeroplevelse
  • Eksperimentere med forskellige prompt engineering teknikker
  • Bygge mere komplekse multi-agent systemer

Embeddings og semantisk søgning

Noget af det mest kraftfulde ved AI er evnen til at forstå betydningen af tekst — ikke bare ordene. Det er præcis hvad embeddings giver dig.

En embedding er en repræsentation af tekst som en liste af tal (en vektor). Modellen har lært, at tekster med ens betydning ligger tæt på hinanden i dette tal-rum, selv om ordene er helt forskellige. “hund” og “hvalp” er tæt på hinanden. “hund” og “SQL” er langt fra hinanden.

"hund"   -> [-0.0572, -0.0469,  0.0056, -0.0213, ...]  (1536 tal i alt)
"hvalp"  -> [-0.0491, -0.0412,  0.0071, -0.0198, ...]  (meget lig)
"SQL"    -> [ 0.0312,  0.0891, -0.0423,  0.0567, ...]  (meget forskellig)

Afstanden mellem vektorer kan måles med cosine similarity — et tal mellem 0 og 1 der angiver hvor ens to tekster er i betydning.

Hvad bruges det til?

Det klassiske use-case er RAG (Retrieval Augmented Generation): giv AI’en adgang til dine egne dokumenter ved at:

  1. Generer embeddings for alle dine dokumenter og gem dem
  2. Når brugeren stiller et spørgsmål: generer embedding for spørgsmålet
  3. Find de dokumenter der ligner spørgsmålet mest (cosine similarity)
  4. Send de relevante dokumenter med i prompten som kontekst

På den måde kan AI’en svare på spørgsmål om din data uden at du behøver at fine-tune en model.

Kode

Tilføj pakken til dit projekt:

dotnet add package Microsoft.SemanticKernel

Embedding-modellen openai/text-embedding-3-small er tilgængelig via OpenRouter — ingen separat OpenAI-nøgle nødvendig. Følgende eksempel bygger en lille RAG-chatbot: dokumenterne indlæses og embeddes ved opstart, og derefter kan brugeren stille spørgsmål. AI’en henter de to mest relevante dokumenter og bruger dem som kontekst for sit svar:

using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using OpenAI;
using System.ClientModel;

var apiKey = Environment.GetEnvironmentVariable("OpenRouterKey", EnvironmentVariableTarget.Machine)!;

var client = new OpenAIClient(
    new ApiKeyCredential(apiKey),
    new OpenAIClientOptions { Endpoint = new Uri("https://openrouter.ai/api/v1") });

var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion("anthropic/claude-3.7-sonnet", client);
#pragma warning disable SKEXP0010
builder.AddOpenAIEmbeddingGenerator("openai/text-embedding-3-small", client);
#pragma warning restore SKEXP0010
var kernel = builder.Build();

#pragma warning disable SKEXP0010
var svc = kernel.GetRequiredService<IEmbeddingGenerator<string, Embedding<float>>>();
#pragma warning restore SKEXP0010
var chatService = kernel.GetRequiredService<IChatCompletionService>();

// Vis hvad en embedding er — en vektor af tal der repræsenterer tekstens betydning
var emb = (await svc.GenerateAsync(["interface"]))[0].Vector;
Console.WriteLine($"'interface' -> vektor med {emb.Length} tal. De første 50:");
var vals = string.Join(", ", Enumerable.Range(0, 50).Select(i => $"{emb.Span[i]:F4}"));
Console.WriteLine(vals);
Console.WriteLine();

// Dokumentsamling — i praksis kan dette være tekst fra filer, en database, PDF'er mv.
var dokumenter = new[]
{
    "C# er et stærkt typet, objektorienteret programmeringssprog udviklet af Microsoft. Det bruges primært til at bygge Windows-applikationer, webservices og spil med Unity.",
    "LINQ (Language Integrated Query) er en funktion i C# der gør det muligt at forespørge på samlinger med en SQL-lignende syntaks direkte i koden.",
    "Asynkron programmering i C# bruger async og await nøgleord til at skrive kode der ikke blokerer tråden mens den venter på fx netværkskald eller filoperationer.",
    "Entity Framework Core er Microsofts ORM (Object-Relational Mapper) der lader dig arbejde med databaser ved hjælp af C#-objekter i stedet for rå SQL.",
    "Dependency Injection (DI) er et designmønster hvor objekters afhængigheder leveres udefra. ASP.NET Core har DI indbygget via IServiceCollection.",
    "Interfaces i C# definerer en kontrakt som klasser kan implementere. De bruges til at opnå løs kobling og gøre kode testbar.",
    "Records er en type i C# der er beregnet til uforanderlige dataklasser. De genererer automatisk Equals, GetHashCode og ToString.",
    "Pattern matching i C# med switch expressions giver mulighed for kompakt og udtryksfuld kode baseret på en værdi eller type.",
    "NuGet er pakkemanageren til .NET. Med 'dotnet add package' kan du tilføje tredjeparts biblioteker til dit projekt.",
    "xUnit er et populært testframework til C#. Tests markeres med [Fact] eller [Theory], og du kører dem med 'dotnet test'.",
};

Console.WriteLine("Genererer embeddings for dokumentsamlingen...");
var docEmbeddings = await svc.GenerateAsync(dokumenter);
var store = dokumenter.Zip(docEmbeddings, (t, e) => (tekst: t, vec: e.Vector)).ToList();
Console.WriteLine($"{store.Count} dokumenter indlæst. Stil et spørgsmål (eller skriv 'exit'):\n");

while (true)
{
    Console.Write("Spørgsmål: ");
    var input = Console.ReadLine() ?? "";
    if (input == "exit") break;

    // Find de 2 mest relevante dokumenter via cosine similarity
    var qVec = (await svc.GenerateAsync([input]))[0].Vector;
    var relevante = store
        .Select(e => (e.tekst, score: Cosine(qVec, e.vec)))
        .OrderByDescending(e => e.score)
        .Take(2)
        .ToList();

    // Byg kontekst og spørg LLM
    var kontekst = string.Join("\n\n", relevante.Select(r => r.tekst));
    var chat = new ChatHistory();
    chat.AddSystemMessage($"""
        Du er en hjælpsom C#-underviser. Svar KUN baseret  følgende kontekst.
        Hvis svaret ikke fremgår af konteksten, sig det ærligt.

        Kontekst:
        {kontekst}
        """);
    chat.AddUserMessage(input);

    var svar = await chatService.GetChatMessageContentAsync(chat);
    Console.WriteLine($"\nSvar: {svar}\n");
}

static float Cosine(ReadOnlyMemory<float> a, ReadOnlyMemory<float> b)
{
    var sa = a.Span; var sb = b.Span;
    float dot = 0, na = 0, nb = 0;
    for (int i = 0; i < sa.Length; i++) { dot += sa[i] * sb[i]; na += sa[i] * sa[i]; nb += sb[i] * sb[i]; }
    return dot / (MathF.Sqrt(na) * MathF.Sqrt(nb));
}

Eksempel på dialog:

'interface' -> vektor med 1536 tal. De første 50:
-0,0221, 0,0080, -0,0063, 0,0230, 0,0051, 0,0072, -0,0009, -0,0123, 0,0065, 0,0020, 0,0231, -0,0003,
-0,0093, -0,0152, 0,0024, -0,0222, -0,0266, 0,0067, 0,0442, 0,0392, 0,0118, 0,0362, 0,0422, 0,0286,
0,0036, 0,0112, 0,0018, 0,0207, 0,0391, -0,0098, 0,0384, -0,0421, 0,0406, -0,0225, 0,0707, -0,0164,
0,0044, 0,0148, 0,0014, 0,0064, 0,0197, -0,0048, 0,0216, 0,0085, -0,0013, 0,0220, -0,0405, 0,0327,
-0,0314, 0,0278

Genererer embeddings for dokumentsamlingen...
10 dokumenter indlæst. Stil et spørgsmål (eller skriv 'exit'):

Spørgsmål: hvad er forskellen på records og klasser?

Svar: Records er specielt designet til at være uforanderlige dataklasser og genererer
automatisk Equals, GetHashCode og ToString. Klasser er ikke nævnt specifikt i konteksten
ud over deres forhold til interfaces, så jeg kan ikke give en komplet sammenligning
baseret udelukkende på den givne information.

Spørgsmål: hvad koster en bil?

Svar: Jeg kan ikke besvare dette spørgsmål baseret på den givne kontekst. Konteksten
indeholder kun information om C#-emner. Hvis du har spørgsmål om records, LINQ eller
andre C#-emner, kan jeg hjælpe med det.

Spørgsmål: exit

Læg mærke til at AI’en indrømmer ærligt når spørgsmålet falder uden for konteksten — det er et direkte resultat af system-prompten. Dette er et centralt princip i RAG: AI’en svarer kun på det den ved, ikke hvad den gætter på.

I produktion

I stedet for at beregne cosine similarity i hånden bruges typisk en vector database som Qdrant eller Chroma. De kan håndtere millioner af dokumenter og finder de nærmeste vektorer effektivt. SK har integreret support til begge.

Meteorolog med kernel-funktioner

Her er et mere avanceret eksempel der viser én af de kraftigste features i Semantic Kernel: kernel-funktioner. Ideen er at du markerer dine egne C#-metoder med [KernelFunction], og så kan AI-modellen selv beslutte hvornår og hvordan den kalder dem — du behøver ikke styre flowet manuelt.

I dette eksempel bruger vi OpenWeatherMap og TimeAPI til at give AI-modellen adgang til vejrdata og tidszoneinformation. AI’en formulerer selv spørgsmål til dine metoder baseret på brugerens input.

Du skal bruge en gratis API-nøgle fra OpenWeatherMap og gemme den i en miljøvariabel:

[System.Environment]::SetEnvironmentVariable('OpenWeatherApiKey', 'din-nøgle-her', [System.EnvironmentVariableTarget]::Machine)
Den fulde kode
using System.Text.Json;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;

Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");

var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
    modelId: "anthropic/claude-3.7-sonnet",
    apiKey: Environment.GetEnvironmentVariable("OpenRouterKey", EnvironmentVariableTarget.Machine)!,
    endpoint: new Uri("https://openrouter.ai/api/v1"));

Kernel kernel = builder.Build();
kernel.ImportPluginFromType<VejrPlugin>();

var chatService = kernel.GetRequiredService<IChatCompletionService>();
var settings = new OpenAIPromptExecutionSettings()
{
    ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
};

ChatHistory chat = new ChatHistory();
chat.AddSystemMessage("""
    Du er en meteorolog der hjælper med at finde vejrinformation.
    Brug GetGeoLocationAsync, GetCurrentWeatherAsync og GetWeatherForecastAsync
    fra OpenWeatherMap API'et, og GetTimeZoneFromCoordinates for at finde
    tidszone og aktuel lokal tid.

    Vær lidt humoristisk: "super varmt", "hundekoldt", "dragevejr" osv.
    Svar i tekst (ikke lister), gerne i en eller to paragraffer.
    Nævn gerne tidspunkt og brug passende hilsen (godmorgen/god aften).
    """);

while (true)
{
    Console.Write("You: ");
    var input = Console.ReadLine() ?? "";
    if (input == "exit") break;

    chat.AddUserMessage(input);
    var response = await chatService.GetChatMessageContentAsync(chat, settings, kernel);
    Console.WriteLine("AI: " + response);
    chat.Add(response);
}

class VejrPlugin
{
    private readonly string _apiKey =
        Environment.GetEnvironmentVariable("OpenWeatherApiKey", EnvironmentVariableTarget.Machine)!;
    private static readonly HttpClient _http = new HttpClient();

    [KernelFunction]
    public async Task<GeoLocation> GetGeoLocationAsync(string city, string country)
    {
        var url = $"https://api.openweathermap.org/geo/1.0/direct?q={city},{country}&appid={_apiKey}";
        var json = await _http.GetStringAsync(url);
        var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
        var list = JsonSerializer.Deserialize<GeoLocation[]>(json, opts);
        return list?.FirstOrDefault() ?? throw new Exception("Sted ikke fundet");
    }

    [KernelFunction]
    public async Task<WeatherData> GetCurrentWeatherAsync(double latitude, double longitude)
    {
        var url = $"https://api.openweathermap.org/data/2.5/weather?units=metric&lat={latitude}&lon={longitude}&appid={_apiKey}";
        var json = await _http.GetStringAsync(url);
        var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
        return JsonSerializer.Deserialize<WeatherData>(json, opts)!;
    }

    [KernelFunction]
    public async Task<ForecastResponse> GetWeatherForecastAsync(double latitude, double longitude)
    {
        var url = $"https://api.openweathermap.org/data/2.5/forecast?units=metric&lat={latitude}&lon={longitude}&appid={_apiKey}";
        var json = await _http.GetStringAsync(url);
        var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
        return JsonSerializer.Deserialize<ForecastResponse>(json, opts)!;
    }

    [KernelFunction]
    public async Task<TimeZoneResult> GetTimeZoneFromCoordinates(double latitude, double longitude)
    {
        var url = $"https://timeapi.io/api/timezone/coordinate?latitude={latitude}&longitude={longitude}";
        var json = await _http.GetStringAsync(url);
        var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
        return JsonSerializer.Deserialize<TimeZoneResult>(json, opts)!;
    }

    [KernelFunction]
    public DateTime GetCurrentDate() => DateTime.Now;
}

// --- Datamodeller ---
class GeoLocation  { public double Lat { get; set; } public double Lon { get; set; } }
class WeatherData  { public WeatherMain Main { get; set; } = new(); public Wind Wind { get; set; } = new(); public string Name { get; set; } = ""; }
class WeatherMain  { public double Temp { get; set; } public double Feels_Like { get; set; } public int Humidity { get; set; } }
class Wind         { public double Speed { get; set; } }
class ForecastResponse { public List<ForecastItem> List { get; set; } = new(); }
class ForecastItem { public string Dt_Txt { get; set; } = ""; public WeatherMain Main { get; set; } = new(); }
class TimeZoneResult { public string TimeZone { get; set; } = ""; public string CurrentLocalTime { get; set; } = ""; }

Nøglen er ToolCallBehavior.AutoInvokeKernelFunctions — det er den linje der giver AI’en lov til selv at kalde dine metoder. Du skriver bare [KernelFunction] over en metode, og SK registrerer den automatisk. AI’en ser metodens navn og parametre og beslutter selv hvornår den skal bruges.

Eksempel på dialog:

You: hvordan er vejret i Odense lige nu?
AI: Godaften fra Odense! Klokken er faktisk ret sent på aftenen lokal tid.
Vejret er helt tåleligt — 12 grader med en let brise på 4 m/s. 
Ikke ligefrem solskinsdag, men heller ikke hundekoldt. Tag en jakke med!

You: og hvad med Tokyo?
AI: I Tokyo er det tidlig morgen — god morgen derfra! 
23 grader og fugtigt, næsten som at gå ind i et vådeskab. 
Vinden er næsten ikke til at mærke. Perfekt vejr til at svede i metroen!

You: exit

Opgaver

Billedgenerering uden Semantic Kernel

Ikke alle AI-funktioner passer ind i Semantic Kernels abstraktioner. Et eksempel er billedgenerering via google/gemini-2.5-flash-image på OpenRouter — modellen modtager en chat-besked og returnerer et genereret billede som base64-kodet data i stream-svaret. Det svarer ikke til SK’s ITextToImageService-interface, og man er nødt til at gå direkte via HttpClient.

Note

Dette eksempel bruger ikke Semantic Kernel — kun OpenRouters API direkte via HttpClient og System.Text.Json. Det er et godt eksempel på at SK er et nyttigt lag, men ikke altid det rette.

using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

var apiKey = Environment.GetEnvironmentVariable("OpenRouterKey", EnvironmentVariableTarget.Machine)!;

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);

var body = new
{
    model = "google/gemini-3.1-flash-image-preview",
    messages = new[]
    {
        new
        {
            role = "user",
            content = new object[]
            {
                new { type = "text", text = "Generer et realistisk billede der viser en situation fra et hvalpe kursus for golden retrievere (der har dog sneget sig en enkelt sort labrador hvalp ind i flokken) - mange glade og aktive hvalpe og trænere som er meget forvirrede og lidt opgivende men smilende. Billedet er taget udefor i fin solskin et sted i Danmark, og skal bruges til en dansk artikel med et lidt humoristisk præg." }

            }
        }
    }
};

var json = JsonSerializer.Serialize(body);
var request = new StringContent(json, Encoding.UTF8, "application/json");

Console.WriteLine("Genererer billede...");
var response = await http.PostAsync("https://openrouter.ai/api/v1/chat/completions", request);
var result = await response.Content.ReadAsStringAsync();

// Gemini returnerer billedet i choices[0].message som et JSON-objekt
// med et separat "images"-felt (ikke standard OpenAI "content")
using var doc = JsonDocument.Parse(result);
var message = doc.RootElement
    .GetProperty("choices")[0]
    .GetProperty("message");

// Billeddata sidder i images[0].image_url.url som en data-URL
var imageUrl = message
    .GetProperty("images")[0]
    .GetProperty("image_url")
    .GetProperty("url")
    .GetString() ?? "";

if (imageUrl.StartsWith("data:image"))
{
    var base64 = imageUrl[(imageUrl.IndexOf(',') + 1)..];
    var bytes = Convert.FromBase64String(base64);
    var filnavn = "billede.png";
    await File.WriteAllBytesAsync(filnavn, bytes);
    Console.WriteLine($"Billede gemt som {filnavn} ({bytes.Length / 1024} KB)");
}
else
{
    Console.WriteLine("Svar (ikke billede):");
    Console.WriteLine(message.GetProperty("content").GetString());
}

Output:

Genererer billede...
Billede gemt som billede.png (142 KB)

Her er et eksempel på billedet - men prøv selv:

Spørgsmål til AI

For at få mest muligt ud af AI-værktøjer som ChatGPT, er det vigtigt at stille klare og præcise spørgsmål (og skabe det rigtige kontekst - se her). Her er nogle spørgsmål til denne side:

Grundlæggende spørgsmål til AI

  • Hvad er Microsoft Semantic Kernel og hvornår skal jeg bruge det?
  • Hvad er forskellen på IChatCompletionService og Kernel i Semantic Kernel?
  • Hvad er ChatHistory og hvorfor er den vigtig for at AI’en kan “huske”?
  • Hvad er en SystemMessage og hvornår skal jeg bruge den?
  • Hvad er kernel-funktioner ([KernelFunction]) i Semantic Kernel og hvordan virker de?
  • Hvad er ToolCallBehavior.AutoInvokeKernelFunctions og hvad gør det?
  • Hvad er OpenRouter og hvad er fordelen frem for at bruge OpenAI direkte?