見出し画像

CHATGPT4とGemini Proに会話してもらった/実装編

こんにちはmakokonです。
openaiのGPT4とgoogleのGemini Proに前回会話してもらいました。なかなかに良い感じの会話を生成してくれたので、今回はそのコードを紹介します。
初めてpythonでclassをつかったので、復習のために、詳しく説明しています。タイトル画は、前回と同じです。


準備

いつものように必要なライブラリをインポートします。
GPT4のためには、openai,langchain
Gemini Proのためには、google.generativeai
を利用します。
また、環境変数にOPENAI_API_KEY,GEMINI_API_KEYを設定しておきます。
ついでに、今回の会話のお題を設定しておきます。これは、もちろんinput文で入力してもいいし、ファイルから読み込むことにしたほうが便利かもしれませんが、決め打ちしておきました。

import os
import datetime
# google
import google.generativeai as genai
# openai
import openai
from langchain.chat_models import ChatOpenAI
from langchain.chains import ConversationChain
# Memory
from langchain.memory import ConversationBufferWindowMemory

#今回のお題
odai="『週末に二人で出かけるキャンプの計画を意見の異なった部分をすり合わせて決めたい』"

OPENAIの準備

次に、GPT4周りの設定をしておきます。
変数が似通って混乱しやすかったので、今回は初めてclassを使いました。

Pythonのclassを使用する利点はいくつかあります。


  1. 再利用性: 一度定義したクラスは何度でもインスタンス化でき、同じ動作を持つオブジェクトを容易に作成できます。

  2. カプセル化: クラス内のデータとメソッドを一つにまとめることで、外部から直接アクセスできないようにすることができます。これにより、不必要なデータ変更を防ぎ、プログラムの安全性を高めます。

  3. 継承: 一つのクラスが別のクラスの属性やメソッドを継承できます。これにより、コードの再利用性が向上し、新たな機能を簡単に追加できます。

  4. 抽象化: 実際の世界のオブジェクトをコードで表現することができます。これにより、問題をより直観的に理解し、解決策を見つけやすくなります。

今回は、特にカプセル化の効果を期待しています。なんせ、chatとかmemoryとかhistoryとかpromptとか似たような名前の変数をたくさん使いそうですからね。とても覚えていられません。
メインからは、結果=LLM(質問)みたいな単純なやり取りだけを残したかった。

class gpt4:

# openai chatgpt class
class gpt4:
  def __init__(self):
    self.initialized = False # first execute flag
    
  def chat(self,str):
    if not self.initialized : # LLMモデル設定
      self.initialized = True # change flag
      global odai
      from langchain.prompts.chat import (
        ChatPromptTemplate,
        MessagesPlaceholder, 
        SystemMessagePromptTemplate,
        HumanMessagePromptTemplate,
      )
      chat = ChatOpenAI(model_name="gpt-4",temperature=0.7)
      memory = ConversationBufferWindowMemory(k=8, return_messages=True)
      template=f"""これは、ガプトさんとゲミニさんの人間同士のフレンドリーな会話です。
あなたはガプトさんとして、ガプトさんのセリフのみ生成してください。
お題または、ゲミニさんの発言に対し、簡潔に300文字未満で意見を述べるとともに、発言内容に関する質問を一つしてください。
回答の出だしは。”ゲミニさん。”です。
今回のお題は、{odai}です。
会話の要約を求められたときには、600文字未満で要点を整理して答えてください。
"""
      prompt = ChatPromptTemplate.from_messages([
        SystemMessagePromptTemplate.from_template(template),
        MessagesPlaceholder(variable_name="history"),
        HumanMessagePromptTemplate.from_template("{input}")
      ])
      self.conversation = ConversationChain(llm=chat, memory=memory, prompt=prompt)
      
    result = self.conversation.predict(input=str)
    return result

def init(self): はこのクラスの関数が呼び出されたとき、最初に実行されます。ここに初期化処理をたくさん書いてもいいのですが、今後の練習もあるので、初期化済みフラグだけを設定しました。まだ、「初期化していないよ」ということですね。
chat((self,str)が外部から呼び出される関数です。selfを利用することが大切で、selfがついている変数とかは、2回目に呼び出されたときにも保持しているのでとても便利です。
if not self.initialized : で初期化フラグがTrueでなければ、各種初期化をします。モデル、メモリ、システムプロンプト、プロンプトテンプレート、ラングチェインを設定します。
大切なのは、self.conversation()でこれが毎回初期化されないように、selfをツケています。
システムプロンプトに含まれる{odai}はグローバル宣言して、メインの最初D設定したodaiを使える方にしています。グローバル変数を嫌う人もいて、コーディングの趣味はそれぞれなんですけど、私は一回きりで使われるものはあまり気にせずにグローバル扱いします。
このシステムプロンプトで、今回のGPT4の役割を決めています。
つまり、「あなたはガプトさんで、ゲミニさんとお話して、キャンプの計画を決めるんだよ」ということと、最後に要約してもらうかもねということです。
strは、ユーザープロンプトです。
result = self.conversation.predict(input=str)
return result

本番は、self.coversation.predict()を呼び出して、結果をメインに返します。

Gemini Proの準備

こちらもやっぱりclassでまとめておきます。
また、今回の会話の手順ですが、
ゲミニ(呼びかけ)
ガプト(意見と質問)、ゲミニ(意見と質問) ーー繰り返し
ガプト(最後の意見と会話の要約)という手順を想定しています。
そこで、最初の呼びかけの部分を特別に記述しています。

class geminipro:

# google gemini class
class geminipro:
  def __init__(self):
    self.initialized = False # first execute flag
    self.messages = []
    
  def chat(self,str):
    if not self.initialized : # LLMモデル設定
      self.initialized = True # change flag
      global odai
      GEMINI_API_KEY=os.getenv('GEMINI_API_KEY')
      genai.configure(api_key=GEMINI_API_KEY)
      system_prompt_gemini=f"""これは、ガプトさんとゲミニさんの人間同士のフレンドリーな会話です。
あなたはゲミニさんとして、条件を厳守して、ゲミニさんのセリフのみ生成してください。
条件:
- お題または、ガプトさんの発言に対し、簡潔に300文字未満で意見を述べるとともに、発言内容に関する質問を一つしてください。
- 回答の出だしは、”ガプトさん。”です。
- 今回のお題は、{odai}です。
 """
      # モデルの設定
      generation_config = {
        "temperature": 0.9,  # 生成するテキストのランダム性を制御
        "top_p": 1,          # 生成に使用するトークンの累積確率を制御
        "top_k": 1,          # 生成に使用するトップkトークンを制御
        "max_output_tokens": 512,  # 最大出力トークン数を指定
      }
      
      self.model = genai.GenerativeModel(model_name="gemini-pro",
                           generation_config=generation_config)

      self.messages = [
        {'role':'user',
         'parts': [system_prompt_gemini]}
      ]
      
    if str!="first_message":
      self.messages.append({'role':'user',
        'parts':[str]})

    response = self.model.generate_content(self.messages)
    self.messages.append({'role':'model',
      'parts':[response.text]})

    return response.text

 
def init(self): 初期化済みフラグの設定とメッセージリストを初期化します。Gemini Proのチャットにはメモリー機能が標準であるのですが、どうもただ覚えているだけで、一貫性のある会話をしてくれないような気がしました。幸い、generate_content()は、プロンプトとしてリストを渡せる事がわかったので、これで一貫した応答が期待できます。履歴を含めてメッセージリストで自分で管理する必要がありますけどね。
def chat(self,str)が実際には呼び出されます。GPT4と同じ名前にしてしまいましたが、classのお陰でかえってわかりやすいです。
if not self.initialized :で初期化をします。
APIキーの設定、モデルの設定、システムプロンプトの設定
modelは繰り返し呼び出されるのでselfつきです。
システムプロンプトの{odai}はGPT4と同じものです。
ゲミニさんとして、ガプトさんと会話してもらいます。
意見と質問を必ずセットで出力するように指示しているのは、これをしないとどうしても話が一方的で、尋ねるばかりの人と答えるばかりの人に役割分担しがちです。
会話履歴兼プロンプトとなるmessagesは、呼び出されるたびに初期化すると困るので、self付きです。その構造は以下のようにuser,modelの繰り返しです。
self.messages.append({'role':'user', 'parts':[テキスト]})
self.messages.append({'role':'model', 'parts':[テキスト]})

userのときのテキストにプロンプトとしてメッセージリストを渡すので、strをリストに追加して、llmを呼び出し、結果買ってきたらそれも追加します。
会話を始める切っ掛けを作らないといけないので、
"first_message"を入力として受け取ったときは、システムプロンプトを履歴に追加(初めての履歴だけど初期化じゃなく、追加のほうが読みやすいと勝手に思っている)して、会話を始めます。
response = self.model.generate_content(self.messages) self.messages.append({'role':'model', 'parts':[response.text]}) return response.text
本番は普通にself.model.generate_content(self.messages)を呼び出し、その結果の本文(テキスト)部分だけを履歴に追加したうえで、メインに返します。

チャット処理

メインのチャット処理です。


all_history=[]
geminipro_instance = geminipro()
gpt4_instance=gpt4()

response = geminipro_instance.chat("first_message") # first message
print("ゲミニ:"+response)
all_history.append("ゲミニ:"+response)
gemini_response = response

for i in range(6):
  print("ターン",i+1)
  #gpt phase
  gpt_response =  gpt4_instance.chat(gemini_response)
  print("ガプト:"+gpt_response)
  all_history.append("ガプト:"+gpt_response)
  
  #gemini phase
  response = geminipro_instance.chat(gpt_response)
  print("ゲミニ:"+response)
  all_history.append("ゲミニ:"+response)
  gemini_response = response
  
# summary
user=gemini_response + "最後の質問に簡潔に答えた上で、これまでの会話を要約してください。"
gpt_response = gpt4_instance.chat(user)
print(gpt_response)
all_history.append(gpt_response)

#chat history save file
filename = "loggemini" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + ".txt"
print("**************************************")
with open(filename,"w") as outfile:
  for message in all_history:
    result = f'{message}\n'
    print(result)
    outfile.write(result+"\n")


all_history=[] はログ保存及び会話表示用の履歴です。発言者と発言内容をシンプルに保存します。
geminipro_instance = geminipro()
gpt4_instance=gpt4()

classのインスタンスを設定します。とても大事で教科書的にもそう書いてあるのですが、これの意味を知りませんでした。単純に言うと、
gpt4_instance.chat("first") 一回目の処理
gpt4_instance.chat("second") 二回目の処理
となるのに対し、
gpt4().chat("firast") 一回目の処理
gpt4().chat("second") 一回目の処理
となって、毎回初期化プロセスを通ることになります。同じ名前でも別のプロセスなんですね。
続いて最初のターン ゲミニさんの発言
response = geminipro_instance.chat("first_message") # first message
print("ゲミニ:"+response)
all_history.append("ゲミニ:"+response)
gemini_response = response

まだ会話が始まっていないので、"first_message"を渡します。
そうするとGemini Proの中では、システムプロンプトがそのまま処理されます。responseでは「ガプトさん。そろそろ計画の詳細を決めましょうか」みたいなことをいいます。
システムプロンプトは、我々にとっては会話じゃないので、これはall_historyには記録せず、最初のresponseから、会話履歴スタートですね。
responseはgemini_responseとして、次はgpt4にわたります。

for i in range(6): 以下では、会話を繰り返します。
gpt4は gemini_response→呼び出し→gpt_response
Gemini Proは gpt_response→呼び出し→gemini_response
呼び出し部分をclassのお陰で同じ形にしたのでとても見やすくなりました。

ループが終わったときにゲミニさんからガプトさんへの質問で終わっているので最後のsummaryで、総括します。
user=gemini_response + "最後の質問に簡潔に答えた上で、これまでの会話を要約してください。"
gpt_response = gpt4_instance.chat(user)
print(gpt_response)
all_history.append(gpt_response)
 
userにはゲミニの最後のメッセージに加えて、これが最後で、質問に答えた上で要約するようにプロンプトを構成します。userという変数にする必要はないのですが、2つのllmのどちらのセリフでもないので、makokonが割り込んだメッセージであることを明確にしています。
もちろんこのメッセージは、all_historyには含まれません。
最後の回答と要約を、履歴に追加して会話を終了します。

終了処理

最後にall_historyの内容を改めて表示するとともにログファイルに書き出してプログラムは終了です。

まとめ

以上、前回の記事と合わせて、gpt4とGemini Proの人間らしい会話を実現するコードを紹介しました。どちらも言語読解力が素晴らしく、シンプルな指示で、適当な設定を作りながら自然に会話をしてくれました。
プログラミング技術としては、今回始めてclassを使いましたが、非常に見通しの良い、便利な方法だと実感しました。この程度の処理だと、あまり関係ないかもしれませんが、複雑な処理を組み合わせるようになると、必須の構造かもしれません。
プログラムの完成度としてはせいぜい20%でしょうか。

  • 各種お題やロールに対して全く汎用性がありません。

  • token制限に対しても、見切りで実行しています。

  • 会話が収束(合意に達した)かどうかの判定がなく、見切りになっています。

  • エラー対策がありません。レスポンスが帰らないときは勝手に終了します。

  • llmのセーフティ対策もありません。会話の流れによっては、急に終了するかもしれません。

などなど、いくらでもありますが、まあ、初心者のプログラムなのでもし勝手に止まったらごめんなさい。
今後も安全性とかにはまともには対応しないと思います。

#google #openai #gpt4 #gemini -pro #chat #お互いに会話 #python #program #langchain #chat_history #class #system_prompt


いいなと思ったら応援しよう!