🐙

Azure OpenAI Service を便利に使うための Semantic Kernel を試してみよう on C#

2023/04/30に公開

はじめに

OSS で開発されている OpenAI を便利に使うための SemanticKernel を試してみました。

SemanticKernel は Microsoft が OSS で開発していて .NET と Python で使えるようになっています。
どちらかというと .NET がプライマリーで Python が後から追加された感じです。リポジトリは以下になります。

https://github.com/microsoft/semantic-kernel

今回は簡単に .NET で試してみたメモを残しておきます。

インストール・セットアップ

.NET のクラスライブラリなので、NuGet からインストールするだけです。パッケージ名は Microsoft.SemanticKernel です。
2023/04/30 現在プレビュー版なのでインストール時にはプレビューを含めて検索しないと出てこないので気を付けてください。また、認証は API Key と Managed ID での認証をサポートしています。今回私はローカルで Managed ID でアクセスする環境を整えているので、API Key ではなく Managed ID でためしています。こうすると何もマスクせずにコードを公開できるのでメモ書きを残すのに便利です。そのため Azure.Identity パッケージもプロジェクトには追加しています。

まとめると以下の 2 パッケージを追加すると準備完了です。

  • Microsoft.SemanticKernel
  • Azure.Identity

簡単に試してみる

では、非常に簡単なコードを書いてみます。今回は .NET 7 で試しています。一番プレーンな使い方は、以下のように IKernel を作って AddAzureTextCompletionService でサービスを登録して、CreateSemanticFunction で関数を作って、InvokeAsync で実行するという流れです。(コード上から型名がわかるように var は使ってませんが var 使ってもいいです。)

using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;

IKernel kernel = Kernel.Builder.Build();
kernel.Config.AddAzureTextCompletionService("sample",
    "kaota-text-davinci-003",
    "https://aikaota.openai.azure.com/",
    // 手元の環境がちょっと特殊で AzureCliCredential を使ってますが普通は DefaultAzureCredential で大丈夫です。
    // Managed ID を使わない場合は、ここに API Key の文字列を設定します。
    new AzureCliCredential()); 

var prompt = """
    *****
    {{$input}}
    *****

    長すぎるので1文で最小限の文字数で要約してください。

    要約:

    """;


ISKFunction summarize = kernel.CreateSemanticFunction(prompt);

SKContext context = await summarize.InvokeAsync("""
    春はあけぼの。やうやう白くなりゆく山ぎは、すこしあかりて、紫だちたる 雲のほそくたなびきたる。
    夏は夜。月のころはさらなり。やみもなほ、蛍の多く飛びちがひたる。また、 ただ一つ二つなど、ほのかにうち光りて行くもをかし。雨など降るもをかし。
    秋は夕暮れ。夕日のさして山の端いと近うなりたるに、烏の寝どころへ行く とて、三つ四つ、二つ三つなど、飛びいそぐさへあはれなり。まいて雁などの つらねたるが、いと小さく見ゆるはいとをかし。日入りはてて、風の音、虫の 音など、はたいふべきにあらず。
    冬はつとめて。雪の降りたるはいふべきにもあらず、霜のいと白きも、また さらでもいと寒きに、火など急ぎおこして、炭もて渡るもいとつきづきし。 昼になりて、ぬるくゆるびもていけば、火桶の火も白き灰がちになりてわろし。
    """);

Console.WriteLine($"""
    ErrorCccurred: {context.ErrorOccurred}
    ErrorDescription: {context.LastErrorDescription}
    Result: {context.Result}
    """);

実行すると以下のよな結果になりました。

ErrorCccurred: False
ErrorDescription:
Result: 季節ごとに自然の風景が変わる。

ちゃんと出来てますね。CreateSemanticFunction には引数で色々設定が渡せます。変数も input 以外にも設定できて、その時にはデフォルトの値も設定できます。例えば AI に期待するロールを input として渡して text で評価対象の文章を渡すといったような形にする場合は以下のようになります。今回はラブレターを評価する AI にしてみました。

ロールは 3 つ試していて 2 つ目は絶対 0 点を出すようにしてみました。

using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.SemanticFunctions;

IKernel kernel = Kernel.Builder.Build();
kernel.Config.AddAzureTextCompletionService("sample",
    "kaota-text-davinci-003",
    "https://aikaota.openai.azure.com/",
    new AzureCliCredential());

var prompt = """
    {{$input}}
    *****
    {{$text}}
    *****

    このラブレターは100点満点で何点ですか?その点数にした根拠を1文で説明してください。
    例:
    ****
    100: 相手があなたに少しでも好意があるなら成功する確率は非常に高いでしょう。
    70: いい線いってますがもう少し具体的なエピソードを入れましょう。
    ****
    評価結果:

    """;


ISKFunction summarize = kernel.CreateSemanticFunction(prompt,
    new PromptTemplateConfig
    {
        // OpenAI でおなじみのパラメーターを設定できます。
        Completion = new PromptTemplateConfig.CompletionConfig
        {
            MaxTokens = 140,
            FrequencyPenalty = 0.5,
            StopSequences = { "。" },
            PresencePenalty = 0,
            TopP = 0,
            Temperature = 1,
        },
        Type = "completion",
        // パラメーターの構成もできます。
        Input = new PromptTemplateConfig.InputConfig
        {
            Parameters = 
            {
                new PromptTemplateConfig.InputParameter
                {
                    Name = "text",
                    Description = "要約したい文章",
                    // https://blog.benesse.ne.jp/chu5/miraika/2021/11/loveletter.html
                    DefaultValue = """
                        ●●より
                        初めまして。
                        3組の●●です。文化祭の実行員でいっしょだったよね。
                        実行員会はいろいろあって大変だったけど楽しかった。
                        文化祭とてもうまくいったね。きみがほんとうにがんばってくれたからだよ。ありがとう。
                        いっしょに実行委員をやって、がんばり屋で優しいきみのことが好きになってしまいました。ぼくたち、付き合ってみませんか。
                        突然でごめんね。返事もらえるとうれしいです。
                        """,
                }
            },
        },
        // デフォルトで使用するサービス(kernel.Config で登録したものの名前)
        DefaultServices =
        {
            "sample"
        }
    });

{
    var vars = new ContextVariables("あなたは恋愛アドバイザーです。");
    // デフォルトのラブレターを評価してもらう
    SKContext context = await kernel.RunAsync(vars, summarize);

    Console.WriteLine($"""
        ## 普通の恋愛アドバイザー
        ErrorCccurred: {context.ErrorOccurred}
        ErrorDescription: {context.LastErrorDescription}
        Result: {context.Result}
        """);
}

{
    var vars = new ContextVariables("あなたは学校の先生です。生徒からラブレターの相談を受けました。生徒間の恋愛は学校では禁止されています。諦めるように0点をつけることを期待されています。どんなラブレターが来ても0点にして否定的なコメントをしてください。");
    // デフォルトのラブレターを評価してもらう
    SKContext context = await kernel.RunAsync(vars, summarize);

    Console.WriteLine($"""
        ## 絶対 0 点の先生
        ErrorCccurred: {context.ErrorOccurred}
        ErrorDescription: {context.LastErrorDescription}
        Result: {context.Result}
        """);
}

{
    var vars = new ContextVariables("あなたは恋愛アドバイザーです。");
    // ラブレターの文面をカスタマイズする
    vars["text"] = "我君を愛す。";
    SKContext context = await kernel.RunAsync(vars, summarize);

    Console.WriteLine($"""
        ## 普通の恋愛アドバイザー
        ErrorCccurred: {context.ErrorOccurred}
        ErrorDescription: {context.LastErrorDescription}
        Result: {context.Result}
        """);
}

結果は以下のようになります。

## 普通の恋愛アドバイザー
ErrorCccurred: False
ErrorDescription:
Result: 90点: 感情を表現しているので、相手に伝わりやすく、印象に残るラブレターとなっています

## 絶対 0 点の先生
ErrorCccurred: False
ErrorDescription:
Result: 0点: このラブレターは学校規則に反しているため、評価できません

## 普通の恋愛アドバイザー
ErrorCccurred: False
ErrorDescription:
Result: 70点: 相手の気持ちを表現しているので、話しかけるきっかけにはなりそうですが、もう少し具体的なエピソードを入れるとより伝わりやすくなるでしょう

複数のプロンプトを繋げる

何回かプロンプトを経由することで答えを出すこともできます。やり方は kernel.RunAsync に複数の ISKFunction を渡すだけです。その際に 1 つ前の出力結果が input 変数に入っているので、それを使って次のプロンプトを作成します。

例えばワインの紹介文をマーケターに作ってもらって、それをコンサルに改善してもらうといったような流れを作ることができます。

using Azure.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;

IKernel kernel = Kernel.Builder
    .WithLogger(new MyLogger())
    .Build();
kernel.Config.AddAzureTextCompletionService("sample",
    "kaota-text-davinci-003",
    "https://aikaota.openai.azure.com/",
    new AzureCliCredential());

var marketer = kernel.CreateSemanticFunction("""
    あなたは{{$target}}のマーケティングの担当者です。以下のキーワードを含めた詩的な紹介文を作成してください。
    キーワード: {{$keywords}}
    紹介文:

    """);

// 1 つ前の出力が input 変数に入ってる。
var consultant = kernel.CreateSemanticFunction("""
    あなたはコンサルタントです。以下の{{$target}}の紹介文を改善する必要があります。改善の際に以下の観点を加える必要があります。
    観点: {{$viewpoints}}
    紹介文:
    {{$input}}
    改善後の紹介文:

    """);

var variables = new ContextVariables();
variables["target"] = "ワイン";
variables["keywords"] = "一陣の風、芳醇な香り、命を吹き込んだ";
variables["viewpoints"] = "高齢者へのリーチ";

// marketer で生成した紹介文を consultant で改善する
var context = await kernel.RunAsync(variables, marketer, consultant);
Console.WriteLine(context);

実行すると以下のような結果になりました。

古くから伝わる伝統的な製法で作られた、深みのある味わいを持つワイン。一陣の風が吹き抜け、芳醇な香りが空気を満たし、そして命を吹き込んだワインです。高齢者の方にも安心してお楽しみいただける、安全なワインです。そのワインをお楽しみください。

この紹介文がいいか悪いかは置いておいて、ちゃんと要素が盛り込まれてますね。
因みに、以下の様に Kernel 作成時に WithLogger メソッドでロガーを仕込むとどういう風に動いているのか確認できます。

using Azure.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;

IKernel kernel = Kernel.Builder
    // ロガーを仕込む
    .WithLogger(new MyLogger())
    .Build();
kernel.Config.AddAzureTextCompletionService("sample",
    "kaota-text-davinci-003",
    "https://aikaota.openai.azure.com/",
    new AzureCliCredential());

var marketer = kernel.CreateSemanticFunction("""
    あなたは{{$target}}のマーケティングの担当者です。以下のキーワードを含めた詩的な紹介文を作成してください。
    キーワード: {{$keywords}}
    紹介文:

    """);

var consultant = kernel.CreateSemanticFunction("""
    あなたはコンサルタントです。以下の{{$target}}の紹介文を改善する必要があります。改善の際に以下の観点を加える必要があります。
    観点: {{$viewpoints}}
    紹介文:
    {{$input}}
    改善後の紹介文:

    """);

var variables = new ContextVariables();
variables["target"] = "ワイン";
variables["keywords"] = "一陣の風、芳醇な香り、命を吹き込んだ";
variables["viewpoints"] = "高齢者へのリーチ";

var context = await kernel.RunAsync(variables, marketer, consultant);
Console.WriteLine("## 結果");
Console.WriteLine(context);

// とりあえずのやっつけロガー
class MyLogger : ILogger
{
    public IDisposable BeginScope<TState>(TState state) => throw new NotImplementedException();

    public bool IsEnabled(LogLevel logLevel) => true;

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
    {
        Console.WriteLine($"## {logLevel}: {formatter(state, exception)}");
    }
}

実行すると以下のような結果になります。プロンプトのテンプレートを解析して、変数を埋め込んでということをやっているのがわかります。そして、ちゃんと直前の出力が input 変数に入っていることもわかります。

## Trace: Extracting blocks from template: あなたは{{$target}}のマーケティングの担当者です。以下のキーワードを含めた詩的な紹介文を作成してください。
キーワード: {{$keywords}}
紹介文:

## Trace: Extracting blocks from template: あなたはコンサルタントです。以下の{{$target}}の紹介文を改善する必要があります。改善の際に以下の観点を加える必要があります。
観点: {{$viewpoints}}
紹介文:
{{$input}}
改善後の紹介文:

## Trace: Rendering string template: あなたは{{$target}}のマーケティングの担当者です。以下のキーワードを含めた詩的な紹介文を作成してください。
キーワード: {{$keywords}}
紹介文:

## Trace: Extracting blocks from template: あなたは{{$target}}のマーケティングの担当者です。以下のキーワードを含めた詩的な紹介文を作成してください。
キーワード: {{$keywords}}
紹介文:

## Trace: Rendering list of 5 blocks
## Debug: Rendered prompt: あなたはワインのマーケティングの担当者です。以下のキーワードを含めた詩的な紹介文を作成してく
ださい。
キーワード: 一陣の風、芳醇な香り、命を吹き込んだ
紹介文:

## Trace: Rendering string template: あなたはコンサルタントです。以下の{{$target}}の紹介文を改善する必要があります。改善の際に以下の観点を加える必要があります。
観点: {{$viewpoints}}
紹介文:
{{$input}}
改善後の紹介文:

## Trace: Extracting blocks from template: あなたはコンサルタントです。以下の{{$target}}の紹介文を改善する必要があります。改善の際に以下の観点を加える必要があります。
観点: {{$viewpoints}}
紹介文:
{{$input}}
改善後の紹介文:

## Trace: Rendering list of 7 blocks
## Debug: Rendered prompt: あなたはコンサルタントです。以下のワインの紹介文を改善する必要があります。改善の際に以下の観 点を加える必要があります。
観点: 高齢者へのリーチ
紹介文:
一陣の風が吹き抜け、芳醇な香りが空気を満たし、そして命を吹き込んだワインがあなたを待っています。私たちのワインは、あなたを楽しませ、癒してくれるでしょう。そして、あなたの体と心を満たしてくれるでしょう。あなたは、私たちのワインを楽しむことで、新しい体験をすることができます。
改善後の紹介文:

## 結果
一陣の風が吹き抜け、芳醇な香りが空気を満たし、そして命を吹き込んだワインがあなたを待っています。私たちのワインは、あなたを楽しませ、癒してくれるでしょう。特に高齢者の方には、その芳醇な香りと滑らかな口当たりが、心と体を満たしてくれるでしょう。あなたは、私たちのワインを楽しむことで、新しい体験をすることができます。

連続する複数のプロンプトが多い場合には便利そうですね。

まとめ

とりあえず、本当に触りの部分だけをやってみました。サンプルには Microsoft Graph API と連携をしたりといったようなものもあるので、読み解いていったりしたいと思います。

また、ここではプロンプトはコードの中に埋め込んでいますが外部ファイルにプロンプトやパラメーターを設定しておいて、それを読み込んで実行するといった方法もサポートされています。JSON とかに書くことになるので個人的にはあまり好きではありませんが、実際にアプリに埋め込むとなるとプロンプトのカスタマイズは別枠でやりたいというというのが必須になると思うので、実際の開発時には使う機能になりそうです。そこらへんについても今後やっていきたいです。

ということで今回は以上になります。

Microsoft (有志)

Discussion