Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

Semantic Kernelを使ってGPTと外部ツールを簡単に連携してみる

こんにちは。最近湿度が上がってきてつらい@Ssk1029Takashiです。
最近当社ではAzure OpenAI Serviceを活用した検索ソリューションに取り組んでおり、私も開発として携わっています。
www.acroquest.co.jp

そんな中でもOpenAIのGPT周りのアップデートが激しく、GPT-4のリリースなどニュースに事欠きません。
特にChatGPT PluginsというChatGPTと外部のデータソースやツールなどを連携する枠組み発表され、よりChatGPTにできることが広がっています。
その中で先月MicrosoftがSemantic KernelというSDKを発表しました。

Semantic KernelとはGPT-3などの大規模言語モデルアプリ開発に統合するC#で開発されたOSSSDKです。
これを使うことで、ChatGPT PluginsのようにGPTと外部ツールを組み合わせたアプリが簡単に開発できるようになります。
今回は、このSemantic Kernelを使ってElasticsearch検索した結果の要約を出力するアプリを作ってみます。

Semantic Kernelとは

Semantic KernelとはMicrosoftが発表した大規模言語モデルを使って外部ツールと連携したアプリ開発ができるようになるSDKです。
詳しくは以下の記事で説明されています。
qiita.com

簡単に動作イメージの例を図にすると以下のようになります。

個人的にSemantic Kernelのうれしいポイントは以下の2つになります。

  1. Plannerという機能を使えばユーザーの入力に対してどのスキルをどの順番に実行するか分解できる
  2. スキルはテンプレートを使ってGPTに聞くものと自分で作ったプログラムを実行するものが定義できる

それぞれのポイントについて簡単に説明します。

Planner機能

一つ目のポイントについては、Semantic Kernelではスキルの組み合わせ方が主に2通りあります。

  1. プログラムとして事前にどの順番に実行するか実装しておいてそのロジック通りに実行する
  2. Planner機能を使ってプロンプトからどのスキルをどの順番に実行するかGPTに推測させる

例えばアプリに検索スキルと要約スキルが登録されている状態で、ユーザーが「Pythonについて検索した結果を要約して下さい」という入力をしたとします。
そうすると、Planner機能はこの入力から検索スキル→要約スキルという順番で実行すると結果を得られる、というように入力をタスクごとに分解して実行計画を作成します。

スキル拡張

2つ目のポイントについて、Semantic Kernelでは自分で作成できるスキルには2種類あります

  1. Semantic Function: テンプレートを使ってGPTを呼び出しGPTの出力を結果として返すスキル
  2. Native Function: 自分で作成した関数を実行して結果を返すスキル

これにより、要約などGPTを駆使したスキルを作ってもよいし、Elasticsearchを検索するなど外部ツールを活用したスキルも作れるようになります。

実際に作ってみる

ここまでSemantic Kernelについてポイントを説明したので実際に作ってみます。
冒頭で書いた通り、今回作るアプリはElasticsearch検索した結果を要約できるアプリなので、以下の手順が必要になります。

  1. 要約するSemantic Functionとキーワード抽出するSemantic Functionを作成する
  2. Elasticsearchに検索するNative Functionを作成する
  3. ユーザーの入力から適切なスキルを呼び出して結果を得るプログラムを作成する

それでは順番にやっていきましょう。

1. Semantic Functionを作る

まずは、Semantic Functionを作成します。
Semantic Functionの作成のために用意するのは以下の2ファイルになります。

  1. config.json
  2. skprompt.txt

例として、前段の出力を要約したものを出力するスキルを定義してみます。
まず、config.jsonは以下のような内容になっており、アプリの説明や、GPTへのパラメータ、スキルの引数などを定義します。

{
    "schema": 1,
    "type": "completion",
    "description": "Summarize given text or any text document",
    "completion": {
        "max_tokens": 1024,
        "temperature": 0.0,
        "top_p": 0.0,
        "presence_penalty": 0.0,
        "frequency_penalty": 0.0
    },
    "input": {
        "parameters": [
            {
                "name": "input",
                "description": "Text to summarize",
                "defaultValue": ""
            }
        ]
    }
}

特に注意が必要なのは各descriptionの内容になります。
これはPlannerで実行計画を出すときに、GPTがどのツールを使うかを出力するときの入力に使用されます。
なので、このdescriptionの記述が明確でなかったり、事実と異なるという場合には適切にPlannerが実行されません。

次に、skprompt.txtの内容は以下のようになっています。

Summarize input sentence in three lines in Japanese.

input={{$input}}
summary=

内容としてはGPTに入力するプロンプトをテンプレートとして書いています。
スキルの引数は{{$input}}の部分に挿入されます。

このようにSemantic Functionではスキル自体の定義とGPTへの入力テンプレートを定義することで作成します。

2. Native Functionを作る

次にプログラムを動作させるNative Functionを作成します。
Native Functionについても用意するものはシンプルで以下のようなC#ソースコードを用意するだけです。

using Microsoft.SemanticKernel.SkillDefinition;
using Microsoft.SemanticKernel.Orchestration;

namespace <スキルを配置するディレクトリ名に合わせる>;

public class <スキル名>
{
    [SKFunction("<作成するFunctionの説明>")]
    public string Search(string input)
    {
        <ここに実行するコードを書く>
    }
}

今回は入力された文字列でElasticsearchを検索して、検索結果を返すので、以下のように実装します。

using Microsoft.SemanticKernel.SkillDefinition;
using Microsoft.SemanticKernel.Orchestration;
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;

namespace MySkillsDirectory;

public class MyRetrieverSkill
{
    private static ElasticsearchClientSettings settings = new ElasticsearchClientSettings(new Uri("xxx"))
            .Authentication(new BasicAuthentication("xxx", "xxx"));
    private ElasticsearchClient client = new ElasticsearchClient(settings);

    [SKFunction("Search for documents in Japanese relevant to your input and return 1 document")]
    public string Search(string input)
    {
        var response = client.Search<Blog>(s => s 
            .Index("target-document")
            .From(0)
            .Size(1)
            .Query(q => q
                .Match(c => c
                    .Field(b => b.Content)
                    .Query(input)
                ) 
            )
        );
        var document = response.Documents.First();
        string result = "title:" + document.Title + "\n" + "content:" + document.Content;
        return result.Substring(0,1500);
    }
}

public class Blog
{
    public string? Title { get; set; }
    public string? Content { get; set; }
}

今回Elasticsearchには当ブログの直近5記事分をtitleに記事タイトル、contentに本文を入れています。

{
  "title": "NFLのPlayer Contact Detectionで金メダル獲得&コンペ振り返り",
  "content": """
  皆さんこんにちは
機械学習チームYAMALEXの@tereka114です。
YAMALEXは Acroquest 社内で発足した、会社の未来の技術を創る、機械学習がメインテーマのデータサイエンスチームです。
~~~"""
}

作成したスキルを配置する

上記で作成したスキルはプロジェクト内のディレクトリに配置します。
以下のようなディレクトリ構成になります。

プロジェクトディレクトリ
├── MySkillsDirectory
│   ├── MyRetrieverSkill
│   │   └── SummaryDocument
│   │       ├── config.json
│   │       └── skprompt.txt
│   └── MyRetrieverSkill.cs

また、注意が必要なのはスキルを配置するディレクトリ名はNative Functionのコード中のnamespaceで定義した値に合わせましょう。
今回でいうと、MySkillsDirectoryとなっています。

実際に動かしてみる

ここまででスキルの作成はできたので実際に動かしてみましょう。

まずは、以下のコードでOpenAI APIを使用する準備とスキルのインポートを行います。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.CoreSkills;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.KernelExtensions;

using MySkillsDirectory;

IKernel kernel = Microsoft.SemanticKernel.Kernel.Builder.Build();
kernel.Config.AddOpenAITextCompletionService(
    "davinci", "text-davinci-003", ""
);

// Plannerスキルを使用できるようにする。
var planner = kernel.ImportSkill(new PlannerSkill(kernel));

var skillsDirectory = Path.Combine(System.IO.Directory.GetCurrentDirectory(), "MySkillsDirectory");
// Semantic Functionをインポート
kernel.ImportSemanticSkillFromDirectory(skillsDirectory, "MyRetrieverSkill");
// Native Functionをインポート
kernel.ImportSkill(new MyRetrieverSkill(), "MyRetrieverSkill");

次にPlannerを使って入力プロンプトから実行計画を立ててみましょう。

var ask = "GPTをアシスタントとして使うための情報があるか検索して要約してください";
var originalPlan = await kernel.RunAsync(ask, planner["CreatePlan"]);

Console.WriteLine("Original plan:\n");
Console.WriteLine(originalPlan.Variables.ToPlan().PlanString);

すると以下のようにXMLで作成された実行計画が出力されます。

<goal>
GPTをアシスタントとして使うための情報があるか検索して要約してください
</goal>
<plan>
  <function.MyRetrieverSkill.Search input="GPTをアシスタントとして使うための情報" setContextVariable="SEARCH_RESULT"/>
  <function.MyRetrieverSkill.SummaryDocument input="$SEARCH_RESULT" setContextVariable="SUMMARY_RESULT"/>
  <function._GLOBAL_FUNCTIONS_.BucketOutputs input="$SUMMARY_RESULT" bucketCount="3" bucketLabelPrefix="Result" />
</plan>

想定通り、検索→要約で回答が得られるというように計画を立ててくれています。
さらに、検索スキルに入力される文字列が「GPTをアシスタントとして使うための情報」となっているので、ユーザーの入力から自動で抽出してくれているようです。

ためしに少し複雑な命令を入れてみましょう。
「GPTに関して、アシスタントとして使う方法と、出力が事実か確かめる方法についてそれぞれ検索して結果をまとめて要約してください」
この場合だと2回検索が走りそれぞれの結果をまとめて要約という形が想定されますが、Plannerの結果はどうなるでしょうか。

<goal>
GPTに関して、アシスタントとして使う方法と、出力が事実か確かめる方法についてそれぞれ検索して結果をまとめて要約してください
</goal>
<plan>
  <function.MyRetrieverSkill.Search input="GPT and how to use it as an assistant" setContextVariable="SEARCH_RESULT_1"/>
  <function.MyRetrieverSkill.Search input="GPT and how to verify the output is true" setContextVariable="SEARCH_RESULT_2"/>
  <function._GLOBAL_FUNCTIONS_.BucketOutputs input="$SEARCH_RESULT_1" bucketCount="1" bucketLabelPrefix="RESULT_1"/>
  <function._GLOBAL_FUNCTIONS_.BucketOutputs input="$SEARCH_RESULT_2" bucketCount="1" bucketLabelPrefix="RESULT_2"/>
  <function.MyRetrieverSkill.SummaryDocument input="$RESULT_1;$RESULT_2" appendToResult="RESULT__SUMMARY"/>
</plan>

検索のinputが英語になっていますが、実行計画としては期待通りになってそうです。
SummaryDocumentのinputで検索結果がまとめられていますね。すごい。。

Plannerで実行計画を立てた後はそれを順番に実行していくだけです。
以下のようにスキルの終了状態を見ながら順番に実行していきます。

var executionResults = originalPlan;
int step = 1;
int maxSteps = 10;
while (!executionResults.Variables.ToPlan().IsComplete && step < maxSteps)
{
    var results = await kernel.RunAsync(executionResults.Variables, planner["ExecutePlan"]);
    if (results.Variables.ToPlan().IsSuccessful)
    {
        Console.WriteLine($"Step {step} - Execution results:\n");
        Console.WriteLine(results.Variables.ToPlan().PlanString);

        if (results.Variables.ToPlan().IsComplete)
        {
            Console.WriteLine($"Step {step} - COMPLETE!");
            Console.WriteLine(results.Variables.ToPlan().Result);
            break;
        }
    }
    else
    {
        Console.WriteLine($"Step {step} - Execution failed:");
        Console.WriteLine(results.Variables.ToPlan().Result);
        break;
    }
    
    executionResults = results;
    step++;
    Console.WriteLine("");
}

上記で「GPTをアシスタントとして使うための情報があるか検索して要約してください」を通して実行してみると以下のような出力が得られます。

Original plan:

<goal>
GPTをアシスタントとして使うための情報があるか検索して要約してください
</goal>
<plan>
  <function.MyRetrieverSkill.Search input="GPTをアシスタントとして使うための情報" setContextVariable="SEARCH_RESULT"/>
  <function.MyRetrieverSkill.SummaryDocument input="$SEARCH_RESULT" setContextVariable="SUMMARY_RESULT"/>
  <function._GLOBAL_FUNCTIONS_.BucketOutputs input="$SUMMARY_RESULT" bucketCount="3" bucketLabelPrefix="Result" />
</plan>
Step 1 - Execution results:

<goal>
GPTをアシスタントとして使うための情報があるか検索して要約してください
</goal><plan>
  <function.MyRetrieverSkill.SummaryDocument input="$SEARCH_RESULT" setContextVariable="SUMMARY_RESULT" />
  <function._GLOBAL_FUNCTIONS_.BucketOutputs input="$SUMMARY_RESULT" bucketCount="3" bucketLabelPrefix="Result" />
</plan>

Step 2 - Execution results:

<goal>
GPTをアシスタントとして使うための情報があるか検索して要約してください
</goal><plan>
  <function._GLOBAL_FUNCTIONS_.BucketOutputs input="$SUMMARY_RESULT" bucketCount="3" bucketLabelPrefix="Result" />
</plan>

Step 3 - Execution results:

<goal>
GPTをアシスタントとして使うための情報があるか検索して要約してください
</goal><plan>
</plan>
Step 3 - COMPLETE!
Ssk1029TakashiはGPT-3を使ってAIアシスタントを作る第一歩を検証します。命令文からタスクの種類、時間情報、コンテンツ内容、誰がを抜き出す必要があります。GPT-3は事前学習の段階でInformation Extractionに近いタスクをすでに学習していることが推測されます。試してみると、素の言語モデルで解けてしまうことがわかりました。

Elasticsearchに入っているブログを検索してほしい結果を取得することができました。
順番に実行している様子もわかります。

まとめ

今回はMicrosoft発のSemantic Kernelを使って、ユーザーの入力を解釈して外部ツールと連携するアプリを作ってみました。
こちらがロジックを決めなくても入力をGPTが解釈してできるというのは夢が広がりますね。
スキルを作れば拡張もしやすいので様々な局面で使用できそうです。
一応、Pythonで書かれた機能を限定したPreviewのブランチもあるので、こちらも期待したいところです。
それではまた。

Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

www.wantedly.com