見出し画像

OpenAI、Google、Anthropicの3つのAIに同じ質問をして、その結果を集約する「MAGI」システムを作る #役に立たないLLM

新世紀エヴァンゲリオンという名作アニメに、MAGIというシステムが出てきます。主人公たちが属する組織の中枢システムを担っており、3つの独立した思考回路を持っていて決議する仕組みです。

オタクとしてはMAGIに憧れがあります。

最近、Google CloudのPaLM2、AnthropicのClaudeがそれぞれ日本でも使えるようになり、API経由で利用することが出来るようになりました。AnthropicのClaudeのAPIはAWSのBedrockから利用します。ここにOpenAIのGPT-3.5-Turbo、GPT-4を合わせれば、「それぞれ独立したAI」によるMAGIを作れそうだと思ったので、さっそくやってみることにしました。

準備

私はWindows 11環境のローカルで動かしました。多分、すこし調整すればGoogle Colabでも動かせると思います。

まず、Google cloud、AWSそれぞれのCLI(コマンドラインインターフェース)をインストールして、初期設定を行い、CLI上でログインや認証を済ませておきます。このあたりは良い解説がたくさんあるので、それらに譲ります。

Google Cloudは、Vertex AIというサービス群の一部が言語AIです。以下の記事を参考に、プロジェクトを作成し、必要なAPIを有効にしておきます。

AWSでは、Bedrockが一番手軽に言語AIを利用出来るサービスです。Bedrockでは、モデル利用をリクエストしておかないと利用出来ないので、以下のページを参考にしてリクエストします。なお、リージョンはus-east-1が一番たくさんの種類のモデルを使えるのでこれがオススメです(東京リージョンはまだ使えるモデルが少ない)

(ちなみにこのAWSの解説ページは、機械翻訳の文章で意味が分かりにくいので、機会があればこの部分の解説記事も書きます…)

OpenAIのほうは、API経由での利用ができるようになっていればOKです。

必要なライブラリのインストール

venv環境を作って立ち上げた後で、以下のpipコマンドで必要なライブラリをインストールします

pip install openai gradio python-dotenv google-cloud-aiplatform boto3

openaiがOpenAIのGPT-3.5-Turbo/GPT-4、google-cloud-aiplatformがGoogle cloudのVertex AI、boto3がAWS Bedrockを、それぞれ扱う際に必要になるライブラリです。

OpenAIのAPI Keyや、Google cloudのプロジェクトIDなどは、.envファイルに保存してあるので、それを読み込むためにpython-dotenvも入れています。

ソースコード

以下の様になります。試行錯誤のあとがあるのであまり綺麗ではありませんが。

# GPT-4、Google PaLM2、Claudeに同じ質問をし、その賛成反対の多数決をとるシステム

import json
import openai
import gradio as gr
import dotenv
import vertexai
from vertexai.language_models import TextGenerationModel
import boto3
from concurrent.futures import ThreadPoolExecutor

dotenv.load_dotenv('.env')
openai_api_key = dotenv.get_key('.env', 'OPENAI_KEY')
vertexai_project = dotenv.get_key('.env', 'VERTEXAI_PROJECT')


# OpenAI GPT-3.5に質問を送信する関数
def ask_gpt35turbo(question: str) -> str:
    openai.api_key = openai_api_key
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[
            {"role": "system",
             "content": "あなたは答えが1つには決まらない問題に対して、"
                        "一つの視点を提供するアドバイザーとしての役割を持っています。"
                        "なので、以下の質問に対して、"
                        "必ず「賛成」または「反対」のどちらかの立場を表明してください。"
                        "最初に賛成であるか反対であるかを明示し、その後に理由を述べてください。"},
            {"role": "user",
             "content": question},
        ],
        temperature=0.2,
        max_tokens=512
    )
    answer = response.choices[0].message["content"]
    print(f"Response from GPT-3.5-Turbo: {answer}")
    return answer


# OpenAI GPT-4に質問を送信する関数
def ask_gpt4(final_answer: str, gpt35answer: str, palm2answer: str,
             claude2answer: str, question: str) -> str:
    openai.api_key = openai_api_key
    response = openai.ChatCompletion.create(
        model="gpt-4",
        messages=[
            {"role": "system",
             "content": "あなたは答えが1つには決まらない問題に対して、"
                        "一つの視点を提供するアドバイザーとしての役割を持っています。"
                        f"あなたは以下の問題に対して{final_answer}であるという立場を動かさずに、"
                        f"以下の3人の賢者の意見を参考にしながら視点をまとめてください。"
                        f"賢者カスパーの意見: {gpt35answer}"
                        f"賢者バルタザールの意見: {palm2answer}"
                        f"賢者メルキオールの意見: {claude2answer}"},
            {"role": "user",
             "content": question},
        ],
        temperature=0.2,
        max_tokens=1024
    )
    answer = response.choices[0].message["content"]
    return answer


# Google PaLM2に質問を送信する関数
def ask_palm2(question: str) -> str:
    vertexai.init(project=vertexai_project,
                  location="us-central1")
    parameters = {
        "candidate_count": 1,
        "max_output_tokens": 512,
        "temperature": 0.2,
        "top_p": 0.8,
        "top_k": 40
    }
    model = TextGenerationModel.from_pretrained("text-bison")
    response = model.predict(
        "あなたは答えが1つには決まらない問題に対して、"
        "一つの視点を提供するアドバイザーとしての役割を持っています。"
        "なので、以下の質問に対して、"
        "必ず「賛成」または「反対」のどちらかの立場を表明してください。"
        "最初に賛成であるか反対であるかを明示し、その後に理由を述べてください:"
        "====================\n"
        "質問:"
        + question,
        **parameters
    )
    print(f"Response from PaLM2: {response.text}")
    return response.text


# AWS BedrockのClaudeに質問を送信する関数
def ask_claude(question: str) -> str:
    bedrock = boto3.client(service_name='bedrock-runtime',
                           region_name='us-east-1')
    body = json.dumps({
        "prompt": "\n\nHuman: あなたは答えが1つには決まらない問題に対して、"
                  "一つの視点を提供するアドバイザーとしての役割を持っています。"
                  "なので、以下の質問に対して、"
                  "必ず「賛成」または「反対」のどちらかの立場を表明してください。"
                  "最初に賛成であるか反対であるかを明示し、その後に理由を述べてください:"
                  "====================\n"
                  "質問:"
                  f"\n{question}\n\nAssistant:",
        "max_tokens_to_sample": 512,
        "temperature": 0.2,
        "top_k": 250,
        "top_p": 1,
        "stop_sequences": [
            "\\n\\nHuman:"
        ],
        "anthropic_version": "bedrock-2023-05-31"
    })
    kwargs = {
        "modelId": "anthropic.claude-instant-v1",
        "contentType": "application/json",
        "accept": "*/*",

    }
    response = bedrock.invoke_model(body=body, **kwargs)
    response_body = json.loads(response.get('body').read().decode('utf-8'))
    print(f"Response from Claude: {response_body.get('completion')}")
    return response_body.get('completion')


# それぞれの回答の冒頭20文字を見て、「賛成」または「反対」の文字を探す関数
def find_answer(answer: str) -> str:
    first_20_chars = answer[:20]
    position_agree = first_20_chars.find("賛成")
    position_disagree = first_20_chars.find("反対")

    if position_agree != -1 and position_disagree != -1:
        # 両方が見つかった場合、最初に出現した方を返す
        return "賛成" if position_agree < position_disagree else "反対"
    elif position_agree != -1:
        return "賛成"
    elif position_disagree != -1:
        return "反対"
    else:
        return "不明"


# それぞれの回答を集計し、多数決をとる関数
def vote(answers: list) -> str:
    agree = 0
    disagree = 0
    for answer in answers:
        if answer == "賛成":
            agree += 1
        elif answer == "反対":
            disagree += 1
    if agree > disagree:
        return "賛成"
    elif agree < disagree:
        return "反対"
    else:
        return "不明"


# 質問を受け取り、それぞれのシステムに質問を送信し、回答を集計する関数
def print_result(question: str) -> str:
    answers = [find_answer(ask_gpt35turbo(question)),
               find_answer(ask_palm2(question)),
               find_answer(ask_claude(question))]
    final_answer = vote(answers)

    return f"OpenAI GPT-3.5 Turbo: {answers[0]}\n" \
           f"Google PaLM2: {answers[1]}\n" \
           f"AWS SageMaker Claude: {answers[2]}\n" \
           f"多数決: {final_answer}\n"


# 3つのAIの回答をGPT-4に渡して最終結果を得る関数
def magi_answer(question: str) -> str:
    with ThreadPoolExecutor() as executor:
        future_gpt35 = executor.submit(ask_gpt35turbo, question)
        future_palm2 = executor.submit(ask_palm2, question)
        future_claude = executor.submit(ask_claude, question)

        # 各関数の実行が終了するまで待つ
        gpt35answer = future_gpt35.result()
        palm2answer = future_palm2.result()
        claude2answer = future_claude.result()

    vote_result = vote([find_answer(gpt35answer),
                        find_answer(palm2answer),
                        find_answer(claude2answer)])

    final_answer = str(ask_gpt4(vote_result, gpt35answer, palm2answer,
                            claude2answer, question))

    final_answer = ("3賢者の回答:\n"
                    f"カスパー(GPT-3.5-Turbo){find_answer(gpt35answer)}、 "
                    f"バルタザール(PaLM2){find_answer(palm2answer)}、 "
                    f"メルキオール(Claude instant){find_answer(claude2answer)} \n"
                    "多数決: " + vote_result + "\n\n"
                    "マギの回答:\n"
                    ) + final_answer
    return final_answer


# Gradioインターフェースの作成
iface = gr.Interface(
    fn=magi_answer,  # 上で定義した関数
    inputs="text",  # 入力はテキスト
    outputs="text"  # 出力もテキスト
)

# インターフェースの起動
iface.launch()

このスクリプトを起動すると、`Running on local URL: http://127.0.0.1:7860` と、Gradioのインターフェースが立ち上がります。http://127.0.0.1:7860 にアクセスすると、以下の画面がブラウザに表示されます。

このquestion部分に質問を入れると、3つのAIに質問をして、その答えをGPT-4がまとめて回答してくれます。それぞれのAPIの利用料金が発生するので、あんまり連発するとお金がかかるので慎重にやりましょう(笑)

ちなみに、Gradioの仕様なのか、Enterキーを押すと送信されてしまうので、別のテキストエディタなどで質問文を作ってからquestionのフォームに貼り付けるようにするといいです(この辺の回避方法を調査中…)

こういうシステムには、良い悪いを簡単には決められないことを聞いてみると面白い気がします。たとえば、つまらない金持ちとドキドキする貧乏のうち、貧乏と付き合いたいと言うと、賛成2、反対1という結果が出ました。

ちなみに、GradioのUI上には表示させていませんが、それぞれのAIは以下の様に回答しています。

GPT-3.5-Turbo
私は賛成です。
理由は、人間関係やパートナーシップは単純な外見や財産だけではなく、相性や価値観の一致、感情の共有など、さまざまな要素によって形成されるからです。お金持ちで堅実なおじさんとの関係は安定感や経済的な安心感をもたらすかもしれませんが、私が求めているのはドキドキ感や刺激です。若いイケメンとの関係は、新たな経験や冒険、若さと活気を共有することができる可能性があります。
また、外見や財産だけでなく、人間性や性格も重要な要素です。暴力的な性格を持つ人との関係は、身体的な危険や精神的な苦痛をもたらす可能性があります。一方、イケメンであるからといって必ずしも性格が悪いわけではありません。若いイケメンの方が私の理想に近い性格や価値観を持っている可能性もあるので、彼との関係を選ぶことにしました。

PaLM2 Text bison
賛成です。
お金持ちで堅実だけどなんの面白みもない不細工なおじさんと、暴力的で貧乏だが貯まらなくドキドキする若いイケメンの二人に交際を申し込まれている場合、私はイケメンの方と交際したいと考えるようになりました。
その理由は、イケメンの方が私をドキドキさせてくれるからです。私は、私をドキドキさせてくれる人と付き合いたいと思っています。
最終的には、私自身が幸せになるために自分の感情や直感に従うべきだと考えます。他人の意見や社会の期待に囚われず、自分自身が望む関係を築くことが大切です。

Claude instant(AWS Bedrock経由)
反対です。
理由は以下の通りです。
外見や金銭面だけでは相手の価値は判断できません。暴力性や貧困状態は長期的な関係を築く上で大きな問題になる可能性があります。むしろ、性格や価値観、長期的な目標の一致性など、内面の部分で互いを尊重し合えるかが重要だと思います。両者のメリットデメリットを慎重に比較検討した上で、内面のコミュニケーションがとれそうな方を選択することをおすすめします。

これらをGPT-4がまとめてくれたのが、上記のスクリーンショットで得られた回答です。

別にこれが何かの役に立つことはないかもしれませんが、オタクとして、MAGIっぽいことができたのはとても楽しいです。

補足


GPT3の時代に、note CXOの深津さんがGPT3だけを使ってMAGIを作る記事もアップされていました。合わせて参考にしてみてください


宣伝

こういう、特に役に立たないけどなんだか面白いLLMの活用法を集めた技術同人誌を技術書典15に出そうと思って原稿を書いています。内容はnoteに書いた記事の再編集+noteには書かないタイプの描き下ろし記事になります。是非よろしくお願いいたします。

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