メインコンテンツへスキップ
見出し画像

OpenAIとAzureOpenAIServiceを使い分けるTip

    こんにちは。YOSHINA SaaS事業部エンジニアの高瀬です。

    Ruby on RailsでChatGPTと連携するアプリケーションを開発するためのTipsをご紹介していきます。第一弾として、ruby-openaiのgemを使ってOpenAIとAzure OpenAI Serviceの2つを使い分ける話をしたいと思います。
    尚、本記事で取り扱うOpenAIのエンドポイントは Chat と Embeddings の二種類です。

    OpenAIを利用する2種類の方法とAPI利用方法の違いについて

    2023年7月現在、「OpenAI」のapiを利用するには、直接使う方法と、Azureから使う方法の2種類があります。それぞれの利用開始方法と、APIのエンドポイントの違いについてご紹介します。

    OpenAIを直接使う場合

    OpenAIにアカウントを作成し、 API_KEY を発行するとAPIが使えるようになります。リクエスト時にはAuthorizationヘッダーのBearerトークンに、発行した API_KEY をセットします。

    Chat, Embeddingsのエンドポイントと利用例は以下の通りです。(OpenAIのドキュメントより抜粋)

    # Chatエンドポイント
    https://api.openai.com/v1/chat/completions
    
    # curlによる利用例
    curl https://api.openai.com/v1/chat/completions \
      -X POST \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -d '{
        "model": "gpt-3.5-turbo",
        "messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello!"}]
      }'
    
    
    # Embeddingsエンドポイント
    https://api.openai.com/v1/embeddings
    
    # curlによる利用例
    curl https://api.openai.com/v1/embeddings \
      -X POST \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "input": "The food was delicious and the waiter...",
        "model": "text-embedding-ada-002"
      }'
    

    シンプルですね。

    Azure OpenAI Serviceを使う場合

    Azureにサブスクリプションを作成し、Azure OpenAI Service へのアクセスを申請します。

    レトリバは Microsoft for Startups Founders Hub を通じ Microsoft for Statups プログラムへの参加をしています。サブスクリプション作成からアクセス申請までをスムーズに進められるようにご支援いただきました。

    Azure OpenAI Serviceの利用開始後、まずAzureのCognitive Servicesで、Azure OpenAIにリソースを作ります。リソースを作成すると、URLとAPI_KEYが払い出されます。

    続いて、リソース配下に利用したいModelの デプロイメント を作成します。2023年7月現在、自然言語処理系のタスクでは、chatやcompletionのためのGPT-3系モデル(gpt-35-turbo, gpt-35-turbo-16k)と、文や単語の分散表現を獲得するためのモデル(text-embedding-ada-002)がデプロイできます。

    デプロイしたChat, Embeddingsそれぞれのモデルにおける、APIエンドポイントの利用例は以下の通りです。(Azure OpenAI Service REST API referenceより抜粋)

    # Chatエンドポイント
    https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/chat/completions
    
    # curlによる利用例
    curl https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/completions?api-version=2023-05-15\
      -H "Content-Type: application/json" \
      -H "api-key: YOUR_API_KEY" \
      -d "{
      \"prompt\": \"Once upon a time\",
      \"max_tokens\": 5
    }"
    
    
    # Embeddingエンドポイント
    https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/embeddings
    
    # curlによる利用例
    curl https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/embeddings?api-version=2023-05-15 \
      -H "Content-Type: application/json" \
      -H "api-key: YOUR_API_KEY" \
      -d "{\"input\": \"The food was delicious and the waiter...\"}"

    ちょっとだけ複雑になりました。

    特筆すべきOpenAIとの差は以下の通りです。

    • URLの構成(base部分)が異なる

      • OpenAI: https://api.openai.com/v1/{APIのPATH(chat/completions or embeddings)} 

      • Azure: https:// YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/{APIのPATH(chat/completions or embeddings)} 

    • AzureではQueryStringで api-version={api-version} の指定が必要

      • api-versionは YYYY-MM-DD という形式の文字列で、2023年7月現在は 2023-05-15 が最新のstableバージョンです。

    • API_KEYを設定するHeaderが異なる

      • OpenAI: { Authorization: Bearer API_KEY }

      • Azure: { api-key: API_KEY }

    その他、Azure版の詳細な仕様は Azure OpenAI Service REST API reference で確認することができます。

    ruby-openaiを使うTips

    OpenAIの Documentation に紹介されているRubyコミュニティの活動では、ruby-openai がAzureへの対応を含んだ v4.2.0 を6月下旬にリリースしています。

    以上を踏まえたうえで、Rubyで両者を使い分ける実装の話に移ります。

    OpenAIのchatとembeddingsを使う

    基本的にruby-openaiのトップページにある指示に従いコードを書くだけで問題ありません。

    OpenAI.configure でaccess_tokenにapi_keyを設定し、OpenAI::Client を初期化します。

    require 'openai'
    
    openai_api_key = 'your-api-key'
    
    OpenAI.configure do |config|
      config.access_token = openai_api_key
    end
    
    client = OpenAI::Client.new()

    あとは、OpenAI::Client クラスの #chat や #embeddings メソッドを呼び出せばOpenAIのAPIを利用することができます。

    question = "はじめまして。あなたはだれですか"
    
    # chatを使う
    response = client.chat(
      parameters: {
        model: "gpt-3.5-turbo", # OpenAIのchatモデル名
        messages: [{ role: "user", content: question}]
      }
    )
    pp response.dig("choices", 0, "message", "content")
    
    
    # embeddinsを使う
    response = client.embeddings(
        parameters: {
            model: "text-embedding-ada-002", # OpenAIのembeddingsモデル名
            input: question
        }
    )
    vector = response.dig("data", 0, "embedding")
    pp vector.size # vectorの次元数を確認
    pp vector[0..3] # vectorの中身(最初の4つ)を確認

    実行結果

    ruby sample_openai.rb
    "はじめまして、私はAIアシスタントです。お手伝いすることができます。"
    1536
    [0.004445781, -0.008630607, -0.02124555, -0.0236387]

    Azure OpenAI Serviceのchatとembeddingsを使う

    続いて、Azure OpenAI ServiceのAPIを使います。

    OpenAI.configure で必要な設定項目が増えますが、その中に uri_base が含まれています。前述した通り、Azure OpenAI ServiceではAPIのエンドポイントに「デプロイ名」を含んでいます。

    require 'openai'
    
    azure_api_key = 'your-api-key'
    resource_name = 'your-resource-name'
    deployment_name_chat = 'your-chat-deployment-name' 
    deployment_name_embeddings = 'your-embeddings-deployment-name' 
    
    # chatとembeddingではuri_base部分が異なる
    chat_uri_base = "https://#{resource_name}.openai.azure.com/openai/deployments/#{deployment_name_chat}"
    embedding_uri_base = "https://#{resource_name}.openai.azure.com/openai/deployments/#{deployment_name_embeddings}"

    これを踏まえて、uri_base 以外の共通設定で OpenAI::Client を初期し、利用するAPIに合わせて uri_base を再設定します。

    OpenAI.configure do |config|
      config.access_token = azure_api_key # リクエストヘッダーの使い分けはGem側がやってくれる
      config.api_type = :azure
      config.api_version = "2023-05-15" # api-versionのQueryString処理はGemがやってくれる
    end
    
    question = "はじめまして。あなたはだれですか"
    
    
    # chatを使うために、chatモデルのデプロイ名を含んだbase_uriを設定する
    OpenAI.configure { |config| config.uri_base = chat_uri_base }
    response = client.chat(
      parameters: {
        messages: [{ role: "user", content: question}] 
      }
    )
    pp response.dig("choices", 0, "message", "content")
    
    
    # embeddingsを使うために、embeddinsモデルのデプロイ名を含んだbase_uriに再設定する
    OpenAI.configure { |config| config.uri_base = embedding_uri_base }
    response = client.embeddings(
      parameters: {
        input: question
      }
    )
    vector = response.dig("data", 0, "embedding")
    pp vector.size
    pp vector[0..3]
    

    実行結果

    ruby sample_azure.rb
    "初めまして!私はOpenAIのAIアシスタントです。どのようにお力になれますか?"
    1536
    [0.0044068773, -0.008616479, -0.02130574, -0.023736682]

    ruby-openaiの実装確認

    chat か embeddings のどちらか片方だけを使いたいのであれば全く気にならないのですが、分散表現を利用した前処理(類似文検索など)が伴う機能開発においては、 embeddings も一緒に使いたいユースケースは多いのではないかと思います。となると、Azure版の複雑さがGemのユーザーインターフェースにも現れているのがちょっと気になりますね。そこで、ruby-openaiの関連部分の実装を把握しておきたいと思います。

    尚、本記事執筆時点のruby-openaiのバージョンは4.2.0です。

    client.rb

    重要なクラスは、APIの呼び出しを行う lib/openai/client.rb と faraday を使ってrequest&responseを管理する lib/openai/http.rb の2つです。

    OpenAI::Client クラスが OpenAI::HTTP モジュールを mixin して両者が連携しています。

    1 module OpenAI
    2   class Client
    3     extend OpenAI::HTTP

    OpenAI::Client クラスでは、chat, embeddings 両メソッドとも、 api_type による分岐が無く共通化されています。

    12    def chat(parameters: {})
    13      OpenAI::Client.json_post(path: "/chat/completions", parameters: parameters)
    14    end
    
    ...
    
    24    def embeddings(parameters: {})
    25      OpenAI::Client.json_post(path: "/embeddings", parameters: parameters)
    26    end

    OpenAI:HTTP#json_post メソッドに、それぞれのAPIの path を渡していますが、これはopenaiとazure両方に共通する、APIエンドポイント末尾の部分のパスですね。

    http.rb

    となると、openaiとazureの違いを捌いてくれているのはこのモジュールでしょう。

    では早速 json_post を見ていきます。

    9    def json_post(path:, parameters:)
    10      to_json(conn.post(uri(path: path)) do |req|

    10行目で一気に色々な仕事をしていますので、呼び出される順番を整理しながら一つずつ紐解いていきましょう。

        to_json(              # 4. OpenAI::HTTP#to_json:  ResponseをJSON.parseする
          conn                # 2. OpenAI::HTTP#conn: Faraday.newしてFaraday::Connectionクラスを作る
            .post(            # 3. Faraday::Connection#postメソッドを呼ぶ
              uri(path: path) # 1. OpenAI::HTTP#uri:  uriを組み立てる !!!
          ) do |req|

    1, 2, 4に関しては同じ OpenAI::HTTP モジュール内に定義されたメソッド呼び出しで、3は Faraday::Connection#post の呼び出しです。

    最初に呼ばれる HTTP#uri が今回のテーマの全ての鍵を握る中心地になっていそうな雰囲気です。

    72    def uri(path:)
    73      if OpenAI.configuration.api_type == :azure
    74        base = File.join(OpenAI.configuration.uri_base, path)
    75        "#{base}?api-version=#{OpenAI.configuration.api_version}"
    76      else
    77        File.join(OpenAI.configuration.uri_base, OpenAI.configuration.api_version, path)
    78      end
    79    end
    • 73行目のifブロックで、 api_type == :azure の分岐

    • 74行目でリクエストパス全体を組み立て

      • OpenAI.configuration.uri_base と client.rbから渡された path の結合

    • 75行目でQueryStringに api-version= を設定

    という訳で、ruby-openaiのv4.2.0時点の実装だと、APIのURL組み立ては、 OpenAI.configuration.uri_base に対して、 /chat/completion か /embeddings のパスを追加する処理になっています。そのため、Azure OpenAI Serviceを使う場合、 uri_base の中に「デプロイ名」を含んでいる chatとembeddingのapiを使い分けるには、 uri_base の再設定が必要になってしまう、ということが分かりました。

    終わりに

    ソースコードを見てみると、このくらいの処理・規模感であればGemに依存せず、自分たちのユースケースに合わせて自前クラスを実装しても良いかなという気もします。

    しかしながら、LLMと連携したシステムにおいて、もう一つ LangChain という便利な技術を組み合わせる場合が多くなります。Rubyでは langchainrb というGemがあり、これが ruby-openai と依存関係にあるため、 ruby-openai の内部実装を把握しておくのは重要かと思われます。

    langchainrb に関してはまた別の記事で触れていきたいと思います。

    レトリバでは、一緒に働く仲間を募集しています。
    詳細はこちら下のリンクから見られますので、ぜひご覧ください。

    https://herp.careers/v1/retrieva

     
     
    株式会社レトリバの公式アカウントです。 自然言語処理AI技術を用いて、人々を幸せにするミッションのもと、日々技術を研究し格闘しています。

    あなたへのおすすめ