見出し画像

自己改変するAI~プロンプトでプロンプトを改善できるか?

はじめに

ふたつの実験により、自然文から必要な情報を抜き出して構造化データに変換することに成功した。

そこでふと疑問がわいた。今回は自力でプロンプトを改善したが、所望の結果が得られない場合、プロンプトを改善してくれるプロンプトを書くことはできないだろうか?もしうまくいくならば、プロンプトエンジニアリングももうちょい手抜きできるのじゃないだろうか?

これは自らpythonコードを修正している例だが、これをプロンプトに対して適用することはできないのだろうか?

さっそくやってみよう。

実験

実験の前提

まず、以下のコードを用意する。これは前回、トークン節約の実験に使用し、明らかに所望の結果とは異なる結果が得られたプロンプトである。

# 環境変数の準備
import os
os.environ["OPENAI_API_KEY"] = "INPUT_YOUR_OWN_API_KEY"

from langchain.llms import OpenAI, OpenAIChat
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain

# 例示付きプロンプトの設定
template = """あなたは入力された文章から必要な情報を抽出し、指定のJSON形式に整形するAIです。以下に示す形式に従い、結果を出力してください。

# 指定のJSON形式
---
(
  'event':(
    name:'$本文のタイトル(入力された文章をもっともよく表すワードを選択)',
    'start_year':$入力された出来事が発生した年を西暦で出力,
    'end_year':$入力された出来事が終結した年を西暦で出力,
    'related_persons':['$関与した人名を出力','$関与した人名を出力'],
    'related_events':(
        'event':(
            'name':'$関連する出来事を出力',
            'summary':'$出力した関連する出来事の概要を50文字以内で出力'
        ),
    ),
    'details':(
      'place':['$関与した地名を出力','$関与した地名を出力'],
      'related_nations':['$関与した国名を出力','$関与した国名を出力'],
      'battle':(
        'name':'$発生した戦闘の名称を出力',
        'start_date':'$戦闘が開始された年月日をxxxx年mm月dd日の形式で出力',
        'end_date':'$戦闘が終了した年月日をxxxx年mm月dd日の形式で出力',
        'result':'$フランス軍の勝敗を出力',
      ),
    )
  )
)
---

Human: {input}
AI:"""

prompt = PromptTemplate(
    input_variables = ['input'],
    template = template
)

llm = OpenAIChat(model_name="gpt-3.5-turbo",
                 temperature = 0.2)

llm_chain = LLMChain(
    llm = llm,
    verbose = True,
    prompt = prompt
)

このプロンプトに第一次イタリア遠征の説明文を入力すると、related_eventsやbattleなど、複数回発生するものが適切に抽出できないのであった。

output = llm_chain.predict(input = """第一次イタリア遠征(第一次対仏大同盟)
1792年のフランス革命戦争の勃発により、1793年にイギリス、オーストリア、プロイセン、スペインなどによって第一次対仏大同盟が結成された。この戦いにおいてフランスの総裁政府は、ライン方面から2個軍、北イタリア方面から1個軍をもってオーストリアを包囲攻略する作戦を企図していた。
1796年3月、イタリア方面軍の司令官に任命されたナポレオン・ボナパルトは攻勢に出る。まず、これまで最前線でフランス軍と対峙してきたサルデーニャ王国をわずか1か月で降伏させ、オーストリア軍の拠点マントヴァを包囲した。オーストリア軍はマントヴァ解放を目指して反撃に出るが、ナポレオンの前にカスティリオーネの戦い(8月5日)、アルコレの戦い(11月15日-17日)、リヴォリの戦い(1797年1月14日)で敗北する。2月2日にマントヴァは開城。オーストリアは停戦を申し入れ、4月18日にレオーベンの和約が成立した。
10月17日、フランスとオーストリアはカンポ・フォルミオの和約を締結。フランスは南ネーデルラントとライン川左岸を併合し、北イタリアにはチザルピーナ共和国などのフランスの衛星国が成立した。オーストリアの脱落で第一次対仏大同盟は崩壊した。""")
{
  'event':{
    'name':'第一次イタリア遠征',
    'start_year':1796,
    'end_year':1797,
    'related_persons':['ナポレオン・ボナパルト'],
    'related_events':{
        'event':{
            'name':'第一次対仏大同盟',
            'summary':'1792年のフランス革命戦争の勃発により、1793年にイギリス、オーストリア、プロイセン、スペインなどによって結成された同盟'
        },
    },
    'details':{
      'place':['サルデーニャ王国','マントヴァ','カスティリオーネ','アルコレ','リヴォリ'],
      'related_nations':['フランス','オーストリア'],
      'battle':{
        'name':'カスティリオーネの戦い',
        'start_date':'1796年8月5日',
        'end_date':'1796年8月5日',
        'result':'フランス軍の勝利',
      },
    }
  }
}

改善提案するプロンプト

このダメな出力を改善してくれるプロンプトを書いてみよう。

template = """あなたはAIへの指示を改善するAIです。これからAIへの指示、実際の出力、所望の出力を提示します。所望の出力が得られるよう、AIへの指示を改善してください。

Human: {input}
AI:"""

prompt = PromptTemplate(
    input_variables = ['input'],
    template = template
)

llm = OpenAIChat(model_name="gpt-3.5-turbo",
                 temperature = 0.2)

llm_chain = LLMChain(
    llm = llm,
    verbose = True,
    prompt = prompt
)

ここでは「プロンプト」を「AIへの指示」と置き換えている。GPT3.5はたしか2022年の何月だかの時点でのデータで学習されており、プロンプトという用語で「AIへの指示」であるとは理解できない可能性があると考えたためである。

さて、このプロンプトに対し、以下のように入力する。かなり冗長ではあるが、ダメな出力例と望ましい出力例を併記する形をとっている。

output = llm_chain.predict(input = """# AIへの指示
あなたは入力された文章から必要な情報を抽出し、指定のJSON形式に整形するAIです。以下に示す形式に従い、結果を出力してください。

# 指定のJSON形式
---
(
  'event':(
    name:'$本文のタイトル(入力された文章をもっともよく表すワードを選択)',
    'start_year':$入力された出来事が発生した年を西暦で出力,
    'end_year':$入力された出来事が終結した年を西暦で出力,
    'related_persons':['$関与した人名を出力','$関与した人名を出力'],
    'related_events':(
        'event':(
            'name':'$関連する出来事を出力',
            'summary':'$出力した関連する出来事の概要を50文字以内で出力'
        ),
    ),
    'details':(
      'place':['$関与した地名を出力','$関与した地名を出力'],
      'related_nations':['$関与した国名を出力','$関与した国名を出力'],
      'battle':(
        'name':'$発生した戦闘の名称を出力',
        'start_date':'$戦闘が開始された年月日をxxxx年mm月dd日の形式で出力',
        'end_date':'$戦闘が終了した年月日をxxxx年mm月dd日の形式で出力',
        'result':'$フランス軍の勝敗を出力',
      ),
    )
  )
)
---

# 得られた出力
(
  'event':(
    'name':'第一次イタリア遠征',
    'start_year':1796,
    'end_year':1797,
    'related_persons':['ナポレオン・ボナパルト'],
    'related_events':(
        'event':(
            'name':'第一次対仏大同盟',
            'summary':'1792年のフランス革命戦争の勃発により、1793年にイギリス、オーストリア、プロイセン、スペインなどによって結成された。'
        ),
    ),
    'details':(
      'place':['サルデーニャ王国','マントヴァ','カスティリオーネ','アルコレ','リヴォリ','チザルピーナ共和国'],
      'related_nations':['フランス','オーストリア'],
      'battle':(
        'name':'カスティリオーネの戦い',
        'start_date':'1796年8月5日',
        'end_date':'1796年8月5日',
        'result':'フランス軍の勝利',
      ),
    )
  )
)

# 望ましい出力
(
  'event':(
    'name':'第一次イタリア遠征',
    'start_year':1796,
    'end_year':1797,
    'related_persons':['ナポレオン・ボナパルト'],
    'related_events':(
        'event':(
            'name':'第一次対仏大同盟',
            'summary':'1792年のフランス革命戦争の勃発により、1793年にイギリス、オーストリア、プロイセン、スペインなどによって結成された。'
        ),
        'event':(
            'name':'レオーベンの和約',
            'summary':'オーストリアは停戦を申し入れ、4月18日に成立した。'
        ),
        'event':(
            'name':'カンポ・フォルミオの和約',
            'summary':'フランスとオーストリアが10月17日に締結し、フランスは南ネーデルラントとライン川左岸を併合した。'
        ),
    ),
    'details':(
      'place':['サルデーニャ王国','マントヴァ','カスティリオーネ','アルコレ','リヴォリ','チザルピーナ共和国'],
      'related_nations':['フランス','オーストリア'],
      'battle':(
        'name':'カスティリオーネの戦い',
        'start_date':'1796年8月5日',
        'end_date':'1796年8月5日',
        'result':'フランス軍の勝利',
      ),
      'battle':(
        'name':'アルコレの戦い',
        'start_date':'1796年11月15日',
        'end_date':'1796年11月17日',
        'result':'フランス軍の勝利'
      ),
      'battle':(
        'name':'リヴォリの戦い',
        'start_date':'1797年1月14日',
        'end_date':None,
        'result':'フランス軍の勝利'
      ),
    )
  )
)

""")

さて、結果や以下に。

結果

指示に従い、以下の点を改善します。

1. 'related_events'の出力を1つのリストにまとめ、複数の関連する出来事を出力できるようにします。
2. 'details'の出力に、複数の戦闘情報を出力できるように、'battle'を複数回出力できるようにします。

改善後の指示は以下の通りです。

Human: # AIへの指示
あなたは入力された文章から必要な情報を抽出し、指定のJSON形式に整形するAIです。以下に示す形式に従い、結果を出力してください。

# 指定のJSON形式
---
(
  'event':(
    'name':'$本文のタイトル(入力された文章をもっともよく表すワードを選択)',
    'start_year':$入力された出来事が発生した年を西暦で出力,
    'end_year':$入力された出来事が終結した年を西暦で出力,
    'related_persons':['$関与した人名を出力','$関与した人名を出力'],
    'related_events':[
        'event':(
            'name':'$関連する出来事を出力',
            'summary':'$出力した関連する出来事の概要を50文字以内で出力'
        ),
        'event':(
            'name':'$関連する出来事を出力',
            'summary':'$出力した関連する出来事の概要を50文字以内で出力'
        ),
    ],
    'details':(
      'place':['$関与した地名を出力','$関与した地名を出力'],
      'related_nations':['$関与した国名を出力','$関与した国名を出力'],
      'battle':[
        'battle':(
          'name':'$発生した戦闘の名称を出力',
          'start_date':'$戦闘が開始された年月日をxxxx年mm月dd日の形式で出力',
          'end_date':'$戦闘が終了した年月日をxxxx年mm月dd日の形式で出力',
          'result':'$フランス軍の勝敗を出力',
        ),
        'battle':(
          'name':'$発生した戦闘の名称を出力',
          'start_date':'$戦闘が開始された年月日をxxxx年mm月dd日の形式で出力',
          'end_date':'$戦闘が終了した年月日をxxxx年mm月dd日の形式で出力',
          'result':'$フランス軍の勝敗を出力',
        ),
        'battle':(
          'name':'$発生した戦闘の名称を出力',
          'start_date':'$戦闘が開始された年月日をxxxx年mm月dd日の形式で出力',
          'end_date':'$戦闘が終了した年月日をxxxx年mm月dd日の形式で出力',
          'result':'$フランス軍の勝敗を出力',
        ),
      ],
    )
  )
)
---

まじか。

確かによくよく考えるとおおもとの指示はJSON形式としては微妙だった。
related_eventsはリストにしていなかったし、battleもただだらだらならべているだけだったからである。それをちゃんとリストにしましょう、といってくれている。

1. 'related_events'の出力を1つのリストにまとめ、複数の関連する出来事を出力できるようにします。
2. 'details'の出力に、複数の戦闘情報を出力できるように、'battle'を複数回出力できるようにします。

いくらなんでもすごすぎないか。
さすがに「$eventが複数ある場合は同じフォーマットで繰り返し」までは提案してくれなかったが、そのあたりは人手で直してもよかろう。少なくともJSON形式の間違いは直してくれたようなので、その点は素晴らしいと思った。

まとめ

指示の内容そのものを改変してくれるところまではいかなかったが、出力形式を改善することはできたようである。おそらくコードの誤りを正してくれるのと同じ考え方なのかもしれない。
いずれにせよ、非常にポテンシャルのある結果となったと思う。みなさまの楽しいプロンプトエンジニアリング生活に新しい手法が加わることを切に願う。

この記事が気に入ったらサポートをしてみませんか?