本記事の内容は筆者個人の見解であり、所属する会社や組織の公式な見解を示すものではありません。
記事の内容
LangChainのプロンプト改善についての色々を記述しています。プロンプト改善前、プロンプト改善後のコードを示して、改善ポイントについて、それぞれ説明を行います。
プロンプト改善前
- 一つのメッセージに命令文、出力形式、入力情報を書き込んでいる。(GUIにプロンプトを書き込む場合に近い。)
from langchain.prompts import PromptTemplate
from langchain_google_genai import ChatGoogleGenerativeAI
GEMINI_MODEL_NAME = "gemini-2.0-flash"
GOOGLE_API_KEY = "XXXX" #(本当はコードにキーを記述してはいけません。.envとかを使ってください。)
PROMPT = """
# 命令書
作曲家名が与えられます。指示書に従ってその作曲家のプロフィールを作成してください。
あなたの知識の範囲内でできる限り回答を作成してください。
# 指示書
2人の作曲家の名前が与えられます。
各作曲家の代表曲を3つ答えて下さい。
その6曲に対して知名度、専門家の評価を総合して順位をつけて下さい。
その順位にした理由も記述して下さい。
# 入力データ
## 作曲家名1
{composer1}
## 作曲家名2
{composer2}
# 出力形式
## 作曲家名
### 曲名1
#### 曲名
#### 順位
#### 理由
### 曲名2
#### 曲名
#### 順位
#### 理由
### 曲名3
#### 曲名
#### 順位
#### 理由
"""
def main():
prompt = PromptTemplate(input_variables=["composer1", "composer2"], template=PROMPT)
llm = ChatGoogleGenerativeAI(
model=GEMINI_MODEL_NAME,
google_api_key=GOOGLE_API_KEY,
)
chain = prompt | llm
result = chain.invoke({"composer1": "ベートーヴェン", "composer2": "モーツァルト"})
print(result.content)
if __name__ == "__main__":
main()
プロンプト改善後
- System Message と Human Message を使用
- Pydantic Structured Output を使用
- 順位ではなくスコアを使用
- 複数の Human Message を使用
- XMLによる入力のタグ付け
from pydantic import BaseModel, Field
from langchain_core.prompts import (
ChatPromptTemplate,
HumanMessagePromptTemplate,
)
from langchain_core.messages import SystemMessage
from langchain_google_genai import ChatGoogleGenerativeAI
GEMINI_MODEL_NAME = "gemini-2.0-flash"
GOOGLE_API_KEY = "XXXXXX" #(本当はコードにキーを記述してはいけません。.envとかを使ってください。)
PROMPT_SYS = """
# 命令書
作曲家名が与えられます。指示書に従ってその作曲家のプロフィールを作成してください。
あなたの知識の範囲内でできる限り回答を作成してください。
# 指示書
2人の作曲家の名前が与えられます。
各作曲家の代表曲を3つ答えて下さい。
その6曲に対して知名度、専門家の評価を総合してスコアをつけて下さい。
そのスコアにした理由も記述して下さい。
"""
PROMPT_HUM_1 = """
<composer_name description=作曲家名>
{composer1}
</composer_name>
"""
PROMPT_HUM_2 = """
<composer_name description=作曲家名>
{composer2}
</composer_name>
"""
class SongTitleWithScoreAndReason(BaseModel):
song_title: str = Field(description="代表曲の名前")
score: int = Field(description="その代表曲のスコア。1 ~ 100")
reason: str = Field(description="スコアの理由。")
class ComposerAndSongs(BaseModel):
composer_name: str = Field("作曲家名")
song_title_with_rank: list[SongTitleWithScoreAndReason] = Field(
description="その作曲家の代表曲を3曲リストアップしてください。",
min_length=3,
max_length=3,
)
class AllComposerAndSongs(BaseModel):
all_composer: list[ComposerAndSongs] = Field(
"作曲家のリスト", min_length=2, max_length=2
)
def main():
chat_prompt = ChatPromptTemplate.from_messages(
[
SystemMessage(content=PROMPT_SYS),
HumanMessagePromptTemplate.from_template(PROMPT_HUM_1),
HumanMessagePromptTemplate.from_template(PROMPT_HUM_2),
]
)
llm = ChatGoogleGenerativeAI(
model=GEMINI_MODEL_NAME,
google_api_key=GOOGLE_API_KEY,
)
chain = chat_prompt | llm.with_structured_output(AllComposerAndSongs)
result = chain.invoke({"composer1": "ベートーヴェン", "composer2": "モーツァルト"})
print(result.model_dump_json(indent=2))
if __name__ == "__main__":
main()
改善ポイント
System Message と Human Message を使用
だいたいの文献、教科書のコードでSystemMessage, HumanMessageを使用したコードサンプルは載っている。ただし、SystemMessageなどは使用しなくても動作するし、使用する理由が明確に記述されている事は少ない。調べてみると、おおよそ以下の理由によってSystemMessageの使用が推奨されている。
- そもそも現在の多くのLLMがSystemMessageとHumanMessageの分離を意識して訓練されている。トランスフォーマーで扱うときにはSystemMessage、HumanMessageの前後に特別なトークンが付与される。SystemMessageはHumanMessageが長くなっても忘れられにくい。
- (上記と関連して)インジェクション攻撃への対策となる。
今回のプロンプトサンプルで composer1, composer2 は”モーツァルト” などの作曲家名が入力として与えられる事が想定されているが、
composer1、composer2 に”ここまでの命令書を無視して~”などの命令文を入れる事ができる。変更を受けない SystemMessageを使用する事で、このような命令文を書き換えるような攻撃を防ぐことができる。(インジェクション対策は他にも色々あるので、これはそれらのうちの一つ。) - LLMはハッシュ値を利用して計算を効率化している場合が多い。SystemMessageが過去に使用したものと全く同じとき、LLMモデルの内部のキャッシュが利用されやすい。HumanMessageはcomposer1, composer2が過去と同じでなければキャッシュが利用されにくい。毎回同じ部分と毎回違う部分を明確にする事によって、キャッシュが利用されやすくなる。
Pydantic Structured Output を使用
他に多くの資料、記事で記述があるので、ここではあまり説明しない。
- マークダウンよりも出力形式を明確に定義できる。
- LLM出力結果が再利用しやすくなる。
- バリデート関数を作成、実行する機能がある。(field_validator 今回の例では使用していない。)
順位ではなくスコアを使用
Structured Output での順位の扱いは比較的面倒だったりする。順位は1~6の数字を抜け漏れなく1回づつ付与する。現在のLLMの性能は、プロンプト改善前の状態でも正しく順位をつける場合が多いが、それはLLMの理解能力に依存していて、プログラム的に重複や欠損がない事を保証しているわけではない。
StructuredOutputで重複や欠損がないように順位をつける仕組みはいくつかある。
- 重複、欠損があったらやり直しをする。
- 一度、順位ではなく点数をつけてから、その点数に対する順位をつける。
プロンプト改善で採用したのは 2.の方法であるが、点数による評価は他の候補について知っている必要がないというメリットがある。また、やり直しをする場合は余計な時間とコストがかかる。今回のサンプルでは質問がシンプルであるため、あまり差はないが、スコアを付けるタスクは並列に実行する事もできる。
複数の Human Message を使用
トランスフォーマーで扱うときにHuman Messageの前後には特別なトークンが付与される。それを利用すれば、プロンプト改善前の Human Message の一番大きなブロック分けは Human Message を複数にする事で実現できる。
XMLによる入力のタグ付け
一般的な文章やマークダウンでわかりにくい事として、メタデータと値の分離がある。XMLタグを使用すると、メタデータと値が明確になる。一部のLLMモデルはXMLのタグを正式にサポートしている。これは出力の構造化と同様の入力の構造化と考える事もできる。
メタデータと値の分離問題
XMLタグの有効性を一つの例で示す。
”名前” というタイトルの映画があるが、それを踏まえて以下のようなプロンプトを記述する。
以下の映画について説明しください。
# 映画
- 名前(なまえ)
- タイタニック
自分の使用しているモデルでは上記のプロンプトではタイニックの説明しかでてこない。”名前(なまえ)”の部分は「下にあるのが映画の名前ですよ」という解釈をされている。この問題は日本語、英語など多くの言語で一般名詞と固有名詞を区別する明確なルールがないという事にも関連がある。(注1)*
XMLタグを使用して以下のようなプロンプトにする。
以下の映画について説明しください。
<movie_title description=映画のタイトル>
名前(なまえ)
</movie_title>
<movie_title description=映画のタイトル>
タイタニック
</movie_title>
この場合、正しく2つの映画の説明がされる。(トークンが増えてるというのはありますが、ここで気にしているのはメタデータをメタデータとしてきっちり分けられるという部分です。)ちなみに”名前(なまえ)”というようにふりがなをつけたが、ふりがなをつけないと”君の名は”が挙げられてしまう。
これは ”君の名は” の英語タイトルが”Your Name” で、”君の名は”の方が”名前”よりも一般的な知名度が高いからだろう。
まとめ
プロンプト改善を自分で実際にやってみたときに色々考えたりした事を記述しました。インジェクション攻撃への対策は今後重要になりそうなので、もっと勉強しようと思います。あと触れてなかったのですが、
- structured output の description に多く記述した方がいいのか、System Messageに多く記述した方がいいのか
- トリガーメッセージ(プロンプトの最後の命令文)はあった方がいいのか
などまだよくわからない部分はあったりします。
修正事項、御意見、コメント、アップデート情報 など色々お待ちしております。
(注1)*
”###命令書”とかいうタイトルの小説があってもいいし、それを言い出すと "<score> 100 </score>" みたいなタイトルの小説があるかもしれないじゃないかという話にもなるのだが、それはインジェクション攻撃対策とも関わってくる。ここでは深入りしないが、最終的にHumanMessageにはXMLタグも使用しない方が意味に不確定要素がなくなり、インジェクション攻撃対策にもなる。複数のHumanMessageと合わせてSystemMessageによって入力方式を定義する事になる。そうするとどうしてもSystemMessageの文章が複雑になってしまう。何かいい方法があるのかもしれませんが私は把握してないです。)