みなさんこんにちは。 MLエンジニアのたかぱいです。
本日は、LLMを用いた機能をリリースするにあたり、どのように実装を進めていったのかをお話したいと思います。
完璧でなくとも実際に動くものを見せることで「触ってもらう→フィードバックをもらう→改善する」といったループを素早く回しながら実装していったよ、という内容になっています。
プロジェクト推進の話に関しては先日公開したブログに書いているので、興味がある方はこちらも見てみてください!
目次
一言で言うと「自動で文章の要約を作成してタイトルとして挿入できる」という機能です(以降、タイトル生成と呼びます) 先日のブログ記事でも紹介しましたが、実際にユーザーさんからも反応があり、今後も改善しながらより良いものを作っていきたいと思っています💪 自動で要約を作成して挿入できる、って… ママリ機能すごい✨ たしかにめちゃくちゃ長文で、途中で読むの断念することよくあるから 助かる😂 要約機能が面白くて遊んでしまう! ママリの要約を挿入するを使ってみました! AI搭載なんかな??今すごいよね。 以降、バックエンド(ML側の処理)とアプリケーション(iOS側の処理)に分けて、実装プロセスの裏側を紹介していこうと思います。 タイトル生成の裏側の処理には、Open AI APIとLangChainを利用しました。 今回はどのようなプロンプトを作成するか?が肝になってきます。 そこで、まずは過去ママリに投稿されている文章を使い、様々なプロンプトを検証していきました。 ある程度良いプロンプトができたタイミングで、検証用アプリを社内メンバーに配布して、実際にスマホから触ってもらいフィードバックをもらいつつ、プロンプトの微修正を行っていきました。 今回はiOS端末限定で検証していたこともあり、検証用アプリがインストールできない人も何人かいたため、Redash上で同じ機能が触れるようにすることで、なるべく多くの人に使ってもらう工夫もしました。 以下のようなPythonコードをRedashで記述することにより、特定のエンドポイントからのリクエストを取得し、Redashで扱える形式で表示することができます。 LangChain とはGPT-3のようなLLMを利用したサービス開発を支援してくれるライブラリです。 複数のモジュールを組み合わせることで、複雑なLLMアプリケーションを作成するプロセスを簡易化してくれます。 今回は以下のような要約クラスを作成し、ユーザーがママリ上で入力したテキストを要約しています。 LangChainの具体的な使い方などは以下のブログにも書いているので、興味のある方は見てみてください。 以降のiOSパートはiOSエンジニアのゆりこにバトンタッチしてご紹介していきます。 タイトル生成機能のiOSアプリ実装を担当したゆりこです。 実装の進め方で工夫した点を挙げたいと思います。 施策具体化された6月1週目の打ち合わせでPdMより3週間後の4週目のリリースを目標にすることが伝えられました。この段階では仕様・デザインのFixがまだできていない状態。Miroでワイヤーを作りながらディスカッションを行なっていたので大まかな仕様とざっくりデザインはありました。 アプリ実装担当として、まずは仮実装のものが開発環境で動かせる状態を早く作りたいと思いました。 理由は以下の通りです。 ( ※ コネヒトではFirebaseのAppDistributionを使いリリース前のバージョンを検証端末として登録しているデバイスに配布することができるようになっています) 実際のところ、3週間後にMiroの大まかな仕様とざっくりとしたデザインが形になりユーザーが使っている状態をチームとしてはまだ想像しきれていなかったと思います。 また、デザイナーもこれまで打ち合わせには参加していないため、デザイン相談するにしてもキャッチアップに時間がかかりそうでした。 このあたりの課題を早い段階に仮実装アプリを配布することで一部解決できるのではと考えました。 当然、機能として動かすにはML側のAPIとの接続も必要でしたが、仮実装ではスピード重視でモックを使いAPI接続なしで動かしました。 最初に配布した仮実装は粗々なものでしたが、PdM・デザイナーに手元のデバイスで動かしてもらったことにより、仕様やデザイン観点ですぐにフィードバックがもらえ、最終的な「良さそう!」がもらえるまでスピード感を持ちながら進めることができました。 結果的に目標通りの期日に機能リリースできた点を踏まえると、粗々なものでも早い段階で仮実装を配布する動き方は施策をスピード感持って進める上で効果的であったと思います。 また開発環境への仮実装配布はチーム外の人も触れる状態になるので、この施策が閉じた環境で動いてないことをアピールする形にもなったと感じます。 この経験を他の施策にも活かしてアプリの機能改善をスピード感持ちながら進めていきたいです! コネヒトでは一緒に働く仲間を募集しています! そして興味持っていただけた方は気軽にご連絡ください!
今回実装した機能
ML
プロンプト周りの検証
import json
import requests
url = 'URL'
payload = {
'user_id': 9999999,
'content': "{{content}}"
}
headers = {
'Content-Type': 'application/json',
'User-Agent': 'sample-UA'
}
response = requests.post(url, json=payload, headers=headers, timeout=(10.0, 17.5))
title = json.loads(response.text)['title']
result = {}
add_result_row(result, {'title': title, 'content': "{{content}}"})
add_result_column(result, 'title', '', 'string')
add_result_column(result, 'content', '', 'string')
LangChainを用いた実装
import openai
from dotenv import load_dotenv
from langchain.chains import LLMChain
from langchain.chat_models import ChatOpenAI
from langchain.prompts.chat import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)
load_dotenv()
openai.api_key = os.environ["OPENAI_API_KEY"]
class OpenAISummarizer:
def __init__(self, human_message: str) -> None:
self.llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0.2)
self.delimiter = "####"
self.system_template = """
ここにプロンプトを挿入
以下のように記述することで、どこからどこまでがユーザー入力文(質問文)かを明確にしている
The questions are separated by {delimiter} characters.
"""
self.human_message = human_message
def _create_prompt_template(self) -> ChatPromptTemplate:
# systemメッセージプロンプトテンプレートの準備
system_message_prompt = SystemMessagePromptTemplate.from_template(self.system_template)
# humanメッセージプロンプトテンプレートの準備
human_template = """{delimiter}\{human_message}\{delimiter}"""
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)
# chatプロンプトテンプレートの準備
prompt = ChatPromptTemplate.from_messages([system_message_prompt, human_message_prompt])
return prompt
def run(self):
# プロンプトの定義
chat_prompt = self._create_prompt_template()
# LLMチェーンの準備
summary_chain = LLMChain(llm=self.llm, prompt=chat_prompt)
result = summary_chain.run(delimiter=self.delimiter, human_message=self.human_message)
return result
# 〜リクエストを受け取る部分は省略〜
# 要約の実行
summarizer = OpenAISummarizer(human_message=text)
response = summarizer.run()
iOS
最後に