langchianlogo

LangChain入門(2) – プロンプト/LECLの使い方

本記事はFuture Coders独自教材からの抜粋です。変化の早い分野なので記事の内容が古くなっている可能性もあります。ご注意ください。

プロンプト

プロンプトとは、「促す」という意味の英単語です。生成AIへの入力を意味します。生成AIからよりよい応答を得るためには、良いプロンプトが必要です。プロンプトエンジニアリングという単語もあるくらい重要な分野です。ここでは、プロンプトの使い方、生成AIの出力の加工方法、LCEL(LangChain Expression Language)などについて説明します。

会話の要素

会話では「だれが何を発言したか」がとても重要です。生成AIにこちら側の意図を伝えるのに、会話形式での入力を行うことができますが、LangChainでは話者に応じた専用のクラスが用意されています。

langchain.schema モジュールに以下のようなクラスが用意されています。

  • SystemMessage = 背景(文脈・コンテキスト)の説明
  • HumanMessage = 人の発言
  • AIMessage = 生成AIの発言、

以下はそれぞれのオブジェクトを作成したサンプルです。

from langchain.schema import SystemMessage, HumanMessage, AIMessage
msg1 = SystemMessage('あなたは有名店のシェフです')
msg2 = AIMessage('ようこそ、何名様ですか?')
msg3 = HumanMessage('3人です')
print(f"SystemMessage: {msg1}")
print(f"AIMessage: {msg2}")
print(f"HumanMessage: {msg3}")

出力は以下の通りです。

SystemMessage: content='あなたは有名店のシェフです'
AIMessage: content='ようこそ、何名様ですか?'
HumanMessage: content='3人です'

このように「誰が何を話したか」をオブジェクトを使って表現します。これらのオブジェクトをリストとしてまとめると会話となります。

以下は、SystemとHumanの発言をオブジェクトとし、それをリストとして会話の形式にし、生成AIに入力したサンプルです。

from langchain_openai import ChatOpenAI
from langchain.schema import SystemMessage, HumanMessage
model = ChatOpenAI(model="gpt-4o")
model.invoke([
    SystemMessage("あなたは日本の地理に詳しい学者です。"),
    HumanMessage("日本で2番目に高い山は?")
])

以下は上記プログラムの出力例です。

AIMessage(content='日本で2番目に高い山は北岳(きただけ)です。北岳は山梨県に位置し、その標高は3,193メートルです。ちなみに、日本で最も高い山は富士山であり、標高は3,776メートルです。', response_metadata={'token_usage': {'completion_tokens': 66, 'prompt_tokens': 35, 'total_tokens': 101}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_319be4768e', 'finish_reason': 'stop', 'logprobs': None}, id='run-f547cb16-4089-4f76-9200-3864a3c5a926-0')

生成AIからの出力はAIMessageのオブジェクトとなっていることがわかります。話者が生成AIなので、AIMessageというのは自然な感じではないでしょうか。その応答のテキストを抽出するには、contentプロパティにアクセスします。

このように生成AI(ChatOpenAI)への入力は会話が想定されていますが、以下のように単なるテキストの入力にも対応しています。

model = ChatOpenAI(model="gpt-4o")
model.invoke("日本で2番目に高い山は?")

基本的には会話形式の入力を想定していますが、面倒なときのため、単なる文字列も受け付ける、という思想なのかと思われます。

テンプレート

生成AIへの入力をプロンプトと呼びますが、その内容はしばしば定型的なものになりがちです。質問の一部分を置き換えて、繰り返し質問するようなイメージです。

  • システム
    • あなたは{和食}の調理師です
    • あなたは{中華}の調理師です。
  • Human
    • {国名}で{番号}番目に高い山は?
    • {材料}を活かしたレシピを教えてください
    • {俳優}が出演している映画を教えてください

都度作成するのは面倒です。あらかじめひな形を用意しておき、一部を置き換える方が簡単です。このような用途のためにテンプレート(雛形)という仕組みが用意されています。

それぞれの話者用に専用のテンプレート生成用のクラスが用意されています。

  • SystemMessage を作るには SystemMessagePromptTemplate
  • HumanMessage を作るには HumanMessagePromptTemplate
  • AIMessage を作るには AIMessagePromptTemplate

実際の利用例をみるのがわかりやすいでしょう。以下は、 SystemMessagePromptTemplateからテンプレートを作って、そこからSystemMessageのプロンプトを生成する例です。

from langchain_core.prompts import SystemMessagePromptTemplate

template = SystemMessagePromptTemplate.from_template("私は{genre}の専門家です")
for g in ["和食", "洋食", "中華"]:
    prompt = template.format(genre=g)
    print(prompt)

出力は以下の通りです。

content='私は和食の専門家です'
content='私は洋食の専門家です'
content='私は中華の専門家です'

テンプレートは”from_template”メソッドで作成します。そのテンプレートのformatメソッドに必要な情報をわたすことでプロンプトが作成されます。

prompt1
LangChain入門(2) - プロンプト/LECLの使い方 7

Pythonのf文字列では、文字列中の{}の中の部分を実際の値で置き換えることができました。そのような使い方をイメージすると良いかもしれません。

prompt2
LangChain入門(2) - プロンプト/LECLの使い方 8

このように、テンプレートとf文字列、似ているように見えますが、LangChainで作成されたプロンプトはSystem, Human, AIの種類を区別したり、いろいろな情報が付与されています。

System, Human, AIと個別のテンプレートを作成することもできますが、会話全体としてのテンプレートも用意されています。

from langchain_core.prompts import ChatPromptTemplate,SystemMessagePromptTemplate, HumanMessagePromptTemplate

system_template = SystemMessagePromptTemplate.from_template("{lang_from}から{lang_to}への翻訳を行います")
human_template = HumanMessagePromptTemplate.from_template("「{text}」を翻訳してください")

template = ChatPromptTemplate.from_messages([
    system_template,
    human_template
])
prompt = template.format(lang_to="英語", lang_from="日本語", text="今日はいい天気ですね")
print(prompt)

出力は以下の通りです。

System: 日本語から英語への翻訳を行います
Human: 「今日はいい天気ですね」を翻訳してください

個々の話者のテンプレートをChatPromptTemplateクラスでまとめて会話のテンプレートを作る、そのようなイメージです。

prompt5
LangChain入門(2) - プロンプト/LECLの使い方 9

それぞれの会話の要素もテンプレートです。置き換えたい部分を”{キーワード}”として記述しておきます。ChatPromptTemplateは会話のリストを引数として受け取ります。 formatメソッドで全体を置き換えることができます。そのときの引数として、「キーワード=値」の形式で指定します。

以下により具体的なイメージを示します。

prompt3
LangChain入門(2) - プロンプト/LECLの使い方 10

いろいろなテンプレート、from_template, from_messagesなど似たようなメソッドが多いので混乱するかもしれません。

  1. SystemMessagePromptTemplate、HumanMessagePromptTemplate等のクラスを使って各話者のテンプレートを作成
  2. それらをまとめる会話のテンプレートをChatPromptTemplateを使って作成
  3. 必要なパラメタをformatメソッドで埋める

という手順になります。

実は、それぞれの話者用のオブジェクトを作成する代わりに以下のように単なるタプル形式で記述することも可能です。

template = ChatPromptTemplate.from_messages([
    ("system", "{lang_from}から{lang_to}への翻訳を行います"),
    ("user", "「{text}」を翻訳してください")
])
prompt = template.format(lang_to="英語", lang_from="日本語", text="今日はいい天気ですね")
print(prompt)

それぞれの話者のオブジェクトを (話者, 内容) というタプル形式で記述します。話者に使用できる文字列は以下のどれかとなります。

‘human’, ‘user’, ‘ai’, ‘assistant’, ‘system’

こちらの記述方法は公式サンプルなどでも広く使われているようです。

  • オブジェクトを生成する方法 [ SystemMessage, HumanMessage, … ]
  • タプルで記述する方法 [ (“system”, “…”), (“human”, “…”), ]

どちらでも好きな方をつかいながら読み進めてください。

上記のプロンプトを生成AIに入力してみましょう。

from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4")
response = llm.invoke(prompt)
print(response.content)

以下のような応答が得られました。正しく英語に翻訳されています。

"The weather is nice today, isn't it?"

テンプレートの利点は使いまわせることです。for文を使っていくつかの言語に翻訳してみましょう。

for lang in ["英語", "ドイツ語", "中国語"]:
    prompt = template.format(lang_to=lang, lang_from="日本語", 
        text="どうもありがとう!")
    response = llm.invoke(prompt)
    print(f"{lang}: {response.content}")

出力は以下のようになりました。

英語: "Thank you very much!"
ドイツ語: "Vielen Dank!"
中国語: 「非常感谢你!」

プロンプト | 生成AI

ここまで、生成AIを実行するときに、生成AI . invoke(プロンプト)という形式で実行してきました。この方法でも生成AIを実行することはできますが、現在は以下のように記述することが推奨されています。

chain = プロンプト | 生成AI
chain.invoke( 入力 )

縦棒は「左側の出力を右側の入力につなぐ」という意味になります。上記の例をイメージにすると以下のようになります。

prompt4
LangChain入門(2) - プロンプト/LECLの使い方 11

上図左は、プロンプトの出力を生成AIへつなぐ様子、右は単なるイメージです。Aの出力をBへ、Bの出力をCへつなぐ様子を表しています。

LangChainのChainは鎖という意味です。いろんな部品を鎖のようにつないで、複雑な処理も可能にする、そんなイメージから命名されたものと思われます。

それぞれのつながりをChainと呼び、そのChainに対してinvokeメソッドを実行する、という使い方になります。

LangChainでは、このように縦棒を要素を連結してゆく記法(文法)を使いますが、これを LCEL (Lang Chain Expression Language)と呼びます。LangChainではLCELが主流になってゆくようです。

先ほどの例をLCELを使って書き直すと以下のようになります。

from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4")
chain = template | llm
response = chain.invoke({"lang_to":"英語", "lang_from":"日本語", "text":"今日はいい天気ですね"})
print(response.content)

この程度のシンプルな例では、あまり恩恵を感じにくいかもしれませんが、複雑な処理を行うようになるとLCELを使うとシンプルに記述できることが実感できると思います。

OutputParser

llmをinvokeで実行したり、chainを実行したり、そのときの戻り値はAIMessage型です。

from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4")
res = llm.invoke("一番広い都道府県は?")
res

以下のように出力されます。

AIMessage(content='北海道\n', response_metadata={'token_usage': {'completion_tokens': 4, 'prompt_tokens': 19, 'total_tokens': 23}, 'model_name': 'gpt-4', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-9c135b24-77c6-458a-90be-f75797c206d5-0')

AIMessageはオブジェクトの型を表しています。生成AIからの応答なのでAIMessageという型は自然な命名だとおもいます。このオブジェクトにはトークンの数はモデル名などさまざまな情報が含まれています。

生成AIの出力は単に文字として出力するだけでなく、他の処理の入力として使いたいこともあるでしょう。そのときには処理しやすい形式になっていることが望ましいはずです。

LangChainではそのような用途のために様々なOutputParserが用意されています。

https://python.langchain.com/v0.1/docs/modules/model_io/output_parsers

今回はそのなかでも利用頻度が高いと思われる以下のクラスについて説明します。

  • StrOutputParser
  • SimpleJsonOutputParser
  • CommaSeparatedListOutputParser

StrOutputParser

応答の文字列だけが必要なことも多いでしょう。contentプロパティを参照すればよいだけですが、StrOutputParserを使用するとテキスト部分だけを簡単に取得できます。

from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
llm = ChatOpenAI(model="gpt-4")
str_parser = StrOutputParser()
chain = template | llm | str_parser
response = chain.invoke({"lang_to":"英語", "lang_from":"日本語", "text":"今日はいい天気ですね"})
response

出力は以下のようになります。

'"It\'s nice weather today, isn\'t it?"'

明示的にresponseオブジェクトのcontentを参照しなくてもテキストの部分だけをとりだせていることが分かります。

以下のようにLCELに直接オブジェクトを記述することも良く行われます。

chain = template | llm | StrOutputParser()

SimpleJsonOutputParser

生成AIからの出力を、さらにプログラムで利用したいときには、何らかのオブジェクトの形式になっていると便利です。その中でも用途が多いと思われるのがJSONです。生成AIの出力をJSON形式に変換するためにSimpleJsonOutputParserというクラスが用意されています。

以下は翻訳した結果をJSONのオブジェクトとして取得するサンプルです。

from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4")

human_str = """
ランダムなデータが必要です。生徒{count}人分のダミーデータをJSON形式で生成してください。

"name": 氏名,
"age": 年齢,
"hobby": 趣味
"""
human_template = HumanMessagePromptTemplate.from_template(human_str)

template = ChatPromptTemplate.from_messages([
    human_template
])

chain = template | llm 
response = chain.invoke({"count":3})
print(response.content)
print(type(response.content))

以下のような出力が得られました。正しいJSONが得られたように見えるかもしれませんが、最後にtype関数で型を調べたところstr(文字列)となっていることが分かります。

[
    {
        "name": "山田太郎",
        "age": 16,
        "hobby": "サッカー"
    },
    {
        "name": "鈴木花子",
        "age": 15,
        "hobby": "ピアノ"
    },
    {
        "name": "田中一郎",
        "age": 17,
        "hobby": "読書"
    }
]
<class 'str'>

以下のようにLCELにSimpleJsonOutputParserを追加してみます。

from langchain.output_parsers.json import SimpleJsonOutputParser
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4")

human_str = """
ランダムなデータが必要です。生徒{count}人分のダミーデータをJSON形式で生成してください。

"name": 氏名,
"age": 年齢,
"hobby": 趣味
"""
human_template = HumanMessagePromptTemplate.from_template(human_str)

template = ChatPromptTemplate.from_messages([
    human_template
])

chain = template | llm | SimpleJsonOutputParser()
response = chain.invoke({"count":3})
print(response)
print(type(response))

出力はいかのようになりました。

[{'name': '山田太郎', 'age': 15, 'hobby': 'サッカー'}, {'name': '鈴木花子', 'age': 16, 'hobby': '読書'}, {'name': '佐藤一郎', 'age': 14, 'hobby': 'ゲーム'}]
<class 'list'>

responseの型をしらべたところ、list型でした。単なる文字列ではなく、プログラムで処理しやすい型になっていることが分かります。

CommaSeparatedListOutputParser

CSVフォーマットとは、Comma Separated Valueの略でカンマ区切るフォーマットです。CommaSeparatedListOutputParserそのようなフォーマットで出力するParserです。

from langchain_core.prompts import HumanMessagePromptTemplate, ChatPromptTemplate
from langchain.output_parsers import CommaSeparatedListOutputParser
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4")

template = ChatPromptTemplate.from_messages([
    HumanMessagePromptTemplate.from_template(
        "{item}の種類を{count}個列挙してください。要素は,で区切ってください"
    )
])
chain =  template | llm | CommaSeparatedListOutputParser()
response = chain.invoke({"item":"アイスクリーム","count":"5"})
print(response)
print(type(response))

出力は以下のようになりました。最終的にlistの形式になっています。

['バニラ', 'チョコレート', 'ストロベリー', '抹茶', 'クッキー&クリーム']
<class 'list'>

プロンプトハブ

プロンプトの内容は多くの人で似通ったものになるでしょう。そんな状況を鑑みて、プロンプトのディレクトリ(カタログのようなもの)が用意されています。必要なプロンプトをダウンロードして利用できます。

https://smith.langchain.com/hub

ほとんどが英語ですが、どんなプロンプトがあるのか読むだけでも勉強になります。

以下はその一例です。

from langchain import hub
prompt = hub.pull("pollychi/rag_japanese_1")

出力は以下のようになりました。

PromptTemplate(input_variables=['context', 'question'], metadata={'lc_hub_owner': 'pollychi', 'lc_hub_repo': 'rag_japanese_1', 'lc_hub_commit_hash': '68f63cabc71c457f06ecdf5a525c27742c5bec0a8a982f85635746d1ba0254ef'}, template='User:\nあなたは質問応答タスクのアシスタントです。\n以下のコンテキストに基づいて質問に答えます。答えがわからない場合は、わからないと言ってください。最大 3 つの文を使用し、回答は簡潔にしてください。\nQuestion : {question}\nContext : {context}\nAnswer :')

読みづらいのでprint命令をつかって改行してみます。

User:
あなたは質問応答タスクのアシスタントです。
以下のコンテキストに基づいて質問に答えます。答えがわからない場合は、わからないと言ってください。最大 3 つの文を使用し、回答は簡潔にしてください。
Question : {question}
Context : {context}
Answer :

このプロンプトを使って実行してみましょう。

from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
from langchain import hub
prompt = hub.pull("pollychi/rag_japanese_1")
llm = ChatOpenAI(model="gpt-4")
chain = prompt | llm | StrOutputParser()
chain.invoke({"context":"税務関係に詳しい専門家です。小学生にもわかるようシンプル・平易に説明します。",
              "question":"累進課税とはどのような制度ですか?" })

以下のような出力が得られました。

'累進課税とは、所得が増えるほど税率が高くなる税制のことです。つまり、お金を多く稼いでいる人ほど多く税金を払う仕組みです。これにより、所得格差の緩和を目指すことができます。'

Future Coders

Future Codersではほかにも多くの独自教材を用意しています。少人数個別指導・リモート対応でレッスンを行っています。レッスン以外にも出張授業やコンサルタントも行っております。興味のある方はお気軽にお問い合わせください。

Categories: LangChain, OpenAI