Azure OpenAI で不適切な表現や、特定の語句・話題を回避する方法と実装例

この記事は [NTTコムウェア AdventCalendar 2023](https://qiita.com/advent-calendar/2023/nttcomware) 20日目の記事です。

# はじめに
NTT コムウェア株式会社の佐々木です。

みなさ～ん、Azure OpenAI、使ってますか～？

私は業務でほぼ毎日のように使っているのですが、最近、生成 AI 系のプロダクトにはフィルタリング機能が必要不可欠だな～だと考えることが多くなりました。もし自分のプロダクトが社会倫理に反するような文章を出力してしまったら、最悪の場合、顧客からの信頼を失ってしまうといった結果も招きかねません。

そういった経緯で、今回は Azure OpenAI で不適切な表現や特定の語句、話題をフィルタリングする簡単な実装方法を実装例とともに３つ紹介していこうと思います。

# 方法１. Azure OpenAI Service の Content filters を利用する
Azure OpenAI には、デフォルトで **コンテンツフィルタ** と呼ばれるフィルタリング機能が有効になっています。

「憎悪表現 (Hate) ／ 性的表現 (Sexual) ／ 自傷表現 (Self-harm) ／ 暴力表現 (Violence) 」の４カテゴリが用意されており、それぞれのカテゴリにおけるフィルタリング閾値も調節することが出来ます。また、オプトインで「悪意のあるプロンプト入力 (Jailbreak)」や、「歌詞／ソースコードなどの著作物の出力」も回避することが可能です。

コンテンツフィルタ には NG ワード機能も存在しており、特定の語句をフィルタリングすることが出来ます。NG ワードはAzure Portal や Azure OpenAI API で登録することができ、正規表現や完全一致検索で文章に含まれているかを判断してくれます。

## 1-A. 利用方法：不適切な表現に関するフィルタリングを有効化する
コンテンツフィルタを使って、不適切な表現をフィルタリングする機能を有効にしましょう。
手順：[Azure OpenAI Service でコンテンツ フィルターを構成する方法](https://learn.microsoft.com/ja-jp/azure/ai-services/openai/how-to/content-filters#configuring-content-filters-via-azure-openai-studio-preview) 

1. Azure OpenAI Studio の左メニューにある **コンテンツ フィルター** を選択し、「カスタマイズ済みコンテンツフィルターの作成」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058767/3a71506b-1816-00f2-5f11-6c1ac9523fba.png)<br><br>
1. フィルタリングを行う閾値が設定できます。**Prompt** と **Completion** はそれぞれ「Azure OpenAI に送信される入力」と「Azure OpenAI から返却される出力」のことです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058767/1c3f4be0-f1a0-9dfd-de89-074e67d82210.png)
フィルタリング指標は [セキュリティレベル - コンテンツのフィルター処理](https://learn.microsoft.com/ja-jp/azure/ai-services/openai/concepts/content-filter?tabs=warning%2Cpython#severity-levels) の定義に従っています。内部的には Azure AI Content Safety が使われているため [^1] 、[Content Safety Studio](https://contentsafety.cognitive.azure.com/text) でいろいろ試すことができます。<br><br>
1. Jailbreak や著作物のフィルタリングは、次ページの **追加モデル** から設定します。英語以外の著作物 [^2] や、インターネットで公開されていないソースコードは、このフィルタリングの対象外となるので注意しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058767/c8662ff1-48e6-7e4b-0f12-811f893af7d0.png)<br><br>

1. 以上の設定を終えたら、Azure OpenAI Studio の **デプロイ** からデプロイとの紐付けをして完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058767/17a57488-123c-698f-6501-aebbdf48ea52.png)<br><br>


[^1]: "*Azure OpenAI content filtering is powered by Azure AI Content Safety.*", [Azure OpenAI Service content filtering - Azure OpenAI Service Documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/content-filter?tabs=warning%2Cpython#next-steps)

[^2]: "*This feature can be used to identify and block known text content from being displayed in language model output (English content only).*" [Quickstart: Detect protected material (preview) - Azure AI Content Safety Documentation](https://learn.microsoft.com/en-us/azure/ai-services/content-safety/quickstart-protected-material)

## 1-A. 利用方法：特定の語句に対するフィルタリングを有効化する
コンテンツフィルタを使って、NG ワードをフィルタリングする機能を有効にしましょう。
手順：[Azure OpenAI でブロックリストを使用する](https://learn.microsoft.com/ja-jp/azure/ai-services/openai/how-to/use-blocklists) 

1. Azure OpenAI Studio の左メニューにある **コンテンツ フィルター** を選択し、「ブロックリストの作成」からブロックリストを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058767/2cb9220b-51ae-60c3-3ec9-516a5fc50e58.png)<br><br>

1. ブロックリストの作成が完了したら、ブロックリストを選択して **用語の登録** から禁止ワードを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058767/fa7c86fb-9329-375c-6f0a-ef7fa2e63a53.png)<br><br>

1. 登録には 完全一致 と 正規表現 の２種類が使えて、正規表現を使う場合は「Regex」にチェックを入れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058767/82af8b38-d0ea-bbae-58a8-5916c6fd6cfa.png)
<br><br>

1. 登録が終わったらブロックリストの有効化を行います。適用するコンテンツフィルタを編集して、**ブロックリストの追加** から対象のブロックリストにチェックを入れて有効にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058767/35fcf64d-602d-4346-f7bf-7b98af9f8179.png)
<br><br>

1. 以上の設定を終えたら完了です。

## 1-B. 実際に使用した例
実際に Azure OpenAI にプロンプトを送信して、フィルタリング付きの挙動を調べてみます。

### 不適切な表現のある文章のフィルタリング
コンテンツフィルタから **自傷** の閾値を「低を許可 / 中および高をブロック」に設定して、自傷表現が入った文章でテストしてみます。

- テスト用 Python スクリプト
```python: Python
import os
import openai

client = openai.AzureOpenAI(
    api_version="2023-10-01-preview",
    azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
    api_key=os.getenv("AZURE_OPENAI_KEY"),
)

try:
    response = client.chat.completions.create(
        model="chat",
        messages=[
            {
                "role": "user",
                "content": "I cut my finger and the bleeding never stopped.",
            },
        ],
        temperature=0,
    )

except openai.BadRequestError as error:
    print(error)
```

- レスポンス内容
```json: JSON
{
   "error":{
      "message":"The response was filtered due to the prompt triggering Azure OpenAI's content management policy. Please modify your prompt and retry. To learn more about our content filtering policies please read our documentation: https://go.microsoft.com/fwlink/?linkid=2198766",
      "type":"None",
      "param":"prompt",
      "code":"content_filter",
      "status":400,
      "innererror":{
         "code":"ResponsibleAIPolicyViolation",
         "content_filter_result":{
            "hate":{
               "filtered":false,
               "severity":"safe"
            },
            "self_harm":{
               "filtered":true,
               "severity":"medium"
            },
            "sexual":{
               "filtered":false,
               "severity":"safe"
            },
            "violence":{
               "filtered":false,
               "severity":"safe"
            }
         }
      }
   }
}
```
文章がフィルタリングされた場合、Azure OpenAI からは "400 Bad Request" で応答されます。詳しいフィルタリング結果は error.innererror.content_filter_result に出力され、今回は self_harm カテゴリが `"filtered":true` となっていることが確認できます。

これによって、プリセットにあるフィルタリングルールを使用しつつ、個別にフィルタリングのレベルを調節することができるようになり、使用目的に合わせたフィルタリングをデプロイ別に素早く実装することが可能になりました。

### NGワードがある文章のフィルタリング
NGワードに "Hello" を設定して、文章に NG ワードが入っている場合をテストしてみます。

- テスト用 Python スクリプト
```python: Python
import os
import openai

client = openai.AzureOpenAI(
    api_version="2023-10-01-preview",
    azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
    api_key=os.getenv("AZURE_OPENAI_KEY"),
)

try:
    response = client.chat.completions.create(
        model="chat",
        messages=[
            {
                "role": "user",
                "content": "Hello, World!",
            },
        ],
        temperature=0,
    )

except openai.BadRequestError as error:
    print(error)
```

- レスポンス内容
```json: JSON
{
   "error":{
      "message":"The response was filtered due to the prompt triggering Azure OpenAI's content management policy. Please modify your prompt and retry. To learn more about our content filtering policies please read our documentation: https://go.microsoft.com/fwlink/?linkid=2198766",
      "type":"None",
      "param":"prompt",
      "code":"content_filter",
      "status":400,
      "innererror":{
         "code":"ResponsibleAIPolicyViolation",
         "content_filter_result":{
            "custom_blocklists":[
               {
                  "filtered":true,
                  "id":"TestBlockList"
               }
            ],
            "hate":{
               "filtered":false,
               "severity":"safe"
            },
            "self_harm":{
               "filtered":false,
               "severity":"safe"
            },
            "sexual":{
               "filtered":false,
               "severity":"safe"
            },
            "violence":{
               "filtered":false,
               "severity":"safe"
            }
         }
      }
   }
}
```
こちらも文章がフィルタリングされた場合、Azure OpenAI からは "400 Bad Request" で応答されます。 詳しいフィルタリング結果は error.innererror.content_filter_result に出力されており、今回は custom_blocklists が `filtered:true` になっていることが確認できます。

これで新たにソフトウェア側で NG ワード機能を実装しなくとも、Azure Portal 上で禁止ワードを登録して同様の機能が実現できるようになりました。

## 1-C. より詳しく知るための文献
具体的な手順や、トラブルシューティング集は公式ドキュメントに記載されています。
詳しくは以下をご覧ください。

https://learn.microsoft.com/ja-jp/azure/ai-services/openai/concepts/content-filter?tabs=warning%2Cpython

https://learn.microsoft.com/en-us/azure/ai-studio/concepts/content-filtering

# 方法２. Nemo Guardrails 等のガードレール系ライブラリを導入する
コンテンツフィルタだけでも大半のフィルタリング要素をカバー出来ますが、話題ベースのフィルタリングはカバー出来ていません。

そこで利用できるのが、[Nemo Guardrails](https://github.com/NVIDIA/NeMo-Guardrails) や [LLM Guard](https://github.com/laiyer-ai/llm-guard) といったガードレール系ライブラリです。

LLM における **ガードレール (*Guardrails*)** とは、大規模言語モデルの出力を制御し、特定のプロンプトに対して予め定義したフローで返したり、指定したフォーマットで出力を強制したり、特定の話題を出力させないようにする方法のことをいいます。

ここでは Nemo Guardrails を中心に、Azure OpenAI に特定の話題を出力させないようにする方法をご紹介します。

## 2-A. 利用方法と実際に使用した例
Nemo Guardrails は LLM とアプリケーションの間をプロキシとして仲介し、CoT やベクトル検索を活用してガードレールを実現しています。このため、複雑な部分はラッピングされており、容易に高度なガードレール機能を実装することが可能となっています。

今回は、**政治** 関連の話題を禁止するガードレールを実装してみます。[^3]

[^3]: 2023/12/13 時点で Nemo Guardrails は `openai==0.28.1, Python v3.8-v3.10` に対応済。[NeMo-Guardrails/docs/getting_started/installation-guide.md at develop · NVIDIA/NeMo-Guardrails](https://github.com/NVIDIA/NeMo-Guardrails/blob/develop/docs/getting_started/installation-guide.md#optional-dependencies)

### インストールと実装の手順
1. まずは Nemo Guardrails を pip 経由でインストールします。
執筆時点のバージョンは v0.5.0 です。
    ```bash: 手順1：Nemo Guardrails のインストール
    $ pip install nemoguardrails
    ```
    <br>
1. インストールが完了したら、以下の通りに `guardrail_test.py` / `config.yml` / `rails.co` を作成します。
    ```text: 手順2：フォルダ構成
    root/
    ├── guardrail_test.py
    └── config/
    　  ├── config.yml
        └── rails.co
    ```
    <br>
1. `config.yml` には LLM のモデル名を記載します。モデル名の一覧は [Models - OpenAI API](https://platform.openai.com/docs/models) に記載されています。
    ```yaml: 手順3：config.yml
    models:
     - type: main
       engine: openai
       model: gpt-3.5-turbo
    ```
    <br>
1. `rails.co` にはガードレールの具体的な中身を定義します (※)。
    ```colang: 手順4：rails.co
    define user ask about politics
      "What is the name of the current president?"
      "Tell me about parliamentary democracy."
      "What do you think about the current situation of the government?"
      
    define bot refuse to respond about politics
      "I'm sorry, but I can't provide information or support on politics."
    
    define flow
      user ask about politics
      bot refuse to respond about politics
    ```
    ガードレール制御には Colang と呼ばれる自然言語ライクな独自言語が使われています。
    言語仕様は [Colang Guide](https://github.com/NVIDIA/NeMo-Guardrails/blob/develop/docs/user_guides/colang-language-syntax-guide.md) に載っています。

    ※)
    `define user ask about politics`：政治関連の話題が振られたタイミングを定義
    `define bot refuse to respond about politics` ：政治関連の話題を拒否する時の応答文
    `define flow`：定義とフローの関連付け
    <br>
1. 最後に、実行スクリプトの `guardrail_test.py` を実装します。
    スクリプトでは政治に関連する「あなたの政治的信念は何ですか？ ("*What is your political belief?*")」 を質問に設定しています。
    ```python: 手順5：guardrail_test.py
    import os
    from langchain.chat_models import AzureChatOpenAI
    from nemoguardrails import LLMRails, RailsConfig

    # LangChain 経由でクライアントを作成
    client = AzureChatOpenAI(
        openai_api_version="2023-10-01-preview",
        deployment_name="chat",
        openai_api_base=os.getenv("AZURE_OPENAI_ENDPOINT"),
        openai_api_key=os.getenv("AZURE_OPENAI_KEY"),
    )

    # 定義ファイルの配置場所
    config = RailsConfig.from_path("./config")

    # LLMRails でガードレールを適用
    # https://github.com/NVIDIA/NeMo-Guardrails/blob/develop/docs/api/nemoguardrails.rails.llm.llmrails.md
    app = LLMRails(config, llm=client)
    response = app.generate(messages=[{
        "role": "user",
        "content": "What is your political belief?"
    }])
    
    print(response["content"])
    ```

### スクリプトの実行結果
`rails.co` で指定した定型文が返却されており、話題ベースのフィルタリングが出来ていることが確認できました。

- 出力結果
```plaintext: bash
$ python guardrail_test.py
I'm sorry, but I can't provide information or support on politics.
```

今回の例では政治を対象にしましたが、同様にして `rails.co` に禁止する話題を何個も定義することが出来ます。また、文脈に関係する話題だけを禁止するように NeMo Guardrails がシステムプロンプトを組むため、トークンを節約しながら出力を制御することが可能です。

## 2-C. より詳しく知るための文献
NeMo Guardrails は Fact Checking や Hallucination Detection などの様々なガードレールが実装可能です。詳細は以下をご覧ください。

https://arxiv.org/abs/2310.10501

https://github.com/NVIDIA/NeMo-Guardrails/blob/develop/docs/user_guides/guardrails-library.md

# 方法3. Prompt Engineering でフィルタリングを実装する
最後に、プロンプトエンジニアリングで特定のワードや話題を回避する方法をご紹介します。

これまでコンテンツフィルタ、NeMo Guardrails と紹介してきましたが、実装の簡単さでいえばこの方法が一番手っ取り早いと思います。

より高度に Azure OpenAI の出力を制御したい、なるべくトークンを少なく抑えたい、などの思いあれば、コンテンツフィルタや NeMo Guardrails での実装をオススメします。

## 3-A. 利用方法と実際に使用した例
実装の仕方は様々ありますが、ここでは３つのプロンプト手法をご紹介します。

### ケース１. 特定の話題に対して定型文を返却させる
指定した話題が会話に出てきたら、定型文を返すように指示します。
このケースは、汎用的な目的で Azure OpenAI が使用されるような場面に向いています。

- システムプロンプト
```plaintext: 
Say only "I'm sorry, I can't assist with that." if asked anything related to the following topics: {禁止にする話題}.
```
上記の例では、**{禁止にする話題]** に関連する話題が出てきたときに、"I'm sorry, I can't assist with that." という定型文を返します。

試しに「お金」という話題を禁止して、"What is the Japan's currency name?" と尋ねてみます。
- 出力結果
```plaintext: 
# ユーザの質問
user: What is the Japan's currency name?

# Azure OpenAI の回答
assistant: I'm sorry, I can't assist with that.
```
うまくお金に関する話題を回避出来ています。

禁止にする話題の粒度が粗いと範囲が広くなってしまい、指定した話題以外の会話もままならなくなってしまう可能性があることには注意しましょう。

### ケース２. GPT の役割を明確にする
LLM の役割を明確化し、役割以外のタスクは実行しないように指示します。
このケースは、Azure OpenAI の利用目的がひとつに定まっているような場面に向いています。

- システムプロンプト
```plaintext:
You are a {役割}, that helps users by {タスク内容}.
{その他の制約条件(任意)}
Do not perform actions that are not related to {役割}.
```
上記のプロンプトにより、**{役割}** や **{タスク内容}** に関連が無い場合は、LLM に回答を拒否させることが出来ます。

試しに、四則演算を実行する役割を以下のシステムプロンプトで設定し、全く関係のない "What is the capital city of Paris?" という質問を尋ねてみます。

- 出力結果
```plaintext: 
# システムプロンプト
system: You are a mathematical calculator, that helps users by perform basic addition, subtraction, multiplication, and division.
Use only symbols and numbers.
Do not perform actions that are not related to mathematics.

# ユーザの質問
user: What is the capital of Paris?

# Azure OpenAI の回答
assistant: I'm a mathematical calculator and I can help you with mathematical problems. If you need information about capitals of countries, please feel free to ask and I can provide that information to you.
```
四則演算を行うという役割以外の話題に対して、回答を避けることが出来ています。


### ケース３. 別の GPT に話題の関連度を評価してもらう
少し切り口が変わりますが、別の GPT に話題の関連度を評価してもらう方式をご紹介します。

例えば次のような文章が Azure OpenAI によって生成されたとします。
```plaintext: 評価対象の文章
最も価値のある硬貨はオーストラリアの「1トンゴールドコイン」で、その価値は約5300万ドルです。また、最
も古い形式の紙幣は中国の唐朝時代に遡ります。さらに、アメリカの1ドル紙幣の寿命は約5.8年で、その後リサ
イクルされて新しい紙幣になります。また、ビットコインのような仮想通貨は、デジタル化とグローバリゼーシ
ョンの波に乗って急速に普及しています。
```

この文章に対して、次のプロンプトで「お金」との関連度を評価します。
- システムプロンプト
```plaintext: 
You are a helpful assistant designed to output JSON.
If the sentence is not in English, translate the sentence into English and think.
The output JSON contains only "relevance_score" as a key.

Rate the relevance score of the following sentence to "{topic}" on a scale of 0.0 to 1.0.

###
{評価対象の文章}
###
```

- 出力結果
```plaintext: 出力
{
  "relevance_score": 0.9
}
```
「お金」という単語が明示的に評価対象の文章で使用されていないのにも関わらず、関連度が 0.9 と高いスコアで関連した話題であることが導き出せました。

システムとしては関連度スコア (relevance_score) を 0.5 などの閾値と比較することで、特定の話題に関連するかどうかを判断することができます。

## 3-C. もっと詳しく知るための文献
プロンプトエンジニアリングを学ぶにあたり、体系良くまとまっている以下のウェブサイトが参考になると思います。

https://www.promptingguide.ai/jp

https://learn.microsoft.com/ja-jp/azure/ai-services/openai/concepts/system-message


## 最後に
今回は Azure OpenAI にフォーカスしたフィルタリング方法として コンテンツフィルタ、ガードレール系ライブラリ、プロンプトエンジニアリング の３つの手法をご紹介しました。

コンテンツフィルタ単体でも十分に満足なフィルタリング機能を実現できますが、話題ベースの出力制御などしたいのであれば、NeMo Guardrails を初めとしたガードレール系ライブラリや、プロンプトエンジニアリングの活用も選択肢に入ってきます。

ユースケースやシステム構成に適した組み合わせでフィルタリングを導入し、プロダクトの安全性を保証しましょう！


※ 本記事における画面や記載内容は 2023/12/12 時点のものとなります。
※ 記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。