LoginSignup
9
9

OpenAIのAssistants APIでRAG(検索拡張生成)を実装してみた

Posted at

Supershipの名畑です。ドラゴンボールちびまる子ちゃんも子どもの頃からずっとそばにいてくれました。

はじめに

Retrieval-Augmented Generation (RAG) という技術があります。

Retrieval-Augmented Generation (RAG) は、大規模言語モデル(LLM)によるテキスト生成に、外部情報の検索を組み合わせることで、回答精度を向上させる技術のこと。

「検索拡張生成」、「取得拡張生成」などと訳されます。外部情報の検索を組み合わせることで、大規模言語モデル(LLM)の出力結果を簡単に最新の情報に更新できるようになる効果や、出力結果の根拠が明確になり、事実に基づかない情報を生成する現象(ハルシネーション)を抑制する効果などが期待されています。

参考:RAG | 用語解説 | 野村総合研究所(NRI)

OpenAIAssistants APIを用いてRAGを実装しようというのが今回の記事となります。
あらかじめ用意しておいた資料(テキストファイル)に書かれた内容を元にした回答をGPT4にしてもらいます。

GPTsと同じことをAssistants API経由で行ったとも言えます。Assistants APIGPTsも同一タイミング(2023年11月)でリリースされたものです。

GPTsについて詳しくは過去記事「ChatGPTの新機能であるGPTsを使って私のコピーを作ってみた」をご覧ください。

注意

Assistants APIをはじめとして、今回使用するAPIの多くはまだBeta扱いとなります。

料金

Pricingのページをご覧いただければと思いますが、モデル利用に加えてCode interpreterRetrievalの料金がかかります。

私の環境

OSはmacOS 14 Sonomaです。

今回の使用言語はPythonです。
バージョンは3.12.2です。

$ python --version
Python 3.12.2

openaiのライブラリもインストール済みです。

$ openai --version
openai 1.13.3

入っていない人はinstallしておきましょう。

$ pip install openai

API KeyはOpenAIのAPI keysのページで発行済みで、OPENAI_API_KEYという環境変数名で保存してあるとします。

export OPENAI_API_KEY=ここに取得したAPI Keyを書く

テキストファイルの準備

今回はお手製のテキストファイルをRAGにおけるデータソースとします。

下記をcompany.txtというファイル名で保存しておきます。

会社名:株式会社あいうえおほげ
社員数:65536人
事業内容:アニメと漫画についての話し相手になること
所在:東京都港区
社長:漫画好木
社風:静か
休日:水曜

import

ここからがPythonのコードとなります。

まずは必要なモジュールのimportを行います。

from openai import OpenAI
import os

OpenAIのインスタンス生成

OpenAIのインスタンス生成を行います。

client = OpenAI()
client.api_key = os.environ['OPENAI_API_KEY']  # 環境変数から取得

ファイルのアップロード

用意しておいたcompany.txtをOpenAIのサーバ上にFiles APIでアップロードします。

my_file = client.files.create(
  file=open("company.txt", "rb"),
  purpose="assistants"
)
file_id = my_file.id

purposeにはassistantsを指定しています。

次に使うため、レスポンスのidを保持しています。

アップロードしたファイルはOpenAIのサーバ上に保存されますので、使い回し可能です。

アシスタントの生成

Assistants APIを用いてアシスタントを生成します。file_idsとして先ほどファイルアップロード時に保持していたidを指定します。今回は一つですが、複数指定可能です。

my_assistant = client.beta.assistants.create(
    instructions="添付されたファイルは日本語のテキスト形式です。このファイルを開いて質問に答えてください。",
    name="secretary",
    model="gpt-4-turbo-preview",
    tools=[{"type": "code_interpreter"}],
    file_ids=[file_id]
)
assistant_id = my_assistant.id

modelはここではgpt-4-turbo-previewとしました。以下の記載があったためです。

The Retrieval tool requires at least gpt-3.5-turbo-1106 (newer versions are supported) or gpt-4-turbo-preview models.

instructionsnameはオプションですが書いておきました。「添付されたファイルは日本語のテキスト形式です。このファイルを開いて質問に答えてください。」という指示を与えてあります。

以降で使うため、生成されたアシスタントのidを保持しておきます。

生成したアシスタントはファイル同様にOpenAIのサーバ上に保存されますので、使い回し可能です。

スレッドの生成

アシスタントとの一連の流れを処理するためにThreads APIを用いてスレッドを生成します。

スレッドに対してアシスタントとのやりとりを紐づけ、連続したものとして扱うことができます。

thread = client.beta.threads.create()
thread_id = thread.id

やはりidを保持しておきます。

生成したスレッドも使い回し可能です。

スレッドに紐づけたメッセージの生成

Messages APIを用いてメッセージを生成します。この際、生成済みのスレッドを紐づけています。こうすることでやり取りが保持されます。

message = client.beta.threads.messages.create(
    thread_id= thread_id,
    role="user",
    content="会社名を教えてください。"
)

今回は「会社名を教えてください。」と聞いています。

スレッドの実行

Runs APIを用いてスレッド上で処理を投げます。保持しておいたアシスタントとスレッドのidを指定します。

statuscompletedになるのを待ち、messagesから結果を取得します。今回のコードではqueuedin_progresscompletedの3つのみを判定材料としていますが、他にもあります。

run = client.beta.threads.runs.create(
  thread_id=thread_id,
  assistant_id=assistant_id,
)
run_id = run.id

while run.status in ["queued", "in_progress", "completed"]:
    run = client.beta.threads.runs.retrieve(
        thread_id=thread_id,
        run_id=run_id
    )
    if run.status == "completed":
        messages = client.beta.threads.messages.list(
            thread_id=thread_id
        )
        print(messages.data[0].content[0].text.value)
        break

出力結果は以下でした。

会社名は「株式会社あいうえおほげ」です。

無事にファイルの内容が取得できています。

ここまでで目的の実装は完了となります。

ファイル等の削除

実務では使い回すことの方が多いかと思いますが、今回はファイルやインスタンスを削除しておくことにします。

print(client.files.delete(file_id=file_id))  # ファイルの削除
print(client.beta.assistants.delete(assistant_id))  # アシスタントの削除
print(client.beta.threads.delete(thread_id=thread_id))  # スレッドの削除

以下が出力されましたので、きちんと削除されていそうです。

FileDeleted(id='file-hogehoge', deleted=True, object='file')
AssistantDeleted(id='asst_hogehoge', deleted=True, object='assistant.deleted')
ThreadDeleted(id='thread_hogehoge', deleted=True, object='thread.deleted')

余談ですが、filesassistantsはlist取得のAPIが見つかるのですが、threadsはありませんでした。OpenAI Developer ForumでもList and delete all threadsなどの議論が行われているのが見つかります。

今回のコードをまとめると

今回のコードをまとめると以下になります。

from openai import OpenAI
import os
client = OpenAI()
client.api_key = os.environ['OPENAI_API_KEY']  # 環境変数から取得

# ファイルのアップロード
my_file = client.files.create(
  file=open("company.txt", "rb"),
  purpose="assistants"
)
file_id = my_file.id

# アシスタントの生成
my_assistant = client.beta.assistants.create(
    instructions="添付されたファイルは日本語のテキスト形式です。このファイルを開いて質問に答えてください。",
    name="secretary",
    model="gpt-4-turbo-preview",
    tools=[{"type": "code_interpreter"}],
    file_ids=[file_id]
)
assistant_id = my_assistant.id

# スレッドの生成
thread = client.beta.threads.create()
thread_id = thread.id

# スレッドに紐づけたメッセージの生成
message = client.beta.threads.messages.create(
    thread_id= thread_id,
    role="user",
    content="会社名を教えてください。"
)

# スレッドの実行
run = client.beta.threads.runs.create(
  thread_id=thread_id,
  assistant_id=assistant_id,
)
run_id = run.id

while run.status in ["queued", "in_progress", "completed"]:
    run = client.beta.threads.runs.retrieve(
        thread_id=thread_id,
        run_id=run_id
    )
    if run.status == "completed":
        messages = client.beta.threads.messages.list(
            thread_id=thread_id
        )
        print(messages.data[0].content[0].text.value)
        break

# ファイル等の削除
print(client.files.delete(file_id=file_id))  # ファイルの削除
print(client.beta.assistants.delete(assistant_id))  # アシスタントの削除
print(client.beta.threads.delete(thread_id=thread_id))  # スレッドの削除

おまけ 複数指示の実施

メッセージの生成スレッドの実行を書き換えて、配列に格納した3つの指示を順番に投げてみます。

  • 会社の休日を教えてください
  • 社員数を教えてください
  • 私が社員数を聞く前にした質問を教えてください

3つ目は一連の流れが保持されているかを確認できるような質問にしてあります。

my_instructions = [
    "会社の休日を教えてください",
    "社員数を教えてください",
    "私が社員数を聞く前にした質問を教えてください"
]

for instruction in my_instructions:
    message = client.beta.threads.messages.create(
        thread_id=thread.id,
        role="user",
        content=instruction
    )

    run = client.beta.threads.runs.create(
      thread_id=thread_id,
      assistant_id=assistant_id,
    )
    run_id = run.id

    while run.status in ["queued", "in_progress", "completed"]:
        run = client.beta.threads.runs.retrieve(
            thread_id=thread_id,
            run_id=run_id
        )
        if run.status == "completed":
            messages = client.beta.threads.messages.list(
                thread_id=thread_id
            )
            print(messages.data[0].content[0].text.value)
            break

出力は以下でした。

会社の休日は水曜日です。
社員数は65536人です。
あなたが社員数を聞く前にした質問は「会社の休日を教えてください」でした。

やりとりがきちんと保持されていそうです。

Playground

Playgroundというものが用意されており、ブラウザ上で動作を見ることも可能です。アップロードしたファイルや生成したアシスタントの一覧もブラウザ上で確認可能です。

最後に

まだBetaではあるものの、かなり便利ですね。

宣伝

SupershipのQiita Organizationを合わせてご覧いただけますと嬉しいです。他のメンバーの記事も多数あります。

Supershipではプロダクト開発やサービス開発に関わる方を絶賛募集しております。
興味がある方はSupership株式会社 採用サイトよりご確認ください。

9
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9