🧩

LLM毎に最適化されたチャット プロンプトをテンプレートから効率的に作成する

2024/08/15に公開

はじめに

新しい大規模言語モデル(Large Language Models, LLM)が次々とHugging Face Hub上に公開され、LLMを利用したチャットを試す楽しみが増えました。そこで問題となるのがプロンプトの作成方法です。現在のところ、LLMへ入力するプロンプトの形式に共通性は乏しく、LLM毎に要求される形式のプロンプトを入力する必要があります。LLMは自然言語を処理するので、必ずしもプロンプトの形式を厳守する必要はなく柔軟に対応できるのですが、筆者が興味を持っているオンデバイス(ローカル)実行できる、比較的規模の小さいLLM[1]では、そのLLMのプロンプト形式に従わないと(筆者の感想ですが)出力精度が良くない気がします。これにどう対策すべきかと考えていたところ、Hugging Face社のウェブサイトで、Templates for Chat Modelsという記事を見つけ、ほぼ解決方法になりそうなことが解説されていたので、その方法を利用して、筆者自身で試したことを本記事で説明いたします。

チャット プロンプトの形式

例えば、Llama 2 Chatのプロンプト形式は以下のとおりです。

<s>[INST] <<SYS>>
{{ system_prompt }}
<</SYS>>

{{ user_message }} [/INST]

一方、Llama 3 Instructのプロンプト形式は以下のように、同じLlamaモデルでも、かなり変更されています。

<|begin_of_text|><|start_header_id|>system<|end_header_id|>

You are a helpful AI assistant for travel tips and recommendations<|eot_id|><|start_header_id|>user<|end_header_id|>

What can you help me with?<|eot_id|><|start_header_id|>assistant<|end_header_id|>

そのため、複数のLLMに対応するチャット アプリケーションでは、LLM毎にプロンプトを整形するコードを作成して対応するしかないと思っていました。

ところが、Templates for Chat Modelsを読むと、Transformersトーカナイザーにプロンプトのテンプレートが含まれていると書いてあるではありませんか!

Transformers チャット テンプレート

Transformersトーカナイザーが持つチャット プロンプト用のテンプレート(以下、Transformersチャット テンプレート)はtokenizer.chat_template属性に格納され、そのテンプレート自体の形式はJinjaというテンプレート エンジンが定める形式を採用しているそうです。ウィキペディアによると、Jinjaはプログラミング言語Python用のテンプレートエンジンであり、HTMLやXMLだけでなく、どのようなマークアップの文書でも生成できるとあります。[2]

本記事では、このJinjaエンジンには立ち入らず、チャット テンプレートの使い方にフォーカスしたいと思います。

Transformersチャット テンプレートの使い方

ここで説明する内容は、以下のGitHubリポジトリで公開しているJupyterノートブックで実際にお試しいただけます。

https://github.com/tsutof/nlp-notebooks

まずは、transformers.AutoTokenizerモジュールをインポートしましょう。

from transformers import AutoTokenizer

今回の実験では、Transformersの機能の内、トーカナイザーのみを利用します。そのため、PyTorch, TensorFlow, JAXなどのディープラーニング フレームワークがインストールされていない環境でも大丈夫です。そのような環境では、上記インポート時に、以下のような警告が出ますが問題ありません。

None of PyTorch, TensorFlow >= 2.0, or Flax have been found. Models won't be available and only tokenizers, configuration and file/data utilities can be used.

チャット内容は、以下の通り、辞書型で記述したメッセージを、リスト型でまとめて表現します。

chat = [
    {"role": "system", "content": "あなたは日本語ネイティブで親切なAIアシスタントです。"},
    {"role": "user", "content": "こんにちは。ご機嫌いかがですか?"},
    {"role": "assistant", "content": "とても元気です。あなたのお役にたてることがあれば何なりとお尋ねください。"},
    {"role": "user", "content": "大阪の知人へ贈る、東京の土産を提案してください。"},
    {"role": "assistant", "content": ""}
]

上記を入力として、apply_chat_templateメソッドでプロンプト文字列が生成されます。ここでは、Llama 3 Instruct形式のプロンプトを生成します。

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3.1-8B-Instruct")
tokenizer.use_default_system_prompt = False
prompt_string = tokenizer.apply_chat_template(chat, tokenize=False)
print(prompt_string)

以下のように、Llama 3 Instruct形式のプロンプトが生成されました。

<|begin_of_text|><|start_header_id|>system<|end_header_id|>

Cutting Knowledge Date: December 2023
Today Date: 26 Jul 2024

あなたは日本語ネイティブで親切なAIアシスタントです。<|eot_id|><|start_header_id|>user<|end_header_id|>

こんにちは。ご機嫌いかがですか?<|eot_id|><|start_header_id|>assistant<|end_header_id|>

とても元気です。あなたのお役にたてることがあれば何なりとお尋ねください。<|eot_id|><|start_header_id|>user<|end_header_id|>

大阪の知人へ贈る、東京の土産を提案してください。<|eot_id|><|start_header_id|>assistant<|end_header_id|>

<|eot_id|>

今度は、先程と同じ入力から、Llama 2 Chat形式のプロンプトを生成してみましょう。

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
tokenizer.use_default_system_prompt = False
prompt_string = tokenizer.apply_chat_template(chat, tokenize=False)
print(prompt_string)

以下のようにLlama 2 Chat形式のプロンプトが生成されました。

<s>[INST] <<SYS>>
あなたは日本語ネイティブで親切なAIアシスタントです。
<</SYS>>

こんにちは。ご機嫌いかがですか? [/INST] とても元気です。あなたのお役にたてることがあれば何なりとお尋ねください。 </s><s>[INST] 大阪の知人へ贈る、東京の土産を提案してください。 [/INST]

LangChain プロンプト テンプレート

ところで、LangChainにもプロンプト テンプレート機能が存在します。こちらのプロンプト テンプレートは、RAG機能を組み込んだ少し複雑なチェインを構築するときなど、LangChain Expression Language (LCEL)で記述するチェインの一要素として機能します。こちらも試してみましょう。

まずは、langchain_core.prompts.ChatPromptTemplateモジュールをインポートしましょう。

from langchain_core.prompts import ChatPromptTemplate

以下のとおり、メッセージをタプル型で記述して、リスト型でまとめます。記述方法は多少異なりますが、Transformersチャット テンプレートの場合ととても似ています。{question} で記述したところは、後で、実際の質問文に置き換えられます。

prompt_template = ChatPromptTemplate.from_messages([
    ("system", "あなたは日本語ネイティブで親切なAIアシスタントです。"),
    ("user", "こんにちは。ご機嫌いかがですか?"),
    ("assistant", "とても元気です。あなたのお役にたてることがあれば何なりとお尋ねください。"),
    ("user", "{question}"),
    ("assistant", "")
])

どのような、ブロンプトが生成されるか確認してみましょう。

prompt_value = prompt_template.invoke({"question": "大阪の知人へ贈る、東京の土産を提案してください。"})
print(prompt_value.to_string())

以下のように、(LLMの種類を指定する術がないので当然ですが、)LLM毎の形式には従わない、一般的なプロンプトが生成されました。

System: あなたは日本語ネイティブで親切なAIアシスタントです。
Human: こんにちは。ご機嫌いかがですか?
AI: とても元気です。あなたのお役にたてることがあれば何なりとお尋ねください。
Human: 大阪の知人へ贈る、東京の土産を提案してください。
AI: 

Transformers チャット テンプレートと、LangChain プロンプト テンプレートを組み合わせて使う方法

便利なTransformersチャット テンプレートの機能ですが、LangChainと組み合わせて使いたい場合は、どうすれば良いでしょうか?インターネット検索でいろいろと調べましたが、本記事執筆時(2024年8月)現在、どうやら、LangChainは、Transformersチャット テンプレートをサポートしていないようです。そこで、次のような手順を考えました。

  1. Transformersチャット テンプレートの機能でプロンプトを生成する。その際、後で変更したい、ユーザーからの質問文は {question} としておく。{question} をTransformersチャット テンプレートは、特別の記法とは認識せず、単なる文字列として扱うので、そのままプロンプトの一部として出力される。
  2. 上記で、生成されたプロンプト文字列から、langchain_core.prompts.ChatMessagePromptTemplateオブジェクトをfrom_templateクラス メソッドで生成する。(前節でご紹介したChatPromptTemplateとは異なり、一つのメッセージとして取り扱います。)
  3. 生成したオブジェクトを使って、LCELチェインを作成する。後は、通常のLCELチェインとして、LLMとチャットを行う。

早速、試してみましょう。Transformersチャット テンプレートの入力データを作成します。ユーザーからの質問文は {question} となっているところが、以前と異なります。

chat = [
    {"role": "system", "content": "あなたは日本語ネイティブで親切なAIアシスタントです。"},
    {"role": "user", "content": "こんにちは。ご機嫌いかがですか?"},
    {"role": "assistant", "content": "とても元気です。あなたのお役にたてることがあれば何なりとお尋ねください。"},
    {"role": "user", "content": "{question}"},
    {"role": "assistant", "content": ""}
]

Llama 3 Instruct用のプロンプトを生成します。

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3.1-8B-Instruct")
tokenizer.use_default_system_prompt = False
prompt_string = tokenizer.apply_chat_template(chat, tokenize=False)
print(prompt_string)

以下の通り、 {question} はそのまま出力されました。

<|begin_of_text|><|start_header_id|>system<|end_header_id|>

Cutting Knowledge Date: December 2023
Today Date: 26 Jul 2024

あなたは日本語ネイティブで親切なAIアシスタントです。<|eot_id|><|start_header_id|>user<|end_header_id|>

こんにちは。ご機嫌いかがですか?<|eot_id|><|start_header_id|>assistant<|end_header_id|>

とても元気です。あなたのお役にたてることがあれば何なりとお尋ねください。<|eot_id|><|start_header_id|>user<|end_header_id|>

{question}<|eot_id|><|start_header_id|>assistant<|end_header_id|>

<|eot_id|>

langchain_core.prompts.PromptTemplateモジュールをインポートします。

from langchain_core.prompts import PromptTemplate

上記で作成したプロンプト文字列をテンプレートとして、PromptTemplateオブジェクトを作成します。

prompt_template = PromptTemplate.from_template(prompt_string)

このPromptTemplateオブジェクトから、プロンプトを生成してみます。

prompt_value = prompt_template.invoke({"question": "大阪の知人へ贈る、東京の土産を提案してください。"})
print(prompt_value.to_string())

期待通りのプロンプトが生成されました。

<|begin_of_text|><|start_header_id|>system<|end_header_id|>

Cutting Knowledge Date: December 2023
Today Date: 26 Jul 2024

あなたは日本語ネイティブで親切なAIアシスタントです。<|eot_id|><|start_header_id|>user<|end_header_id|>

こんにちは。ご機嫌いかがですか?<|eot_id|><|start_header_id|>assistant<|end_header_id|>

とても元気です。あなたのお役にたてることがあれば何なりとお尋ねください。<|eot_id|><|start_header_id|>user<|end_header_id|>

大阪の知人へ贈る、東京の土産を提案してください。<|eot_id|><|start_header_id|>assistant<|end_header_id|>

<|eot_id|>

LLMでテスト

記事が長くなるので、省略しますが、以下で公開しているJupyterノートブックで、4ビット量子化版のLlama 3.1 8B Instructモデルを利用した例を実際にお試しいただけます。

https://github.com/tsutof/nlp-notebooks

なお、お試しになる場合は、Llama 2とLlama 3.1の利用承認をMeta社から事前に得る必要があります。利用承認はHugging Face社ウェブサイトのMeta Llamaページから可能です。

まとめ

Transformersチャット テンプレートで、各LLMに最適なプロンプトを容易に生成する方法と、それを、LangChainのLCELチェインで使う方法をご紹介しました。LCELチェインで使う方法は、あまりスマートとは言えない自己流ですが、期待通りに動作しました。近い将来、チャットプロンプトの標準化[3]が進めば、本記事の内容は不要になると思いますが、とりあえず、現時点のご参考まで。

脚注
  1. 最近では、小規模言語モデル(Small Language Models, SLM)と呼ぶこともあるようですが、そのように言葉を使い分けると混乱しそうなので、数十億パラメータ規模のモデルでも、大規模言語モデル(Large Language Models, LLM)と呼ぶことにします。 ↩︎

  2. ウィキペディア「Jinja」 ↩︎

  3. OpenAI社が推進するChatMLというものがあるようです。 ↩︎

Discussion