はじめに
この記事では当時、配属一ヶ月の新卒バックエンドエンジニアの私が自社SaaSの生成AI機能を担当したときの経験を体験談として紹介しようと思います。当初、エンジニアとして右も左もわからない自分がどのようにしてこの機能の制作を担当できたのか、そこから得られる学びもあると思うので是非ご覧ください!
事の経緯
その当時、私はおよそ二ヶ月半の研修を終え、配属から一ヶ月ほど経過したときでした。プロジェクトにもなんとか慣れてきた際に私はこの機能を任されることになりました。MTGで突然、私がこの機能を担当すると聞いた時、正直ビックリしたのを覚えています。ここからおよそ数週間による私と生成AIとの戦いが始まりました。
要件(言える範囲で)
言える範囲にはなりますが要件は以下のようになります。
- html形式のデータを今までの入力項目を元に別の入力項目の回答を生成する
- このデータは割と文字数が多い
- 入力項目はユーザーがが自由に作れる可変項目
- 可変項目にはいくつかの種類が存在する
- インフラにはAWSを用いており、それもあってAmazon Bedrockを用いることが検討されている
- SaaSはNext.jsでのフルスタック構成
まずはデモ作り
まず上司から指示されたのはデモ作りです。Next.jsのフルスタック構成でどのようにAmazon Bedrockを導入すればいいのかしらべてほしいとのことでした。ここでも沢山の課題が降りかかってきました
環境構築
エンジニアが何かを始めるとき、大体苦しめられるのがこの環境構築です。とはいえ今時はDockerがあるのでそんなに問題にならないケースも多いのですが、新卒の自分はDockerで環境構築などしたことがなく、それ故に苦戦しました。 ただ、現代ではありがたいことにGPTがあるので知らなくてもそれなりのことはできてしまいます。
GPTに安直にしたがった結果
我ながら非常に愚かだと思うのですが、私はGPTを過信していました。指示した通りのDockerFileを用いてもエラーの連続です(当然と言えば当然なのですが)。ここからエラーとの戦いです。エラーの意味をGPTに聞いて修正をかけ、おかしいと思った部分は指摘したり自分で調べたりしながら原因を探っていきます。
原因を探っていくとどうやらネットに転がってるNext.jsのイメージ生成の記事は基本的にnpmでpackage.jsonをローカルで生成してそれをコンテナにコピーしてコンテナを作る構成となっており、おそらくそれを参考に回答を生成していることが原因のようでした。
例えばこれとか
ただローカル環境をいじるのも嫌だったため別コンテナでpackage.jsonを生成してからイメージ生成するという形で落ち着きました。
AWS、Amazon Bedrockとの接続
新卒エンジニアの私は自分でAWSをいじった経験がなく、接続方法もよくわかりませんでした。ただここに関しては別チームのエンジニアの方がサポートして下さりました。この方をAさんとしますが、Aさんのおかげで事なきを得ました。また、そのエンジニアの方の勧めでモデルにはClaudeを用いることになりました。
実装
で、どうやって実装していくの?と思ったのですがここは以外とシンプルでモロに公式ドキュメントに実装方法がのってました。
あとはこれを元にフロントやバックのコードを作っていけばデモの完成です。
プロンプト研究
生成AIなのでもちろんプロンプトを考えなければいけません。チャット形式の返答をそのまま採用するわけにもいかず、指定した回答項目を埋める回答を的確に生成されるプロンプトを考えなくてはなりません。
一体何を参考にしたものか、、、と当初は悩んだのですがこの疑問もアッサリ解決することになります。というのもプロンプトエンジニアリングの方法も公式ドキュメントに載っていたからです
トライ&エラー
とはいえここからはトライ&エラーとなります。ドキュメントに記載されているテクニックを適用しつつ、ベストな回答を生成できるように調整をかけていきます。
公式ドキュメントには以下の9つの項目が紹介されていました。
- プロンプトジェネレーター
- 明確かつ直接的であること
- 使用例(マルチショット)
- クロードに考えさせる(思考の連鎖)
- XMLタグを使用する
- クロードに役割を与える(システムプロンプト)
- クロードの回答を事前に入力する
- 複雑なプロンプトを連鎖させる
- 長いコンテキストのヒント
この中から
2.明確かつ直接的であること
4.クロードに考えさせる(思考の連鎖)
5.XMLタグを使用する
6.クロードに役割を与える(システムプロンプト)
また、
9.長いコンテキストのヒント
から以下の知見をヒントにプロンプトを組みました。
長文データを上部に配置する: 長い文書や入力(~20K+トークン)をプロンプトの上部、クエリ、指示、例の上に配置します。これにより、すべてのモデルにわたってClaudeのパフォーマンスが大幅に向上する可能性があります。
すると以下のようなプロンプトを組む事ができます。(本記事で紹介するプロンプトは実際にSaaSに導入されたものとは異なります)
あなたはhtmlをもとに回答を生成するエージェントです。
今までの回答を参考にquestionの回答を考えて
<htmlData>
htmlのデータ
</htmlData>
<draftAnswers>
今までの項目の回答
</draftIdeas>
<question>
回答したい項目
</question>
提案するにあたり、まず関連する背景情報を調べて引用してください。
次に、回答について段階的に思考を練り上げてください。
最後にN文字程度の回答を出力してください。 出力は以下のフォーマットに従ってください。
<quotes> 関連する背景情報の引用 </quotes>
<thoughtProcess> 思考を練り上げる過程 </thoughtProcess>
<answer>
回答(N文字程度)
</answer>
ですが思うようにいかず、期待した通りでない出力が続きました。その中でも最たるものがなぜかanswerタグが回答内容に沿ったタグに変更されている点です。
後々にしてわかったことですがこのプロンプトには以下の問題がありました
html形式のデータが長く、うまくclaudeがプロンプト全体を把握できない
生成AIは長い長文を認識しずらい、という話を聞いた事がある方は多いのではないでしょうか?これは実際その通りでして、公式ドキュメントにも9. 長いコンテキストのヒントが紹介されていました。
ですがここには有効そうな手段がありませんでした。そこで次は
- 複雑なプロンプトを連鎖させる
を参考にHTML形式のデータ要約を生成させることにしました。
あなたはプロンプトエンジニアです
プロンプトに読み込ませたいをHTML形式のデータ要約することを求められています
以下のHTML形式のデータをテキストとして要約して
<htmlData>
${siteProgramRequirement.html}
</htmlData>
回答は以下の形式に従って
<outputFormat>
<htmlDataSummary>
募集要項の要約
</htmlDataSummary>
</outputFormat>
このプロンプトで生成された要約を用いることによって精度を向上できました。
回答形式の指定が弱く他に引っ張られてしまう
現在、以下のような形で回答形式を組んでいます
<quotes> 関連する背景情報の引用 </quotes>
<thoughtProcess> 思考を練り上げる過程 </thoughtProcess>
<answer>
回答(N文字程度)
</answer>
現状この状態では質問内容などに回答形式が引っ張られてしまいます。これに対してどうにかするにはこれが回答形式であることをより明示的にする必要がありました。
また、回答がanswerタグで指定しています。 公式ドキュメントの例には回答してほしいものをより明示的にタグにしていることが多かったのですが、それができていないのが原因なのではないのかと考えれらました。例えばタイトルを考えて欲しかったら<title>
というタグで囲む必要があるのです。
これらを考慮して以下の改善を加えることにしました
- 回答形式を
<outputFormat>
タグで囲む - answerタグを質問内容のタグに変えてしまう。
また、このときついでにdraftAnswersの内容も同様に質問内容のタグで補強することでより精度を向上できそうなので改良しました。
最終的にはこのようなプロンプトを作り上げることができました。
あなたはhtmlをもとに回答を生成するエージェントです。
今までの回答を参考にquestionの回答を考えて
<htmlDataSummary>
htmlのデータ
</htmlDataSummary>
<draftAnswers>
<今までの質問1>
今までの回答1
</今までの質問1>
<今までの質問2>
今までの回答2
</今までの質問2>
・
・
・
<今までの質問N>
今までの回答N
</今までの質問N>
</draftIdeas>
<question>
回答したい項目
</question>
提案するにあたり、まず関連する背景情報を調べて引用してください。
次に、回答について段階的に思考を練り上げてください。
最後にN文字程度の回答を出力してください。 出力は以下のフォーマットに従ってください。
<outputFormat>
<quotes> 関連する背景情報の引用 </quotes>
<thoughtProcess> 思考を練り上げる過程 </thoughtProcess>
<回答したい項目>
回答(N文字程度)
</回答したい項目>
</outputFormat>
「その他」というには多すぎる考慮事項
プロンプトも出来たし、あとは組み込めばOK!
そんなことを思っていた時期が私にもありました。ですが新しいものを組み込むとなると考慮、調査しなければならないものが沢山あったのです。
思い出せる限りでも多くのことがありましたが、これらの考慮事項を列挙するのも大変でした。
これらの考慮事項はプロンプトの設計過程で発覚するものもありましたし、一方の調査を進めていたら他の問題が発覚したり、上司に指摘されたり、、、と次々に増えていき、当時の自分はこれが収束するのかどうか怪しいと感じていました、、、
ですが、上司等の協力を経てなんとか収束させることができました。
以下がその一部です。
モデルの種類
これはAさんのおかげで比較的スムーズに選定できましたが、当然課題になってきます。モデルやそのプランによって料金、使えるトークン数、性能が変わってきます。これも他記事や公式ドキュメントを参考に比較、整理する必要がありました。とくに料金に関してはコスト感を会社で把握しておく必要があるので重要になってきます。
ストリームを用いるかどうか
ストリーム処理を用いることで、回答を画像のように連続的に取得し、ユーザーに見せる事ができます。
これによってユーザビリティは向上しますが、xml形式で読み取るのが不可能だったため今回は見送ることにしました。
回答項目のサニタイズ
今回、回答項目をそのままxmlのタグに切り出すと特殊文字が入ってしまいます。そのためのサニタイズを行う必要がありました。また、単純にサニタイズした場合だと生成AIが何故かサニタイズされた特殊文字を元に戻して変換してしまうケースもあったため、プロンプトに特殊文字を元に戻さない命令を追加する必要がありました。
ジョブで処理を切り出すかどうか
Bedorockでの処理はかなり処理時間が長いです。体感ですが一回の処理で20~30秒といったところでしょうか。また、通常であれば、その時間分サーバーのリソースを割くことになってしまいます。
通常であればlambda等に処理を切り出す必要があります。
当初自分はジョブにする気満々だったのですが、上司の方(Aさんとは別人です)の提案でジョブを使わないことにしました。
というのも、今回はNext.jsのフルスタック構成なためNode.jsのノンブロッキングIOの性質を用いることができるためです。
Node.jsも、シングルプロセス・シングルスレッド・非同期I/Oに基づいて設計されています(内部的にスレッドを作成する場合があります)。処理すべきものがどんどんイベントキューに追加され、それらを全て1つのプロセスで捌きます。そしてファイルアクセスや通信などのCPUを使わない処理は非同期で行われ、処理が終わったらコールバック関数が呼ばれます。そのため、きちんと設計すればC10K問題は発生しません。
Bedrockを用いた処理もこちらに該当するため、今回は回答生成にはジョブを用いないことになりました。
完成!
これらの問題を解消し、設計、実装が完了し機能が完成しました!
実際にポチポチ触ってみましたが、かなり高精度に回答を生成できました
この出来事で学んだ事
新しい技術を導入するのは大変
結局コレに尽きると思います。導入に至ってはまず何を考慮すべきなのか考慮できる必要がありました。これが欠けていると後々トラブルを引き起こすことになります。
これを自分でするのがある意味最も大変な事でした。
生成AIの実装は以外とシンプル
プロンプトさえそれなりのものが組めてしまえば、あとの実装は以外と簡単でした。
また、プロンプトそのものも自分にとっては大変でしたが配属一ヶ月の新卒が一週間あれば作れました。つまり多分やること自体はそんなに難しくなさそうです(あくまで主観ですが)。また、プロンプトを組むための情報も公式ドキュメントに丁寧に記載されてますし、世間が思っているよりは簡単なんじゃないかと思います。
実装に至るまでの調査や設計ができる事の大切さ
今までは実装フェーズを担当することが多く、実装をしっかりできることが素晴らしいと思っていたのですが、
実は実装に至るまでの調査や設計ができる事ってコーディングできることより大切なのではないか???と思えました。
実際、調査が終わって実装フェーズになってからはそんなに実装に苦戦しませんでした。というのも何をしていいかがちゃんと分かってたからです。調査担当が自分だったというのもあると思いますがこれは他人がやっても概ね同じだったと思います。
一方、先述したとおり何を考慮すべきなのか考慮し、調査する方が何倍も大変でした。これは普段の設計業務にも言えることで、仕様やその機能でやりたい事、保守性、拡張性などあらゆることを考慮したベストプラクティスを考える必要があります。一方でそのベストプラクティスが用意されている実装フェーズでも考慮することはありますが、それは設計の範囲の話に収まります(もちろん例外や実装フェーズで発生する問題もあると思いますが)
こういった事から私は実装に至るまでの調査や設計ができる事の大事さを学べました。
さいごに
長くなってしまいましたが、以上が
配属一ヶ月の新卒バックエンドエンジニアの私が自社SaaSの生成AI機能を担当したときの経験を体験談とその気づきです。
この体験談がみなさんにとって何かしらの気づきになることを願っています。