🫠

【LangChain】で【Transformer】を利用する場合に【chat_template】を設定する方法【エラー対処記事】

2025/02/10に公開

はじめに

今回の内容は、以前の記事でも触れた内容ではありますが、エラー対処記事として誰かの役に立つかもなと思い、独立して記事にしようと思います。

Trasfomersの使い方

基本的な使い方

まずはじめに、Transformersの使い方を整理したいと思います。

Transformersを利用する上でのサンプルコードは下記です。
下記のコードを実行すれば、HuggingfaceのモデルをTransformers上で動作させることができます。

transformers.py
# pip install transformers torch accelerate

from transformers import AutoModelForCausalLM, AutoTokenizer

# モデル名

model_name = "elyza/Llama-3-ELYZA-JP-8B"

# モデルのロード
llama = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype="auto",
    device_map="auto",

# トークナイザーのロード
tokenizer = AutoTokenizer.from_pretrained(
    model_name,
)

messages = [
    {"role": "system", "content": "あなたは日本語を話す優秀なアシスタントです。"},
    {"role": "user", "content": "日本で一番高い山は?"}
]

text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True
)

model_inputs = tokenizer([text], return_tensors="pt").to(llama.device)

tokens = llama.generate(
    **model_inputs,
    max_new_tokens=1024,
    do_sample=False
)

# 応答部分だけを抽出
generated_text = tokenizer.batch_decode(tokens)[0]
print(f"Generated Text:\n{generated_text}")

# プロンプト部分を除去
# プロンプト部分を取得(`input_ids` をデコード)
prompt_text = tokenizer.decode(model_inputs["input_ids"][0])
print(f"Prompt Text:\n{prompt_text}")

# 応答部分のみを抽出
response = generated_text[len(prompt_text):].strip()
print(f"Response:\n{response}")

流れとしては単純です

  • モデルとトークナイザーを定義する
  • systemプロンプトとuserプロンプトを辞書型で定義する
  • tokenizer.apply_chat_templateで、適切なchat_templateにプロンプトを埋め込む
  • トークナイザーで自然言語のプロンプトをトークン化
  • lama.generateメソッドにて、モデルに入力し、文章を生成
  • トークナイザーでdocodeして自然言語を出力
    • 入力したプロンプトも一緒に出力されるので、モデルの出力だけを表示するように処理
  • 出力されたテキストを表示する

チャットテンプレート(chat_template)とは

今回の記事で重要なのは、このchat_templateの部分です。
コード的には下記の部分が関連します。

transformers.py

messages = [
    {"role": "system", "content": "あなたは日本語を話す優秀なアシスタントです。"},
    {"role": "user", "content": "日本で一番高い山は?"}
]

text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True
)

LLMの前提

大前提として、LLMがやっていることは、「与えられた次の単語を生成する」です。
これを何度も自己回帰的に繰り返すことで、長文を生成することができます。

例えば、日記の続きなどを出力するのは簡単です。
モデルに

今日は朝早く起きて、

というふうに入力しておけば、勝手に、

動物園に行きました。動物園ではキリンやゾウなどを久しぶりに見ました。何年ぶりに見ただろうか?その雄々しい・・・・

のように、どんどん続きを生成してくれます。これが基本的なLLM(のベースモデル)の動作です。

チャットモデル(Instructモデル)では

チャットモデル(Instructモデル)でも基本的には同じです。
ただし、チャットモデルでは、systemプロンプトとuserプロンプトというものがあります。
また、特性として、LLMの回答はuserプロンプトの続きを生成するのではなく、別人の回答として生成する必要があります。

そこで、どこからどこまでが、systemプロンプトで、どこからどこまでがuserプロンプトで、どこからがAIの出力結果なのかを明示的に示した状態で、LLMを学習しています。
したがって、推論時も学習した時と同じフォーマットでモデルに入力する必要があります。

この明示的にするための文法やタグのルールのことを「チャットテンプレート」と呼びます。

例えば、llama3系統のモデルのチャットテンプレートは下記のような形です。

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

あなたは日本語を話す優秀なアシスタントです。<|eot_id|><|start_header_id|>user<|end_header_id|>

日本で一番高い山は?<|eot_id|><|start_header_id|>assistant<|end_header_id|>
日本で一番高い山は、北海道にある「旭岳」で、標高は2,291メートルです。<|eot_id|>

ここで、systemプロンプトは「あなたは日本語を話す優秀なアシスタントです。」で、userプロンプトは「日本で一番高い山は?」と設定しています。
そして、AIの回答が「日本で一番高い山は、北海道にある「旭岳」で、標高は2,291メートルです。」になります。(残念ながら回答は間違えていますね)

このタグが入っている文章の形で、llama3は学習されています。
推論時は、上記のうち、下記の部分を最初に入力し、次の単語を出力します
この手続きを、<|eot_id|>トークンを出力するまで、自己回帰的に処理します。

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

あなたは日本語を話す優秀なアシスタントです。<|eot_id|><|start_header_id|>user<|end_header_id|>

日本で一番高い山は?<|eot_id|><|start_header_id|>assistant<|end_header_id|>

そして、辞書型のプロンプトから、このチャットテンプレートを反映させた形に整形するのが、下記のtokenizer.apply_chat_templateメソッドです。

transformers.py

messages = [
    {"role": "system", "content": "あなたは日本語を話す優秀なアシスタントです。"},
    {"role": "user", "content": "日本で一番高い山は?"}
]

text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True
)

print(text)
#print(text)のoutput
#<|begin_of_text|><|start_header_id|>system<|end_header_id|>
#
#あなたは日本語を話す優秀なアシスタントです。<|eot_id|><|start_header_id|>user<|end_header_id|>
#
#日本で一番高い山は?<|eot_id|><|start_header_id|>assistant<|end_header_id|>

チャットテンプレート(chat_template)はどこから見れる?

さて、このチャットテンプレートは、
モデルにより異なります

なぜなら、このチャットテンプレートを改善することも、モデルの性能に効いてくるため、世代が変わればチャットテンプレートが変わることも珍しくないからです。
最初は、マークダウンのように簡単なチャットテンプレートでしたが、今はちょっと複雑なタグなどが利用されていたりすることが、その証拠です。

しかし、そうなると、tokenizer.apply_chat_templateメソッドはどうやって、モデルに合わせたチャットテンプレートを選んで、適切な形にプロンプトを整形しているのでしょうか?

これは、Huggingfaceモデルのtokenizer_config.jsonファイルに、chat_template属性が保存されているからできています。

例えば、上記のサンプルコードで利用したelyza/Llama-3-ELYZA-JP-8Bモデルのtokenizer_config.jsonファイルを見てみましょう。

下記です。
https://huggingface.co/elyza/Llama-3-ELYZA-JP-8B/blob/main/tokenizer_config.json

下記の画像の通り、下の方にchat_template属性が保存されていることがわかります。

ちなみに全文は下記になります。(整形しました)

tokenizer_config.json
{% set loop_messages = messages %}
{% for message in loop_messages %}
    {% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' + message['content'] | trim + '<|eot_id|>' %}
    {% if loop.index0 == 0 %}
        {% set content = bos_token + content %}
    {% endif %}
    {{ content }}
{% endfor %}
{% if add_generation_prompt %}
    {{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}
{% endif %}

これは、Jinja2 テンプレートエンジンの構文で書かれているそうです。
上から見ていきます。

チャットテンプレートの解説

変数の設定

tokenizer_config.json
{% set loop_messages = messages %}

これより、トークナイザーに入力されたプロンプトmessagesを元に、loop_messages変数を作成します。

ループの設定

tokenizer_config.json
{% for message in loop_messages %}
    {% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' + message['content'] | trim + '<|eot_id|>' %}
・・・
{% endfor %}

ここでは、ループを設定しています。各ロールごとにチャットテンプレートの行を作成します。
また、message['content'] | trimでは、パイプ処理が入っており、前半のメッセージの中の空白を除去しています。
上記の処理が完了すると、チャットテンプレートのうち下記の部分が完成します。

<|start_header_id|>system<|end_header_id|>
#あなたは日本語を話す優秀なアシスタントです。<|eot_id|><|start_header_id|>user<|end_header_id|>

日本で一番高い山は?<|eot_id|>

残りは、テンプレートの最初を示すトークン<|begin_of_text|>と、AIの出力に入ることを示すトークンを前後に追加する必要があります。

テンプレートの最初を示すトークン

tokenizer_config.json
    {% if loop.index0 == 0 %}
        {% set content = bos_token + content %}
    {% endif %}

ループの中の処理にて、ループの最初にbos_tokenを、contentの前に設定しています。
bos_tokentokenizer_config.jsonに設定されています。

したがって、<|begin_of_text|>トークンが一番最初に追加されます。

AIの出力を示すトークンを追加

tokenizer_config.json
{% if add_generation_prompt %}
    {{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}
{% endif %}

の部分にて、最後にトークンを追加しています。
ただし、if add_generation_promptが成立するときは、です。

下記の通り、add_generation_promptにTrueを設定しているため、このif文が成立します。

transformers.py
text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True
)

また、その直前に{{ content }}があると思います。
この二重の{{}}の中にある変数や文字列は、その場に埋め込まれます。

したがって、ここまでcontentの中に格納されている文字列は確定して利用されるということになります。
その後、ここでは{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}をなっているので、{{ content }}の後にくっつく構成になります。

別のモデルのチャットテンプレート

また、例えば上記はllama3系統のチャットテンプレートですが、llama3.1やllama3.3はチャットテンプレートがちょっと変わります。
(llama3.1とllama3.3のチャットテンプレートは完全に一致しています)

下記はmeta-llama/Llama-3.3-70B-Instructモデルのチャットテンプレートを示します。

meta-llama/Llama-3.3-70B-Instruct

llama3.3系統は、llama3系統に加えて、カットオフの日付が入るようになっていたり、tool functionが利用できるようになっていたり、json出力や、code_interpreterが利用できるようになっていることもあり、それが可能なようにチャットテンプレートがリッチになっているようです。

ただし、プロンプトを整理するタグなどの基本的な部分は、llama3系統と同じです。
なので、上記のリッチな機能を利用しない場合は、llama3のチャットテンプレートをそのまま利用しても、実はちゃんと回答してくれます。

tokenizer_config.json
{{- bos_token }}
{%- if custom_tools is defined %}
    {%- set tools = custom_tools %}
{%- endif %}
{%- if not tools_in_user_message is defined %}
    {%- set tools_in_user_message = true %}
{%- endif %}
{%- if not date_string is defined %}
    {%- set date_string = "26 Jul 2024" %}
{%- endif %}
{%- if not tools is defined %}
    {%- set tools = none %}
{%- endif %}

{# Extract the system message, so we can slot it into the right place. #}
{%- if messages[0]['role'] == 'system' %}
    {%- set system_message = messages[0]['content']|trim %}
    {%- set messages = messages[1:] %}
{%- else %}
    {%- set system_message = "" %}
{%- endif %}

{# System message + builtin tools #}
{{- "<|start_header_id|>system<|end_header_id|>\n\n" }}
{%- if builtin_tools is defined or tools is not none %}
    {{- "Environment: ipython\n" }}
{%- endif %}
{%- if builtin_tools is defined %}
    {{- "Tools: " + builtin_tools | reject('equalto', 'code_interpreter') | join(", ") + "\n\n" }}
{%- endif %}
{{- "Cutting Knowledge Date: December 2023\n" }}
{{- "Today Date: " + date_string + "\n\n" }}
{%- if tools is not none and not tools_in_user_message %}
    {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }}
    {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }}
    {{- "Do not use variables.\n\n" }}
    {%- for t in tools %}
        {{- t | tojson(indent=4) }}
        {{- "\n\n" }}
    {%- endfor %}
{%- endif %}
{{- system_message }}
{{- "<|eot_id|>" }}

{# Custom tools are passed in a user message with some extra guidance #}
{%- if tools_in_user_message and not tools is none %}
    {# Extract the first user message so we can plug it in here #}
    {%- if messages | length != 0 %}
        {%- set first_user_message = messages[0]['content']|trim %}
        {%- set messages = messages[1:] %}
    {%- else %}
        {{- raise_exception("Cannot put tools in the first user message when there's no first user message!") }}
    {%- endif %}
    {{- '<|start_header_id|>user<|end_header_id|>\n\n' }}
    {{- "Given the following functions, please respond with a JSON for a function call " }}
    {{- "with its proper arguments that best answers the given prompt.\n\n" }}
    {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }}
    {{- "Do not use variables.\n\n" }}
    {%- for t in tools %}
        {{- t | tojson(indent=4) }}
        {{- "\n\n" }}
    {%- endfor %}
    {{- first_user_message + "<|eot_id|>" }}
{%- endif %}

{# Process each message #}
{%- for message in messages %}
    {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %}
        {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' + message['content'] | trim + '<|eot_id|>' }}
    {%- elif 'tool_calls' in message %}
        {%- if not message.tool_calls|length == 1 %}
            {{- raise_exception("This model only supports single tool-calls at once!") }}
        {%- endif %}
        {%- set tool_call = message.tool_calls[0].function %}
        {%- if builtin_tools is defined and tool_call.name in builtin_tools %}
            {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' -}}
            {{- "<|python_tag|>" + tool_call.name + ".call(" }}
            {%- for arg_name, arg_val in tool_call.arguments | items %}
                {{- arg_name + '="' + arg_val + '"' }}
                {%- if not loop.last %}, {% endif %}
            {%- endfor %}
            {{- ")" }}
        {%- else %}
            {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' -}}
            {{- '{"name": "' + tool_call.name + '", ' }}
            {{- '"parameters": ' }}
            {{- tool_call.arguments | tojson }}
            {{- "}" }}
        {%- endif %}
        {%- if builtin_tools is defined %}
            {{- "<|eom_id|>" }}
        {%- else %}
            {{- "<|eot_id|>" }}
        {%- endif %}
    {%- elif message.role == "tool" or message.role == "ipython" %}
        {{- "<|start_header_id|>ipython<|end_header_id|>\n\n" }}
        {%- if message.content is mapping or message.content is iterable %}
            {{- message.content | tojson }}
        {%- else %}
            {{- message.content }}
        {%- endif %}
        {{- "<|eot_id|>" }}
    {%- endif %}
{%- endfor %}
{%- if add_generation_prompt %}
    {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }}
{%- endif %}

チャットテンプレート(chat_template)が設定されていないモデル

ここまでで、チャットテンプレートを見ていましたが、中にはtokenizer_config.jsonchat_templateが設定されていないモデルがあります。

例えば、現時点でOpen Japanese LLM Leaderboardにて、topの性能として評価されているnitky/Llama-3.1-SuperSwallow-70B-Instruct-v0.1モデルに関しては、chat_templatetokenizer_config.jsonに設定されていません。

(これはReadMeにも記載されており、チャットテンプレートを別途指定するように記載されています。)

こういうモデルに対しては、チャットテンプレートを指定する方法があります。

Transformersにてchat_templateを自分で設定する方法

例えば、サンプルコードを下記のように修正することで、chat_templateを後から設定して、tokenizer.apply_chat_templateを実行できます。

transformers.pyコード全文
transformers.py
# pip install transformers torch accelerate

from transformers import AutoModelForCausalLM, AutoTokenizer

# モデル名

model_name = "nitky/Llama-3.1-SuperSwallow-70B-Instruct-v0.1"

# モデルのロード
llama = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype="auto",
    device_map="auto",
)

# トークナイザーのロード
tokenizer = AutoTokenizer.from_pretrained(
    model_name,
)

tokenizer.chat_template = """
{%- if custom_tools is defined %}
    {%- set tools = custom_tools %}
{%- endif %}
{%- if not tools_in_user_message is defined %}
    {%- set tools_in_user_message = true %}
{%- endif %}
{%- if not date_string is defined %}
    {%- set date_string = "26 Jul 2024" %}
{%- endif %}
{%- if not tools is defined %}
    {%- set tools = none %}
{%- endif %}

{#- This block extracts the system message, so we can slot it into the right place. #}
{%- if messages[0]['role'] == 'system' %}
    {%- set system_message = messages[0]['content']|trim %}
    {%- set messages = messages[1:] %}
{%- else %}
    {%- set system_message = "" %}
{%- endif %}

{#- System message + builtin tools #}
{{- "<|start_header_id|>system<|end_header_id|>\n\n" }}
{%- if builtin_tools is defined or tools is not none %}
    {{- "Environment: ipython\n" }}
{%- endif %}
{%- if builtin_tools is defined %}
    {{- "Tools: " + builtin_tools | reject('equalto', 'code_interpreter') | join(", ") + "\n\n" }}
{%- endif %}
{{- "Cutting Knowledge Date: December 2023\n" }}
{{- "Today Date: " + date_string + "\n\n" }}
{%- if tools is not none and not tools_in_user_message %}
    {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }}
    {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }}
    {{- "Do not use variables.\n\n" }}
    {%- for t in tools %}
        {{- t | tojson(indent=4) }}
        {{- "\n\n" }}
    {%- endfor %}
{%- endif %}
{{- system_message }}
{{- "<|eot_id|>" }}

{#- Custom tools are passed in a user message with some extra guidance #}
{%- if tools_in_user_message and not tools is none %}
    {#- Extract the first user message so we can plug it in here #}
    {%- if messages | length != 0 %}
        {%- set first_user_message = messages[0]['content']|trim %}
        {%- set messages = messages[1:] %}
    {%- else %}
        {{- raise_exception("Cannot put tools in the first user message when there's no first user message!") }}
    {%- endif %}
    {{- '<|start_header_id|>user<|end_header_id|>\n\n' -}}
    {{- "Given the following functions, please respond with a JSON for a function call " }}
    {{- "with its proper arguments that best answers the given prompt.\n\n" }}
    {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }}
    {{- "Do not use variables.\n\n" }}
    {%- for t in tools %}
        {{- t | tojson(indent=4) }}
        {{- "\n\n" }}
    {%- endfor %}
    {{- first_user_message + "<|eot_id|>" }}
{%- endif %}

{%- for message in messages %}
    {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %}
        {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' + message['content'] | trim + '<|eot_id|>' }}
    {%- elif 'tool_calls' in message %}
        {%- if not message.tool_calls|length == 1 %}
            {{- raise_exception("This model only supports single tool-calls at once!") }}
        {%- endif %}
        {%- set tool_call = message.tool_calls[0].function %}
        {%- if builtin_tools is defined and tool_call.name in builtin_tools %}
            {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' -}}
            {{- "<|python_tag|>" + tool_call.name + ".call(" }}
            {%- for arg_name, arg_val in tool_call.arguments | items %}
                {{- arg_name + '="' + arg_val + '"' }}
                {%- if not loop.last %}
                    {{- ", " }}
                {%- endif %}
            {%- endfor %}
            {{- ")" }}
        {%- else %}
            {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' -}}
            {{- '{"name": "' + tool_call.name + '", ' }}
            {{- '"parameters": ' }}
            {{- tool_call.arguments | tojson }}
            {{- "}" }}
        {%- endif %}
        {%- if builtin_tools is defined %}
            {#- This means we're in ipython mode #}
            {{- "<|eom_id|>" }}
        {%- else %}
            {{- "<|eot_id|>" }}
        {%- endif %}
    {%- elif message.role == "tool" or message.role == "ipython" %}
        {{- "<|start_header_id|>ipython<|end_header_id|>\n\n" }}
        {%- if message.content is mapping or message.content is iterable %}
            {{- message.content | tojson }}
        {%- else %}
            {{- message.content }}
        {%- endif %}
        {{- "<|eot_id|>" }}
    {%- endif %}
{%- endfor %}
{%- if add_generation_prompt %}
    {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }}
{%- endif %}"""


messages = [
    {"role": "system", "content": "あなたは日本語を話す優秀なアシスタントです。"},
    {"role": "user", "content": "日本で一番高い山は?"}
]

text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True
)

model_inputs = tokenizer([text], return_tensors="pt").to(llama.device)

tokens = llama.generate(
    **model_inputs,
    max_new_tokens=1024,
    do_sample=False
)

# 応答部分だけを抽出
generated_text = tokenizer.batch_decode(tokens)[0]
print(f"Generated Text:\n{generated_text}")

# プロンプト部分を除去
# プロンプト部分を取得(`input_ids` をデコード)
prompt_text = tokenizer.decode(model_inputs["input_ids"][0])
print(f"Prompt Text:\n{prompt_text}")

# 応答部分のみを抽出
response = generated_text[len(prompt_text):].strip()
print(f"Response:\n{response}")

重要なのは下記の部分です。

transformer.py
tokenizer.chat_template = """
{%- if custom_tools is defined %}
・・・
"""

今回利用したモデル、nitky/Llama-3.1-SuperSwallow-70B-Instruct-v0.1はllama3.1系統のモデルのため、llama3.1と同じチャットテンプレートを利用しました。

このように設定することで、モデルにチャットテンプレートが定義されていなかったとしても、後から定義することができます。

langchainでTransformersを利用する方法

さて、もう少しで本題ですが、その前にもう一つだけ準備をさせてください。
ここまでで、transformersとチャットテンプレートに関して記載しました。

ここからは、そもそもTransformersのモデルをLangChainで利用する方法について記載します。

HuggingfaceモデルをLangChainで利用する場合は、HuggingFaceEndpointを利用する方法とHuggingFacePipelineを利用する方法の2つがあります。
https://note.com/npaka/n/nbe332ad7c9f8

しかし今回は、HuggingFacePipelineの方を利用させてください。
こちらの方法を利用することで、一般的なTransformersモジュールの使い方とほぼ同じようにモデルを定義できます。

したがって、LangChain用に下記の通りにサンプルコードを変更します。

LangChainでtransformersで利用するサンプルコード
transformers_Langchain.py
# pip install transformers torch accelerate langchain langchain_core langchain-community huggingface_hub langchain-huggingface

from langchain_huggingface import ChatHuggingFace, HuggingFacePipeline
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

# モデル名

model_name = "elyza/Llama-3-ELYZA-JP-8B"

# モデルのロード
llama = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype="auto",
    device_map="auto",
)

# トークナイザーのロード
tokenizer = AutoTokenizer.from_pretrained(
    model_name,
)

pipe = pipeline(
    "text-generation", model=llama, tokenizer=tokenizer, temperature = 1.0, do_sample=True, max_new_tokens=1024
)

pipe = HuggingFacePipeline(pipeline=pipe)
llm = ChatHuggingFace(llm=pipe)


messages = [
    ("system","あなたは日本語を話す優秀なアシスタントです。"),
    ("human", "{user_input}")
]

query = ChatPromptTemplate.from_messages(messages)
output_parser = StrOutputParser()

#出力から入力プロンプトを消すためには、下記の一文を代わりに利用する
#chain = query | llm.bind(skip_prompt=True) | output_parser
chain = query | llm | output_parser
response = chain.invoke({"user_input":"日本で一番高い山は?"})

print(response)

詳細は下記で説明します。
LangChain自体の利用方法は、参考文献の書籍や、私の過去の記事をご覧ください。
下記では、Langchainで利用できるようにするための、定義の部分を説明します。

モデルの定義

transformers_Langchain.py
# モデルのロード
llama = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype="auto",
    device_map="auto",
)

トークナイザーの定義

transformers_Langchain.py
# トークナイザーのロード
tokenizer = AutoTokenizer.from_pretrained(
    model_name,
)

Transformersのpipelineを定義

transformers_Langchain.py
pipe = pipeline(
    "text-generation", model=llama, tokenizer=tokenizer, temperature = 1.0, do_sample=True, max_new_tokens=1024
)

LangChainのHuggingFacePipelineを定義
Transformersで定義したパイプラインを、LangChainのパイプラインに設定し直す。

transformers_Langchain.py
pipe = HuggingFacePipeline(pipeline=pipe)

LangChainのChatHuggingFaceを定義

transformers_Langchain.py
llm = ChatHuggingFace(llm=pipe)

上記のように設定できれば、あとは通常のlangchainで利用するように、使うことができます。
どんなモデルであっても、共通のコードが利用できるのがlangchainのいいところですね。

使う時は下記のように利用できます。

transformers_Langchain.py
messages = [
    ("system","あなたは日本語を話す優秀なアシスタントです。"),
    ("human", "{user_input}")
]

query = ChatPromptTemplate.from_messages(messages)
output_parser = StrOutputParser()

#出力から入力プロンプトを消すためには、下記の一文を代わりに利用する
#chain = query | llm.bind(skip_prompt=True) | output_parser
chain = query | llm | output_parser
response = chain.invoke({"user_input":"日本で一番高い山は?"})

print(response)

Transformersのtokenizerにchat_templateを自分で設定する落とし穴

さて、ここからが本題です。
問題なのは、Trandformersの時と同様に、モデル自体にchat_template属性が定義されていない場合です。

Transformersの場合と同様に、下記のように設定することを考えます。

transformers_Langchain.py

・・・(以前は、transformers_Langchain.pyと同様)・・・

# トークナイザーのロード
tokenizer = AutoTokenizer.from_pretrained(
    model_name,
)

# ここでチャットテンプレートを設定する
tokenizer.chat_template = """
{%- if custom_tools is defined %}
・・・(省略)・・・
"""

pipe = pipeline(
    "text-generation", model=llama, tokenizer=tokenizer, temperature = 1.0, do_sample=True, max_new_tokens=1024
)

pipe = HuggingFacePipeline(pipeline=pipe)
llm = ChatHuggingFace(llm=pipe)

・・・(以後、transformers_Langchain.pyと同様)

上記のように設定すると、本来であればちゃんと設定されるはずです。
実際に下記のコードでデバックしても、ちゃんと設定されていることがわかります。

print(tokenizer.chat_template)
print(llm.llm.pipeline.tokenizer.chat_template)

しかし、この状態で実行すると下記のエラーが表示されます。

File "/home/sagemaker-user/LLM_Evaluation_Elyza/env/lib/python3.11/site-packages/transformers/tokenization_utils_base.py", line 1785, in get_chat_template
    raise ValueError(
ValueError: Cannot use chat template functions because tokenizer.chat_template is not set and no template argument was passed! For information about writing templates and setting the tokenizer.chat_template attribute, please see the documentation at https://huggingface.co/docs/transformers/main/en/chat_templating

これは、チャットテンプレートが定義されていないことによるエラーメッセージです。
ここがめちゃくちゃ沼でした。
エラーメッセージが出ている状態でも、下記を実行するとちゃんとchat_templateが表示されるので、なぜ設定されているのかが分からなかったです。

print(tokenizer.chat_template)
print(llm.llm.pipeline.tokenizer.chat_template)

解決策

ひたすら沼った末に、下記のissueを発見しました。
https://github.com/langchain-ai/langchain/issues/26656

どうやら、tokenizer.chat_templateでデフォルトのテンプレートと違うテンプレートを、自分で設定する場合は、下記のように書く必要があるらしいです。

llm = ChatHuggingFace(llm=pipe, tokenizer=pipe.pipeline.tokenizer)

ChatHuggingFaceには、llm属性として、pipeを設定しているにも関わらず、まさかtokenizer属性にもpipe.pipeline.tokenizerを設定しないといけないとは思いませんでした。

上記のように設定して、実行することで、問題なくLLMを利用することができました。

下記の書籍のRAGの章でも、HuggingfaceモデルをLangChainで利用しており、そちらにも上記の解決策と同様の記述が記載されておりました。(細かすぎて理解できていなかった・・・)
大規模言語モデル入門Ⅱ〜生成型LLMの実装と評価

まとめ

自分がかなり沼った内容で記事を書きました。
エラー対処記事の予定でしたが、いつもの解説記事のような感じにもなってるかなと思います。
いろんな方の参考になれば幸いです。

ここまで読んでくださってありがとうございます。

参考文献

LangChainとLangGraphによるRAG・AIエージェント[実践]入門
ChatGPT/LangChainによるチャットシステム構築[実践]入門
LangChainを利用することで、あらゆるモデルを統一的なコードで実行できるようになります。
langchainに関しては、こちらの書籍を読めば大体のことはできるようになりますので、おすすめです。

大規模言語モデル入門Ⅱ〜生成型LLMの実装と評価
RAGの章ではありますが、HuggingFaceモデルをLangChainで利用する際のサンプルコードも記載されております。
細かい内容すぎて見逃していたのが悔やまれます・・・

https://note.com/npaka/n/nbe332ad7c9f8

https://note.com/tatsuyashirakawa/n/n0aa9169c99d5

https://huggingface.co/docs/transformers/en/chat_templating

(書籍のリンクはamazonアフィリエイトリンクになります)

Discussion