API)を利用した求人情報を紹介するサービスの立ち上げに開発リーダーとして携わっています。その中で、バックエンド開発における、「クリーンアーキテクチャ+生成AI」の知見が溜まってきたので、それを紹介します。弊社(ディップ株式会社)では「バイトル」、「はたらこねっと」など、求人情報サービスを自社で開発・運営しています。その中で、AIエージェント事業という生成AIを活用した求人紹介機能の開発が進んでおり、2023年内にバイトル(Web版)の中でのリリースを目指しています。生成系AI技術の活用・実用化により雇用創出の可能性を広げる取り組みとして開発を開始した新たな事業です。これにより従来の「大量の求人情報から検索する・選ぶ」方法から「対話しながら最適な仕事に出会える」方法へと進化し、採用率を大幅に高めていくことを目指しています。生成系AIを活用し雇用創出に新たな可能性をディップ、「AIエージェント事業」開発を開始このサービスでは、具体的な機能の例として、生成AIを用いた以下のような機能を実現します。本記事では、生成AIプロダクトの開発をする中で使用したアーキテクチャを紹介した後、具体的な実装を見ながら、その実装に至ったポイントを紹介していきます。こんな人・プロジェクトにおすすめOpenAIを使ったプログラムは書いたことがあるが、例外処理や拡張性、保守性の高く書きたい。開発者やデータサイエンティスト・プロンプトエンジニア複数人での開発のため、責任分解をして改修コストを減らしたい。手軽にOpenAIをPythonで触りたい人向けの内容この記事では保守性が高くなるような設計&製品レベルのエラー処理などがサンプルコードに書かれているので、手軽にOpenAIを触りたい場合はここまでやる必要はないと思います。プロンプトの内容・プロンプトエンジニアリングの内容この記事では具体的なプロンプトの内容には触れません。3.5)Langchainは使っておりませんPydanticV2とバージョン競合を起こすため(執筆時点)2.クリーンアーキテクチャについて筆者の考えるクリーンアーキテクチャについての詳細およびレイヤーの役割については以下の記事でまとめています。大事なのは以下の2点です。レイヤーに分離することで、関心事の分離を行う依存性は内側だけに向かっていなければならないこの記事では生成AIアプリケーションを作成するためのコアとなるモジュールを紹介していきますが、それらはクリーンアーキテクチャの何らかのレイヤーに属しています。そのレイヤーの役割を把握しておくと理解がしやすいかと思います。APIキーを取得しますここからAPIキーを取得し、環境変数に登録します。以降のサンプルプログラムでは、os.environ.get("OPENAI_API_KEY")でAPIキーを取得できる前提で話を進めます。4.設計の全体像この記事では一番単純な例として、ユーザーからの入力をGPTに投げて、その回答を返すAPIを考えます。全体の流れを図示すると次のようになります。その流れをレイヤーごとに分けると次の図のようになります。以降の章ではOpenAIモジュール(緑)プロンプトモジュール(オレンジ)プロンプト取得モジュール(青)ロジック本体(赤)の順番で詳細を紹介していきます。5.OpenAIモジュール(gateway層)私のプロジェクトでは外部サービスへのアクセスは全てgateway層として切り出し、そのサービスに関わる処理をまとめて書いています。その中で、OpenAIへのアクセスとして、具体的には以下のようなモジュールを定義しています。モックに本実装と同様のインターフェイスを持たせるために抽象クラスを用意してある。response_strポイントOpenAIクラスをインスタンス化することでGPTが利用できます。その時、OpenAIのモデルやMaxトークン数を指定することができるようにしてあります。メソッドとしては、GPT呼び出しのためのメソッドを定義していて、プロンプトEntityを受け取って、GPTの回答をstrとして返します。プロンプトEntity(ChatQueryList)は次章で説明します。OpenAIから起きうる例外を細かく定義しています。これにより、システムエラーが起きた時、GPT起因なのかそれ以外なのかが分かります。生成AIはランダムな出力という性質上、テストがしにくい。それを解決するために使う抽象クラスを定義している。エラー処理について起きうるエラーについてはopenAIの公式ページに載っています。分間リクエスト数の上限を超えた場合クレジットが足りなかった場合に発生するものでした。(クレジットが足りないというのは盲点になりがち)except句の中には、エラーが起きた時の処理を書けば良いのですが、ここではそのエラーに対応する自作例外であるerrors.XxxxxErrorをraiseしています。抽象クラスについてOpenAIを使ったUseCaseのテストをする時に、本当に生成AIが叩かれてしまうと、予測不可能な文章が生成されてしまい、テストしにくいということが起こります。それを防ぐためにはこのクラスをモックする必要があり、そのモック先のクラスにも同様のインターフェイスを持たせるために、抽象クラスを用意しています。その他、抽象クラスを定義するメリットとして、FastAPIのDependsと利用することで、OpenAIに依存したロジックのテストや改修をする時に、影響範囲をOpenAIと切り分けることができます。(8章に利用例があります)運用保守の観点で実装しているものなので、なくてもOpenAIクラスとしては問題なく機能します。self.queries))ポイント1つのプロンプトEntityを役割とプロンプト内容として定義しているプロンプトには役割があり、それを列挙型として定義している(存在しない役割を指定できないように)プログラム内ではChatQueryListオブジェクトを介して、GPTに指示を出す設計ChatRoleクラス(Chatメッセージの種類)ChatRoleとして役割を列挙しています。公式ドキュメントを読むと、全部で5つの役割があるようです。実際には、systemプロンプトとuserプロンプトしか使いませんでした。ChatQueryクラス(単体のプロンプト)このクラスはプロンプト単体を表すもので、上で定義したChatメッセージの種類とメッセージ本体を持ちます。ChatQueryListクラス(一連のChat内容)このクラスは上のChatQueryクラスをリストとして持っており、OpenAIクラスに投げる時のEntityになります。実際にopenaiで使う時には辞書型にする必要があるので、decodeメソッドを実装しています。EntityBaseModel(Entityの基底クラス)EntityBaseModelはプロジェクト内の全てのEntityに継承させているクラスで、pydanticのBaseModelを継承元としています。Entityインスタンスにさまざまな効果を与えられるもので、ここではイミュータブルにする設定をして使っています。(クリーンアーキテクチャでは原則Entityはイミュータブルであることが望ましいとされている)7.プロンプト取得モジュール(repository層)プロンプト取得のrepositoryの役割としては、外部ストレージからリソース(プロンプト本文)を取得して、それをプログラムで使える形(上で説明したChatQueryListクラス)にしてUseCaseに返すことです。例として、システムプロンプトとユーザープロンプトの2つを取得して返すコードを示します。content=prompt)ポイントプロンプトを1つの外部リソースとして扱っているプロンプトの改修作業がしやすい。ロジックに必要なプロンプトをChatQueryListとしてUseCaseに返す。その中には、システムプロンプトとヒューマンプロンプトが入っている。プロンプトをload_human_prompt()/load_system_prompt()として関数の返り値に記述している。テキストファイルやDBへ保存も考えられるが、いずれにせよ、保存媒体に関係なく、ChatQueryListに変換する役割を持つ。(repository層でリソースの媒体の違いを吸収する)プロンプトを関数として持っておくメリット/デメリットメリットこの例のヒューマンプロンプトのように、ユーザー入力を簡単にプロンプトの内容に関与させられるデータの読み込みが発生しないため、高速にプロンプトを取得することができます。デメリットプロンプトチューニングをする時に、コードに修正がかかるということやプロンプトエンジニアがコードを触る必要があること。プロンプトの種類が増えたときにその数だけ関数が増える。(ディレクトリ構造で管理するしかない)8.ロジック本体(UseCase層)単にGPTにユーザーの入力を投げるだけのロジック例です。result_strポイントuse_caseではロジックに必要な流れのみ書かれているOpenAIの例外処理や呼び出しなども、適切な場所に書いてあるため、1行で済むできるだけ、use_caseはロジックの流れがわかるように記述する。細かい処理は他のレイヤーに役割を任せられないかを考える。UseCaseは外部のリソースの形式を知る必要がないプロンプトがテキストファイルなのか、関数の返り値なのかを気にすることなく、ChatQueryList(Entity)として扱える。openAIのオブジェクトを外部化している(依存性注入)本番で使うもの、テストで使うものを外から指定でき、ロジックとOpenAIの影響範囲を切り分けている。今回のUseCaseは単純なものを想定しましたが、実際のUseCaseはGPTのレスポンスを使って何をするとか、DBから情報を取ってくるとかもう少し複雑になります。生成AIを使ったプロダクトを年内にリリースする。目標にチーム一丸となって開発を進めてきました。私自身、生成AIを使ったプロダクトの開発経験が無い中で、手探りで進んできました。負荷テストとして大量のアクセスをすると、OpenAIはタイムアウトを起こしたり、リクエスト上限がきたりと、不安定な挙動を起こし大変な思いをしましたし、出力が変わるシステムをどうテストすれば良いのかといった問題にもぶつかりました。そんな生成AIを使いこなすために、生成AIプロダクト関連の記事や参考書を読んでも、実際の運用に足る情報が書いてあることは少なかった気がしています。0から実装して試行錯誤をする経験はとても勉強になりますね。この記事の内容が、部分的にでもどこかのプロダクトにとってプラスになることがあれば嬉しいです。