CCCMKホールディングス TECH LABの Tech Blog

TECH LABのエンジニアが技術情報を発信しています

ブログタイトル

LangChainのprompt周りの機能を手を動かしながら理解してみる!

こんにちは、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はnameweatherという入力を受け付けます。

from langchain.prompts import PromptTemplate,ChatPromptTemplate

prompt_template = PromptTemplate.from_template(
  "こんにちは、{name}さん。今日の天気は{weather}です。どんな服がいいでしょう?"
)

chat_prompt_template = ChatPromptTemplate.from_template(
  "こんにちは、{name}さん。今日の天気は{weather}です。どんな服がいいでしょう?"
)

prompt_templateStringPromptValue形式のpromptを、chat_prompt_templateChatPromptValue形式のpromptを生成します。これらのprompt templateからpromptを生成するには、.format_promptを使ってtemplateの中のnameweatherの具体的な値を入力してあげます。

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のライブラリchromadbtiktokenが必要なので別途インストールしておきます。

では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という面白い機能があって、そこも分かったような分かっていないような状態なので、今回と同じように手を動かして試しながら今度まとめてみたいと思います。