ChatGPT API × Azure Cognitive Searchで求人情報検索を試みる

# はじめに
ChatGPT API（OpenAI APIのChat completion）とAzure Cognitive Searchを使って求人情報検索アプリケーションを試作してみました。
![スクリーンショット 2023-04-23 122203.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/7c5c26a9-1d46-9984-20be-681a3f3fa61d.png)

@nohanaga さんの記事「[Azure で ChatGPT × Cognitive Search を使ったエンタープライズサーチを実現](https://qiita.com/nohanaga/items/803c09b5a3a4e2d1776f)」を読み、この仕組みを使って何か自分でも作ってみようと思ったのがきっかけです。

まだまだ課題は多いですが、何かの参考になれば幸いです。

# アーキテクチャ

![クラウドアーキテクチャ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/c5c69a96-6dec-b11a-eada-828c37f8633e.png)


仕組みはシンプルで、
- ユーザーからの入力をもとに、ChatGPT APIでCognitive Searchの検索クエリを生成。
- 検索クエリをCognitive Searchで実行し結果を取得。
- 検索結果をChatGPT APIに渡し、ユーザーへの提案文章を生成。

という流れです。

できればAzure OpenAI Serviceを使いたかったのですが、着手時点（2023年4月中旬）では個人のメールアドレスでは利用申請できなかったため、OpenAIのAPIを使用しています。

検索対象となる求人情報はjsonファイルで用意し、Azure Blob Storageに格納しています。
Cognitive Searchのデータソースとしては様々なものを選択できますが、今回は[チュートリアル](https://learn.microsoft.com/ja-jp/azure/search/search-semi-structured-data)もあり手軽に用意できるこの形式を選択しました。

また、フロントはReact(Next.js) × Tailwind CSS、バックエンドはPython(Flask)で構築しています。
フロントのコードはほぼすべてChatGPT(GPT-4)に書いてもらいました。
これくらいシンプルなUIとリクエスト処理であればもう人の手は不要なんですね。本当に驚かされます。
バックエンドは、前述の記事でも紹介されているエンタープライズサーチを実現するための[サンプルコード](https://github.com/Azure-Samples/azure-search-openai-demo)でPython(Flask)採用されていたのでそれに倣っています。
バックエンドのコードもChatGPT(GPT-4)、GitHub Copilotの恩恵を大いに受けています。

# 検索クエリの生成
Microsoftのサンプルでは検索キーワードのみ生成しているように見受けられますが、求人検索の場合、例えば「残業20時間以内」「年収500万以上」のような検索をしたくなるでしょう。
それをfilterクエリパラメータで実現できるよう、プロンプト内に使用可能なクエリパラメータとして明記しています。

```
system_prompt_for_search = """質問を元に、Azure Cognitive SearchのPython用クライアントライブラリで使用するためのjson形式のクエリパラメータを生成してください。
質問履歴が存在する場合は、それを考慮して適切なクエリパラメータを生成してください。

このプロンプトに対するあなたの回答は、json形式の文字列のみとします。

# 使用可能なクエリパラメータ
- search_text
- filter

# サンプルデータ
(省略)

# 出力例
{"search_text":"フロントエンドエンジニア"}
{"filter":"max_annual_salary ge 500"}
{"search_text":"バックエンドエンジニア","filter": "overtime_working_hours lt 20"}
"""
```
このプロンプトを"system"ロールとして設定し、その後ユーザーの質問を別のプロンプトとして設定します。
出力例も記載し、かつ「このプロンプトに対するあなたの回答は、json形式の文字列のみとします。」と制約しているものの出力がブレることがあるため、以下のように出力に対してjson文字列の抽出も行っています。

```python
# 文字列内の最初のJSON構造の文字列を抽出
match = re.search(r'\{.*?\}', answer, re.DOTALL)
first_json_string = match.group()
```

このjsonをパースし、Cognitive Searchのクエリパラメータとしてリクエストをします。

# ユーザーへの提案
まずは以下のようなプロンプトを"system"ロールとして設定します。
```
initial_prompt_for_propose = """あなたは転職エージェントです。
これまでの会話を踏まえ、検索結果を元にユーザーに対して求人を提案してください。
日本語で分かりやすく説明してください。
"""
```
その後、フォーマットした検索結果（今回は簡単のために最初の検索結果のみ抽出）をプロンプトに設定してChatGPT APIに渡します。

また、これまでのユーザーとのやりとりもmessagesに含めるようにしています。
```python
# プロンプトを作成
messages = [{"role": "system", "content": initial_prompt_for_propose}]
for message in history:
    messages.append(
        {"role": message["role"], "content": message["content"]})
messages.append({"role": "user", "content": question})

first_result = next(search_results, None)
if first_result:
    formatted_result = self._format_result(first_result)
    messages.append(
        {"role": "system", "content": prompt_for_search_result.format(formatted_result)})

    response = openai.ChatCompletion.create(
                model=self.model,
                messages=messages
            )
```
得られた提案を画面に返すことで、一連の流れが完了となります。

# 実際に動かしてみる
「残業時間が20時間以内のフロントエンドエンジニアの求人はありますか。」と質問してみると、以下のような回答が返ってきました。
![スクリーンショット 2023-04-23 151823.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/b68308c4-ac64-699d-ca04-04c10634fb97.png)
実際の求人情報はこちら。
```json
{
  "id": "000002",
  "company_name": "イノベーションフュージョン株式会社",
  "job_category": "フロントエンドエンジニア",
  "employment_type": "正社員",
  "location": "神奈川県横浜市港北区",
  "number_of_employees": "80",
  "years_in_business": "12",
  "job_description": "担当プロジェクトのフロントエンド設計・開発を行い、ユーザーエクスペリエンスの最適化と高品質なコードの実装を目指します。また、アジャイル開発に取り組みながら、チームとコミュニケーションを密にし、積極的な改善提案を行ってください。",
  "required_experience_and_skills": "Vue.js経験が必須で、HTML5, CSS3, JavaScript, Git, レスポンシブデザイン実装、アクセシビリティに関する知識が求められます。",
  "min_annual_salary": "550",
  "max_annual_salary": "1000",
  "business_start_hour": "10",
  "business_end_hour": "19",
  "overtime_working_hours": "10",
  "holiday_and_vacation": "週休二日制（土・日）、祝日、年末年始、有給休暇、慶弔休暇、リフレッシュ休暇",
  "benefits": "各種社会保険完備、交通費支給、定期健康診断、社内イベント、研修制度、資格取得支援制度、育児・介護休暇制度"
}
```
質問の条件を満たしつつ、会社名や仕事内容、年収などおおむね正しく提案できています。
このときの検索クエリは`{"search_text":"フロントエンドエンジニア","filter": "overtime_working_hours lt 20"}`ですので、質問の内容通りですね。
一方で、「福利厚生はありません。」とありますが、しっかり福利厚生も備えているのでこちらは誤りです。

次に、条件を追加して質問してみます。
まずは「Reactのスキルを活かせる仕事はありますか。」と質問。
その後、「残業時間30時間以内で探してください。」と追加で質問した結果がこちらです。
![スクリーンショット 2023-04-23 152916.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/cd318d07-1a7a-5899-ed5b-c6f475075cd1.png)
2回目の質問時に生成された検索クエリは`{"search_text":"React","filter":"overtime_working_hours lt 30"}`なので、最初の質問の内容を考慮できているようです。

ただし、何度か試していると過去の質問の内容が考慮されていない検索クエリが生成されることが多々あったため、プロンプト等の改良が必要そうです。

他にも触っていると粗が目立つので、改良を重ねていきたいですね。

# まとめ
ソースコードは以下で公開しています。
https://github.com/keitomatsuri/ai-job-search-experiment

今後の展望としては、[Semantic Kernel](https://github.com/microsoft/semantic-kernel)のPython版がリリースされたようなので、それを組み込んで高度化していきたいと考えています。
求人検索だけでなくあらゆる関連タスクをこなせる"AI転職エージェント"ができたらいいな、という夢も見てみたり。
また、他のジャンルにも応用利きそうなので色々考えてみようと思います。

最後に、twitterやっているのでもしよければフォローしてください！
https://twitter.com/keitomatsuri