Zenn
🐕

OpenAIのResponses APIでRAGをやってみる

2025/03/24に公開
1

OpenAIが新たにリリースした Responses API を利用して簡単なRAGを構築してみました。Responses APIの会話履歴機能と、組み込みツールの一つであるファイル検索(ベクトル検索)を組み合わせると、いとも簡単なコードでRAGを実現できます。

OpenAIの新しいAPI: Responses API

OpenAIは、2025年3月11日に、これまでの Chat Completions の完全上位互換となる新しいAPI Responses API をリリースしました。

https://openai.com/ja-JP/index/new-tools-for-building-agents/

Chat Coompletions との主な違いは以下のとおりです。

  1. 会話履歴をOpenAI側で管理することが可能
  2. 画像ファイルに加えて、PDFファイルを参照しての会話が可能
  3. Web検索、ファイル検索(ベクトルストア)、コンピュータ操作の3つの組み込みツールを利用可能

このうち、RAGを実現するために必要な機能は、1.の会話履歴の管理機能と、3.のファイル検索機能の2つです。

2.のPDFファイルを参照しての回答生成は、検索機能はなく、コンテキストに指定したPDFファイルの内容(テキストと画像)を展開する機能のようです。単一ファイルの検索であれば、RAGではなく、2.の機能を用いたもよいかもしれません。ただし、2025年3月時点では、画像ファイルとPDFファイルしか対応していません。

ということで、1.の会話履歴と、3.のファイル検索の機能を組み合わせて、簡単なRAGを作ってみます。

なお、今回は、OpenAIのResponses APIの使い方を学ぶため、LangChainを用いず、OpenAIのPython SDKを用いることにします。

会話履歴機能の確認

RAGを構築する前に、会話履歴の動作について確認しておきます。

これまでの Chat Completions API はステートレスなAPIであったため、会話履歴の管理はプログラム側(APIを呼び出す側)で実施しておく必要がありました。これまでの会話のリストを保持しておき、次の会話の入力に、これまでの会話履歴を添付する実装が一般的です。

以下の記事では、会話履歴をキーバリューストアのRedisで管理していました。

https://zenn.dev/khisa/articles/fbfed2f10c1ad7

今回リリースされたResponses APIでは、デフォルトで、API側で会話履歴を管理することができます。そのため、プログラム側(呼び出し側)での会話履歴の管理が不要となりました。

Responses API 会話履歴機能の実装

簡単なコードで、Responses APIの会話履歴機能をテストしてみます。

実行する前に関連パッケージをインストールしておいてください。

$ pip install openai python-dotenv

以下のような、会話履歴を用いる簡単なコードで動作を確認します。

conversation_history_test.py
"""
OpenAI Responses API で会話履歴のテストをする
"""
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()
client = OpenAI()

count = 0
previous_response_id = None
while True:

    # 会話を入力
    print("---")
    input_message = input(f"[{count}]あなた: ")
    if input_message.lower() == "終了":
        break

    # 生成AIが回答を生成
    response = client.responses.create(
        model="gpt-4o-mini",
        input=input_message,
        previous_response_id=previous_response_id
    )

    # 回答を表示
    print(f"[{count}]AI: {response.output_text}")
    previous_response_id = response.id
    count += 1

ポイントとなる部分は、以下のclient.responses.create()です。

# 生成AIが回答を生成
response = client.responses.create(
  model="gpt-4o-mini",
  input=input_message,
  previous_response_id=previous_response_id
)

previous_response_idに、前回client.responses.createをコールした時のレスポンスIDを渡します。すると、OpenAI側で自動的に保持している会話履歴を参照するようになります。

1回目の会話では、会話履歴はありませんので、previous_response_id = Noneとしておきます。

2回目以降は、以下のように、前回の会話のレスポンスに含まれるIDを渡します。

previous_response_id = response.id

たったこれだけで会話履歴を考慮したAIとのチャットができるようになります。

Responses API 会話履歴機能の動作確認

実際に動作させてみました。

$ python .\conversation_history_test.py       
---
[0]あなた: こんにちは。私は黒猫のキキを飼っています。
[0]AI: こんにちは!キキちゃん、かわいい名前ですね。どんな性格の猫ちゃんですか?また、キキちゃんとのエピソードなどあれば教
えてください!
---
[1]あなた: とてもやさしく、ボール遊びが好きな猫です。
[1]AI: やさしくてボール遊びが好きなんですね!楽しそうですね。ボール遊びは猫ちゃんにとってもいい運動になりますし、一緒に遊
ぶと絆が深まりますね。キキちゃんの遊び方や、お気に入りのボールなどはありますか?
---
[2]あなた: 私のペットの名前は?
[2]AI: あなたのペットの名前はキキですね!黒猫のキキちゃんとの楽しい時間を素敵に過ごしていることが伝わってきます。
---
[3]あなた: 私のペットの鳴き声は?
[3]AI: 黒猫のキキちゃんの鳴き声は、一般的には「ニャー」とか「ニャーオ」といった感じでしょうか。猫によって鳴き声には個性が
ありますが、声のトーンや仕方はどんな感じですか?

[2]や[3]の質問にAIが正確に答えています。このように、AIが前回までの会話内容を踏まえた回答をしていることがわかります。

OpenAIのダッシュボードのLogsでも確認できます。

上の図は[3]の会話のログです。Inputの上部と、Previous Responseのところに、前回の会話内容が保持されています。ここでは1つしか会話履歴が見えませんが、実際には過去のPrevious ResponseがすべてInputとして与えられています。

それは、Inputのトークン数を確認するとわかります。

会話回数 入力トークン 出力トークン
1回目 38 39
2回目 100 68
3回目 183 38
4回目 239 62

会話の回数を経るごとに、入力トークン数が増加しています。これは、過去の会話履歴がすべてInputとして入力されているためです。

Files API とベクトルストアを利用したResponses APIでのRAG構築

それでは、Files APIとベクトルストアを利用したRAGを構築していきます。なお、各コードについては、OpenAIの以下のクックブックにあるRAGのサンプルコードを参考にさせてもらっています。

https://cookbook.openai.com/examples/file_search_responses

ベクトルストアの構築

組み込みツールとして提供されているファイル検索は、いわゆるベクトル検索が可能です。ベクトル検索を実施するには、以下の準備が必要となります。

  1. ベクトルストアを新規に作成
  2. Files API を用いてファイルをアップロード
  3. 2.でアップロードしたファイルをベクトルストアに格納

まず、ベクトルストアを新規に構築します。ベクトルストアを構築するには、client.vector_stores.create()関数を利用します。

def create_vector_store(store_name: str) -> dict:
    """Vector Store を作成
    Args:
        store_name (str): Vector store name
    Returns:
        dict: 作成したvector storeの情報
    """
    try:
        vector_store = client.vector_stores.create(name=store_name)
        details = {
            "id": vector_store.id,
            "name": vector_store.name,
            "created_at": vector_store.created_at,
            "file_count": vector_store.file_counts.completed
        }
        print("Vector store created:", details)
        return details
    except Exception as e:
        print(f"Error creating vector store: {e}")
        return {}

ベクトルストアを構築するのはとても簡単で、client.vector_stores.create(name=store_name)を実行するだけです。

後ほど、ファイルを追加していくときに、ベクトルストアのIDが必要になります。前述のclient.vector_stores.create()の返り値に含まれるvector_store.idを保存しておきます。

なお、ベクトルストアの情報は、OpenAI API ダッシュボードのStorageVector storesタブからも確認できます。ここで、指定したnameでのベクトルストアが作成されていればOKです。

https://platform.openai.com/storage/vector_stores/

次に、Files APIを利用してファイルをアップロードし、それぞれのファイルをベクトルストアに格納していきます。OpenAIのベクトルストアは、いったんアップロードしたファイルを単位としてベクトルストアに格納していくようになっています。

今回は、Wordpressで運営しているブログの記事をアップロードします。Wordpressのエクスポート機能でエクスポートしたXMLファイルを処理して、各記事のコンテンツとメタデータを以下の形式のdictとして格納したリストを渡します。

- content: 記事内容(HTMLの文字列)
- url: 記事のソースURL
- post_id: 記事のポストID
- title: 記事のタイトル
- category: カテゴリのリスト
- modified: 更新日時

コードは以下のようになります。

def upload_single_content(article: dict, vector_store_id: str):
    """
    記事の単一コンテンツをベクターストアにアップロードする関数
    """

    # ファイル名
    filename = article["url"].split("/")[-1]
    if "." not in filename:
        filename += ".html"

    try:
        # ファイルとして記事のコンテンツをアップロード
        file = client.files.create(
            file=(filename, article["content"].encode("utf-8")),
            purpose="assistants"
        )

        # アップロードしたファイルを Vector store に追加
        metadata = {k: v for k, v in article.items() if k != "content"}
        client.vector_stores.files.create(
            vector_store_id=vector_store_id,
            file_id=file.id,
            attributes=metadata,
        )

        return {"file": filename, "status": "success"}
    
    except Exception as e:
        print(f"Error with {filename}: {str(e)}")
        return {"file": filename, "status": "failed", "error": str(e)}

順番に見ていきます。まず、Files API を用いて、ファイルをアップロードします。

# ファイルとして記事のコンテンツをアップロード
file = client.files.create(
    file=(filename, article["content"].encode("utf-8")),
    purpose="assistants"
)

client.files.create()では、fileにファイルのパスを渡すことでファイルがアップロードされますが、今回はXMLファイルを解析して抽出したHTMLを文字列として渡します。そのような場合は、file引数にtuple形式で、(ファイル名, 文字列)を渡します。

次に、アプロードしたファイルをベクトルストアに追加します。

# アップロードしたファイルを Vector store に追加
metadata = {k: v for k, v in article.items() if k != "content"}
client.vector_stores.files.create(
    vector_store_id=vector_store_id,
    file_id=file.id,
    attributes=metadata,
)

ファイルをベクトルストアに追加するには、client.vector_stores.files.create()関数を利用します。各引数は以下のとおりです。

  • vector_store_id: ベクトルストアのID
  • file_id: ファイルID
  • attributes: メタデータのdict

vector_store_idにはベクトルストアを作成したときの戻り値に含まれるidを、file_idには先ほどアップロードしたファイルの戻り値に含まれるidを渡します。

attributesはオプションで、ベクトルストアにメタデータを付与するときに利用します。今回は、記事のURLや投稿日時、カテゴリなどを入れたdictをそのまま渡しています。

なお、チャンキングのオプションもchunking_strategyとして指定できます。OpenAIのAPIドキュメントによると、以下のような設定ができるようです。

  • auto: The default strategy. This strategy currently uses a max_chunk_size_tokens of 800 and chunk_overlap_tokens of 400.
  • static: Customize your own chunking strategy by setting chunk size and chunk overlap.

デフォルトでチャンクサイズは800トークン、オーバーラップは400トークンに設定されているようです。これらはカスタマイズすることも可能です。詳しくは、以下のOpenAIのAPIドキュメントを参照ください。

https://platform.openai.com/docs/api-reference/vector-stores-files

RAGの実装

ベクトルストアの準備ができたので、次はベクトルストアを利用した会話履歴付きのRAGを実装していきます。

…といっても、ベクトルストアまで構築できていれば、RAGの実装はとても簡単です。全体のコードは以下のようになります。

query_vectorsearch_with_history.py
from dotenv import load_dotenv
from openai import OpenAI

# .env
load_dotenv()

# OpenAI client
client = OpenAI()

# Vector Store ID
VECTOR_STORE_ID = "<vector store id>"

count = 0
previous_response_id = None
while True:

    # 会話を入力
    print("---")
    input_message = input(f"[{count}]あなた: ")
    if input_message.lower() == "終了":
        break

    # 生成AIが回答を生成
    response = client.responses.create(
        model="gpt-4o-mini",
        input=input_message,
        tools=[{
            "type": "file_search",
            "vector_store_ids": [VECTOR_STORE_ID],
            "max_num_results": 5
        }],
        include=["file_search_call.results"],
        previous_response_id=previous_response_id
    )

    # Extract annotations from the response
    annotations = None
    if len(response.output) > 1:
        annotations = response.output[1].content[0].annotations

    # Get top-k retrieved filenames
    retrieved_files = set([result.filename for result in annotations]) if annotations else None

    print(f'Files used: {retrieved_files}')
    print(f"[{count}]AI:\n")
    print(response.output_text)

    # 会話履歴を更新
    previous_response_id = response.id
    count += 1

ポイントとなる部分は、以下のclient.responses.create()です。

# 生成AIが回答を生成
response = client.responses.create(
    model="gpt-4o-mini",
    input=input_message,
    tools=[{
        "type": "file_search",
        "vector_store_ids": [VECTOR_STORE_ID],
        "max_num_results": 5
    }],
    include=["file_search_call.results"],
    previous_response_id=previous_response_id
)

client.responses.create()に与えているパラメータの意味は以下のとおりです。

  • model: LLMのモデル名
  • input: ユーザーの入力メッセージ
  • tools: 利用するツールとしてファイル検索("file_search")を設定
    • vector_store_ids: ベクトルストアのID
    • max_num_results: ベクトルストアから検索するドキュメントの最大数
  • include: "file_search_call.results"を設定すると検索されたドキュメントも取得できる(オプション)
  • previous_response_id: 会話履歴(前回のレスポンスID)

これだけでRAGを実現できます。とても簡単ですね。

Responses APIを利用したRAGの動作確認

それでは、動作確認をしてみます。前述のコードは、whileループで繰り返し会話ができるようになっています。

今回ベクトルストアに取り込んだドキュメントは、私が運営している鉄道関連の以下のブログです。青春18きっぷ関連の記事が100記事程度あります。

https://www.kzlifelog.com/seishun18/

1ファイル1記事として、HTMLのままファイルをアップロードしています。ベクトルストアの chunking strategy はデフォルトのままです。

今回実装したRAGでの会話例は以下のとおりです。(長いのでトグルを開いてご覧ください)

RAGでの会話例
$ python .\query_vectorsearch_with_history.py 
---
[0]あなた: こんにちは。
[0]AI: Files:None
こんにちは!何かお手伝いできることがありますか?アップロードされたファイルについてお探しの情報があれば教えてください。
---
[1]あなた: 青春18きっぷの魅力について教えて
[1]AI: Files:{'seishun18-enjoy-train-trip.html', 'jr-seishun18-tips1.html'}
青春18きっぷの魅力は、以下のポイントに集約されます:

1. **無限の自由度**: 青春18きっぷは、日本全国のJR線(普通列車・快速列車)が乗り放題です。このため、旅行先を決めるのではな
く、きっぷを買ってから「どこへ行こうか」と考える楽しみがあります。

2. **気ままな途中下車**: フリーエリアが広いため、途中下車をしながら自分のペースで旅行を楽しめます。気に入った風景や町にふ
らりと立ち寄ることができ、 JRの在来線の延長距離は約1万7千キロと広大です。

3. **旅行コストの削減**: 1日あたりの料金が2,410円で、これを利用することでほぼ全国を旅行できるというコストパフォーマンスも
魅力の一つです。

4. **旅行の自由度**: 時間さえ許せば、どこへでも行ける「夢のフリーきっぷ」です。特定の目的地を決めずに、旅を進めていく楽し
さがあります。

このように、青春18きっぷは、安さだけでなく、自由な旅行スタイルやその体験自体を楽しむことができる点が大きな魅力です。     
---
[2]あなた: いつ利用できますか?               
[2]AI: Files:{'seishun18-basic1.html'}
青春18きっぷは、特定のシーズンに限って利用できるきっぷです。具体的な利用可能期間と発売期間は以下の通りです:

| シーズン | 利用可能期間            | 発売期間               |
|----------|-------------------------|-----------------------|
| 春季     | 3月1日~4月10日         | 2月20日~3月31日      |
| 夏季     | 7月20日~9月10日        | 7月1日~8月31日       |
| 冬季     | 12月10日~1月10日      | 12月1日~12月31日     |

また、2024-25年冬季からは、「連続する3日間」と「連続する5日間」の有効期間になり、利用開始日からその期間内での使用に限られることに注意が必要です。
---
[3]あなた: 価格は?
[3]AI: Files:{'seishun18-basic1.html'}
青春18きっぷの価格は以下の通りです:

- **3日間用**: 10,000円(税込)
- **5日間用**: 12,050円(税込)

3日間用は1日あたり3,333円、5日間用は1日あたり2,410円となります。
---
[4]あなた: 乗れる列車は?
[4]AI: Files:None
青春18きっぷで乗れる列車は以下の通りです:

1. **全国のJR線の普通・快速列車**の普通車自由席
2. **BRT(バス高速輸送システム)**
3. **JR西日本宮島フェリー**
4. 普通・快速列車の普通車指定席には、別途指定席券を購入することで乗車可能
5. 普通・快速列車として運転されるライナー列車は、ライナー券を別途購入で乗車可能
6. 普通・快速列車のグリーン車自由席は、別途グリーン券を購入することで乗車可(ただし、グリーン車指定席には乗車不可)  。  

一般的には、普通列車や快速列車の普通車自由席を利用することが基本となります。
---
[5]あなた: おすすめの路線や列車をいくつか教えて 
[5]AI: Files:{'seishun18-osusume-trains-2023summer.html', 'seishun18-twoday-koumi-iiyama.html', 'seishun18-long-trip-osusume-route.html'}
青春18きっぷでおすすめの路線や列車はいくつかあります。

1. **小海線**:
   - 標高1,300メートルを超える高原路線で、夏でも涼しい場所として知られています。特に観光列車「HIGH RAIL 1375」は、景色を 
楽しむのに最適で、指定席券を追加することで乗車可能です。

2. **中央本線**:
   - 長距離の普通列車で、特に大月から長野までを結ぶ441Mの列車は、東京駅や新宿駅からもアクセス可能で、南アルプスや八ヶ岳の
景観が楽しめます。

3. **飯山線**:
   - 千曲川に沿って走る川の路線で、信州の大自然を満喫できるルートです。日帰りルートとして、小海線と共に利用することが推奨
されています。

4. **観光列車**:
   - 「おいこっと」という観光列車もおすすめで、風光明媚な景色とともに旅行を楽しむことができます。

これらの路線や列車は、車窓からの眺めや快適な移動を提供してくれるため、青春18きっぷの魅力を最大限に引き出す旅が楽しめます 
。

Responses API はベクトルストアの検索の要否を判断している?

最初に「こんにちは。」と言ってみると、以下のような回答が返ってきました。

[0]あなた: こんにちは。
[0]AI: Files:None
こんにちは!何かお手伝いできることがありますか?アップロードされたファイルについてお探しの情報があれば教えてください。

AI:の横にあるFilesは、参照したドキュメントのファイル名(ブログのURLの末尾+.html)を出力していますがNoneとなっています。

一方、以下のようにドキュメントを参照して回答した場合には、そのファイル名が表示されます。

[2]あなた: いつ利用できますか?               
[2]AI: Files:{'seishun18-basic1.html'}
青春18きっぷは、特定のシーズンに限って利用できるきっぷです。具体的な利用可能期間と発売期間は以下の通りです:

| シーズン | 利用可能期間            | 発売期間               |
|----------|-------------------------|-----------------------|
| 春季     | 3月1日~4月10日         | 2月20日~3月31日      |
| 夏季     | 7月20日~9月10日        | 7月1日~8月31日       |
| 冬季     | 12月10日~1月10日      | 12月1日~12月31日     |

また、2024-25年冬季からは、「連続する3日間」と「連続する5日間」の有効期間になり、利用開始日からその期間内での使用に限られることに注意が必要です。

このように、ユーザーの入力に対して、ベクトルストアを検索すべきかをAIが判断しているようです。

[2]の会話のときのログを見てみると、以下のようになっています。

Outputの欄にFile Searchとあり、ベクトルストアを検索した結果のファイル名が表示されています。また、その下には、以下のように表示されています。

"青春18きっぷの利用可能時期"
"青春18きっぷの販売期間"
"青春18きっぷ 使用期間"

これは、ユーザーが入力した「いつ利用できますか?」というメッセージでベクトルストアを検索するのではなく、メッセージの内容を言い換えて、精度が上がるようにしていると思われます。

会話履歴から青春18きっぷについての話をしているとわかっているので、"青春18きっぷの利用可能時期"のような検索クエリを生成しているようです。

会話履歴には検索結果のドキュメントも含まれる?

会話履歴には、どうやらベクトルストアを検索した結果、取得できたドキュメントの内容も含まれているようです。そのため、会話のやりとりが続くと、どんどん入力トークン数が増えていきます。

会話回数 入力トークン 出力トークン 備考
1回目 815 34 検索なし
2回目 5,032 383 検索あり
3回目 19,062 112 検索あり
4回目 25,293 244 検索あり
5回目 31,582 382 検索あり

このように、入力トークン数が、毎回のベクトルストアの検索結果のぶんだけ増加していきます。

現在のところ、会話履歴から検索結果のドキュメントの内容を除外するオプションなどはないようです。そのため、コンテキストの上限に注意する必要がありそうです。

また、会話履歴にコンテキストを含めないようにするには、これまでの Chat Completions API のように、プログラム側(呼び出し側)で会話履歴を管理する必要があります。

簡単にRAGを構築できるもカスタマイズは限定的

OpenAIの新しい Responses API を、RAG構築の観点で試してみました。

会話履歴やベクトルストアの管理を自前で実装しなくて良いので、とても簡単にRAGを構築できます。AIチャットボットのバックエンドなどでは会話履歴が必須ですが、サーバーレス環境では、別途Redisなどのキーバリューストアを用意する必要があって煩雑でした。Responses APIでは、その処理をすべてOpenAI側に任せられるのは大きな利点です。

一方で、RAGにおいては、会話履歴に無条件に検索結果の内容が含まれてしまうという問題もあります。また、ベクトルストアの chunking strategy についても、設定できるのはチャンクの最大トークン数とオーバーラップのトークン数だけです。内部的にはもう少し賢い処理をしているのではないかと想像しますが、たとえば段落やページ単位でチャンクを区切りたいというようなカスタマイズはできません。

実装がとても簡単になる反面、カスタマイズ性が犠牲になるというトレードオフの関係にあります。今後の機能追加も期待したいですが、現状では、バリバリにカスタマイズしたければ、自前でベクトルストアを構築するほうが良さそうです。

とはいえ、こんなに簡単にRAGを実装できてしまうのは驚きです。ファイルのアップロードは、OpenAI APIのダッシュボードからもできるので、(現状はあまり使いやすいとはいえませんが)ドキュメントの追加・削除をブラウザから実行できるのも良いと思います。

1

Discussion

ログインするとコメントできます