連載第2回です。本記事ではSemantic KernelでChatGPT(gpt-35-turbo)を使用し、チャットをするSemantic functionを実装して、チャット機能を実現します。
※本領域は変化が激しいです。内容は23年7月9日時点の情報となります
まずはじめに、本連載記事で最終的に完成するアプリケーションの動作の様子を示します。本連載の最後にはこのような動作をするAIアシスタント(※Jupyter Notebook)が完成します(※こちら第1回記事に掲載していた動画と同じものです)。
(内容)最初に雑談を交わし、その後、Azureのストレージアカウントを作成してもらいます。動画の最後に、きちんと作成されたかをAzureポータル上で確認します。
【目次】
第1回 Microsoft Semantic Kernelとは
・Semantic Kernelを一言で表すと?
・Semantic Kernelの公式情報集
・Semantic Kernelの構成要素
・Semantic Kernelを効率良く勉強する方法(Python版)
・Semantic KernelをChatGPT(gpt-35-turbo)、GPT-4で使用する際の注意点
[link: 第1回記事はこちら]
(本記事)
第2回 Semantic KernelでChatGPTを使用したチャットボットを作成
・Microsoft Semantic Kernelのインストール
・Azure OpenAI ServiceのAPI情報を記載したenvファイルを作成
・Kernelのインスタンスを作成
・Semantic functionとしてスキル名(Plugins名)JarvisのChat functionを作成追加
・JarvisのChat機能の動作確認
・会話の初期化関数を定義(messagesを初期化する)
第3回以降 to be continued
今回の記事から、実装をベースに解説を進めます。
【本記事の実装コードのGitHubリポジトリはこちらです】
(注)実装コードはGoogle Claboratory環境で動作を確認しています
本連載ではLLMとして「Azure OpenAI Serviece」を使用します(言語はPythonです)。
「Semantic Kernel」自体はAzureに限定するOSS(SDK)ではないため、「OpenAI」のGPT-4やgpt-35-turboのAPIを利用してもまったく同じことが実現できます。
Azure OpenAI Serviceの場合とOpenAIのAPIの場合、それぞれでの使い方については、「semantic-kernel/samples/notebooks/python/」の「サンプルNotebook」の00番にて詳しく解説されています。
第2章 Semantic KernelでChatGPTを使用したチャットボットを作成
Microsoft Semantic Kernelのインストール
# パッケージのインストール
!pip install semantic-kernel==0.3.1.dev0
バージョンは「0.3.1.dev0」を使用します。リリース日は2023年6月13日です。
PyPI:semantic-kernel
これでインストールは終了です。すぐにSemantic Kernelが使用できます。
Azure OpenAI ServiceのAPI情報を記載したenvファイルを作成
Azure OpenAI ServiceでChatGPT(gpt-35-turbo)をデプロイしておいてください。
そしてその情報を取得します。
必要なのは、「API_key」、「endpoint(URL)」、「デプロイした名前」の3つとなります。
本来は、Azure Key Vaultの利用などでセキュアに行きたいですが、今回はひとまず簡単にベタ打ちします
※注意:ベタ打ちしたコードをGitHubなどにpushしないように注意してください!
# gpt-35-turbo(0613)
%%writefile .env
AZURE_OPENAI_API_KEY="98hogehoge"
AZURE_OPENAI_ENDPOINT="https://azureopenaihogehoge.azure.com/"
AZURE_OPENAI_DEPLOYMENT_NAME="deployhogehoge
上記のhogehogeの部分をご自身の環境のものに書き換えてください。
これで「.env」ファイルに情報が用意できました。
Kernelのインスタンスを作成
それではメインとなる「Semantic Kernel」のKernelクラスのインスタンスを作成します。
このKernelインスタンスが、LLM系の部分と従来のプログラム部分とを繋ぐ役割を果たします。
import semantic_kernel as sk
import semantic_kernel.connectors.ai.open_ai as sk_oai
# [1] Semantic Kernelのインスタンスを作成
kernel = sk.Kernel()
# [2] envファイルから情報取得
# (azure key vault等を使用してセキュアにいきたいですが今は簡易に)
# 【Azure OpenAI Service】の場合
deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()
これで、Kernelのインスタンス「kernel
」の用意と、「.env」ファイルに書き込んだ情報の読み込みが完了しました。
続いて、kernel
に「.env」の情報を渡して、LLM(今回はChatGPT)を使用できるようにします。
# [3] Semantic Kernelに追加するCompletionサービスを用意します
completion_service = sk_oai.AzureChatCompletion(deployment, endpoint, api_key)
一往復だけ用のTextCompletionと、何往復もするChatCompletionがありますが、今回はChatCompletionを使用しています。
このchat_serviceをkernel
に追加します。
# [4] Semantic Kernelに上記のcompletion_serviceを、chat_serviceとして追加します
kernel.add_chat_service("chat-gpt-jarvis", completion_service)
上記の実装の引数一つ目の"chat-gpt-jarvis"は適当なid名です。
Semantic functionとしてスキル名(Plugins名)JarvisのChat functionを作成追加
それでは「Semantic function」としてChat functionを作成します。
AI assistantといえば、「J.A.R.V.I.S.」が想像しやすいので、本連載ではPlugin名(スキル名)をJarvisとします。
続く、Semantic functionの定義方法が、従来のGPT-3系と、ChatGPTやGPT-4は作り方が異なります。
第1回でも紹介しましたが、従来のGPT-3系ですと、Semantic functionのディレクトリ構造は以下のように「GetIntent」フォルダの中に「skprompt.txt」と「config.json」を用意しています。
MS日本の解説動画のスライド、202304_Azure OpenAI Developers Seminar.pdf
のp. 87のスライド左側部分を以下に掲載します。
セマンティック関数の「skprompt.txt」と「config.json」の中身が記載されています。
「config.jsonのパラメータで、skeprompt.txtの内容でプロンプトが送られる関数なんだな~」、「{{$input}}にプログラムから文章をプロンプトに挿入でき、それを3つのポイントに要約させるのだな~」と、なんとなく理解できます。
また、semantic-kernel/samples/notebooks/python/の「02-running-prompts-from-file.ipynb」にて、詳しく実装解説されています。
02-running-prompts-from-file.ipynb
ただし、以下のようなドキュメントが用意されていることからも分かるように、ChatGPT(gpt-35-turbo)や、GPT-4はそれまでのGPT-3系とは使用方法が少し異なるので、「Semantic Kernel」で使用する際も使い方が異なります。
大雑把には、
- messagesで会話を管理する点
- roleとして、system、user、assistantを扱うという点
が、大きな違いです。
この違いに対応したSemantic functionの実装方法は次の通りになります。
# プロンプトの設定
# 【注意】ここからGPT3-系までとは異なり、ChatGPT(gpt-3.5系)、GPT-4系特有
# [3-1] APIが返すtoken数やランダムさを設定
prompt_config = sk.PromptTemplateConfig.from_completion_parameters(
max_tokens=800, temperature=0.5, top_p=0.5
)
# [3-2] 会話のtemplateを作成(Semantic KernelのGPT-3系のスキルのskprompt.txt内と同様に、
# {{$user_input}}などを使用して構築します)
prompt_template = sk.ChatPromptTemplate(
"{{$user_input}}", kernel.prompt_template_engine, prompt_config
)
ChatPromptTemplateクラスを使用して、messagesに対応できるようにします。
続いて、messagesに初期設定のpromptを格納していきます。
# [3-3] 1つ目の「"role": "system"」のcontent内容をmessagenに設定します
system_message = """
You are a chat bot. Your name is Jarvis in Japanese ジャービス and you have one goal: figure out what people need.
Your full name, should you need to know it, is Just A Rather Very Intelligent System.
You communicate effectively and tend to give short, precise answers.
"""
prompt_template.add_system_message(system_message)
# system_messageはもっと短い例ですと、
# "Assistant is a large language model trained by OpenAI."だけもありです。
add_system_messageメソッドを使用します。続いて、userとassistantです。
# [3-4] 最初の1往復の会話
# ("role": "user"のcontentと、"role": "assistant"のcontent)を
# messagesに追加します
prompt_template.add_user_message("Hi there, who are you?")
prompt_template.add_assistant_message(
"I am Jarvis, a chat bot. I'm trying to figure out what people need."
)
これで、ChatGPTやGPT-4を使用するための初期のprompt3つを格納できました。
最後に以下のようにして、Semantic functionとしてスキル(Plugin)JarvisのChat functionをkernel
に追加します。
# [3-5] Semantic functionとしてスキルJarvis(Plugin)に
# function名ChatとしてSenmantic Kernelに追加
function_config = sk.SemanticFunctionConfig(prompt_config, prompt_template)
chat_function = kernel.register_semantic_function(
skill_name="Jarvis", function_name="Chat", function_config=function_config
)
以上で完了です。
ここで、「いや、何もfunction内容を定義していないのでは?」と感じるかもしれません。
今回はChatGPTのチャット機能を使用するだけです。
ユーザーの入力をChatGPTに送り、それに対する出力を受け取るだけのため、以上で完了となります(その部分は上記の[3-2]にて定義されています)。
このように、ChatGPTやGPT-4を使用したSemantic function(≒LLMのAPIを叩く関数)は、「skprompt.txt」と「config.json」を用意して、ファイルから読み込む作戦ではなく、「ChatPromptTemplate」(とPromptTemplateConfig)を使用して構築します。
(注1)GPT-3系でも「skprompt.txt」と「config.json」を用意せず、上記と同様にしてプログラム上で(=inlineで)、Semantic functionを作成できます。ただしその場合も上記の実装とは少し異なります。
(注2)現時点ではChatGPT、GPT-4を使用する場合、事前に用意したファイルを読み込んでSemantic functionを構築できるのかは不明です。私は上記のinlineに実装する方法しか実現できていません。
JarvisのChat機能の動作確認
実装が完了したので動作を確認します。
Kernelのfunctionに変数(入力したい文章の内容)を渡す方法は、
- contextに埋め込む方法(contextに変数を辞書のように追加する)
- ContextVariableseクラスを使用する方法(contextとは別変数で用意する)
の2通りがあります。
今回はContextVariablseクラスを使用します。
# [1] 入力文の作成
context_vars = sk.ContextVariables()
user_input = "お名前はなんですか?"
context_vars["user_input"] = user_input
ここでuser_input
とは、[3-2]でプロンプト内に挿入すると設定した変数名となっています。
# [2] 上記の入力を送って、回答を得ます
answer = await kernel.run_async(chat_function, input_vars=context_vars)
print(f"Jarvis:> {answer}")
(出力)Jarvis:> 私の名前はジャービスです。
messageのroll:systemの文章に、システムの名前としてJarvis(ジャービス)と記載しておいたので、きちんと自分の名前を答えてくれました。
このようにチャットができます。
さらにチャットを続けてみます。
# [3] さらに会話を続けます
context_vars["user_input"] = "私は小川雄太郎といいます。"
answer = await kernel.run_async(chat_function, input_vars=context_vars)
print(f"Jarvis:> {answer}")
(出力)Jarvis:> はじめまして、小川雄太郎さん。何かお手伝いできることはありますか?
出力はtemperature=0.5, top_p=0.5と設定しているため、文章の生成は確率的で不確実性を持ち、出力文章が毎回同じとは限りません。
ただし、ほぼ同じ内容にはなるかと思います。
続けて、先ほど会話で伝えた私の名前を憶えているか確かめてみます。
# [4] さらに会話を続けます
context_vars["user_input"] = "あれ、私の名前は何でしたっけ"
answer = await kernel.run_async(chat_function, input_vars=context_vars)
print(f"Jarvis:> {answer}")
(出力)おっしゃる通り、お名前は小川雄太郎さんですね。ご安心ください。何かお困りのことはありますか?
今回はサービスとして、一往復だけのTextCompletionではなく、何往復もするChatCompletionを使用しています。
そのため会話内容の一時保存は実装者が明示的に行わなくても、会話内容がmessagesに残っており、そのため、私の名前を答えることができます。
第2回の最後に、ここまでの会話内容を初期化する関数を定義します。
会話履歴が残ったままでは面倒なことも多いので、初期化関数は重要です。
(注)この初期化関数は自分で定義せずとも、SDK側に用意されているのだろうか?ひとまず見つからない。Semantic KernelはまだPythonはAPIのドキュメントすら整っていないので、分からない・・・
会話の初期化関数を定義(messagesを初期化する)
会話内容は変数messagesに蓄えられています。
会話をリセットしたい場合には、変数messagesに追加された会話を消して、デフォルトで設定した最初の3つだけを残すようにします。
# [1] チャットの全体の長さ(messagesの長さ)をチェック
print(len(prompt_template._messages))
# -> 9 となるはずです。最初にdefault設定で、system、user、assistantの3つを追加しました
# その後、会話を3往復したのので、3 + 3x2 = 9 です
(出力)9
# [2] messagesの中身を確認しましょう
print(prompt_template._messages[7])
print("------------")
print(prompt_template._messages[7][0])
print(prompt_template._messages[7][1]._template)
# ._templateに追加されたstrが格納されています
print(prompt_template._messages[8][0])
print(prompt_template._messages[8][1]._template)
(出力)
('user', <semantic_kernel.semantic_functions.prompt_template.PromptTemplate object at 0x7fe148ddbd60>)
------------
user
あれ、私の名前は何でしたっけ
assistant
おっしゃる通り、お名前は小川雄太郎さんですね。ご安心ください。何かお困りのことはありますか?
このように、prompt_template._messagesに、これまでのChat内容のroleとcontentが追加されているので、初期に設定した先頭3つだけを残す関数を定義します。
# [3] 初期の3つのmessage設定に戻す関数を定義します
def init_chat_messages(prompt_template):
"""prompt_templateに溜まったmessagesを初期設定した最初の3つの状態に戻します"""
prompt_template._messages = prompt_template._messages[:3]
# [4] 初期化の動作確認
# 初期化実施
init_chat_messages(prompt_template)
print(len(prompt_template._messages))
# 会話
context_vars["user_input"] = "私の名前は何でしたっけ"
answer = await kernel.run_async(chat_function, input_vars=context_vars)
print(f"Jarvis:> {answer}")
# messagesの長さが3になりました。
# そして会話の履歴がなくなっているので、私の名前を答えることができなくなりました
(出力)3 Jarvis:> 私にはあなたの名前はわかりません。お名前を教えていただけますか?
# [5] 初期化の動作確認その2
# 初期化実施
init_chat_messages(prompt_template)
print(len(prompt_template._messages))
# 会話
context_vars["user_input"] = "お名前はなんですか?"
answer = await kernel.run_async(chat_function, input_vars=context_vars)
print(f"Jarvis:> {answer}")
# 初期の3つの設定は残っているので、自分の名前は答えられます
(出力)3 Jarvis:> 私の名前はジャービスです。
以上で初期化関数の動作確認は完了です。
先ほどは会話内容が残っていたため、私の名前をきちんと答えてくれていましたが、初期化後には答えらなくなっています。
またmessagesも内容が3つになっており、先頭から3つ=初期の3つ、のみが残っています。
最後に再度初期化しておきます。
# [6] 最後に初期化
init_chat_messages(prompt_template)
以上で、「Semantic_Kernel」で「ChatGPT(gpt-35-turbo)」を(skill_name="Jarvis", function_name="Chat")という設定の「Semantic function」として使用できるようになりました。
これで、Python言語のプログラム内で、ChatGPTと会話し放題です!
(Azure OpenAIのAPI叩いているコストはかかります)。
【本記事の実装コードのGitHubリポジトリはこちらです】
(注)実装コードはGoogle Claboratory環境で動作を確認しています
次回、第3回では、Semantic Kernelの「Native function」を作成し、Jarvisを起動させるfunctionを作成します。そして今回作成したChat functionの両方を含む、Jarvisの「Plugins」を作成します。
to be continued...
小川 雄太郎
【免責】
本記事の内容は執筆者の意見/発信であり、執筆者が属する企業等の公式見解ではございません