📘

Semantic Kernel でプランナーを使って自発的に解決策を考えて実行する AI を作ってみよう

2023/05/02に公開

今まで、地道にセマンティック スキルやネイティブ スキルを試して来ましたが、今回は毛色を変えてプランナーについて試してみたいと思います。
プランナーは、やりたいことを入力すると、それを実現するための手順を考えてくれるものだと思います。(まだよくわかってない)

カーネルに沢山スキルを登録して、そのカーネルに対してプランナーを作成してやりたいことを渡すとやりたいことをするための手順を返してくれます。

var planner = new SequentialPlanner(kernel);
Plan plan = await planner.CreatePlanAsync("やりたいこと");
SKContext result = await kernel.RunAsync(plan);
Console.WriteLine(result);

Plan には Steps プロパティがあって、これに手順が入っています。kernel.RunAsync メソッドに渡すことでプランを実行して結果を返してくれます。ちょっとやっつけですが、以下のようなコードを書いてみました。

using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Planning.Planners;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;

// Kernel 作って
var kernel = Kernel.Builder.Build();

// Azure OpenAI Service と繋いで
kernel.Config.AddAzureChatCompletionService(
    "chat",
    "モデル名",
    "https://xxxxxxx.openai.azure.com/",
    new DefaultAzureCredential());

// Skill を登録して
kernel.CreateSemanticFunction(""""
    あなたはテキストのライターです。
    与えられた文章に対してタイトルを付けて、まとめの章と序文を追加してください。

    出力例:
    ****
    # 全体のタイトル

    ここに序文を記載してください。

    {{$input}}
    
    ## まとめ

    入力の要約をここに書いてください
    ****

    文章:
    ****
    {{$input}}
    ****
    """",
    config: new()
    {
        Input = new()
        {
            Parameters =
            {
                new() { Name = "input", Description = "文章"}
            },
        },
        Description = "入力された文章に対してタイトルを付けて序文とまとめの章を追加します。。",
        Completion = new()
        {
            MaxTokens = 5000,
        },
    },
    "Summarize",
    "WriterSkill");

kernel.CreateSemanticFunction("""
    与えられたカンマ区切りのキーワードを使って文章を作成してください。
    キーワードが 4 つの場合には、4つのセクションを作成してください。
    セクションの前には、そのキーワードを見出しとして記載してください。

    入力が ***キーワード1,キーワード2,キーワード3,キーワード4*** の場合の出力例は以下のようになります。
    出力例:
    ****
    ## キーワード1
    このキーワードに関する文章を2から3段落で記載します。
    
    ## キーワード2
    このキーワードに関する文章を2から3段落で記載します。
    
    ## キーワード3
    このキーワードに関する文章を2から3段落で記載します。
    
    ## キーワード4
    このキーワードに関する文章を2から3段落で記載します。
    ****

    キーワード: {{$input}}
    """,
    config: new()
    {
        Input = new()
        {
            Parameters =
            {
                new() { Name = "input", Description = "カンマ区切りのキーワードの一覧" }
            }
        },
        Description = "キーワードを元に文章を作成します。",
        Completion = new()
        {
            MaxTokens = 2000,
        }
    },
    "Write",
    "WriterSkill");

kernel.CreateSemanticFunction(""""
    与えられたトピックから関連性の強い単語を4つ回答してください。
    単語はカンマ区切りで1行で回答してください。

    出力例:
    入力が """リンゴ""" の場合の出力例は以下のようになります。
    ****
    ビタミンC,果物,リンゴ酸,青森
    ****

    トピック: {{$input}}
    """",
    config: new()
    {
        Input = new()
        {
            Parameters =
            {
                new() { Name = "input", Description = "話題の創出元のトピック" },
            }
        },
        Description = "話題を創出します。",
    },
    "CreateTopics",
    "IdeaSkill");

kernel.CreateSemanticFunction("""
    あなたは天才コメディアンです。アイテムに対して面白い返答をしてください。
    アイテム: {{$input}}
    """,
    config: new()
    {
        Input = new()
        {
            Parameters =
            {
                new() { Name = "input", Description = "アイテム" },
            },
        },
        Description = "アイテムに対して面白い話を返します。",
    },
    "Joke",
    "FunSkill");

// Planner を作って、やりたいことを渡して実行
var planner = new SequentialPlanner(kernel);
Plan plan = await planner.CreatePlanAsync("ミカンが健康にいいことについての記事を書きたい。");

Console.WriteLine("作成されたプラン");
Console.WriteLine(JsonSerializer.Serialize(plan, options: new()
{
    WriteIndented = true,
    Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
}));

SKContext result = await kernel.RunAsync(plan);
Console.WriteLine(result);

スキルの中には記事を書くのには関係ない冗談を言うだけのスキルも入っていますが、今回のやりたいこととは関係ないのでプランには含まれないことを期待しています。では実行してみましょう。最初に作成されたプランの JSON が出力されます。以下のような結果になりました。

steps に 2 つの Step が追加されていることが確認できます。

{
  "state": [
    {
      "Key": "INPUT",
      "Value": ""
    }
  ],
  "steps": [
    {
      "state": [
        {
          "Key": "INPUT",
          "Value": ""
        }
      ],
      "steps": [],
      "named_parameters": [
        {
          "Key": "INPUT",
          "Value": "ミカン, 健康, 記事"
        }
      ],
      "named_outputs": [
        {
          "Key": "INPUT",
          "Value": ""
        }
      ],
      "next_step_index": 0,
      "name": "Write",
      "skill_name": "WriterSkill",
      "description": "キーワードを元に文章を作成します。"
    },
    {
      "state": [
        {
          "Key": "INPUT",
          "Value": ""
        }
      ],
      "steps": [],
      "named_parameters": [
        {
          "Key": "INPUT",
          "Value": ""
        }
      ],
      "named_outputs": [
        {
          "Key": "ARTICLE_TEXT",
          "Value": ""
        },
        {
          "Key": "INPUT",
          "Value": ""
        }
      ],
      "next_step_index": 0,
      "name": "Summarize",
      "skill_name": "WriterSkill",
      "description": "入力された文章に対してタイトルを付けて序文とまとめの章を追加します。。"
    }
  ],
  "named_parameters": [
    {
      "Key": "INPUT",
      "Value": ""
    }
  ],
  "named_outputs": [
    {
      "Key": "INPUT",
      "Value": ""
    }
  ],
  "next_step_index": 0,
  "name": "ミカンが健康にいいことについての記事を書きたい。",
  "skill_name": "Microsoft.SemanticKernel.Orchestration.Plan",
  "description": "ミカンが健康にいいことについての記事を書きたい。"
}

JSON の後に記事が出力されます。なんか記事という余計なキーワードも入っていますね…。

# ミカンと健康について

健康には、食事や運動、睡眠などが大切です。バランスの良い食事を心がけ、適度な運動をすることで、健康を維持することができます。また、ストレスをためないようにすることも大切です。ストレスは、体に悪影響を与えるだけでなく、精神的な健康にも悪影響を与えます。

## ミカン
ミカンは、冬の代表的な果物の一つです。ビタミンCが豊富で、風邪予防にも効果的です。また、ミカンに含まれるカロテンは、美肌 効果があるとされています。ミカンは、手軽に食べられるので、健康に気を遣う人にはおすすめの果物です。

## 記事
記事を書く際には、ターゲットとなる読者層を意識することが大切です。また、記事のテーマに合わせて、適切な情報を収集することも必要です。記事を書く際には、正確な情報を提供することが求められます。また、読みやすい文章を心がけることも大切です。読者が興味を持ち、読み進めたくなるような文章を書くことが、良い記事を書くためのポイントです。

## まとめ
健康には、バランスの良い食事や運動、ストレスをためないことが大切です。ミカンは、ビタミンCが豊富で、風邪予防に効果的です 。記事を書く際には、ターゲットとなる読者層を意識し、正確な情報を提供することが求められます。また、読みやすい文章を心がけることも大切です。これらのポイントを意識して、健康に関する記事を書くことができます。

ゴールを変えて動かしてみましょう。先ほどは「ミカンが健康にいいことについての記事を書きたい。」というゴールでしたが、今度は「ミカンに関連した記事を書きたい。」というゴールに変えてみます。若干曖昧にお願いしてみました。

{
  "state": [
    {
      "Key": "INPUT",
      "Value": ""
    }
  ],
  "steps": [
    {
      "state": [
        {
          "Key": "INPUT",
          "Value": ""
        }
      ],
      "steps": [],
      "named_parameters": [
        {
          "Key": "INPUT",
          "Value": "ミカン"
        }
      ],
      "named_outputs": [
        {
          "Key": "INPUT",
          "Value": ""
        },
        {
          "Key": "TOPICS",
          "Value": ""
        }
      ],
      "next_step_index": 0,
      "name": "CreateTopics",
      "skill_name": "IdeaSkill",
      "description": "話題を創出します。"
    },
    {
      "state": [
        {
          "Key": "INPUT",
          "Value": ""
        }
      ],
      "steps": [],
      "named_parameters": [
        {
          "Key": "INPUT",
          "Value": "$TOPICS"
        }
      ],
      "named_outputs": [
        {
          "Key": "INPUT",
          "Value": ""
        },
        {
          "Key": "ARTICLE",
          "Value": ""
        }
      ],
      "next_step_index": 0,
      "name": "Write",
      "skill_name": "WriterSkill",
      "description": "キーワードを元に文章を作成します。"
    },
    {
      "state": [
        {
          "Key": "INPUT",
          "Value": ""
        }
      ],
      "steps": [],
      "named_parameters": [
        {
          "Key": "INPUT",
          "Value": "$ARTICLE"
        }
      ],
      "named_outputs": [
        {
          "Key": "INPUT",
          "Value": ""
        }
      ],
      "next_step_index": 0,
      "name": "Summarize",
      "skill_name": "WriterSkill",
      "description": "入力された文章に対してタイトルを付けて序文とまとめの章を追加します。。"
    }
  ],
  "named_parameters": [
    {
      "Key": "INPUT",
      "Value": ""
    }
  ],
  "named_outputs": [
    {
      "Key": "INPUT",
      "Value": ""
    }
  ],
  "next_step_index": 0,
  "name": "ミカンに関連した記事を書きたい。",
  "skill_name": "Microsoft.SemanticKernel.Orchestration.Plan",
  "description": "ミカンに関連した記事を書きたい。"
}

今度は 3 つのステップになりました。ミカンからいくつかのキーワードを作って、文章を書いてタイトルや序文などをつけるといった流れになりました。

生成された記事は以下のようなものです。序文がプロンプトの例示そのままだ…。

# 果物の栄養素と選び方

ここに序文を記載してください。

## 柑橘類
柑橘類は、ビタミンCが豊富で、風邪予防に効果的です。また、果物の中でも比較的安価で手軽に手に入るため、毎日の食生活に取り 入れやすいと言えます。ただし、皮が厚くてむきにくいものもあるため、選ぶ際には注意が必要です。

## ビタミンC
ビタミンCは、免疫力を高める効果があり、風邪予防に効果的です。また、美肌効果もあるため、女性にも人気があります。ビタミンCは、柑橘類だけでなく、野菜や果物にも含まれています。ただし、加熱すると壊れやすいため、生で食べることがおすすめです。

## 果物
果物には、ビタミンやミネラル、食物繊維など、体に必要な栄養素が豊富に含まれています。また、甘みがあるため、おやつ代わりにもなります。ただし、果物にも糖分が含まれているため、過剰に摂取するとカロリーオーバーになることもあるため、適量を守るようにしましょう。

## 皮がむきやすい
皮がむきやすい果物は、手軽に食べられるため、忙しい朝食やおやつにもおすすめです。また、皮には栄養素が豊富に含まれているため、できるだけむかずに食べることがおすすめです。ただし、農薬や汚染物質が付着している場合もあるため、よく洗ってから食べるようにしましょう。

## まとめ
果物には、体に必要な栄養素が豊富に含まれていますが、適量を守ることが大切です。皮がむきやすい果物は手軽に食べられますが、農薬や汚染物質に注意が必要です。選ぶ際には、皮の厚さや色、形状などを確認し、新鮮で健康的なものを選びましょう。

最後にゴールを「ミカンに関する冗談を作りたい」にしてみました。
作成されたプランは面白い話を作るスキルだけが含まれています。いい感じですね。

{
  "state": [
    {
      "Key": "INPUT",
      "Value": ""
    }
  ],
  "steps": [
    {
      "state": [
        {
          "Key": "INPUT",
          "Value": ""
        }
      ],
      "steps": [],
      "named_parameters": [
        {
          "Key": "INPUT",
          "Value": "ミカン"
        }
      ],
      "named_outputs": [
        {
          "Key": "INPUT",
          "Value": ""
        }
      ],
      "next_step_index": 0,
      "name": "Joke",
      "skill_name": "FunSkill",
      "description": "アイテムに対して面白い話を返します。"
    }
  ],
  "named_parameters": [
    {
      "Key": "INPUT",
      "Value": ""
    }
  ],
  "named_outputs": [
    {
      "Key": "INPUT",
      "Value": ""
    }
  ],
  "next_step_index": 0,
  "name": "ミカンに関する冗談を作りたい",
  "skill_name": "Microsoft.SemanticKernel.Orchestration.Plan",
  "description": "ミカンに関する冗談を作りたい"
}

出力された冗談は以下のようなものです。面白い…?

「ミカンって、なんであんなにかわいいんだろうね。でも、食べるときは手がベトベトになっちゃうから、ちょっと面倒くさいよね。でも、その甘酸っぱい味は最高だから、手を洗ってでも食べたい!って思っちゃうんだよね。ミカンって、手を汚すことで人間の本能をくすぐるんだろうね。」

ステップ実行したい

実際のアプリでは、恐らくステップの進捗とかを画面に出したくなると思います。
そういう場合は IKernelStepAsync を呼び出すことで 1 つずつステップを実行することができます。これを PlanHasNextStepfalse を返すまで繰り返すことで、プランを実行することができます。

以下のようなコードになります。

while (plan.HasNextStep)
{
    Console.WriteLine($"- {plan.Steps[plan.NextStepIndex].Name}を実行しています。");
    await kernel.StepAsync(plan);
    Console.WriteLine($"出力: {plan.State.Input}");
    Console.WriteLine();
}

ステップの実行部分を上記のコードにしてゴールを「ミカンに関連した記事を書きたい。」にして実行すると以下のような結果になります。

- CreateTopicsを実行しています。
出力: 柑橘類,ビタミンC,果物,皮がむきやすい

- Writeを実行しています。
出力: ## 柑橘類
柑橘類は、ビタミンCが豊富で、風邪予防に効果的です。また、果物の中でも皮が薄く、剥きやすいので、手軽に食べることができま
す。代表的な柑橘類には、オレンジ、グレープフルーツ、レモン、ライムなどがあります。

## ビタミンC
ビタミンCは、免疫力を高める効果があり、風邪予防に効果的です。また、美肌効果もあるため、女性にも人気があります。ビタミンCが豊富な食品には、柑橘類のほかに、キウイフルーツ、イチゴ、パプリカなどがあります。

## 果物
果物には、ビタミンやミネラル、食物繊維などが豊富に含まれており、健康に良いとされています。また、甘味があるため、おやつ代わりにもなります。代表的な果物には、リンゴ、バナナ、イチゴ、ブドウなどがあります。

## 皮がむきやすい
皮がむきやすい果物は、手軽に食べることができるため、忙しい人にもおすすめです。また、皮には栄養素が多く含まれているため、できるだけむかずに食べることが望ましいです。代表的な皮がむきやすい果物には、バナナ、キウイフルーツ、柑橘類などがあります。

- Summarizeを実行しています。
出力: # 果物の健康効果とおすすめの食べ方

果物には、ビタミンやミネラル、食物繊維などが豊富に含まれており、健康に良いとされています。特に、柑橘類はビタミンCが豊富
で、風邪予防に効果的です。また、果物の中でも皮が薄く、剥きやすいので、手軽に食べることができます。

## 序文

健康に良い食べ物として、果物は欠かせない存在です。しかし、どのように食べるのが一番効果的なのか、また、どの果物がおすすめなのか、知らない人も多いのではないでしょうか。この記事では、果物の健康効果とおすすめの食べ方について紹介します。

## 柑橘類

柑橘類は、ビタミンCが豊富で、風邪予防に効果的です。また、代表的な柑橘類には、オレンジ、グレープフルーツ、レモン、ライム などがあります。

## ビタミンC

ビタミンCは、免疫力を高める効果があり、風邪予防に効果的です。また、美肌効果もあるため、女性にも人気があります。ビタミンCが豊富な食品には、柑橘類のほかに、キウイフルーツ、イチゴ、パプリカなどがあります。

## 皮がむきやすい

皮がむきやすい果物は、手軽に食べることができるため、忙しい人にもおすすめです。また、皮には栄養素が多く含まれているため、できるだけむかずに食べることが望ましいです。代表的な皮がむきやすい果物には、バナナ、キウイフルーツ、柑橘類などがあります。

## まとめ

果物には、健康に良い栄養素が豊富に含まれています。特に、柑橘類はビタミンCが豊富で、風邪予防に効果的です。また、皮がむき やすい果物は手軽に食べることができ、栄養素も多く含まれています。健康的な生活を送るために、果物を積極的に取り入れてみましょう。

ちゃんと進捗が表示されてますね。

まとめ

ということでプランナーを試してみました。
ここでは SequentialPlanner を使いましたが他にも 1 つだけスキルを選ぶ ActionPlanner というものもあります。今後もしかしたら他にもプランナーが追加されるかもしれません。

基本的には Kernel にスキルを追加してプランナーでプランを作って実行!といった流れになります。実際のアプリに組み込むときはプランが出来たタイミングでユーザーに確認してもらってから実行したり、実行中には進捗を表示したりといったことをすると良いと思います。

ということで今日はプランナーを試してみました。

Microsoft (有志)

Discussion