ES自動生成AIを作ってみた

この記事では以下の拡張機能(ES-writer)を作成した時のことについて記していく。

https://github.com/Yutosaki/es-writer/tree/main

https://chromewebstore.google.com/detail/es-writer-extension/jkencchebhkbaomammmgbhnpalgkchkm?hl=ja

目次
* なぜ作ったか
* どの様に作り始めたか (技術構成など)
* 機能
* 推しポイント
* 上手くいったところ
* どこが苦労したか
* 今後の展望

また、別記事でコードについての解説などもした。
* [SignUp(サインアップ)](https://qiita.com/Yutosaki/items/5fec33696105678aa416)
* [SignIn(ログイン)](https://qiita.com/Yutosaki/items/c52da4b98cdd3a934271)
* [経歴入力(自己PR、経験、今まで作った作品](https://qiita.com/Yutosaki/items/b3c1a35a6e3c1306cd3f)
* [回答生成(埋め込み含む)](https://qiita.com/Yutosaki/items/b3c1a35a6e3c1306cd3f)
* [SignOut(ログアウト)](https://qiita.com/Yutosaki/items/c52da4b98cdd3a934271)

# なぜ作ったか
現在自分が大学3年で様々な企業にエントリーシート (ES) を出していたが、毎回おんなじ様な内容をちょっとフォーマットが違ったり、文字数の制限があったりでわざわざ書き換えるのが面倒に感じたため、これらをもっと簡単にできないかと考えた。

# どの様に作り始めたか
そもそもなのだが、これはハッカソンで作ったものである。
（Progateハッカソン powered by AWS　エムスリー株式会社賞　受賞　🎉）
最初は、普通にWebアプリケーションで作ろうと考えていた。期間も約1週間ほどで、５人で挑んだのだが5人中2人がハッカソンなどの開発が初めて、という状況もあり、安直に作りやすそうなもので考え始めた。
しかし、実際に技術構成を考えたり機能要件などを考えたりしていく時に壁に当たった。

最初に考えた機能としては、ESの画面上で使うものではなく、ESの URL をWebアプリに渡して、そこから質問文などを読み取って、事前に入力させた自分の経歴に基づいて回答を生成させ、それをコピペして自分で入力する、というものであった。しかし、Webアプリにすることで、問題点が出てきた。

一つ目は、認可の問題だ。そもそもESを入力する際は個人のページから入力するのが普通で、ESを外から見ようとする時はCookieに保存されている`Access　Token`を使用して閲覧することができる (`ID Token`ではないことに注意。`Access Token`は許可証で`ID Token`はその人であることの証明書、マイナンバーカードの様なイメージ)。しかし、WebアプリにするとURLだけでなくどうにかしてCookieの情報なども渡さないといけない、というところが問題になった。
二つ目は、わざわざこのWebアプリを使うに値するか、というところだ。わざわざURLを投げて答えをコピペするだけなら自分でChatGPTなどに自分の経歴を入れて聞けばいいだけなので、画面遷移はユーザーの使用率を下げることにつながるのではないかと考えた。

これらの問題を解決するために考えたのが、拡張機能にする、ということである。
拡張機能にすることで上記の問題が解決した。
* 認可の問題
 -> 拡張機能にすることでそのサイトのCookieをバックエンドが直接操作したり、参照したりする必要がなく、ブラウザのAPIを通じてCookieに簡単にアクセスし、フロントエンドから`Authorizationヘッダー`を通じてフロントからバックに投げることができる。また、拡張機能は特定のドメインに対してのみ権限を持つことができるので、不要なクロスサイトアクセスを減らすことができる。
* ユーザーエクスペリエンス
 -> 拡張機能なので、ESのページで画面遷移なく、画面内で簡単に回答を作成できる。また、回答をコピペするのではなく、実際のESに書き込めるので圧倒的に使いやすくなる

 よって、拡張機能として作る、という考えでまとまった。

次に、技術構成について考えた。
フロントは、拡張機能を作る上で、主要なブラウザをサポートしていることや、これは開発者目線なのだが、自動リロード機能があり、コードを変更するとそれに応じて自動でリロードしてくれるのできちんと動いているかの確認がスムーズにできて作業効率が向上するという点が気に入り、 `plasmo`という`React`のフレームワークを使用することに決めた。また、簡単に見た目を整えるために`TailwindCSS`を使用した。

バックエンドでは、ESの質問を一問ずつ生成させていては時間がかかりすぎると考え、並列処理などが簡単に書ける`Go`を採用することにした。また、他の言語にはあるが`Go`には意図的に作られていないであろう多様な書き方がなく、誰が書いても同じ様なコードになり、チーム開発にも向いているというところも魅力に感じた。フレームワークは高速で拡張性の高い`Echo`を利用した。

DBには`PostgreSQL`を使用した。オープンソースでスケーラビリティにも優れていることから採用した。また、`ORM`ライブラリとして`Gorm`という`Go`用の`ORM`を用いた。
>[ORMとは](https://qiita.com/minimabot/items/0a3fcc41fd7140dfdc41)

認証は`AWS Cognito`でユーザの管理を、`JWT`で認証トークンの発行や検証を行った。

インフラ周りは`Docker`と`AWS`を使用して`terraform`で管理した。

実際の構成は以下の通り。
![インフラ図](https://i.imgur.com/6odYuVC.png)

他にも開発ツールやパイプラインとして、`Webpack`と`Github Actions`を使用した。後々話すが、`Github Actions`に単なるCI/CDだけではなく、コード品質を担保するために`prettier`などを組み込めば良かったなどいう反省などもある、、、

# 機能
元々欲しかった機能は先ほど軽く上げた通りなので、ハッカソン中に制作した機能やブラッシュアップした上で作成した機能などをまとめる。

* SignUp(登録）
* SignIn(ログイン)
* 経歴入力(自己PR、経験、今まで作った作品)
* 回答生成(埋め込み含む)
* SignOut(ログアウト)

主な主要な要素は以上の5つである。別記事でそれぞれについて少し述べる。

# 推しポイント
やはり拡張機能にしたところだ。ユーザーに対する使いやすさについてちゃんと考え、実際に画面の遷移やコピペすることなく、ボタンひとつで全てを自動で行ってくれるという圧倒的便利さがとても良いと思う。また、経歴入力などにおいても昔の経歴などを見返して修正することができたり、回答生成を並列に行っているため短時間で実行が完了したりなど、使いやすさを追い求めた。

# 上手くいったところ
個人的に開発を進めていく上で良かったと思ったところは機能ごとに開発を進めたところである。ハッカソン中は初学者がいたということもあり、フロントでこういう機能を作って、バックでこれをやるからDBのとこ書いて〜など、それぞれの分野で分けたりしていたのだが、ハッカソン終了後も継続開発をし、その際は開発経験が多少ある3人で行ったということもあり、機能ごとに制作し、issueを立ててpull requestをきちんと書いてコードレビューを行なっていくという形式にした。これが個人的にはとても良かったと感じ、機能ごとに作っていくので、他の人の書いたコードと自分のコードを繋げたりする手間や、フロント、バックなどではなく、全体の構成についても詳しく触れながらコードを書いていけたので、そこがとても良いと感じた。

また、クリーンアーキテクチャに沿って書けたのも良かったと思う。アーキテクチャなどを意識してコードを書いたことが今まで無く、実際にコードを書いている際もなんでこんなに`repository`なり`controller`なり分けて書かないといけないんだ、と思いながら書くこともあったが(今もめんどくさいと思う時は多々ある、、、)実際に見返したり、初見の人がこのコードを見た時にはやはり読みやすく、この構造体どこで定義されているんだ、、という様な時にも、ディレクトリを軽く見ただけで`model`というディレクトリにまとまっていそう、と分かったり、`usecase層`や`repository層`を分けることで、どの層からDBにアクセスできて〜などより大きいサービスになればなるほどメリットがありそうだな、と感じた。

# どこが苦労したか
山ほどあるが、簡単にまとめたいと思う。

まずは、認証周りである。
ハッカソン終了時はJWTを使ってるのに認証系APIしかJWTの検証してないというガバガバなセキュリティになっていた。つまり、ログイン後にアクセスできる他のエンドポイントではJWTトークンを検証しておらず、セキュリティが不十分だった。この状況では不正アクセスやトークンの盗用のリスクが高い。
そのため、セキュリティを強化するために継続開発でリプレイスを行った。具体的には、ミドルウェア構築して`Authorizationヘッダー`にJWTトークンを入れることでログイン後に想定されるアクセス先は全てJWT検証が入るようにした。これにより、全てのアクセスでJWTトークンが検証され、想定される全てのアクセスにおいてセキュリティチェックが行われるはずであった。
しかしここで問題が起こった。フロントで`Authorizationヘッダー`にJWTトークンの値を入れたいのだが、なぜか`Cookie`からJWTトークンを取得できなかった。(`CORS`の設定やブラウザの制限など色々原因がありそうだが分からない...)
諦めた。`Authorizationヘッダー`を使用したJWTの検証が難しくなったため、バックエンドのミドルウェアでクッキーからJWTトークンを取得して検証する方法に切り替えた。これによって、ログイン後のセキュリティの安全性は確保されたが、JWTの主なメリットである、ヘッダーによる簡易なトークン管理というメリットが活かされない結果となってしまった。

次に、クリーンアーキテクチャの理解である。ハッカソン中はあまり何も意識せずにコードを書いており、継続開発を始めるタイミングでクリーンアーキテクチャに沿ったコードに変更してから、再度開発を進めた。最初に変更したときは今まで自分が書いていたコードがどこにいったのかわからず、ファイルをいちいち検索したり、どこからDBを操作しているのかなどもわからず、適当なファイルでDBを操作して`pull request`を出したら`change request`が返ってきたりと、なかなかにコードや概念を理解するのが難しかった。これに関しては割と作り終えたタイミングでクリーンアーキテクチャに沿ったコードに書き換えたために起こったというところもあると思う。最初の設計の時に技術選定などだけでなく、設計、アーキテクチャにもっと時間を費やして、開発を進めていけば良かったと感じた。

また、質問文のスクレイピングのところでは、ESのページのHTMLを全文なげてAIに解読させているのだが(AIは入力が長くても出力が短ければ時間があまりかからないため全文読ませている。これによってどんな入力のESでも対応可能に)、AIに一文を投げると回答も一文で返ってきてしまうという問題があった。一つの質問文をスクレイピングするだけならば問題ないのだが、複数の質問文を取得したいときに、一文で返されると、その後に一問ずつ並列処理でAIに回答を生成させるというロジックを果たせないため、それぞれの質問文を分けて出力させてそれを配列などに入れて、並列でAIに投げかける仕様にしたかった。
最初は、全文章を読ませて何問目の質問文か保持してカウントを増加させ、HTML全文を投げて一問目の質問文を出力させたら、次にまたHTML全文を投げて二問目の質問文を出力させる様にしようかと考えた。しかし何度も質問を投げかけると、上手く行なっても並行に処理するのが限界で時間がかかるという問題点があった。
それを解決するために考えたのが、プロンプトエンジニアリングである。今回は、1回の質問で複数の質問文を出力させたかった`。`しかしAIからのレスポンスは一文で出力されてしまうので、各質問文の間に`###`という文字を入れて出力してください、と頼んだ(`。`で区切ると質問文中に。が入った時に対応できないため)。こうすることで一文でありながらどこで質問が区切れているのかが分かる。そしてAIから返却された文字列を`###`で区切って配列にそれぞれ格納することで上手く切り抜けることができた。後から見ると当たり前の様に思うかもしれないが、AIに一つずつ質問文を出力してもらうという思考から、一文を特定の文字に沿って分ける、という思考にシフトできたのが個人的に楽しかった。

他にも、コードフォーマッターをし忘れすことが多々あり、コードの可読性が低くなってしまった。共同開発なので、そういった点にもう少し注意を向け、 `Github Actions`に`ESLint`や`Prettier`などのコード品質を担保するためのツールを組み込めば良かったという反省がある。

# 今後の展望
実際に自分が過去に送信したESの内容をメールなどに転送して見返せる様にしたい、などの機能を追加したいと考えている。また、現在はあまり上手く使えていない認証技術に対してもっと知見を深めて、きちんとした認証を構築できる様にしたいと考えている。フィードバックなども募集しているのでもしあれば教えていただきたい。