R&D チームの森元と田嶋です。R&D チームは普段、機械学習や LLM を利用した機能の検証と実装を行っています。検証の結果、その機能が追いかけているメトリクスの改善に寄与しそうだと分かった場合はプロダクトに実装することになりますが、このとき「誰がどうやってプロダクトに組み込むか」が、実装とその後の保守・運用の効率を左右します。
LAPRAS ではこれまで「誰がどうやってプロダクトに組み込むか」について、いくつかのアプローチ を試してきました。
この記事では R&D チームが機械学習や LLM を利用した機能を検証して、プロダクトに組み込むときに採用してきたアプローチの変遷について、アプリケーション開発者及び R&D チームの視点から紹介します。また、今現在 R&D チームが採用しているアプローチを具体的にどう実現しているかについても紹介します。
誰がどうやってプロダクトに組み込むか
LLM を利用した機能をプロダクトに実装する際、それをプロダクトに組み込むアプローチがいくつか考えられます。ここでは普段プロダクトの機能を開発しているアプリケーション開発者と、機械学習や LLM を利用した機能の検証と実装をしている我々 R&D チームの 2 つを中心に、アプローチを 3 つ紹介します。
1. アプリケーション開発者が R&D チームの開発したコードをプロダクト側にコピーする
以前は、R&D チームの開発したコードをアプリケーション開発者がコピーしてきてプロダクトに組み込むというアプローチを採用していました。
R&D チームとしてはプロダクトのことをあまり気にせずに検証と実装ができます。手元で動きさえすれば良いので、Python のバージョンも気にせず、好きなライブラリを利用することができます。
R&D チームの実装が終わったら、アプリケーション開発者がこれをプロダクトのコードにコピーします。その上でアプリケーション開発者は、プロダクトと R&D チームの実装した機能のライブラリの依存関係をマージして、両者のインターフェースを設計して実装して、そのインターフェースと R&D チームの提供した機能のユニットテストを実装た上で、最後に動作確認をする必要があります。機械学習系のライブラリは利用できる Python のバージョンに制限があったり、特別なバイナリを必要としたりなど色々と制約があることがありますが、これらもうまく調整する必要があります。
また、責任範囲の設計の問題ではありますが、本番リリース後に R&D チームの提供した機能に不具合があった場合はアプリケーション開発者がこれを修正していました。
以上から、このアプローチは R&D チームとしてはとてもやり易いものの、アプリケーション開発者の負担がとても大きくなっていました。
2. R&D チームがプロダクトに直接コードをコミットする
R&D チームが直接プロダクトに機械学習などを利用する機能を実装するアプローチです。この場合はアプリケーション開発者は特に手を動かすことはありません。そのため「R&D チームの開発したコードをプロダクト側にコピーする」アプローチの課題である、アプリケーション開発者の負担がとても大きくなる点は解消されています。
このアプローチの場合、R&D チームがアプリケーション開発者のリズムに合わせる必要があります。LAPRAS のアプリケーション開発者はスクラムを採用しており、1〜2 週間のスプリントでストーリーポイントを計画通り消費するために定期的にリリースを行っています。一方で、R&D チームは基本的には 1 ヶ月かそれ以上先の施策の検証を行っているため、両チームで見ているスコープが大きく異ります。このアプローチでは R&D チームは自分たちの長めのスコープと、アプリケーション開発者の短めのスコープの間を頻繁に行ったり来たりする必要があることが課題でした。
またこのコンテキストスイッチは本番リリース後もずっと継続することになりました。
3. インターフェースを明確にして、アプリケーション開発者と R&D チームが協業する (協業パターン)
現在採用しているアプローチで、R&D チームが LLM を利用した機能を Python のライブラリとしてアプリケーション開発者に提供しています。アプリケーション開発者にはライブラリのインターフェースと、ライブラリの配信場所の 2 つを共有しています。機能の精度改善などは R&D チームのリズムで行い、完了したらライブラリの新しいバージョン番号をアプリケーション開発者に伝えれば十分になります (当然、ライブラリに不具合があれば R&D チームが修正を行います)。
また、 LLM に特有の話として、プロンプトのチューニングなどを全てライブラリに閉じた形で、R&D チーム内で一貫して担っています。LLM を用いたアプリケーションでは外部の情報をプロンプトに挿入することがよくありますが、この時どんな情報を挿入するのか、どうやって挿入するのかも、機能の精度を左右する検証項目の一部と考えています。プロンプトの記述を R&D チーム以外の人が行う場合と比べて、自分たちのリズムでプロンプトをチューニングできるため検証を進めやすいという点もメリットだと感じています。一方で、このアプローチだと R&D チームが実現したい機能のドメインに詳しくならないとプロンプトが記述できないというデメリットもあります。全てをライブラリに閉じる形式は一長一短だと思うので、組織構造に合った方法を選べると良いと考えています。
協業パターンの実現方法
ここでは「3. インターフェースを明確にして、アプリケーション開発者と R&D チームが協業する」アプローチをどうやって実現しているかを説明します。
最小のインターフェースのライブラリ
協業パターンではアプリケーション開発者と R&D チームの 2 つのチームが連携しています。一方で何でもかんでも 1 つのチームで行う機能横断的なアプローチが考えられます。機能横断的なアプローチではチーム間のコミュニケーションのコストがゼロで、不具合が発生した際もどのチームが対応すべきか悩む必要がない点がメリットと言われており、協業パターンはこの点について劣っています。1
このデメリットを軽減するために、なるべくシンプルで最小限のインターフェースとなるよう心掛けています。
誰が見ても明瞭でシンプルなインターフェースであれば、わざわざコミュニケーションを取る必要がありません (インターフェースがコミュニケーションツールとして十分な機能を果たしているとも考えられます)。
また、大抵の場合ライブラリは必要最小限の機能を実現する関数を 1 つか 2 つだけ提供しています。将来のことを見越して拡張可能なクラスの形で提供することも出来ますが、拡張可能であればある程ライブラリ提供側と利用者側でお互いのことを知る必要が出てきて、コミュニケーションのコストが嵩んでしまいます。また、互いの依存度が高くなることで、将来ライブラリに変更を加えたときの利用者側への影響も大きくなってしまいます。
以上のようになるべくシンプルで小さなインターフェースとすることで、チーム間で発生するコミュニケーションが最小限になるようにしています。
パッケージリポジトリを利用したライブラリの共有
以前は作成した Python ライブラリはファイルでアプリケーション開発チームと共有していました。この方法だと、ファイルの提供自体が煩雑であったり、アプリケーション側でこのファイルをリポジトリにコミットする必要がありました。
そこで現在では Python パッケージのリポジトリからライブラリを配信するようにしています。
R&D チームでは AWS の CodeCommit でプログラムを管理してるため、ライブラリは CodeArtifact で配信しています。特別な release というブランチに PR がマージされたら、CodeBuild でライブラリをビルドして CodeArtifact にライブラリをプッシュするようにしています。
その後、アプリケーション開発者に新しいバージョンを伝えると、アプリケーション開発者側で新しいバージョンを利用するようにプロダクトのコードに変更を入れてくれます。プロダクトのコードは GitHub で管理されていて、CircleCI でビルド、デプロイされるため、OpenID Connect の仕組みを使って、CircleCI から AWS CodeArtifact で配信中のライブラリをインストールできるようにしています。
LLM を利用するライブラリのインターフェースを安定させる
本番にデプロイするまでは仕様変更などによりインターフェースを変更する必要が出てくることはありますが、当然のことながらインターフェースの変更はアプリケーション開発者への影響が大きいためなるべく避けたいです。とても間抜けな話ですが、以前、仕様変更があるわけでもないのに意図せずインターフェースを変更してしまい、アプリケーション開発者の方に迷惑をかけたことがあり、そこからの学びを反映しています。
LLM を利用するライブラリを作る際、R&D チームでは LangChain を利用することがほとんどです。LangChain には、LLM の出力を Pydantic のモデルの形で構造化データに変換する機能が備わっており、LLM に出力してほしいデータ構造を Pydantic の BaseModel を継承する形で表現して、プロンプトにこのデータ構造の JSON スキーマを含めることになります。後は LLM の出力を BaseModel を継承したクラスでパースすれば期待する形で構造化データが手に入ります。
初めて LangChain を利用するライブラリを開発していた当時、我々 R&D チームは深く考えることなく、LangChain が変換してくれた Pydantic のモデルをそのままライブラリのインターフェースである関数の戻り値にしていました。ですが、JSON スキーマをプロンプトに含めている以上、その JSON スキーマを表現する BaseModel を継承したクラスもプロンプトのチューニングの対象となります。一度アプリケーション開発側にライブラリを提供したあとに、プロンプトのチューニングの一環でこのクラスのフィールドを変更した結果、アプリケーション開発者に予告することなしに (そもそも、自分たちでも意図せずに) インターフェースを変更することになってしまいました。
この反省を踏まえて、ライブラリのインターフェースである関数の戻り値には、プロンプトに指定するクラスとは別に用意したインタフェース用のクラスを用いるようにしています。LLM から得た構造化データの値をこのインターフェース用のクラスに詰め替えて関数の戻り値とし、意図しないインターフェースの変更を防いでいます。
まとめ
機械学習や LLM を利用した機能を「誰がどうやってプロダクトに組み込むか」についてのアプローチを紹介し、現在採用している「協業パターン」の実現方法について説明してきました。「協業パターン」が最適解かどうかは現状でも分かりませんが、今後も色々と検証やプロダクトへの組み込みを経験しながら、より良いアプローチを見出していきたいと考えています。
脚注 1: [^1]: Machine Learning Design Patterns