この記事はCyberAgent Developers Advent Calendar 2023 7日目の記事です。

こんにちは、AI事業本部の徳田(@tokkuu)です。

OpenAIのChatGPTが2022年11月に公開されてから1年が経過し、その間に数多くの生成AI系のサービスが世に出てきました。これらのサービスはSaaS形式で提供されるものや、OSSとして公開されるものなど多岐にわたり、OpenAI APIと連携して利用するものも多いです。

しかし、企業やチームによっては、ガバナンスやセキュリティの観点から、生成AI系のサービスを導入する際にAzure OpenAI Serviceを利用したいと考える場合もあります。その一方で、Azure OpenAI Serviceの利用方法は本家のOpenAI APIとは異なり、そのままではAzure OpenAI Serviceをバックエンドとして活用できない場合もあります。

そこで本記事では、Azure OpenAI ServiceをOpenAI APIとして活用するための具体的な手順を解説します。一度作成すれば様々なことに応用できる他、OpenAI APIよりもチューニングの幅が広いため、ぜひ活用してみてください。

TerraformでAzure OpenAI ServiceとAzure Functionsを用意する

まずはAzure OpenAI ServiceとAzure FunctionsをTerraformを用いて構築する方法について説明します。すべてのterraformの定義を記載するわけではないので、この他に必要なリソース(例えばリソースグループなど)は適宜準備してください。

Azure OpenAI Serviceの設定

Azure OpenAI Serviceを作成するためのTerraformファイル(open_ai.tf)を作成します。

resource "azurerm_cognitive_account" "open_ai_api" {
  name                = "openai-account"
  resource_group_name = <your resource group name>
  location            = <your resource group location>
  kind                = "OpenAI"
  sku_name            = "S0"
  public_network_access_enabled = true
}

resource "azurerm_cognitive_deployment" "open_ai_api" {
  name                 = "public_endpoint"
  cognitive_account_id = azurerm_cognitive_account.open_ai_api.id
  model {
    format  = "OpenAI"
    name    = "gpt-35-turbo"
    version = "0613"
  }
  scale {
    type     = "Standard"
    capacity = 30
  }
}

OpenAIのAPIを利用するためのAzure Cognitive Accountを作成し、その後、GPT-3.5-turboモデルを使用するためのサービスデプロイメントを設定します。

モデルやcapacityなどは利用頻度等によって適当に調整してください。

Azure Functionsの設定

次に、Azure Functionsを作成するためのTerraformファイル(app_service.tf)を作成します。

resource "azurerm_storage_account" "storage_account" {
  name                     = "openaistoragesample"
  resource_group_name      = <your resource group name>
  location                 = <your resource group location>
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_service_plan" "app_service_plan" {
  name                = "openai-app-service-plan"
  resource_group_name = <your resource group name>
  location            = <your resource group location>
  os_type             = "Linux"
  sku_name            = "Y1"
}

resource "azurerm_linux_function_app" "function_app" {
  name                = "openai-function-app-sample"
  resource_group_name = <your resource group name>
  location            = <your resource group location>
  service_plan_id     = azurerm_service_plan.app_service_plan.id
  app_settings = {
    "WEBSITE_RUN_FROM_PACKAGE" = "",
    "WEBSITE_MOUNT_ENABLED"    = "1",
    "FUNCTIONS_WORKER_RUNTIME" = "node",
  }
  site_config {
    application_stack {
      node_version = "18"
    }
  }
  storage_account_name       = azurerm_storage_account.storage_account.name
  storage_account_access_key = azurerm_storage_account.storage_account.primary_access_key

  lifecycle {
    ignore_changes = [
      app_settings["WEBSITE_RUN_FROM_PACKAGE"],
    ]
  }
}

Azure Functionsを作成するためのTerraformコードです。ほとんどazurerm_linux_function_appのExample Usageそのままです。

azurerm_function_appはversion 3.0以降deprecatedとなっていますので注意してください。

APIのリクエスト・レスポンスを変換するAzure Functionsの関数を作る

インフラの土台が出来上がったところで、実際にFunctionsを実装していきます。

Azure FunctionsではOpenAI APIへのリクエストとして渡ってきた、Authorizationヘッダーのトークンをapi-keyに詰め直してAzure OpenAI Serviceへ送信します。

import { AzureFunction, Context, HttpRequest } from "@azure/functions";

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  // Baerer tokenで渡ってくるので抜き出してapi-keyに設定する
  const rawToken = req.headers["Authorization"] || req.headers["authorization"];
  const [tokenType, token] = rawToken.split(" ");
  // Bearer tokenとして渡ってこない場合は401を返す
  if (tokenType !== "Bearer") {
    context.log("Unexpected token type");
    context.res = {
      status: 401,
      body: "Unauthorized",
    };
    return;
  }
  const headers = {
    "Content-Type": "application/json",
    "api-key": token,
  };

  // bodyがない場合は400を返す
  if (!req.body) {
    context.log("undefined body");
    context.res = {
      status: 400,
      body: "Bad Request",
    };
    return;
  }
  const body = JSON.stringify(req.body);

  if (!process.env.DEPLOYMENT_ID) {
    context.log("undefined DEPLOYMENT_ID");
    context.res = {
      status: 500,
      body: "Internal Server Error",
    };
    return;
  }
  const url = `https://japaneast.api.cognitive.microsoft.com/openai/deployments/${process.env.DEPLOYMENT_ID}/chat/completions?api-version=2023-07-01-preview`;

  const res = await fetch(url, {
    method: "POST",
    headers,
    body,
  }).then((res) => {
    return res.json();
  });

  context.res = {
    body: res,
  };
};

export default httpTrigger;

Azure FunctionsのTypeScriptを使用していますが、モデルをv4にすると、正しくfunctionを認識できなかったのでv3を使用しました。

参考: コマンド ラインから TypeScript 関数を作成する – Azure Functions | Microsoft Learn

本家OpenAI API用に作られたサービスをつかってみる

前述の通り、今回はCode Rabbitを動かしてみました。

GitHub Appとして動作するサービスの方ではなく、CodeRabbitが提供するGitHub Actionsを利用しています。

coderabbitai/ai-pr-reviewer: AI-based Pull Request Summarizer and Reviewer with Chat Capabilities.

こちらのコードを見ると、中でchatgpt というnpmパッケージを利用しており、こちらは本家OpenAI APIを前提として作成されています。

利用したGitHub Actionsの実装を見ると、openai_base_urlというパラメータを渡すことで、向き先を変更できそうだったので、こちらにAzure Functionsのエンドポイントを設定します。

https://github.com/coderabbitai/ai-pr-reviewer/blob/main/action.yml#L147

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: coderabbitai/ai-pr-reviewer@latest
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          BASE_URL: ${{ secrets.BASE_URL }}
        with:
          debug: true
          review_simple_changes: false
          review_comment_lgtm: false
          language: ja-JP
          openai_base_url: ${{ env.BASE_URL }} # ここにAzure Functionsのエンドポイントを設定する

もう初回コードレビューはAIに任せる時代になった – CodeRabbit – 」という記事にあるような方法で system_messagesummarizesummarize_release_notes を追加してプロンプトをチューニングし、無事以下のように実行できました。

CodeRabbitによるレビュー出力の結果例

まとめ

使いたい生成AIのサービスが本家のOpenAI APIを前提として組まれているような場合に、今回の構成を使えばAzure OpenAI Serviceをバックエンドに据えて利用することができます。

一度用意しておけば、例えば独自にリクエストの内容を書き換えたいような場合や、今後2つのAPIの仕様に差が出てきた場合でも、柔軟に対応することが出来ます。