こんにちは、CCCMKホールディングス TECH LABの三浦です。
80年を秒数に表すと、80x365x24x60x60で、約25億になります。2.5billion(2.5b)ですね。大規模言語モデル(LLM)のパラメータ数がよくbillion単位で表現されているので、ちょっと不思議な感覚になります。
LLMを使ったアプリケーション開発に便利なLangChainというフレームワークを使っているのですが、機能がとても豊富な反面、整理しきれずにちょっと頭がこんがらがってきているな・・・と感じていました。色々なことが出来ることは理解してきたのですが、それらを使いこなせるほど身に付けられていない、という感じです。
なのでこれからはドキュメントを読むだけでなく、実際に手を動かしながら理解を深めていこう!と思い立ち、少しずつコードを書きながらLangChainの機能を理解し、まとめていくことにしました。今回はLangChainにおいて、そしてLLMを扱う上においても重要なprompt周りのLangChainの機能を色々試してみたので、まとめてみたいと思います。
今回参考にしたのはLangChainのPythonのドキュメントのPrompts Moduleに関するページです。
Prompts — 🦜🔗 LangChain 0.0.183
prompt templateの基本
LangChainにはprompt templateというpromptを生成する機能があります。LangChainにおけるpromptは大きく2種類に分けることが出来、1つは"text-davinci-003"をはじめとするLLMsへの入力用のStringPromptValue
で、もう一つは "gpt-35-turbo"をはじめとするChat Modelへの入力用のChatPromptValue
です。StringPromptValue
はその名の通りテキストで構成されるpromptですが、ChatPromptValue
はrole(発言者)とcontent(発言内容)で構成されるmessageのリストで構成されるpromptです。
具体的に見ていきます。まずはそれぞれのprompt templateを生成します。このprompt templateはname
とweather
という入力を受け付けます。
from langchain.prompts import PromptTemplate,ChatPromptTemplate prompt_template = PromptTemplate.from_template( "こんにちは、{name}さん。今日の天気は{weather}です。どんな服がいいでしょう?" ) chat_prompt_template = ChatPromptTemplate.from_template( "こんにちは、{name}さん。今日の天気は{weather}です。どんな服がいいでしょう?" )
prompt_template
はStringPromptValue
形式のpromptを、chat_prompt_template
はChatPromptValue
形式のpromptを生成します。これらのprompt templateからpromptを生成するには、.format_prompt
を使ってtemplateの中のname
とweather
の具体的な値を入力してあげます。
prompt_value = prompt_template.format_prompt(weather='晴れ',name='AI') print(type(prompt_value)) print(prompt_value)
出力結果は以下の様になります。
<class 'langchain.prompts.base.StringPromptValue'> text='こんにちは、AIさん。今日の天気は晴れです。どんな服がいいでしょう?'
chat_prompt_value = chat_prompt_template.format_prompt(weather='晴れ',name='AI') print(type(chat_prompt_value)) print(chat_prompt_value)
出力結果は以下の様になります。
<class 'langchain.prompts.chat.ChatPromptValue'> messages=[HumanMessage(content='こんにちは、AIさん。今日の天気は晴れです。どんな服がいいでしょう?', additional_kwargs={}, example=False)]
prompt templateを生成する時、ここでは.from_template
を使用しました。.from_template
を使うと入力テキスト内の{name}
のような記述の部分を自動的にinput_variables
として認識してくれます。input_variables
を明示的に指定してprompt templateを生成する方法もあります。以下のような方法です。
prompt_template_explicity = PromptTemplate( input_variables=["weather","name"], template="こんにちは、{name}さん。今日の天気は{weather}です。どんな服がいいでしょう?" )
partial prompt templateによる一部変数の固定
.format_prompt
を使用する時は全てのinput_variables
を入力しないとエラーが出てしまいます。しかし最初に1部分の変数だけとりあえず入力しておき、残りの変数は後で用途に応じて入力する、といったことが出来たらよい場合もあります。これは.partial
を使うことで実現できます。
name
だけを固定します。
partial_prompt_value = prompt_template.partial(
name='AI')
残りの変数weather
には.format_prompt
で値を入力します。
print(partial_prompt_value.format_prompt(weather='晴れ')) print(partial_prompt_value.format_prompt(weather='雨'))
生成されたpromptを表示した結果です。
text='こんにちは、AIさん。今日の天気は晴れです。どんな服がいいでしょう?' text='こんにちは、AIさん。今日の天気は雨です。どんな服がいいでしょう?'
独自のprompt templateを作る
独自のprompt templateを定義することも出来ます。これまで見たようにPromptTemplate
でも好きなテキストを使ってprompt templateを作ることが出来ますが、ベースになるテキストを、使うたびに都度コード内に書くのは不便だったりします。独自のprompt templateを作ることで、それらをすべてモジュールとして取り扱うことが出来るようになります。
ここでは料理名を受け取るとそのレシピをLLMに生成させるためのpromptを作成するprompt templateを自作しています。
from langchain.prompts import StringPromptTemplate from pydantic import BaseModel, validator class RecipeMakerTemplate(StringPromptTemplate, BaseModel): """ このクラスは料理名(dish_name)を受け取るとそのレシピを Azure OpenAIで生成して返す機能を持っています。 """ @validator("input_variables") def validate_input_variables(cls, v): """ input_variablesに"dish_name"が入っているか検証する """ if len(v) != 1 and "dish_name" in v: raise ValueError("dish_name must be the only input_variables") return v def format(self, **kwargs) -> str: """ input_variablesで指定された"dish_name"を埋め込んだpromptを生成する """ prompt = f""" 指定された料理を作るためのレシピを作成してください。 料理:{kwargs["dish_name"]} レシピ: """ return prompt def _prompt_type(self): return "recipe-maker"
自作のprompt templateを使ってpromptを生成してみます。
recipe_maker_template = RecipeMakerTemplate( input_variables=["dish_name"]) print(recipe_maker_template.format_prompt(dish_name='カレー'))
出力結果
text='\n 指定された料理を作るためのレシピを作成してください。\n 料理:カレー\n レシピ:\n '
このように独自のpromptをスッキリしたコードで生成することが出来るようになります。
prompt templateとfew show examplesの分離
promptにはモデルへの指示と例示を含めることが多いです。いつも指示と例示をどちらも同じテキストの中に含めてしまい、ごちゃごちゃしてしまうことが多かったのですが、FewShotPromptTemplate
を使うことでprompt templateの作成、つまりprompt全体の大枠の作成と、例示(few shot examples)の作成を切り分けて行うことが出来ます。
LLMにお題にそった内容のクイズを考えさせるpromptを作ります。まずpromptに含めるfew shot examplesを以下の様に用意します。
examples = [ { "question":"お寿司に関するクイズを考えてください。", "answer":""" Intermediate Question:実在するお寿司の種類を1つ挙げてください。 Intermediate Answer:かっぱ巻き Intermediate Question:かっぱ巻きの材料を教えて下さい。 Intermediate Answer:きゅうり So the final answer is:かっぱ巻きは何を使ったお寿司ですか??(答え:きゅうり) """ }, { "question":"雲に関するクイズを考えてください。", "answer":""" Intermediate Question:実在する雲の種類を1つ挙げてください。 Intermediate Answer:いわし雲 Intermediate Question:いわし雲のいわしとは、何のことですか? Intermediate Answer:魚の種類です。 So the final answer is:魚の種類が名前に含まれる雲はなんですか??(答え:いわし雲) """ }, { "question":"ファッションに関するクイズを考えてください。", "answer":""" Intermediate Question:幅広い年代で着られている服の種類を1種類挙げてください。 Intermediate Answer:ジーンズ Intermediate Question:ジーンズの歴史の発祥について教えて下さい。 Intermediate Answer:ジーンズは1800年代後半にアメリカで誕生しました。 So the final answer is:ジーンズが誕生した国はどこですか??(答え:アメリカ) """ } ]
ここで挙げたfew shot examplesをpromptにするためのprompt templateを定義します。
example_prompt = PromptTemplate( input_variables=["question","answer"], template="Qustion: {question}\n{answer}")
では具体的に1つ例を与え、どんなpromptが生成されるか見てみます。
print(example_prompt.format_prompt(**examples[0]))
text='Qustion: お寿司に関するクイズを考えてください。\n\n Intermediate Question:実在するお寿司の種類を1つ挙げてください。\n Intermediate Answer:かっぱ巻き\n Intermediate Question:かっぱ巻きの材料を教えて下さい。\n Intermediate Answer:きゅうり\n So the final answer is:かっぱ巻きは何を使ったお寿司ですか??(答え:きゅうり)\n '
そしたらprompt全体を生成するためのprompt templateをFewShotPromptTemplate
を使って定義します。
from langchain.prompts import FewShotPromptTemplate prompt_template = FewShotPromptTemplate( examples=examples, example_prompt=example_prompt, prefix="ユーザーからのQuestionに答えてください。\n", suffix="Question: {input}", input_variables=["input"])
オプションのprefix
はpromptの最初に含めるテキスト、suffix
は最後に含めるテキストを指定することが出来ます。このprompt templateで生成されるpromptを表示します。
prompt = prompt_template.format_prompt( input="お菓子に関するクイズを考えてください。") print(prompt)
text='ユーザーからのQuestionに答えてください。\n\n\nQustion: お寿司に関するクイズを考えてください。\n\n Intermediate Question:実在するお寿司の種類を1つ挙げてください。\n Intermediate Answer:かっぱ巻き\n Intermediate Question:かっぱ巻きの材料を教えて下さい。\n Intermediate Answer:きゅうり\n So the final answer is:かっぱ巻きは何を使ったお寿司ですか??(答え:きゅうり)\n \n\nQustion: 雲に関するクイズを考えてください。\n\n Intermediate Question:実在する雲の種類を1つ挙げてください。\n Intermediate Answer:いわし雲\n Intermediate Question:いわし雲のいわしとは、何のことですか?\n Intermediate Answer:魚の種類です。\n So the final answer is:魚の種類が名前に含まれる雲はなんですか??(答え:いわし雲)\n \n\nQustion: ファッションに関するクイズを考えてください。\n\n Intermediate Question:幅広い年代で着られている服の種類を1種類挙げてください。\n Intermediate Answer:ジーンズ\n Intermediate Question:ジーンズの歴史の発祥について教えて下さい。\n Intermediate Answer:ジーンズは1800年代後半にアメリカで誕生しました。\n So the final answer is:ジーンズが誕生した国はどこですか??(答え:アメリカ)\n \n\nQuestion: お菓子に関するクイズを考えてください。'
せっかくなのでこのpromptをLLMに与え、クイズを作らせてみます。
from langchain.llms import AzureOpenAI llm = AzureOpenAI( deployment_name="text-davinci-003", temperature=0) response = llm(prompt) print(response)
結果
Intermediate Question:実在するお菓子の種類を1つ挙げてください。 Intermediate Answer:チョコレート Intermediate Question:チョコレートの起源はどこですか? Intermediate Answer:メキシコ So the final answer is:チョコレートの起源はどこですか??(答え:メキシコ)
Wikipediaの"チョコレートの歴史"の"前史"にはメソアメリカというメキシコを含む地域でのカカオの栽培と飲用について書かれており、この回答自体は間違いではないようです。 参考: チョコレートの歴史 - Wikipedia
example selectorによる使用する例の選択
先ほどは3つのfew shot examplesを用意しました。examplesがもっとたくさんあった時、全てをpromptに含めようとするとpromptのトークンサイズが制限を超えてしまう可能性が出てきます。そこでinputに応じてそのinputの内容に意味が近いexampleだけを選択してpromptに含めるといった対応が必要になります。LangChainではexample selectorという機能でそれを実現することが出来ます。
今回はSemanticSimilarityExampleSelector
というexample selectorを使ってみました。これはEmbedding Modelによって埋め込み表現を取得して、その近さで入力とexamplesとの比較を行って選択させる方法です。Embedding Modelを使うため、Pythonのライブラリchromadb
とtiktoken
が必要なので別途インストールしておきます。
ではSemanticSimilarityExampleSelector
でexample selectorを定義します。
from langchain.prompts.example_selector import SemanticSimilarityExampleSelector from langchain.vectorstores import Chroma from langchain.embeddings import OpenAIEmbeddings example_selector = SemanticSimilarityExampleSelector.from_examples( examples, OpenAIEmbeddings( model="text-embedding-ada-002", deployment="text-embedding-ada-002", chunk_size=1), Chroma, k=1)
オプションk
は取得するexampleの数を指定しています。example_selector
がどのように機能するかを見てみます。
question = "お菓子に関するクイズを考えてください。" selected_examples = example_selector.select_examples( {"question":question}) for example in selected_examples: print("\n") for k, v in example.items(): print(f"{k}: {v}")
出力結果
question: お寿司に関するクイズを考えてください。 answer: Intermediate Question:実在するお寿司の種類を1つ挙げてください。 Intermediate Answer:かっぱ巻き Intermediate Question:かっぱ巻きの材料を教えて下さい。 Intermediate Answer:きゅうり So the final answer is:かっぱ巻きは何を使ったお寿司ですか??(答え:きゅうり)
"お菓子に関するクイズを考えてください。"という入力に最も近いと判断されたexampleはお寿司に関するクイズのもので、確かに他のexampleと比較してどちらも食べ物に関するものなので、一番意味が近いものと言えます。
使うときは入力に対して都度もっとも近いexampleを選択することになります。先ほどのFewShotPromptTemplate
を定義する時に、example_selector
を指定するとそれを実現することが出来ます。
prompt_template = FewShotPromptTemplate( example_selector=example_selector, example_prompt=example_prompt, prefix="ユーザーからのQuestionに答えてください。\n", suffix="Question: {input}", input_variables=["input"])
ではこのprompt templateで生成したpromptをLLMに入力してみます。
prompt = prompt_template.format( input="お菓子に関するクイズを考えてください。") response = llm(prompt) print(response)
出力結果
Intermediate Question:実在するお菓子の種類を1つ挙げてください。 Intermediate Answer:プリン Intermediate Question:プリンの材料を教えて下さい。 Intermediate Answer:牛乳 So the final answer is:プリンは何を使ったお菓子ですか??(答え:牛乳)
さっきのチョコレートのクイズとは雰囲気が違うクイズになりました。
prompt templateのファイル出力と読み込み
最後に、作ったprompt templateをjsonファイルに出力する方法と読み込んで復元する方法をまとめます。先ほどのFewShotPromptTemplate
で作ったprompt templateをファイルに出力したい、という場合を考えます。
example selectorを使ったFewShotPromptTemplate
のprompt templateのファイル出力は現在未対応とのことで、実行しようとすると"ValueError: Saving an example selector is not currently supported"が発生しました。なのでexamplesを直接指定した以下のFewShotPromptTemplate
で考えます。
prompt_template = FewShotPromptTemplate( examples=examples, example_prompt=example_prompt, prefix="ユーザーからのQuestionに答えてください。\n", suffix="Question: {input}", input_variables=["input"])
これを"best_prompt.json"という名前のjsonファイルに出力するには以下を実行します。
prompt_template.save("best_prompt.json")
ではこのファイルを読み込んでprompt templateを復元します。
from langchain.prompts import load_prompt reuse_prompt_template = load_prompt("best_prompt.json") prompt = reuse_prompt_template.format(input="お菓子に関するクイズを考えてください。") print(prompt)
出力結果は以下の様になり、元のprompt templateを使って生成されたpromptであることが確認出来ます。
ユーザーからのQuestionに答えてください。 Qustion: お寿司に関するクイズを考えてください。 Intermediate Question:実在するお寿司の種類を1つ挙げてください。 Intermediate Answer:かっぱ巻き Intermediate Question:かっぱ巻きの材料を教えて下さい。 Intermediate Answer:きゅうり So the final answer is:かっぱ巻きは何を使ったお寿司ですか??(答え:きゅうり) Qustion: 雲に関するクイズを考えてください。 Intermediate Question:実在する雲の種類を1つ挙げてください。 ///中略 Question: お菓子に関するクイズを考えてください。
まとめ
ということで、今回はLangChainのprompt周りの便利な機能についてまとめてみました。やっぱり実際に手を動かしながらまとめると頭の中で整理されていいですね・・・。LangChainにはagentという面白い機能があって、そこも分かったような分かっていないような状態なので、今回と同じように手を動かして試しながら今度まとめてみたいと思います。