🔖

【C#】Azure.AI.OpenAI パッケージで AOAI & Azure AI Search を使う RAG と Chat のコード例

2024/04/05に公開

C# で AOAI (Azure OpenAI Service) を使う方法は幾つかあります。そのうちの一つが Azure.AI.OpenAI パッケージ(OpenAI .NET クライアントライブラリ)です。執筆時点のバージョンは 1.0.0-beta.15 となっています。急速に進む各種 AI サービスの進化に合わせるかのように、このライブラリも短い間にどんどんバージョンアップされています。こういう場合にありがちなのが、入手できる情報を基に書いたコードがそのままでは動かない、という問題です。

問題となったこと

Azure OpenAI Studio のサンプルコードがビルドエラーになる

あるプロジェクトで、複数の PDF ドキュメントをデータソースにする RAG を使った実験的なチャット機能を C# で実装することになりました。Azure OpenAI Service と Azure AI Search を使います。

まずは RAG のためのデータソースを準備します。幸い Azure OpenAI Studio のチャットプレイグラウンドを使えば簡単に、ローカルファイルをアップロードして Azure AI Search のインデックスを作成し、ベクトル検索もできるように設定できます。執筆時点ではまだプレビュー機能ですが、今回は正規のプロダクトに組み込む機能ではなく飽くまでお試しなので、これで問題ありません。

Azure AI 検索インデックスを作成したら、そのインデックスをデータソースとして問い合わせに答えるチャットをプレイグラウンドで試すことができます。さらに、その RAG を使ったチャットを C# で実装する場合のサンプルコードも表示できます。このサンプルコードでは、Azure.AI.OpenAI パッケージによって Azure OpenAI Service と Azure AI Search に接続しています。

これで簡単に実装できる、と思ったのですが、そうはいきませんでした。試しに上記のサンプルコードを C# コンソールアプリの Program.cs に貼り付けたところ、ビルドエラーが出てしまいました。どうやら最新バージョンの Azure.AI.OpenAI パッケージで動かすには修正が必要なようです。

Azure.AI.OpenAI のドキュメントにあるコードが実行時エラーになる

それで、GitHub の Azure.AI.OpenAI のリポジトリにあるドキュメントを参考に、以下のようにコードを書き直しました(前述のとおり、ライブラリのバージョンは 1.0.0-beta.15 です)。なお実際に動かしたコードでは、Settings の部分にある各変数に、前項に出てきた Azure OpenAI Studio チャットプレイグラウンドの「コードの表示」ダイアログから取ってきた値を渡しています。

Program.cs
using Azure.AI.OpenAI;
using Azure;

// Settings: Azure OpenAI Service
var azureOpenAIEndpoint = "https://<Resource name of Azure OpenAI Service>.openai.azure.com/";
var azureOpenAIKey = "<API key of Azure OpenAI Service>";
var languageModelDeploymentName = "<Deployment name of the language model>";

// Settings: Azure AI Search
var searchEndpoint = "https://<Resource name of Azure AI Search>.search.windows.net";
var searchKey = "<API key of Azure AI Search>";

// Chat Request
var client = new OpenAIClient(new Uri(azureOpenAIEndpoint), new AzureKeyCredential(azureOpenAIKey));

var chatCompletionsOptions = new ChatCompletionsOptions()
{
    AzureExtensionsOptions = new AzureChatExtensionsOptions()
    {
        Extensions =
        {
            new AzureSearchChatExtensionConfiguration()
            {
                SearchEndpoint = new Uri(searchEndpoint),
                Authentication = new OnYourDataApiKeyAuthenticationOptions(searchKey),
            }
        }
    },
    DeploymentName = languageModelDeploymentName,
    Messages =
    {
        new ChatRequestSystemMessage("You are an AI assistant that helps people find information."),
        new ChatRequestUserMessage("Can you help me?"),
    },
};

var response = await client.GetChatCompletionsAsync(chatCompletionsOptions);
var message = response.Value.Choices[0].Message;

Console.WriteLine($"{message.Role}: {message.Content}");
Console.ReadKey();

ビルド後、コンソールアプリを起動します。そうしたら今度は実行時エラーが出ました。

Azure.RequestFailedException: 'Service request failed.
Status: 400 (Bad Request)

Content:
{"error": {"requestid": "3760d9b5-70bc-46fc-907d-fca0b4786f36", "code": 400, "message": "Validation error at #/data_sources/0/azure_search/parameters/index_name: Input should be a valid string"}}

インデックス名がない、ということのようです。先ほどのコードを見直すと確かに、Azure AI Search のエンドポイントは設定していますが、どの埋め込みモデルと検索インデックスを使うかの情報がありません。要するに、Azure AI Search に必要な設定が不足している、ということで、確かにこれでは動きません。

動いたコード

というわけで、あれこれ試して動くようにしたのが下記のコードです。

Program.cs
using Azure;
using Azure.AI.OpenAI;

// Settings: Azure OpenAI Service
var azureOpenAIEndpoint = "https://<Resource name of Azure OpenAI Service>.openai.azure.com/";
var azureOpenAIKey = "<API key of Azure OpenAI Service>";
var languageModelDeploymentName = "<Deployment name of the language model>";

// Settings: Azure AI Search
var searchEndpoint = "https://<Resource name of Azure AI Search>.search.windows.net";
var searchKey = "<API key of Azure AI Search>";
var searchIndex = "<The name of the search index in Azure AI Search>";
var embeddingModelDeploymentName = "<Deployment name of the embedding model>";
var queryType = AzureSearchQueryType.VectorSimpleHybrid;

// Chat Request
var client = new OpenAIClient(new Uri(azureOpenAIEndpoint), new AzureKeyCredential(azureOpenAIKey));

var chatCompletionsOptions = new ChatCompletionsOptions()
{
    AzureExtensionsOptions = new AzureChatExtensionsOptions()
    {
        Extensions =
        {
            new AzureSearchChatExtensionConfiguration()
            {
                Authentication = new OnYourDataApiKeyAuthenticationOptions(searchKey),
                SearchEndpoint = new Uri(searchEndpoint),
                IndexName = searchIndex,
                VectorizationSource = new OnYourDataDeploymentNameVectorizationSource(embeddingModelDeploymentName),
                QueryType = queryType,
                RoleInformation = "You are an AI assistant that helps people find information.",
            },
        },
    },
    DeploymentName = languageModelDeploymentName,
    Messages =
    {
        new ChatRequestUserMessage("Can you help me?"),
    },
};

var response = await client.GetChatCompletionsAsync(chatCompletionsOptions);
var message = response.Value.Choices[0].Message;

Console.WriteLine($"{message.Role}: {message.Content}");
Console.ReadKey();

コンソールアプリを実行すると、以下のような答えが返ってきました。問題なく動作したことが分かります。

assistant: Of course! I'm here to assist you. Could you please provide more details about the information or help you need?

修正のポイント

比較すれば分かりますが、実行時エラーになったコードから変更したのは以下の点です。

  1. AzureSearchChatExtensionConfiguration の IndexName / VectorizationSource / QueryType の各プロパティに値を与える。
  2. AzureSearchChatExtensionConfiguration の RoleInformation プロパティにプロンプトのシステムメッセージを設定する。

1 は問題に直接関わるポイントです。これらのプロパティは、Azure OpenAI チャットプレイグラウンドでデータソースを追加する場合の以下の設定に対応します。Azure AI Search を使って RAG をする場合には必要な設定です。

プロパティ名 チャットプレイグラウンドの設定項目名
IndexName Azure AI 検索インデックス
VectorizationSource 埋め込みモデルを選択する
QueryType 検索の種類

2 は Azure AI Search とは関係がなく、プロンプトのシステムメッセージをどこに設定するか、という話です。Azure.AI.OpenAI のサンプルコードですと、システムメッセージは ChatCompletionsOptions.Messages の先頭に追加するようになっているのですが、私が試した限りでは、それではプロンプトに反映されていないようでした。その代わりに AzureSearchChatExtensionConfiguration.RoleInformation に設定すると、想定どおりに動作しました。

チャット履歴をコンテキストとして与える

ついでに、チャットの履歴を回答のコンテキストとして与えるコードも見てみます。上記コードの Messages の部分を下記のように書き換えます。

    Messages =
    {
        new ChatRequestUserMessage("My name is Monolith. Can you help me?"),
        new ChatRequestAssistantMessage("Of course! I'm here to assist you. Please provide more details about the information or help you need."),
        new ChatRequestUserMessage("What is my name?"),
    },

コンソールアプリを実行すると、このように回答してきました。コンテキストを与えるのは簡単であることが分かります。

assistant: Based on our previous conversation, your name is Monolith.

その他のパラメータを設定する

ChatGPT を使う際には、Temperature や TopP といったパラメータを設定できます。また、Azure AI Search のインデックスをデータソースとする場合、そのインデックスだけを検索対象として回答するのか、それとも ChatGTP の持つ一般知識からも回答できるのかも設定できます。Azure OpenAI Studio チャットプレイグラウンドでも試せるこれらのパラメータを設定できるように書き換えたコードが以下になります。冗長ですが、分かりやすくするためすべての設定値とパラメータを変数にして先頭に固めてあります。

Program.cs
using Azure;
using Azure.AI.OpenAI;

// Settings: Azure OpenAI Service
var azureOpenAIEndpoint = "https://<Resource name of Azure OpenAI Service>.openai.azure.com/";
var azureOpenAIKey = "<API key of Azure OpenAI Service>";
var languageModelDeploymentName = "<Deployment name of the language model>";

// Settings: Azure AI Search
var searchEndpoint = "https://<Resource name of Azure AI Search>.search.windows.net";
var searchKey = "<API key of Azure AI Search>";
var searchIndex = "<The name of the search index in Azure AI Search>";
var embeddingModelDeploymentName = "<Deployment name of the embedding model>";
var queryType = AzureSearchQueryType.VectorSimpleHybrid;

// Parameters
var systemMessage = "You are an AI assistant that helps people find information.";
var shouldUseOnlyOwnData = true;
var documentCount = 5;
var strictness = 3;

var maxTokens = 800;
var temperature = 0.7f;
var topP = 0.95f;
var frequencyPenalty = 0.0f;
var presencePenalty = 0.0f;

// Chat Request
var client = new OpenAIClient(new Uri(azureOpenAIEndpoint), new AzureKeyCredential(azureOpenAIKey));

var chatCompletionsOptions = new ChatCompletionsOptions()
{
    AzureExtensionsOptions = new AzureChatExtensionsOptions()
    {
        Extensions =
        {
            new AzureSearchChatExtensionConfiguration()
            {
                Authentication = new OnYourDataApiKeyAuthenticationOptions(searchKey),
                SearchEndpoint = new Uri(searchEndpoint),
                IndexName = searchIndex,
                VectorizationSource = new OnYourDataDeploymentNameVectorizationSource(embeddingModelDeploymentName),
                QueryType = queryType,
                RoleInformation = systemMessage,
                ShouldRestrictResultScope = shouldUseOnlyOwnData,
                DocumentCount = documentCount,
                Strictness = strictness,
            },
        },
    },
    DeploymentName = languageModelDeploymentName,
    MaxTokens = maxTokens,
    Temperature = temperature,
    NucleusSamplingFactor = topP,
    FrequencyPenalty = frequencyPenalty,
    PresencePenalty = presencePenalty,
    Messages =
    {
        new ChatRequestUserMessage("My name is Monolith. Can you help me?"),
        new ChatRequestAssistantMessage("Of course! I'm here to assist you. Please provide more details about the information or help you need."),
        new ChatRequestUserMessage("What is my name?"),
    },
};

var response = await client.GetChatCompletionsAsync(chatCompletionsOptions);
var message = response.Value.Choices[0].Message;

Console.WriteLine($"{message.Role}: {message.Content}");
Console.ReadKey();

最後に

繰り返しになりますが、上記のコードは Azure.AI.OpenAI のバージョン 1.0.0-beta.15 で実行しています。ライブラリの今後のバージョンアップで動かなくなる可能性がありますので、その点はご注意いただければと思います。

ジェイテックジャパンブログ

Discussion