未来が来た!!Semantic KernelでAIのタスク実行計画を自動立案!
2023-06-09
azblob://2023/06/08/eyecatch/2023-06-09-automatic-ai-planning-with-semantic-kernel-000.jpg

ChatGPTの登場以来、未曾有のAIブームが到来している昨今。しかしChatGPTだけではできないことも多くあります。例えばWebやデータベースから最新の情報を取得するなどのより複雑なタスクをこなそうと思えば、単純なプロンプトのやりとりだけでないプログラムとの連携が必要になるでしょう。

しかしAIと違ってプログラムは融通の効かないもの。色々な入力や応答があり得るAIとのやり取りの中に、スムーズにプログラムを組み込むのは中々に骨が折れます。一体いくつの場合を想定して、どのような条件分岐をすれば全てを網羅できるのでしょう?

ほとんど無理に近いですよね。

Semantic Kernelって何者?

そこで今回ご紹介するのが、Semantic Kernelです。これを使えばなんと、タスク実行計画を自動で立案してくれるんです!!!凄すぎる!!!もし自動にしたくない部分があれば、手組みもできます。類似のLangChainとの違いが気になる人は、手動にもできる部分が多いのが違いだと思って貰えれば大丈夫です。

更に、プロンプトと通常のプログラムを同じ「スキル」という枠組みで同列に扱ってプログラムが組めるんです。スキルを使うときに気にするのはI/Oだけでよく、中身がプロンプトか普通のプログラムかは全く気にする必要がありません。

これは2023年の4月ごろからMicrosoftがOSSとして開発している、非常に新しいライブラリです。2023年6月現在では、プレビュー版としてGitHubおよびNPMで公開されています。

以下の画像はMicrosoft公式の解説ページに掲載されているもので、①の入力を受けた後に、②で複数の機能を組み合わせてタスクを実行し、③の回答を返す様子が図示されています。

Kernel flow
出典: What is Semantic Kernel? (Microsoft Learn)

実装してみよう

今回はごく簡単な実装で、スキルを2個組み合わせて返答文を作るコードを書いてみます。

作成するもの

  • Semantic Function (プロンプトを使ったスキル)
  • Native Function (コードを使ったスキル)
  • Kernelの設定
  • スキルの実行用コード

事前準備

Azure OpenAI Serviceが使える方は、まず該当のリソースを作成して、gpt3.5-turboのモデルをデプロイしておいてください。作成できない方は、OpenAIのアカウントを通じて直接モデルを利用する方法もあります。その場合OpenAIのアカウントを取得しておいてください。

続いて.NET7が実行できるプロジェクトを作成してください。その後NuGet Package Managerで以下のコマンドを実行し、Semantic Kernelのライブラリをインストールします。

NuGet\Install-Package Microsoft.SemanticKernel -Version 0.15.230531.5-preview

※バージョン更新は頻繁に行われているので、GitHubのReleasesの最新のバージョン番号を用いてください。NuGet Galleryのページからも取得できます。

Package Manager以外を用いてインストールする場合は、NuGet Galleryのページから使いたい方法に合ったコマンドを取得してください。

Semantic Functionの作成

AIへのプロンプトと、APIコール時のパラメーターなどを指定します。設定ファイルとプロンプトのファイルを以下のようなディレクトリ構成で作成してください。

プロジェクトルート
├── Skills
│   └── ResponseSkill
│        └── ExampleGeneralResponse
│            ├── config.json
│            └── skprompt.txt

ディレクトリ名は自由に変えて構いませんが、ファイル名は絶対に変更しないでください。ディレクトリ名を変更した場合は後述の実行コードでの指定ディレクトリ名も適宜変更してください。

Skills以下に全てのスキルを格納する形式となっていて、その直下のディレクトリがスキル群(正式名: Skill)、更にその配下のディレクトリが特定の機能を持つスキル(正式名: Function)です。後述の実行コード上では、スキル群の単位で読み込みを行います。

各ファイルの内容は以下の通りです。

config.json:

{
  "schema": 1,
  "type": "completion",
  "description": "ユーザー入力に対して補足情報を参考に応答を返す",
  "completion": {
    "max_tokens": 1024,
    "temperature": 0.7,
    "top_p": 0.5,
    "presence_penalty": 0.0,
    "frequency_penalty": 0.1
  },
  "input": {
    "parameters": [
      {
        "name": "INPUT",
        "description": "ユーザーの入力文章",
        "defaultValue": ""
      },
      {
        "name": "ADDITIONAL_INFO",
        "description": "補足情報",
        "defaultValue": ""
      }
    ]
  }
}

コンフィグの各項目のうち、よく触るのは descriptioncompletioninput の3項目です。

descriptionにはタスクの実行計画が立てられる際に参照される説明文を記述します(超重要)。

completionにはOpenAIのAPIに送る各種パラメーターを設定します。パラメーターの意味については他に解説記事が多くありますので、調べてみてください。

inputにはこのスキルが受け取る引数を記述します。引数名と説明書き、デフォルト値が指定できます。

skprompt.txt:

あなたは優秀なAIアシスタントです。
補足情報を参考に、ユーザーの入力文章に対して適切な応答を返してください。
ユーザーの入力文章:
```
{{$INPUT}}
```
補足情報:
```
{{$ADDITIONAL_INFO}}
```

プロンプトには一般的なプロンプトの形式に加えて、引数を代入する記述ができます。引数名としてコンフィグで指定したものを記述すれば、実行時に自動で引数を受け取ることができます。なお引数名に大文字小文字の区別はありません。

Native Functionの作成

C#のコードをSemantic Kernelのスキルとして認識させられる形で書きます。C#のファイルを以下のようなディレクトリ構成で作成してください。

プロジェクトルート
├── Skills
│   └── InformationProviderSkill.cs

先程作成したSemantic Functionと合わせると以下のようになっているはずです。

プロジェクトルート
├── Skills
│   └── ResponseSkill
│   │   └── ExampleGeneralResponse
│   │       ├── config.json
│   │       └── skprompt.txt
│   └── InformationProviderSkill.cs

C#のファイルには次のように記述します。

C#using Microsoft.SemanticKernel.SkillDefinition;
namespace ExampleApp.Skills;
public class InformationProviderSkill {
    [SKFunction("補足情報を与える")]
    public string ProvideInfo(string input) {
        return $"""
            {input}
            と言った人の名前は「ジョン・スミス」です。
            """;
    }
}

SKFunction属性をつけ、パラメーターにこのスキルの説明文を記述します。この説明文はSemantic Functionのときと同様に、タスクの実行計画を立てる際に参照されます。

付けられる属性や関数の組み立て方は他にもありますが、その解説は後続の記事に譲ります。

このスキルでは、入力にジョン・スミスという人物の発言だという補足情報を付加しています。

Kernelの設定

全体の管理をするKernelを設定していきます。今回はAIのモデルに対する接続設定のみを行います。

C#var kernel = new KernelBuilder()
	.Configure(config => {
    	// Azure OpenAI Serviceを使う場合はこれ
    	config.AddAzureChatCompletionService("デプロイ名", "エンドポイントURI", "APIキー");
    	// OpenAIから直接使う場合はこれ
    	// config.AddOpenAIChatCompletionService("モデル名", "APIキー");
	})
	.Build();

GPTではなくdavinciなどを用いる場合はメソッド名の ChatText に置き換えてください。

もしメモリー機能やEmbeddingの利用、DBとの接続などを行いたい場合はKernelの作成時に追加で設定を記述します。今回はその具体的な書き方には触れません。

スキルの実行用コードの作成

ここまでに作ったスキルを実際に動かしてみましょう!

Kernelの設定を書いた部分の下へ追記する形で、以下のコードを書いていってください。

C#// Skillsディレクトリのパスを記述
string skillsDirectory = "[プロジェクトルートのパス]\\Skills";
// SkillsディレクトリにあるResponseSkillを読み込む
kernel.ImportSemanticSkillFromDirectory(skillsDirectory, "ResponseSkill");
// SkillsディレクトリにあるInformationProviderSkillを読み込む
kernel.ImportSkill(new InformationProviderSkill(), "InformationProviderSkill");

スキルはKernelにインポートしてから使います。SemanticスキルとNativeスキルで読み込み用の関数が異なるので注意してください。

C#// Kernelを与えてプランナーを準備
var planner = new SequentialPlanner(kernel);

今回は最も簡単に扱えるSequentialPlannerを使います。これは与えられた全てのスキルの中から、タスク遂行に適したスキルの選択と実行順の組み立てを自動でこなしてくれるものです。タスクが単純でスキル数も少ないうちは、これを使うだけで大丈夫です。

C#// 入力例。実際はどこかから入力を受け取って代入する。
string input = "世界を大いに盛り上げるためのジョン・スミスをよろしく!";
// 入力を与えてプランを立案
Plan plan = await planner.CreatePlanAsync(input);

プランナーに入力を与えて、プランを自動で立案させます。ここでは適当な文字列を定義していますが、実際はユーザー入力などを与えてプランを作らせます。

ユーザー入力に追加の指示文などを加えて入力文字列とすると、精度が向上する場合もあります。

C#// 作成されたプランをJSON形式で表示
Console.WriteLine(JsonSerializer.Serialize(plan, options: new() {
    WriteIndented = true,
    Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Create(System.Text.Unicode.UnicodeRanges.All),
}));

作成されたプランを表示して確認したいときは、上記のようにするとJSON形式で表示させられます。

C#// プランを実行
SKContext result = await kernel.RunAsync(plan);
Console.WriteLine(result);

最後にプランを実行し、結果を出力させてみましょう。

実行結果

私の手元で実行したときには、プランナーの立てたプランは以下のように出力されました。

プランは入れ子構造になっていて、最も外側のプランのsteps項目の配下に、各ステップに対応する具体的な内容が書かれています。この場合では、まず ProvideInfo に「ジョン・スミスとは何者か?」という入力を送ってから、その結果を ExampleGeneralResponse のADDITIONAL_INFOへ送り、その入力には「よろしくお願いします!」と送っている様子が見えます。

これが妥当なプランかは微妙なところですが、一応プランとしては成立しているようです。

{
  "state": [
    {
      "Key": "INPUT",
      "Value": ""
    }
  ],
  "steps": [
    {
      "state": [
        {
          "Key": "INPUT",
          "Value": ""
        }
      ],
      "steps": [],
      "parameters": [
        {
          "Key": "INPUT",
          "Value": "ジョン・スミスとは何者か?"
        }
      ],
      "outputs": [
        "INFO"
      ],
      "next_step_index": 0,
      "name": "ProvideInfo",
      "skill_name": "InformationProviderSkill",
      "description": "補足情報を与える"
    },
    {
      "state": [
        {
          "Key": "INPUT",
          "Value": ""
        }
      ],
      "steps": [],
      "parameters": [
        {
          "Key": "INPUT",
          "Value": "よろしくお願いします!"
        },
        {
          "Key": "ADDITIONAL_INFO",
          "Value": "$INFO"
        }
      ],
      "outputs": [],
      "next_step_index": 0,
      "name": "ExampleGeneralResponse",
      "skill_name": "ResponseSkill",
      "description": "ユーザー入力に対して補足情報を参考に応答を返す"
    }
  ],
  "parameters": [
    {
      "Key": "INPUT",
      "Value": ""
    }
  ],
  "outputs": [],
  "next_step_index": 0,
  "name": "",
  "skill_name": "Microsoft.SemanticKernel.Planning.Plan",
  "description": "世界を大いに盛り上げるためのジョン・スミスをよろしく!"
}

このプランを実行した結果は、こう出てきました。

応答:
はじめまして!何かお困りのことはありますか?ジョン・スミスという人物についての情報をお探しでしょうか?

案外悪くないように見えます。冒頭に余計なものが入っていますが、これはスキルのプロンプトにワンショットの例を書いておけば、だいたい消えてくれます。

最後に

今回は簡単な例でしたが、この仕組みを使えばデータベースから検索した情報を差し込んだり、Web検索した情報を差し込んだり、別のプロンプトで解釈させた結果を参照したりなど、様々なことが可能になります。

Semantic Kernelにはこれ以外にもメモリー機能や、外部へのコネクター、より抽象レベルの低いプランの立て方など、まだまだ多くの紹介したい部分がありますが、それらの紹介は次以降の記事ですることにします。

Semantic Kernelは今まさに猛スピードで開発が進んでいるライブラリーなので、今後機能が増えたり、使い方が変わってより便利になる可能性は高いです。今からぜひ触ってみてはいかがでしょう。