🌮

スプレッドシートからRAGを構築してセキュリティチェックシートに回答する。

2024/05/28に公開

背景

先日発表されたChatGPT-4oの性能は素晴らしく、多くの注目を集めていますね。
その他にも様々な生成モデルが登場しており、その利用敷居はどんどん低くなり続けているかと思います。

その一方でベクトルデータベースを使ったRAG(Retrieval-Augmented Generation)の構築や、ファインチューニングなどの生成AIに渡すデータの質が重要となってきます。

特に、日常業務で使用しているGoogleスプレッドシートなどをそのまま学習データとして活用できれば、日々の業務がとても効率化できるのではないでしょうか。

特に身近な部分ではセキュリティチェックシートに関する回答を、過去の情報から作成してもらえるととても助かるなと感じていました。

そこで今回は「Googleスプレッドシート」とマネージドベクトルデータベースの「Pinecone」を組み合わせてRAGを構築し、「OpenAI」に問い合わせるサンプルを実装してみることにしました。

ゴール

Google Sheets APIを利用してGoogleスプレッドシートの内容を取得し、「Pinecone」に保存します。
質問文を基に「Pinecone」に対してベクトル検索を行い、取得した情報を使いOpenAIへの問い合わせを行う一連のスクリプトを書きます。

準備編

Google Sheets API

まずはGoogleスプレッドシートの内容を取得するためにAPIキーを取得します。

最初にGoogle Sheets APIを有効にします。
https://developers.google.com/sheets/api/guides/concepts?hl=ja

GCPのコンソールにログイン後、ナビゲーションメニューから「APIとサービス」>「ダッシュボード」に移動し、Google Sheets APIを検索後、有効にします。

次にAPIキーを作成します。

「APIとサービス」>「認証情報」に移動し、「認証情報の作成」から「APIキー」を押します。

続いて取得先のスプレッドシートの設定を行います。
ここではLectoが外部公開している「経産省クラウドサービスレベルチェックリスト」を「api-sample」という名前でコピーしてサンプルに使用しています。

また取り込み処理を簡略化するため「規定内容」と「設定」の情報のみ残し、他の列は全て削除しました。

作成したスプレッドシートの共有設定を「一般的なアクセス」を「リンクを知っている全員」に変更します。

この状態で以下のURLにアクセスするとスプレッドシートの情報がJSON形式で取得することができます。
https://sheets.googleapis.com/v4/spreadsheets/{スプレッドシートID}/values/{シート名}?key={APIキー}

スプレッドシートIDについては、作成したスプレッドシートのURLの{スプレッドシートID}の部分になります。
https://docs.google.com/spreadsheets/d/{スプレッドシートID}/edit

上記URLにアクセスすると以下の様な内容がJSONで取得することができます。

{
  "range": "sample!A1:W996",
  "majorDimension": "ROWS",
  "values": [
    [
      "サービスを提供する時間帯(設備やネットワーク等の点検/保守のための計画停止時間の記述を含む)",
      "24時間365日(メンテナンス等の計画停止を除く)"
    ],
    [
      "定期的な保守停止に関する事前連絡確認(事前通知のタイミング/方法の記述を含む)",
      "有\nメールやチャットツールなどで個別に通知します。"
    ],
    [
      "サービス提供を終了する場合の事前連絡確認(事前通知のタイミング/方法の記述を含む)",
      "有\n詳細は利用規約に記載しています。\nhttps://lecto-docs.web.app/terms/"
    ],
    [
      "プログラムや、システム環境の各種設定データの預託等の措置の有無",
      "無\n現時点で終了予定はなく、プログラムやデータの預託も未定です。"
    ],
    [
      "サービスを利用できる確率((計画サービス時間-停止時間)÷計画サービス時間)",
      "SLAは未設定です。\n【稼働率実績】\n2023年: 100%"
    ],
    [
      "災害発生時のシステム復旧/サポート体制",
      "有\n複数データセンターで冗長化されています。"
    ],
    [
      "早期復旧が不可能な場合の代替措置",
      "有\n即時復旧が可能なように、データセンターの冗長化、バックアップ、構成管理のコード化を行っております。"
    ],
....

以上でスプレッドシートの内容を取得する準備ができました。

Pinecone

Pineconeは、シンプルなAPIを提供するフルマネージドのベクトルデータベースです。
高性能なベクトル検索アプリケーションを簡単に構築でき、高速なクエリ応答、動的なインデックス更新、メタデータフィルタリングが特徴です。
主なユースケースには、セマンティック検索や非構造化データ検索、レコメンデーションなどがあります。

https://www.pinecone.io/

APIの取得

続いて取得したスプレッドシートの内容をベクトルとして保存するためPineconeのAPIキーを取得します。
アカウント作成後、サイドバーの「API Keys」から作成することが可能です。

また今回の検証で使用するProjectとindexを以下の設定で作成しておきます。

プロジェクト名: sample-project
インデックス名: sample-index
METRIC: cosine
TYPE: Serverless
DIMENSIONS: 1536

DIMENSIONSについてはopen-aiの「text-embedding-ada-002」を使うため1536を設定しておきます。

その他の項目についての詳細は以下をご確認ください。
https://docs.pinecone.io/guides/get-started/quickstart

OpenAI

最後にプロンプトへの質問と、エンべディングを行うためにAPIキーを取得します。
https://platform.openai.com/api-keys

特に特別な設定は必要ないため詳細の手順は省略します。

※ 今回使用する質問(chat)、エンべディング(embeddings)、どちらも料金が発生するのでご注意ください。

エンべディング等については過去の記事を参照していただければと思います。
https://zenn.dev/lecto/articles/00a9ace01eb8b9#embedding

実装編

ここまでで必要なAPIキーが揃ったのでスクリプトの実装に入ります。
実装といっても各種クライアントからデータを受け取ってリクエストするだけになります。

完成したスクリプトはこちら
https://github.com/yassun/sample-spreadsheets-rag

スプレッドシートからデータを取得する

# コマンドライン引数からファイル名とシート名を取得
spreadsheets_name = ARGV[0]
sheets_name       = ARGV[1]

# スプレッドシートからJSON形式でデータを取得
connection = Faraday.new("https://sheets.googleapis.com/v4/spreadsheets/#{spreadsheets_name}/values/") do |builder|
  builder.request :url_encoded
  builder.response :json
  builder.adapter Faraday.default_adapter
end
connection.params[:key] = ENV.fetch('GOOGLE_SPREADSHEETS_API_KEY')
res = connection.get(sheets_name)

実行コマンド

ruby spreadsheets_to_pinecone.rb {スプレッドシートID} {シート名}

スプレッドシートの内容が二次元配列で取得することができます。
レスポンスの形については以下を参照してください。

https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get?hl=ja

スプレッドシートの内容をベクターに変換する

pineconeにスプレッドシートの内容を登録するにあたり、ベクトル検索の対象にする項目をベクターに変換する必要があります。
今回はopenaiのembeddings-apiを使い、モデルはtext-embedding-ada-002を使います。
https://platform.openai.com/docs/guides/embeddings

後ほど作成するプロンプトで必要になるため、metaデータにはファイル名、シート名、質問、回答を付与しています。

# Pineconeに登録するデータを作成
OpenAI.configure do |config|
  config.access_token = ENV.fetch('OPENAI_ACCESS_TOKEN')
  config.organization_id = ENV.fetch('OPENAI_ORGANIZATION_ID') # Optional
end
open_ai = OpenAI::Client.new

vectors = []
res.body['values'].each_with_index do |row, i|
  question = row[0].gsub(/\R/, ' ')
  answer = row[1].gsub(/\R/, ' ')
  vector = open_ai.embeddings(
    parameters: { input: question, model: 'text-embedding-ada-002' }
  ).dig('data', 0, 'embedding')

  vectors << {
    id: "#{spreadsheets_name}_#{sheets_name}_#{i}",
    metadata: {
      file: spreadsheets_name,
      sheet: sheets_name,
      question: question,
      answer: answer
    },
    values: vector
  }
end

pineconeにデータを登録する

pineconeはバルクインサートをサポートしているので、一括で登録することができます。

## Pineconeにデータを登録
Pinecone.configure do |config|
  config.api_key = ENV.fetch('PINECONE_API_KEY')
  config.environment = ENV.fetch('PINECONE_ENVIRONMENT')
end

pinecone = Pinecone::Client.new
pinecone_index = pinecone.index('sample-index')
pp pinecone_index.upsert(
  namespace: 'example-namespace',
  vectors: vectors
)

質問用のスクリプトを作成する

次に質問から類似する設問と回答を取得し、その内容使ってChatGPTに質問するスクリプトを作成します。

Pineconeから類似する設問と回答を取得する

質問の内容をvector化し、類似する質問を10件取得します。

# コマンドライン引数から質問を取得
text = ARGV[0]

# 質問内容をvectorにする
vector = open_ai.embeddings(
  parameters: { input: text, model: 'text-embedding-ada-002' }
).dig('data', 0, 'embedding')

# Pineconeから検索結果を10件取得する
res = index.query(
  vector: vector,
  namespace: 'example-namespace',
  top_k: 10,
  include_values: false,
  include_metadata: true
)

プロンプトを作成して質問する

類似する質問と回答をプロンプトに埋め込んでChatGPTに問い合わせます。
プロンプトの内容は深津式プロンプトを参考に埋め込みました。

# 取得結果からプロンプトを作成する
list = res['matches'].map do
  "質問: #{_1['metadata']['question']}, 回答: #{_1['metadata']['answer']}"
end.join("\n")

question = <<~EOS
  #命令書:
  あなたは、情報セキュリティ責任者です。
  以下の制約条件と過去の回答をもとに、質問に対する回答を出力してください。

  #制約条件:
  ・文字数は 100文字以内

  #過去の回答:
  #{list}

  #質問:
  #{text}

  #出力文:
EOS

# ChatGPTに問い合わせる
response = open_ai.chat(
  parameters: {
    model: 'gpt-4o',
    messages: [{ role: 'user', content: question }]
  }
)
puts response.dig('choices', 0, 'message', 'content')

質問してみる

実際にいくつか質問してみようと思います。

 $ ruby question.rb "収集すべきログ内容とその保管方法・期間を定めていますか?"
 > 操作履歴ログを含む各種ログ内容を定め、1年間保存しています。データは安全に保管されています。

それらしい答えが返ってきました。

次に記載のない内容を問い合わせてみます。

$ ruby question.rb "アカウントの棚卸を定期的に行っていますか。"
> はい、アカウントの棚卸は定期的に実施しており、必要に応じて更新・削除を行っています。

学習元データには記載がないにもかかわらず、それっぽい回答が返ってきてしまいました。

当然プロンプトの調整や、参考に使った過去回答を表示することで信頼度を上げていけますが、どこまでいってもそのまま鵜呑みにするのは危険ですね。

終わりに

今回は、Googleスプレッドシート、Pinecone、OpenAIといった外部サービスを活用することで、簡単なスクリプトでRAGを構築することができました。

生成モデルに加えて、Pineconeのような周辺サービスについても便利なものが続々と登場しており、これらを組み合わせることで、日常業務に適したRAGを今よりもっと手軽に構築できるようになっていくのではないでしょうか。

Discussion